inspector icon indicating copy to clipboard operation
inspector copied to clipboard

add support for JSON Schema $defs and definitions

Open He-Pin opened this issue 6 months ago • 3 comments

refs: https://github.com/modelcontextprotocol/java-sdk/pull/146

When add $defs in the response, the inspector will not work

He-Pin avatar May 26 '25 13:05 He-Pin

When pydantic creates the json schema for inputSchema all of the parameters are referred to by $defs. So if one is creating a tool with input parameters that are derived from a pydantic BaseModel, the parameters won't appear correctly in the inspector.

uppersaranac avatar Aug 12 '25 00:08 uppersaranac

Thanks for this report. I have Pydantic models in my servers too, so I checked on this.

The Inspector will work for both $defs and definitions, but the dynamic form cannot render the input fields (a known limitation at the moment), so only JSON input will work.

@He-Pin By "will not work", do you mean the form, or something else?

@uppersaranac Can you provide an input schema which will not work? I can use it as a test case.

To demonstrate, here's a server with a tool inputSchema using both $defs and definitions, for two different objects. Omitting either object, or required properties, will generate an error (the server does validation).

tool `inputSchema`
{
  "tools": [
    {
      "name": "mixed_ref_tool",
      "description": "A tool with a schema that uses both $defs and definitions.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "user": {
            "$ref": "#/$defs/User"
          },
          "address": {
            "$ref": "#/definitions/Address"
          }
        },
        "required": [
          "user",
          "address"
        ],
        "$defs": {
          "User": {
            "type": "object",
            "title": "User",
            "properties": {
              "name": {
                "type": "string",
                "title": "Name"
              }
            },
            "required": [
              "name"
            ]
          }
        },
        "definitions": {
          "Address": {
            "type": "object",
            "title": "Address",
            "properties": {
              "street": {
                "type": "string",
                "title": "Street"
              },
              "city": {
                "type": "string",
                "title": "City"
              }
            },
            "required": [
              "street",
              "city"
            ]
          }
        }
      }
    }
  ]
}
Server `ref_defs_definitions_server.py`
# ref_defs_definitions_server.py

import asyncio
from typing import Any

import jsonschema
import mcp.server.stdio
import mcp.types as types
from mcp.server.lowlevel import NotificationOptions, Server
from mcp.server.models import InitializationOptions

input_schema = {
    "type": "object",
    "properties": {
        "user": {"$ref": "#/$defs/User"},
        "address": {"$ref": "#/definitions/Address"},
    },
    "required": ["user", "address"],
    "$defs": {
        "User": {
            "type": "object",
            "title": "User",
            "properties": {
                "name": {"type": "string", "title": "Name"},
            },
            "required": ["name"],
        },
    },
    "definitions": {
        "Address": {
            "type": "object",
            "title": "Address",
            "properties": {
                "street": {"type": "string", "title": "Street"},
                "city": {"type": "string", "title": "City"},
            },
            "required": ["street", "city"],
        },
    },
}


# Create the low-level server and tool
server = Server("RefDefsDefinitionsServer")

mixed_tool = types.Tool(
    name="mixed_ref_tool",
    description="A tool with a schema that uses both $defs and definitions.",
    inputSchema=input_schema,
)

@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    return [mixed_tool]

@server.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
    if name == "mixed_ref_tool":
        try:
            jsonschema.validate(instance=arguments, schema=mixed_tool.inputSchema)
            return [types.TextContent(type="text", text=f"Successfully called tool with arguments: {arguments}")]
        except jsonschema.exceptions.ValidationError as e:
            return [types.TextContent(type="text", text=f"Argument validation error: {e.message}")]
    raise ValueError(f"Unknown tool: {name}")


async def main():
    """Run the server over stdio."""
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="RefDefsDefinitionsServer",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )

if __name__ == "__main__":
    asyncio.run(main())

Run it using uv:

$ mkdir refs-example && cd refs-example
$ cat - > ref_defs_definitions_server.py
# paste
$ uv init --bare . && uv add 'mcp[cli]'
$ npx @modelcontextprotocol/inspector uv run python ref_defs_definitions_server.py
Image

richardkmichael avatar Aug 20 '25 19:08 richardkmichael

Sorry, redid my approach and eliminated the code that created the $defs. Should have pasted it in this ticket when I had it.

uppersaranac avatar Aug 21 '25 02:08 uppersaranac