Skip to content

Support _meta field propagation#2178

Merged
crivetimihai merged 14 commits intomainfrom
2094_support_meta_propagation
Jan 20, 2026
Merged

Support _meta field propagation#2178
crivetimihai merged 14 commits intomainfrom
2094_support_meta_propagation

Conversation

@kevalmahajan
Copy link
Copy Markdown
Member

@kevalmahajan kevalmahajan commented Jan 19, 2026

🐛 Bug-fix PR

📌 Summary

Closes #2094

Refactored the mcpgateway RPC handlers and service layers to correctly propagate the _meta field from incoming JSON-RPC requests to upstream MCP servers. This change aligns the gateway with the MCP Specification for tools, resources and prompts for both sse as well as streamable http transports.

🐞 Root Cause

The standard JSON-RPC handlers in mcpgateway/main.py for tools/call, resources/read and prompts/get were retrieving specific arguments (like uri, name, arguments) from the params dictionary but ignoring the _meta field. Furthermore, the underlying service methods (ToolService.call_tool, ResourceService.read_resource, PromptService.get_prompt) and their helper functions (e.g., invoke_tool, get_prompt and invoke_resource) did not accept or propagate this metadata to the mcp.ClientSession.

💡 Fix Description

Implemented a comprehensive propagation path for _meta in the json rpc:

  1. RPC Handlers: tools/call, resources/read and prompts/get
  2. Service Signatures: invoke_tool, get_prompt and invoke_resource
  3. Client Session Integration: Updated internal helpers (connect_to_sse_session, connect_to_streamablehttp_server) to pass the extracted meta_data to the mcp.ClientSession.call_tool method as the meta argument.

This design ensures that _meta allows context such as progress tokens and distributed tracing IDs to flow transparently through the gateway to the destination server, preserving the fidelity of the MCP protocol.

🧪 Testing:

Use this MCP server to test client to server _meta propagation via gateway, as well as server to client via gateway.

  1. Add the below mcp server in the Context Forge Gateway.
  2. Create a virtual server with the MCP server tools.
  3. Connect to the virtual server in MCP inspector, use metadata field for sending the metadata from inspector to the gateway and eventually to the mcp server.

Metadata in MCP inspector:
image

MCP Server to Test with:

from typing import Annotated

from pydantic import BaseModel
import json

# from mcp.server.fastmcp import FastMCP
from fastmcp import FastMCP, Context
from mcp.types import CallToolResult, TextContent

mcp = FastMCP("CallToolResult Example")


class ValidationModel(BaseModel):
    """Model for validating structured output."""

    status: str
    data: dict[str, int]

@mcp.tool()
def advanced_tool(context: Context = None) -> CallToolResult:
    """Return CallToolResult directly for full control including _meta field."""
    meta = context.request_context.meta

    return CallToolResult(
        content=[TextContent(type="text", text="Response visible to the model")],
        _meta={"hidden": "data for client applications only"},
    )


@mcp.tool()
def validated_tool() -> CallToolResult:
    """Return CallToolResult with data visible to the Model."""
    response_data = {"status": "success", "data": {"result": 42}}
    
    return CallToolResult(
        content=[
            TextContent(
                type="text", 
                text=json.dumps(response_data, indent=2)
            )
        ],
        structuredContent=response_data, 
        _meta={"internal": "test metadata for validated_tool"},
        isError=False
    )

@mcp.tool()
def empty_result_tool() -> CallToolResult:
    """For empty results, return CallToolResult with empty content."""
    return CallToolResult(content=[])


# -------------------------------------------------
# Run MCP server
# -------------------------------------------------
if __name__ == "__main__":
    # Use 'sse' for streamable support
    mcp.run(transport="http", host="127.0.0.1", port=8001)

🧪 Verification

Check Command Status
Lint suite make lint
Unit tests make test
Coverage ≥ 90 % make coverage
Manual regression no longer fails steps / screenshots

📐 MCP Compliance (if relevant)

  • Matches current MCP spec
  • No breaking change to MCP clients

✅ Checklist

  • Code formatted (make black isort pre-commit)
  • No secrets/credentials committed

kevalmahajan and others added 14 commits January 20, 2026 13:13
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
…lehttp transport

- Add None check before model_dump() to prevent AttributeError when meta is not present
- Pass _meta_data to prompt_service.get_prompt() call
- Pass meta_data to resource_service.read_resource() call

This completes the _meta field propagation for streamablehttp transport that was
partially implemented in the original PR.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
MCP SDK 1.25.0 ClientSession.read_resource() only accepts uri parameter,
not meta. The meta parameter is only supported by call_tool().

Remove meta=meta_data from all 4 read_resource() calls to prevent TypeError
at runtime. Add comments explaining the SDK limitation.

Note: _meta propagation for resources will require a future MCP SDK version
that adds meta support to read_resource().

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
The internal helper functions connect_to_sse_session() and
connect_to_streamablehttp_server() had meta_data parameters that
were unused after removing the unsupported meta kwarg from
read_resource() calls.

- Remove meta_data parameter from both helper function signatures
- Update docstrings to note MCP SDK 1.25.0 limitation
- Update call sites to not pass meta_data
- Keep meta_data parameter in invoke_resource() and read_resource()
  service methods for future SDK compatibility

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Remove the problematic fallback to ctx.request.params.meta which could
raise AttributeError if ctx.request is None. The ctx.meta field on
RequestContext is the canonical source for _meta and is always available.

Simplified pattern:
  if ctx and ctx.meta is not None:
      meta_data = ctx.meta.model_dump()

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai
Copy link
Copy Markdown
Member

Code Review Fixes Applied

This PR has been rebased onto main and several issues identified during code review have been fixed.

Bugs Fixed

1. model_dump() called on potentially None value (3 places)

  • File: streamablehttp_transport.py
  • Issue: Original code extracted meta_data from request context but called meta_data.model_dump() unconditionally
  • Fix: Added if meta_data is not None: check before calling model_dump()

2. Missing _meta_data parameter for prompts

  • File: streamablehttp_transport.py
  • Issue: meta_data was extracted but not passed to prompt_service.get_prompt()
  • Fix: Added _meta_data=meta_data parameter to the call

3. Missing meta_data parameter for resources

  • File: streamablehttp_transport.py
  • Issue: meta_data was extracted but not passed to resource_service.read_resource()
  • Fix: Added meta_data=meta_data parameter to the call

4. Unsupported meta kwarg for read_resource() (4 places)

  • File: resource_service.py
  • Issue: PR added session.read_resource(uri=uri, meta=meta_data) but MCP SDK 1.25.0 read_resource() only accepts uri parameter
  • Fix: Removed meta=meta_data from all 4 read_resource() calls, added explanatory comments

5. Unused meta_data parameters in helper functions

  • File: resource_service.py
  • Issue: After removing from read_resource() calls, helper functions connect_to_sse_session() and connect_to_streamablehttp_server() had unused meta_data params
  • Fix: Removed meta_data parameter from these internal helper functions

6. Pylint W0613 unused-argument warning

  • File: resource_service.py
  • Issue: invoke_resource() public method had unused meta_data parameter
  • Fix: Added # pylint: disable=unused-argument and comment documenting it's reserved for future SDK support

7. Problematic fallback to ctx.request.params

  • File: streamablehttp_transport.py
  • Issue: Original fallback code getattr(ctx.request.params, "meta", None) could raise AttributeError if ctx.request is None
  • Fix: Removed fallback entirely, now uses ctx.meta directly which is the canonical source

MCP SDK 1.25.0 Limitation

Verified via introspection of installed SDK:

# call_tool - HAS meta support ✅
call_tool(self, name, arguments=None, ..., *, meta=None)

# read_resource - NO meta support ❌
read_resource(self, uri: AnyUrl)

# get_prompt - NO meta support ❌
get_prompt(self, name, arguments=None)

Impact: The _meta field can only be propagated to upstream MCP servers for tool calls. For resource reads and prompt gets, the SDK does not support the meta parameter. The public API signature in invoke_resource() retains the meta_data parameter to maintain API compatibility for when the SDK adds support in the future.

Verification

  • ✅ All 371 tests pass
  • ✅ Pylint passes with 10.00/10 rating
  • _meta propagation works correctly for tool calls via both entry points (main.py and streamablehttp_transport.py)

@crivetimihai crivetimihai self-assigned this Jan 20, 2026
@crivetimihai crivetimihai added this to the Release 1.0.0-BETA-2 milestone Jan 20, 2026
@crivetimihai crivetimihai force-pushed the 2094_support_meta_propagation branch from 894cf37 to b766038 Compare January 20, 2026 16:03
@crivetimihai crivetimihai merged commit b22f05c into main Jan 20, 2026
51 checks passed
@crivetimihai crivetimihai deleted the 2094_support_meta_propagation branch January 20, 2026 16:15
@010gvr
Copy link
Copy Markdown
Contributor

010gvr commented Jan 22, 2026

@crivetimihai @kevalmahajan The _meta support for resources and other capabilities are just not explicitly implemented in python but both MCP protocol and SDK has a way to pass through.

Please have a look at this Resources spec that says these methods support Pagination and _meta can be sent as a PaginationRequestParams , and refer the python implementation here https://github.com/modelcontextprotocol/python-sdk/blob/main/src/mcp/types/_types.py#L70 based on RequestParams.

You can do something like list_resources(params=PaginatedRequestParams(_meta=meta, cursor=cursor)). This will be needed for #2171

kcostell06 pushed a commit to kcostell06/mcp-context-forge that referenced this pull request Feb 24, 2026
* added _meta support for streamable http

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* updated test cases with meta field

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* _meta extraction for sse

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* handle _meta field with resources and prompts

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* update test cases to test _meta field

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* linting

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* linting fixes

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* fix test cases

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* extract _meta for fallback

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* linting fixes

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* fix: Complete _meta propagation for prompts and resources in streamablehttp transport

- Add None check before model_dump() to prevent AttributeError when meta is not present
- Pass _meta_data to prompt_service.get_prompt() call
- Pass meta_data to resource_service.read_resource() call

This completes the _meta field propagation for streamablehttp transport that was
partially implemented in the original PR.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: Remove unsupported meta parameter from read_resource calls

MCP SDK 1.25.0 ClientSession.read_resource() only accepts uri parameter,
not meta. The meta parameter is only supported by call_tool().

Remove meta=meta_data from all 4 read_resource() calls to prevent TypeError
at runtime. Add comments explaining the SDK limitation.

Note: _meta propagation for resources will require a future MCP SDK version
that adds meta support to read_resource().

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* chore: Remove unused meta_data params from resource helper functions

The internal helper functions connect_to_sse_session() and
connect_to_streamablehttp_server() had meta_data parameters that
were unused after removing the unsupported meta kwarg from
read_resource() calls.

- Remove meta_data parameter from both helper function signatures
- Update docstrings to note MCP SDK 1.25.0 limitation
- Update call sites to not pass meta_data
- Keep meta_data parameter in invoke_resource() and read_resource()
  service methods for future SDK compatibility

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: Simplify _meta extraction to use ctx.meta directly

Remove the problematic fallback to ctx.request.params.meta which could
raise AttributeError if ctx.request is None. The ctx.meta field on
RequestContext is the canonical source for _meta and is always available.

Simplified pattern:
  if ctx and ctx.meta is not None:
      meta_data = ctx.meta.model_dump()

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Support _meta field propagation in MCP tool calls

3 participants