fix(mcp): decode URI-encoded schema refs
This commit is contained in:
@@ -317,13 +317,17 @@ def _extract_nullable_branch(options: Any) -> tuple[dict[str, Any], bool] | None
|
|||||||
|
|
||||||
def _resolve_local_schema_ref(root: dict[str, Any], ref: str) -> Any:
|
def _resolve_local_schema_ref(root: dict[str, Any], ref: str) -> Any:
|
||||||
"""Resolve a local JSON Pointer without accepting remote references."""
|
"""Resolve a local JSON Pointer without accepting remote references."""
|
||||||
if ref == "#":
|
if not ref.startswith("#"):
|
||||||
|
raise ValueError("not a local JSON Pointer")
|
||||||
|
|
||||||
|
pointer = urllib.parse.unquote(ref[1:], errors="strict")
|
||||||
|
if not pointer:
|
||||||
return root
|
return root
|
||||||
if not ref.startswith("#/"):
|
if not pointer.startswith("/"):
|
||||||
raise ValueError("not a local JSON Pointer")
|
raise ValueError("not a local JSON Pointer")
|
||||||
|
|
||||||
current: Any = root
|
current: Any = root
|
||||||
for raw_part in ref[2:].split("/"):
|
for raw_part in pointer[1:].split("/"):
|
||||||
part = raw_part.replace("~1", "/").replace("~0", "~")
|
part = raw_part.replace("~1", "/").replace("~0", "~")
|
||||||
if isinstance(current, dict):
|
if isinstance(current, dict):
|
||||||
current = current[part]
|
current = current[part]
|
||||||
@@ -347,15 +351,22 @@ def _rewrite_local_schema_refs(schema: dict[str, Any]) -> dict[str, Any]:
|
|||||||
|
|
||||||
rewritten = dict(value)
|
rewritten = dict(value)
|
||||||
ref = rewritten.get("$ref")
|
ref = rewritten.get("$ref")
|
||||||
is_rewritable_ref = isinstance(ref, str) and (
|
is_rewritable_ref = False
|
||||||
ref == "#" or (ref.startswith("#/") and not ref.startswith("#/$defs/"))
|
if isinstance(ref, str) and not ref.startswith("#/$defs/"):
|
||||||
)
|
try:
|
||||||
|
pointer = urllib.parse.unquote(ref[1:], errors="strict")
|
||||||
|
except (UnicodeDecodeError, ValueError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
is_rewritable_ref = ref.startswith("#") and (
|
||||||
|
not pointer or pointer.startswith("/")
|
||||||
|
)
|
||||||
if is_rewritable_ref:
|
if is_rewritable_ref:
|
||||||
name = rewritten_refs.get(ref)
|
name = rewritten_refs.get(ref)
|
||||||
if name is None:
|
if name is None:
|
||||||
try:
|
try:
|
||||||
target = _resolve_local_schema_ref(schema, ref)
|
target = _resolve_local_schema_ref(schema, ref)
|
||||||
except (KeyError, IndexError, TypeError, ValueError):
|
except (KeyError, IndexError, TypeError, UnicodeDecodeError, ValueError):
|
||||||
logger.warning("MCP tool schema contains an unresolved local $ref: {}", ref)
|
logger.warning("MCP tool schema contains an unresolved local $ref: {}", ref)
|
||||||
else:
|
else:
|
||||||
assert isinstance(ref, str)
|
assert isinstance(ref, str)
|
||||||
|
|||||||
@@ -309,6 +309,27 @@ def test_wrapper_preserves_existing_defs_refs() -> None:
|
|||||||
assert wrapper.parameters["$defs"]["value"]["type"] == "string"
|
assert wrapper.parameters["$defs"]["value"]["type"] == "string"
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrapper_resolves_uri_encoded_json_pointer() -> None:
|
||||||
|
tool_def = SimpleNamespace(
|
||||||
|
name="demo",
|
||||||
|
description="demo tool",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"space name/value": {"type": "string"},
|
||||||
|
"alias": {"$ref": "#/properties/space%20name~1value"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
wrapper = MCPToolWrapper(SimpleNamespace(call_tool=None), "test", tool_def)
|
||||||
|
|
||||||
|
generated_ref = wrapper.parameters["properties"]["alias"]["$ref"]
|
||||||
|
assert generated_ref.startswith("#/$defs/ref_")
|
||||||
|
generated_name = generated_ref.removeprefix("#/$defs/")
|
||||||
|
assert wrapper.parameters["$defs"][generated_name] == {"type": "string"}
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_windows_stdio_command_is_noop_off_windows(
|
def test_normalize_windows_stdio_command_is_noop_off_windows(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user