Skip to content

Commit 487037c

Browse files
SonAIengineclaude
andcommitted
fix: MCP Proxy call_backend_tool — LLM이 arguments를 JSON string으로 보내면 SDK validation 실패하는 버그 수정
근본 원인: inputSchema에서 arguments를 "type": "object"로만 선언 → MCP SDK의 jsonschema.validate()가 handler 도달 전에 string/null 거부. 수정: - inputSchema: oneOf [object, string, null] 허용 - handler: string → json.loads() 파싱, None → {} 폴백 - 테스트 3케이스 추가 (string, null, dict) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9bc6d9c commit 487037c

2 files changed

Lines changed: 80 additions & 3 deletions

File tree

graph_tool_call/mcp_proxy.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -559,8 +559,12 @@ async def list_tools() -> list[types.Tool]:
559559
"description": "Exact tool name from search_tools results",
560560
},
561561
"arguments": {
562-
"type": "object",
563-
"description": "Arguments matching the tool's inputSchema",
562+
"oneOf": [
563+
{"type": "object"},
564+
{"type": "string"},
565+
{"type": "null"},
566+
],
567+
"description": "Arguments for the tool (object or JSON string)",
564568
},
565569
},
566570
"required": ["tool_name"],
@@ -629,7 +633,13 @@ async def call_tool(
629633
# --- Meta-tool: call_backend_tool (fallback) ---
630634
if name == "call_backend_tool":
631635
tool_name = arguments.get("tool_name", "")
632-
tool_args = arguments.get("arguments", {})
636+
tool_args = arguments.get("arguments") or {}
637+
# LLM clients may serialize arguments as a JSON string
638+
if isinstance(tool_args, str):
639+
try:
640+
tool_args = json.loads(tool_args)
641+
except (json.JSONDecodeError, TypeError):
642+
tool_args = {}
633643
result = await proxy.call_tool(tool_name, tool_args)
634644
return result.content
635645

tests/test_mcp_proxy.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,73 @@ class FakeTool:
224224
assert proxy.get_tool_schema("nonexistent") is None
225225

226226

227+
@pytest.mark.asyncio
228+
async def test_call_backend_tool_string_arguments():
229+
"""call_backend_tool should handle arguments serialized as JSON string."""
230+
mcp_mod = pytest.importorskip("mcp", reason="mcp required")
231+
types = mcp_mod.types
232+
233+
from graph_tool_call.mcp_proxy import create_proxy_server
234+
235+
proxy = MCPProxy([], top_k=5, passthrough_threshold=0)
236+
proxy._build_tool_graph()
237+
proxy._all_tools = {"my_tool": None}
238+
proxy._tool_to_backend = {"my_tool": "backend1"}
239+
proxy._gateway_mode = True
240+
241+
received_args = []
242+
243+
# Mock the backend connection
244+
class FakeSession:
245+
async def call_tool(self, name, arguments):
246+
received_args.append(arguments)
247+
return types.CallToolResult(content=[types.TextContent(type="text", text="ok")])
248+
249+
class FakeConn:
250+
session = FakeSession()
251+
252+
proxy._connections = {"backend1": FakeConn()}
253+
254+
server = create_proxy_server(proxy)
255+
handler = server.request_handlers[types.CallToolRequest]
256+
257+
# Case 1: arguments as JSON string (the bug this fix addresses)
258+
request = types.CallToolRequest(
259+
method="tools/call",
260+
params=types.CallToolRequestParams(
261+
name="call_backend_tool",
262+
arguments={"tool_name": "my_tool", "arguments": '{"action": "check"}'},
263+
),
264+
)
265+
result = await handler(request)
266+
assert not result.root.isError
267+
assert received_args[-1] == {"action": "check"}
268+
269+
# Case 2: arguments as None
270+
request2 = types.CallToolRequest(
271+
method="tools/call",
272+
params=types.CallToolRequestParams(
273+
name="call_backend_tool",
274+
arguments={"tool_name": "my_tool", "arguments": None},
275+
),
276+
)
277+
result2 = await handler(request2)
278+
assert not result2.root.isError
279+
assert received_args[-1] == {}
280+
281+
# Case 3: arguments as proper dict (should still work)
282+
request3 = types.CallToolRequest(
283+
method="tools/call",
284+
params=types.CallToolRequestParams(
285+
name="call_backend_tool",
286+
arguments={"tool_name": "my_tool", "arguments": {"action": "check"}},
287+
),
288+
)
289+
result3 = await handler(request3)
290+
assert not result3.root.isError
291+
assert received_args[-1] == {"action": "check"}
292+
293+
227294
def test_create_gateway_server():
228295
"""Gateway mode creates server with meta-tools."""
229296
pytest.importorskip("mcp", reason="mcp required")

0 commit comments

Comments
 (0)