Skip to content

Commit fe40bac

Browse files
authored
Merge pull request #14 from WebexCommunity/claude/webex-message-edit-attachments-SnnRP
2 parents 99daa1a + dc60f68 commit fe40bac

4 files changed

Lines changed: 432 additions & 2 deletions

File tree

src/webex_bot_mcp/main.py

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
# Message functions
3737
send_webex_message, send_webex_message_with_mentions,
3838
list_webex_messages, delete_webex_message,
39+
update_webex_message, get_webex_attachment_action,
3940
# Space message aliases
4041
send_webex_space_message, list_webex_space_messages,
4142
# Adaptive card tools
@@ -105,6 +106,8 @@ async def dispatch(self, request, call_next):
105106
mcp.tool()(send_webex_message_with_mentions)
106107
mcp.tool()(list_webex_messages)
107108
mcp.tool()(delete_webex_message)
109+
mcp.tool()(update_webex_message)
110+
mcp.tool()(get_webex_attachment_action)
108111

109112
# Space message aliases
110113
mcp.tool()(send_webex_space_message)
@@ -1117,6 +1120,124 @@ def webex_design_adaptive_card_prompt():
11171120
}
11181121

11191122

1123+
@mcp.prompt("webex-handle-card-submission")
1124+
def webex_handle_card_submission_prompt():
1125+
"""Process an Adaptive Card form submission received by the bot application"""
1126+
return {
1127+
"name": "Handle Card Submission",
1128+
"description": (
1129+
"Given an action_id from an attachmentActions webhook your bot application "
1130+
"already received, fetch the submitted form data and act on it — reply to "
1131+
"the room, update the original message, or route the inputs to a workflow."
1132+
),
1133+
"arguments": [
1134+
{
1135+
"name": "action_id",
1136+
"description": (
1137+
"The attachment action ID from payload[\"data\"][\"id\"] in the "
1138+
"webhook POST your bot application received from Webex"
1139+
),
1140+
"required": True
1141+
},
1142+
{
1143+
"name": "intended_action",
1144+
"description": (
1145+
"What to do with the submitted data, e.g. "
1146+
"\"approve the request and notify the room\", "
1147+
"\"store the feedback and confirm to the user\", "
1148+
"\"update the original card message with the outcome\""
1149+
),
1150+
"required": True
1151+
}
1152+
],
1153+
"template": """Process an Adaptive Card form submission.
1154+
1155+
IMPORTANT — how action_id reaches you:
1156+
This MCP server cannot receive inbound webhook events. Your separate bot
1157+
application received a POST from Webex at its registered webhook endpoint and
1158+
extracted the action_id from payload["data"]["id"]. That ID is what you are
1159+
working with now.
1160+
1161+
Action ID: {action_id}
1162+
Intended action: {intended_action}
1163+
1164+
Steps:
1165+
1166+
1. Call get_webex_attachment_action(action_id="{action_id}") to retrieve the
1167+
full submission. The response data contains:
1168+
- inputs: dict of form field values the user submitted
1169+
- messageId: the card message that was interacted with
1170+
- roomId: the room where the interaction happened
1171+
- personId: the person who submitted the card
1172+
1173+
2. Inspect the inputs dict and summarise what the user submitted.
1174+
1175+
3. Carry out the intended action: {intended_action}
1176+
Common patterns:
1177+
- Send a confirmation: call send_webex_message(room_id=roomId, markdown="...")
1178+
- Update the original card message: call update_webex_message(
1179+
message_id=messageId, markdown="...outcome summary...")
1180+
- Both: update the card to show it is resolved, then send a threaded reply
1181+
with details using parent_id=messageId
1182+
1183+
4. Report what was done: the action_id, the inputs received, and the actions taken.
1184+
1185+
If get_webex_attachment_action returns a 404, the action_id may be wrong or
1186+
the bot token may not have access to that action. Confirm the ID came from a
1187+
webhook payload for the attachmentActions resource (event: created) and that
1188+
the webhook was registered via create_webex_webhook."""
1189+
}
1190+
1191+
1192+
@mcp.prompt("webex-edit-message")
1193+
def webex_edit_message_prompt():
1194+
"""Correct or update an existing Webex message by its ID"""
1195+
return {
1196+
"name": "Edit Message",
1197+
"description": (
1198+
"Update the content of an existing Webex message — fix a typo, "
1199+
"change a status, or replace stale information — using update_webex_message."
1200+
),
1201+
"arguments": [
1202+
{
1203+
"name": "message_id",
1204+
"description": "ID of the message to edit",
1205+
"required": True
1206+
},
1207+
{
1208+
"name": "new_content",
1209+
"description": "The replacement text or markdown to put in the message",
1210+
"required": True
1211+
},
1212+
{
1213+
"name": "reason",
1214+
"description": "Optional: why the message is being edited (for your own context)",
1215+
"required": False
1216+
}
1217+
],
1218+
"template": """Edit an existing Webex message.
1219+
1220+
Message ID: {message_id}
1221+
New content: {new_content}
1222+
Reason: {reason or "not specified"}
1223+
1224+
Steps:
1225+
1226+
1. Call update_webex_message with:
1227+
- message_id: "{message_id}"
1228+
- markdown: the corrected content (preferred), or text if plain text is sufficient
1229+
1230+
Note: only the bot or user that sent the original message can edit it.
1231+
A 403 means the bot did not send that message; a 404 means the ID is wrong
1232+
or the message has been deleted.
1233+
1234+
2. Confirm the update succeeded and show the new content that was set.
1235+
1236+
If the new content contains formatting (bold, lists, links), pass it as
1237+
markdown. If it is plain text only, either parameter works."""
1238+
}
1239+
1240+
11201241
@mcp.prompt("webex-setup-webhook")
11211242
def webex_setup_webhook_prompt():
11221243
"""Register a new Webex webhook for a specific resource and event"""
@@ -1310,9 +1431,9 @@ def server_version():
13101431
"streamable-http", "stdio",
13111432
"error-handling", "versioning"
13121433
],
1313-
"tools_count": 40,
1434+
"tools_count": 42,
13141435
"resources_count": 11,
1315-
"prompts_count": 11,
1436+
"prompts_count": 13,
13161437
"breaking_changes": {
13171438
"1.0.0": [
13181439
"Initial release with structured error handling",

src/webex_bot_mcp/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .messages import (
1313
send_webex_message, send_webex_message_with_mentions,
1414
list_webex_messages, delete_webex_message,
15+
update_webex_message, get_webex_attachment_action,
1516
send_webex_space_message, list_webex_space_messages,
1617
send_webex_adaptive_card, send_webex_space_adaptive_card,
1718
build_webex_adaptive_card,
@@ -52,6 +53,7 @@
5253
# Message functions
5354
'send_webex_message', 'send_webex_message_with_mentions',
5455
'list_webex_messages', 'delete_webex_message',
56+
'update_webex_message', 'get_webex_attachment_action',
5557
# Space message aliases
5658
'send_webex_space_message', 'list_webex_space_messages',
5759
# Adaptive card tools

src/webex_bot_mcp/tools/messages.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,123 @@ def send_webex_adaptive_card(
553553
return _map_exception_to_error(e)
554554

555555

556+
def update_webex_message(
557+
message_id: str,
558+
text: Optional[str] = None,
559+
markdown: Optional[str] = None,
560+
) -> Dict[str, Any]:
561+
"""
562+
Edit an existing Webex message.
563+
564+
Only the bot or user that sent the original message may edit it.
565+
At least one of text or markdown must be provided.
566+
567+
Args:
568+
message_id: ID of the message to edit (required)
569+
text: New plain text content
570+
markdown: New markdown content
571+
572+
Returns:
573+
Standardized response dictionary with success/error information
574+
"""
575+
try:
576+
if not message_id:
577+
return create_error_response(
578+
error_code=WebexErrorCodes.INVALID_ARGUMENTS,
579+
message="message_id is required"
580+
)
581+
if not (text or markdown):
582+
return create_error_response(
583+
error_code=WebexErrorCodes.MISSING_REQUIRED_FIELD,
584+
message="At least one of text or markdown is required",
585+
details={"required_one_of": ["text", "markdown"]}
586+
)
587+
588+
params: Dict[str, Any] = {}
589+
if text:
590+
params['text'] = text
591+
if markdown:
592+
params['markdown'] = markdown
593+
594+
message = get_webex_api().messages.update(messageId=message_id, **params)
595+
596+
return create_success_response(
597+
data=_message_to_dict(message),
598+
metadata={
599+
'operation': 'update_message',
600+
'message_id': message_id,
601+
'content_type': 'markdown' if markdown else 'text',
602+
}
603+
)
604+
605+
except Exception as e:
606+
return _map_exception_to_error(e)
607+
608+
609+
def get_webex_attachment_action(action_id: str) -> Dict[str, Any]:
610+
"""
611+
Retrieve the form data submitted when a user clicks an Adaptive Card button.
612+
613+
This MCP server makes outbound calls to Webex — it does NOT receive inbound
614+
webhook events. The action_id must be obtained externally: your own bot
615+
application (a separate HTTP server with a public HTTPS endpoint) receives
616+
the webhook POST from Webex, extracts the "id" field from the payload, and
617+
then supplies that action_id here so this tool can fetch the full submission.
618+
619+
Flow:
620+
1. User clicks a card button in Webex.
621+
2. Webex POSTs the event to your bot's registered webhook endpoint.
622+
3. Your bot extracts action_id from payload["data"]["id"].
623+
4. Your bot (or an AI agent) calls this tool with that action_id.
624+
5. This tool calls GET /attachment/actions/{id} and returns the inputs.
625+
626+
A webhook must be registered via create_webex_webhook with
627+
resource="attachmentActions" and event="created" for step 2 to occur —
628+
without it, card submissions are silently dropped by Webex.
629+
630+
Args:
631+
action_id: ID of the attachment action to retrieve (required).
632+
Comes from payload["data"]["id"] in the webhook POST your
633+
bot application receives from Webex.
634+
635+
Returns:
636+
Standardized response dictionary with success/error information.
637+
The data dict contains the submitted form inputs under the "inputs" key,
638+
along with the associated messageId, roomId, and personId.
639+
"""
640+
try:
641+
if not action_id:
642+
return create_error_response(
643+
error_code=WebexErrorCodes.INVALID_ARGUMENTS,
644+
message="action_id is required"
645+
)
646+
647+
action = get_webex_api().attachment_actions.get(action_id)
648+
649+
action_dict: Dict[str, Any] = {
650+
'id': action.id,
651+
'type': getattr(action, 'type', None),
652+
'messageId': getattr(action, 'messageId', None),
653+
'inputs': getattr(action, 'inputs', {}),
654+
'roomId': getattr(action, 'roomId', None),
655+
'personId': getattr(action, 'personId', None),
656+
'created': (
657+
action.created.isoformat()
658+
if hasattr(action.created, 'isoformat')
659+
else str(action.created)
660+
),
661+
}
662+
action_dict = {k: v for k, v in action_dict.items() if v is not None}
663+
664+
return create_success_response(
665+
data=action_dict,
666+
metadata={'operation': 'get_attachment_action', 'action_id': action_id}
667+
)
668+
669+
except Exception as e:
670+
return _map_exception_to_error(e)
671+
672+
556673
# Space message aliases — "room" and "space" are synonymous in Webex
557674

558675
def send_webex_space_message(

0 commit comments

Comments
 (0)