Skip to content

Commit 6fad2a9

Browse files
committed
Fix MCP spec compliance: capabilities, prompts, error codes
- Advertise resources and prompts capabilities in initialize response - Fix prompt messages: role "system" → "user", content string → {type, text} - Return JSON-RPC errors for not-found: -32002 (resource), -32602 (tool/prompt) - Include outputSchema in tools/list when defined - Update tests for new error handling behavior
1 parent 571455d commit 6fad2a9

6 files changed

Lines changed: 55 additions & 25 deletions

File tree

app/main.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ async def mcp(request: Request) -> Response:
186186
"protocolVersion": negotiated_version,
187187
"capabilities": {
188188
"tools": {"listChanged": False},
189+
"resources": {},
190+
"prompts": {},
189191
},
190192
"serverInfo": {"name": "sql-cortex-mcp", "version": "0.1.0"},
191193
}
@@ -220,6 +222,11 @@ async def mcp(request: Request) -> Response:
220222
err = _jsonrpc_error(-32602, "Missing resource uri", req_id)
221223
return _response(err, status_code=400)
222224
result = resources.read_resource(uri)
225+
if result is None:
226+
err = _jsonrpc_error(-32002, f"Resource not found: {uri}", req_id)
227+
if session_id and await _enqueue(session_id, err):
228+
return Response(status_code=202)
229+
return _response(err)
223230
response_payload = _jsonrpc_response(result, req_id)
224231
if session_id and await _enqueue(session_id, response_payload):
225232
return Response(status_code=202)
@@ -231,6 +238,11 @@ async def mcp(request: Request) -> Response:
231238
if not tool_name:
232239
err = _jsonrpc_error(-32602, "Missing tool name", req_id)
233240
return _response(err, status_code=400)
241+
if not registry.has_tool(tool_name):
242+
err = _jsonrpc_error(-32602, f"Unknown tool: {tool_name}", req_id)
243+
if session_id and await _enqueue(session_id, err):
244+
return Response(status_code=202)
245+
return _response(err)
234246
tool_result = registry.call(tool_name, arguments)
235247
is_error = bool(tool_result.get("error"))
236248
content = [
@@ -259,6 +271,11 @@ async def mcp(request: Request) -> Response:
259271
err = _jsonrpc_error(-32602, "Missing prompt name", req_id)
260272
return _response(err, status_code=400)
261273
result = prompts.get_prompt(name, arguments)
274+
if result is None:
275+
err = _jsonrpc_error(-32602, f"Unknown prompt: {name}", req_id)
276+
if session_id and await _enqueue(session_id, err):
277+
return Response(status_code=202)
278+
return _response(err)
262279
response_payload = _jsonrpc_response(result, req_id)
263280
if session_id and await _enqueue(session_id, response_payload):
264281
return Response(status_code=202)

app/mcp/prompts.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ def get_prompt(self, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
5050
)
5151
return {
5252
"messages": [
53-
{"role": "system", "content": content},
53+
{
54+
"role": "user",
55+
"content": {"type": "text", "text": content},
56+
},
5457
]
5558
}
5659

@@ -65,8 +68,11 @@ def get_prompt(self, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
6568
)
6669
return {
6770
"messages": [
68-
{"role": "system", "content": content},
71+
{
72+
"role": "user",
73+
"content": {"type": "text", "text": content},
74+
},
6975
]
7076
}
7177

72-
return {"error": f"Prompt not found: {name}"}
78+
return None

app/mcp/registry.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,26 @@ def list_tools(self) -> Dict[str, Any]:
3737
tools: List[Dict[str, Any]] = []
3838
for tool in self._defs.values():
3939
a = tool.annotations
40-
tools.append(
41-
{
42-
"name": tool.name,
43-
"title": tool.title,
44-
"description": tool.description,
45-
"inputSchema": tool.input_schema,
46-
"annotations": {
47-
"readOnlyHint": a.read_only_hint,
48-
"destructiveHint": a.destructive_hint,
49-
"idempotentHint": a.idempotent_hint,
50-
"openWorldHint": a.open_world_hint,
51-
},
52-
}
53-
)
40+
entry: Dict[str, Any] = {
41+
"name": tool.name,
42+
"title": tool.title,
43+
"description": tool.description,
44+
"inputSchema": tool.input_schema,
45+
"annotations": {
46+
"readOnlyHint": a.read_only_hint,
47+
"destructiveHint": a.destructive_hint,
48+
"idempotentHint": a.idempotent_hint,
49+
"openWorldHint": a.open_world_hint,
50+
},
51+
}
52+
if tool.output_schema:
53+
entry["outputSchema"] = tool.output_schema
54+
tools.append(entry)
5455
tools.sort(key=lambda t: t["name"])
5556
return {"tools": tools}
5657

58+
def has_tool(self, name: str) -> bool:
59+
return name in self._tools
60+
5761
def call(self, name: str, payload: Dict[str, Any]) -> Dict[str, Any]:
58-
if name not in self._tools:
59-
return {"error": f"Tool not found: {name}"}
6062
return self._tools[name](payload)

app/mcp/resources.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def read_resource(self, uri: str) -> Dict[str, Any]:
5656
}
5757
]
5858
}
59-
return {"error": f"Resource not found: {uri}"}
59+
return None
6060

6161

6262
def _json_text(data: Any) -> str:

tests/test_mcp_api.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,10 @@ async def test_unknown_tool_returns_error(client: AsyncClient) -> None:
138138
},
139139
)
140140
assert resp.status_code == 200
141-
content = resp.json()["result"]["structuredContent"]
142-
assert "error" in content
141+
body = resp.json()
142+
assert "error" in body
143+
assert body["error"]["code"] == -32602
144+
assert "nonexistent.tool" in body["error"]["message"]
143145

144146

145147
async def test_tools_list_has_annotations(client: AsyncClient) -> None:

tests/test_registry.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ def test_call_known_tool() -> None:
3131
assert result == {"echo": "hello"}
3232

3333

34-
def test_call_unknown_tool_returns_error() -> None:
34+
def test_call_unknown_tool_raises_key_error() -> None:
3535
reg = _make_registry()
36-
result = reg.call("nonexistent", {})
37-
assert "error" in result
36+
assert not reg.has_tool("nonexistent")
37+
import pytest
38+
39+
with pytest.raises(KeyError):
40+
reg.call("nonexistent", {})
3841

3942

4043
def test_tools_list_sorted() -> None:

0 commit comments

Comments
 (0)