Skip to content

Add "meta" to call_tool method #621

Closed as not planned
Closed as not planned

Description

@jeremyschulman

Is your feature request related to a problem? Please describe.

I would like to be able to pass metadata via the "tools/call" invocation in the call_tool method. This would afford the Caller to use both the pre-defined client_id value anticipated by the Context.client_id attribute, as well as any other Caller specific values.

Describe the solution you'd like

Here is how I have modified my local copy to allow this feature functionality

    async def call_tool(
        self, name: str, arguments: dict[str, Any] | None = None, **meta
    ) -> types.CallToolResult:
        """Send a tools/call request."""
        return await self.send_request(
            types.ClientRequest(
                types.CallToolRequest(
                    method="tools/call",
                    params=types.CallToolRequestParams(name=name, arguments=arguments, _meta=meta)
                )
            ),
            types.CallToolResult,
        )

Could you please consider adding this feature as shown, or an equivalent implementation that would allow for the desired behavior?

Thank you!

Activity

  1. shiwenbin1617 commented on May 6, 2025

    @shiwenbin1617

    mark

  2. 000-000-000-000-000 commented on May 7, 2025

    @000-000-000-000-000

    curious, is this not already addressed? For some reason this: modelcontextprotocol/modelcontextprotocol#414 made me think it was.

  3. jeremyschulman commented on May 7, 2025

    @jeremyschulman
    Author

    @000-000-000-000-000 - thank you for the link. My read on that issue is the discussion about the meta parameter in the context of the protocol specification. My issue is about the implementation of the specification in the python SDK "call_tool" method that would expose that feature of the specification.

  4. pyrotank41 commented on Jun 22, 2025

    @pyrotank41

    I noticed this too @jeremyschulman , following is how I went about it:

    Following is how i was calling it:

    result = await session.call_tool(
                    "echo",
                    arguments={"name": "Hello, World!"},
                    _meta={"custom_param": "custom_value"},
                )

    but the _meta doesnt exist as a parameter for the call_tool method. so to get around this i just created a new class like:

    from mcp import ClientSession, StdioServerParameters, types
    class CustomClientSession(ClientSession):
        async def call_tool(
            self,
            name: str,
            arguments: dict[str, Any] | None = None,
            read_timeout_seconds: Any = None,
            progress_callback: Any = None,
            _meta: Dict[str, Any] | None = None,
        ) -> types.CallToolResult:
            """Send a tools/call request with optional progress callback and meta support."""
    
            # Properly create the Meta object if _meta is provided
            meta_obj = types.RequestParams.Meta(**_meta) if _meta else None
    
            # Pass meta as the 'meta' argument (not '_meta')
            params = types.CallToolRequestParams(
                name=name, arguments=arguments, _meta=meta_obj
            )
    
            # Build the request object
            request_obj = types.ClientRequest(
                types.CallToolRequest(
                    method="tools/call",
                    params=params,
                )
            )
    
            # Print the outgoing JSON-RPC request for debugging
            print("Outgoing JSON-RPC request:")
            print(json.dumps(request_obj.model_dump(mode="json", by_alias=True), indent=2))
    
            return await self.send_request(
                request_obj,
                types.CallToolResult,
                request_read_timeout_seconds=read_timeout_seconds,
                progress_callback=progress_callback,
            )
            
    async def main():
        # Define server parameters
        server_params = StdioServerParameters(
            command="python",  # The command to run your server
            args=["server.py"],  # Arguments to the command
        )
    
        # Connect to the server
        async with stdio_client(server_params) as (read_stream, write_stream):
            # Use our custom session class
            async with CustomClientSession(read_stream, write_stream) as session:
                # Initialize the connection
                await session.initialize()
    
                # List available tools
                tools_result = await session.list_tools()
                print("Available tools:")
                for tool in tools_result.tools:
                    print(f"  - {tool.name}: {tool.description}")
    
                result = await session.call_tool(
                    "echo",
                    arguments={"name": "Hello, World!"},
                    _meta={"user_id": "1234567890"},
                )
                print(f"Stdio Echo: {result.content[0].text}")
    
    
    if __name__ == "__main__":
        asyncio.run(main())

    and in my server, used it like so:

    def extract_meta_from_context(ctx: Context) -> dict:
        """
        Extract meta information from the MCP context.
    
        Args:
            ctx: The MCP context object
    
        Returns:
            dict: Dictionary containing custom meta data (excluding progressToken)
        """
        if not hasattr(ctx, "request_context") or ctx.request_context is None:
            return {}
    
        if not hasattr(ctx.request_context, "meta") or ctx.request_context.meta is None:
            return {}
    
        # Convert meta object to dictionary
        meta_dict = (
            ctx.request_context.meta.model_dump()
            if hasattr(ctx.request_context.meta, "model_dump")
            else {}
        )
    
        return meta_dict
    
    
    # Run the server
    if __name__ == "__main__":
        # parser for the type of message transport channel to run the server
        parser = argparse.ArgumentParser()
        parser.add_argument(
            "--transport",
            type=str,
            default="stdio",
            choices=["stdio", "streamable-http", "websockets"],
        )
        parser.add_argument(
            "--host",
            type=str,
            default="127.0.0.1",
            help="Host to bind to (only used for SSE transport)",
        )
        parser.add_argument(
            "--port",
            type=int,
            default=8050,
            help="Port to bind to (only used for SSE transport)",
        )
        args = parser.parse_args()
    
        # Create an MCP server with host and port from arguments
        mcp = FastMCP(name="DemoServer", host=args.host, port=args.port)
    
        # Simple tool
        @mcp.tool()
        def echo(name: str, ctx: Context) -> str:
            """Echos the input back to the client
    
            Args:
                name: The input to echo back
            """
            # Use stderr for debugging output (won't interfere with stdio communication)
            print(f"Context: {ctx}", file=sys.stderr)
            print(f"Context type: {type(ctx)}", file=sys.stderr)
            print(f"Context content: {ctx.request_context.meta}", file=sys.stderr)
    
            # Use the helper function to get meta information
            meta = extract_meta_from_context(ctx)
    
            # Return the echo with meta information
            return f"{name}! ({meta})"
    
        # run the server
        if args.transport == "stdio":
            mcp.run()
        elif args.transport == "streamable-http":
            mcp.run(transport="streamable-http")
        elif args.transport == "websockets":
            raise NotImplementedError("Websockets are not supported yet")
        else:
            raise ValueError(f"Invalid transport: {args.transport}")

    When i run the client.py i get the following output:

    [06/22/25 09:35:45] INFO     Processing request of type ListToolsRequest                                                     server.py:523
    Available tools:
      - echo: Echos the input back to the client
    
    Args:
        name: The input to echo back
    
    Outgoing JSON-RPC request:
    {
      "method": "tools/call",
      "params": {
        "_meta": {
          "progressToken": null,
          "user_id": "1234567890"
        },
        "name": "echo",
        "arguments": {
          "name": "Hello, World!"
        }
      }
    }
                        INFO     Processing request of type CallToolRequest                                                      server.py:523
    Context: 
    Context type: <class 'mcp.server.fastmcp.server.Context'>
    Context content: progressToken=None user_id='1234567890'
    Stdio Echo: Hello, World!! ({'progressToken': None, 'user_id': '1234567890'})

    As you may notice, the _meta is making it to the server and we are able to get it in the server.

    My only concern is that this is not supported by the tool_call method of the ClientSession, hence the CustomClientSession. its an easy fix. maybe the team will get to it eventually.

    thanks

  5. pyrotank41 commented on Jun 22, 2025

    @pyrotank41

    @jeremyschulman, i created a PR for this as well #1003

  6. felixweinberger commented on Oct 6, 2025

    @felixweinberger
    Contributor

    See closure in #1003, closing this associated issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions