From b115d07289ec3eb13b0be81ac51a032c725e4348 Mon Sep 17 00:00:00 2001 From: Zois Pagoulatos Date: Thu, 30 Jul 2026 11:15:43 +0300 Subject: [PATCH 1/2] fix(prompts): accept string arguments for ticket_id MCP prompt arguments are always transmitted as strings; PromptArgument has no type field. Annotating ticket_id as int made FastMCP's _convert_string_arguments coerce the incoming value via pydantic, so any non-numeric string raised PromptError and the prompt could not be rendered. Clients that materialize MCP prompts into slash commands discover their arguments by rendering them with placeholder values. opencode, for example, issues prompts/get with "$1"/"$2", which fails: PromptError: Could not convert argument 'ticket_id' with value '$1' to expected type This made analyze_ticket and draft_response unusable in those clients, while escalation_summary worked because group is annotated str | None. Annotate ticket_id as str. Both prompts only interpolate it into the returned f-string, so this is behaviour-preserving; the docstrings already document that callers must pass the internal database ID. Add a parametrized regression test that renders all three prompts with non-numeric arguments. It fails on analyze_ticket and draft_response before this change and passes for escalation_summary throughout. --- mcp_zammad/server.py | 4 ++-- tests/test_server.py | 26 ++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/mcp_zammad/server.py b/mcp_zammad/server.py index 4a5f5443..41063515 100644 --- a/mcp_zammad/server.py +++ b/mcp_zammad/server.py @@ -2623,7 +2623,7 @@ def _setup_prompts(self) -> None: """Register all prompts with the MCP server.""" @self.mcp.prompt() - def analyze_ticket(ticket_id: int) -> str: + def analyze_ticket(ticket_id: str) -> str: """Generate a prompt to analyze a ticket. Note: ticket_id must be the internal database ID (NOT the display number). @@ -2642,7 +2642,7 @@ def analyze_ticket(ticket_id: int) -> str: Use appropriate tools to gather any additional context about the customer or organization if needed.""" @self.mcp.prompt() - def draft_response(ticket_id: int, tone: str = "professional") -> str: + def draft_response(ticket_id: str, tone: str = "professional") -> str: """Generate a prompt to draft a response to a ticket. Note: ticket_id must be the internal database ID (NOT the display number). diff --git a/tests/test_server.py b/tests/test_server.py index e5e7e7be..84850a11 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -232,6 +232,28 @@ async def test_prompts(): assert "escalation_summary" in prompt_names +@pytest.mark.asyncio +@pytest.mark.parametrize( + "name,arguments", + [ + ("analyze_ticket", {"ticket_id": "$1"}), + ("draft_response", {"ticket_id": "$1", "tone": "$2"}), + ("escalation_summary", {"group": "$1"}), + ], +) +async def test_prompts_render_with_non_numeric_arguments(name: str, arguments: dict[str, str]) -> None: + """Prompt arguments arrive as strings, so they must not be coerced to int. + + Clients that turn MCP prompts into slash commands render them with placeholder + values such as "$1" to discover their arguments. Annotating ticket_id as int made + FastMCP raise PromptError on those placeholders, so the prompts were unusable. + """ + result = await mcp.render_prompt(name, arguments) + + rendered = result.messages[0].content.text # type: ignore[union-attr] + assert "$1" in rendered + + @pytest.mark.asyncio async def test_initialization_failure(): """Test that initialization handles failures gracefully.""" @@ -1579,13 +1601,13 @@ def test_prompt_handlers(decorator_capturer): # Test analyze_ticket prompt assert "analyze_ticket" in test_prompts - result = test_prompts["analyze_ticket"](ticket_id=123) + result = test_prompts["analyze_ticket"](ticket_id="123") assert "analyze ticket with ID 123" in result assert "get_ticket tool" in result # Test draft_response prompt assert "draft_response" in test_prompts - result = test_prompts["draft_response"](ticket_id=123, tone="friendly") + result = test_prompts["draft_response"](ticket_id="123", tone="friendly") assert "draft a friendly response to ticket with ID 123" in result assert "add_article" in result From e7003087ba3fcc48e2577ac33e64132890cb0246 Mon Sep 17 00:00:00 2001 From: Zois Pagoulatos Date: Thu, 30 Jul 2026 11:28:14 +0300 Subject: [PATCH 2/2] test: use tuple form for parametrize argument names Addresses ruff PT006. The repo's ruff config does not select PT, so this was not flagged locally, but the tuple form is clearer and costs nothing. --- tests/test_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_server.py b/tests/test_server.py index 84850a11..dcae29cc 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -234,7 +234,7 @@ async def test_prompts(): @pytest.mark.asyncio @pytest.mark.parametrize( - "name,arguments", + ("name", "arguments"), [ ("analyze_ticket", {"ticket_id": "$1"}), ("draft_response", {"ticket_id": "$1", "tone": "$2"}),