Skip to content

Add automatic _meta parameter extraction support#172

Open
erickreutz wants to merge 1 commit intomodelcontextprotocol:mainfrom
erickreutz:add-meta-extraction-support
Open

Add automatic _meta parameter extraction support#172
erickreutz wants to merge 1 commit intomodelcontextprotocol:mainfrom
erickreutz:add-meta-extraction-support

Conversation

@erickreutz
Copy link

@erickreutz erickreutz commented Oct 26, 2025

Summary

This PR adds native support for the MCP protocol's _meta parameter, eliminating the need for manual extraction in controllers.

Background

The MCP specification defines a _meta field that allows clients to pass request-specific metadata. Previously, Ruby SDK users had to manually extract this field from request parameters.

Changes

  • Automatic extraction: The server now automatically extracts _meta from request parameters in call_tool and get_prompt methods
  • Nested structure: _meta is passed as a nested field within server_context (accessible via server_context[:_meta])
  • Compatibility: This implementation matches the TypeScript and Python SDKs, which also nest _meta within the context
  • Efficient context creation: Context is only created when there's either server_context or _meta present

Testing

  • Added comprehensive tests for _meta extraction and nesting behavior
  • All tests pass with the new implementation
  • Provider-agnostic test examples (no vendor-specific references)

Documentation

  • Updated README with _meta usage examples
  • Added link to official MCP specification
  • Included both access patterns and client request examples

Usage Example

class MyTool < MCP::Tool
  def self.call(message:, server_context: nil)
    # Access request-specific metadata
    session_id = server_context&.dig(:_meta, :session_id)
    
    # Access server's original context
    user_id = server_context&.dig(:user_id)
    
    MCP::Tool::Response.new([{
      type: "text",
      text: "Processing for user #{user_id} in session #{session_id}"
    }])
  end
end

Breaking Changes

None - this is backwards compatible. Tools that don't use server_context or don't access _meta will continue to work unchanged.

Notes

While implementing this feature, we discovered that the README incorrectly states that server_context is passed to exception and instrumentation callbacks, though the actual implementation only passes contextual error information to these callbacks.

@erickreutz erickreutz force-pushed the add-meta-extraction-support branch from fdb400a to ad9677a Compare October 26, 2025 22:05
@erickreutz erickreutz force-pushed the add-meta-extraction-support branch from 0a22bf1 to d20733b Compare January 10, 2026 14:10
@erickreutz
Copy link
Author

@scutuatua-crypto lgtm?

context
end

call_prompt_template_with_args(prompt, prompt_args, context_with_meta)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code duplication caught my attention. Could it be updated as a change like the following overall?

index e9a4e50..862c3cd 100644
--- a/lib/mcp/server.rb
+++ b/lib/mcp/server.rb
@@ -419,7 +419,7 @@ module MCP
         end
       end
 
-      call_tool_with_args(tool, arguments)
+      call_tool_with_args(tool, arguments, server_context_with_meta(request))
     rescue RequestHandlerError
       raise
     rescue => e
@@ -445,7 +445,7 @@ module MCP
       prompt_args = request[:arguments]
       prompt.validate_arguments!(prompt_args)
 
-      call_prompt_template_with_args(prompt, prompt_args)
+      call_prompt_template_with_args(prompt, prompt_args, server_context_with_meta(request))
     end
 
     def list_resources(request)
@@ -488,7 +488,7 @@ module MCP
       parameters.any? { |type, name| type == :keyrest || name == :server_context }
     end
 
-    def call_tool_with_args(tool, arguments)
+    def call_tool_with_args(tool, arguments, server_context)
       args = arguments&.transform_keys(&:to_sym) || {}
 
       if accepts_server_context?(tool.method(:call))
@@ -498,12 +498,25 @@ module MCP
       end
     end
 
-    def call_prompt_template_with_args(prompt, args)
+    def call_prompt_template_with_args(prompt, args, server_context)
       if accepts_server_context?(prompt.method(:template))
         prompt.template(args, server_context: server_context).to_h
       else
         prompt.template(args).to_h
       end
     end
+
+    def server_context_with_meta(request)
+      meta = request[:_meta]
+      if @server_context && meta
+        context = @server_context.dup
+        context[:_meta] = meta
+        context
+      elsif meta
+        { _meta: meta }
+      elsif @server_context
+        @server_context
+      end
+    end
   end
 end

response[:result][:messages][0][:content][:text]
end

# _meta extraction tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment appears unnecessary since it is already explained in the test code.


class << self
def call(message:, server_context: nil)
meta_info = server_context&.dig(:_meta, :provider, :metadata) || "no metadata"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test, it appears that a server_context with _meta set is always passed.

Suggested change
meta_info = server_context&.dig(:_meta, :provider, :metadata) || "no metadata"
def call(message:, server_context:)
meta_info = server_context.dig(:_meta, :provider, :metadata)

class << self
def call(message:, server_context: nil)
user = server_context&.dig(:user) || "unknown"
session_id = server_context&.dig(:_meta, :session_id) || "unknown"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
session_id = server_context&.dig(:_meta, :session_id) || "unknown"
def call(message:, server_context:)
user = server_context[:user]
session_id = server_context.dig(:_meta, :session_id)

class << self
def call(server_context: nil)
priority = server_context&.dig(:priority) || "none"
meta_priority = server_context&.dig(:_meta, :priority) || "none"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
meta_priority = server_context&.dig(:_meta, :priority) || "none"
def call(server_context:)
priority = server_context[:priority]
meta_priority = server_context.dig(:_meta, :priority)


class << self
def template(args, server_context: nil)
meta_info = server_context&.dig(:_meta, :request_id) || "no request id"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
meta_info = server_context&.dig(:_meta, :request_id) || "no request id"
def template(args, server_context:)
meta_info = server_context.dig(:_meta, :request_id)

Comment on lines +600 to +602
def template(args, server_context: nil)
user = server_context&.dig(:user) || "unknown"
trace_id = server_context&.dig(:_meta, :trace_id) || "unknown"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def template(args, server_context: nil)
user = server_context&.dig(:user) || "unknown"
trace_id = server_context&.dig(:_meta, :trace_id) || "unknown"
def template(args, server_context:)
user = server_context[:user]
trace_id = server_context.dig(:_meta, :trace_id)

README.md Outdated
Comment on lines +312 to +318
def self.call(message:, server_context: nil)
# Access provider-specific metadata
session_id = server_context&.dig(:_meta, :session_id)
request_id = server_context&.dig(:_meta, :request_id)

# Access server's original context
user_id = server_context&.dig(:user_id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the example would be simpler if written as follows.

Suggested change
def self.call(message:, server_context: nil)
# Access provider-specific metadata
session_id = server_context&.dig(:_meta, :session_id)
request_id = server_context&.dig(:_meta, :request_id)
# Access server's original context
user_id = server_context&.dig(:user_id)
def self.call(message:, server_context:)
# Access provider-specific metadata
session_id = server_context.dig(:_meta, :session_id)
request_id = server_context.dig(:_meta, :request_id)
# Access server's original context
user_id = server_context.dig(:user_id)

@erickreutz erickreutz force-pushed the add-meta-extraction-support branch from d20733b to 879856e Compare March 4, 2026 14:35
The MCP protocol specification includes a _meta parameter that allows
clients to pass request-specific metadata. This commit adds automatic
extraction of this parameter and makes it available to tools and prompts
as a nested field within server_context.

Key changes:
- Extract _meta from request params in call_tool and get_prompt methods
- Pass _meta as a nested field in server_context (server_context[:_meta])
- Only create context when there's either server_context or _meta present
- Add comprehensive tests for _meta extraction and nesting
- Update documentation with _meta usage examples and link to spec

This maintains compatibility with TypeScript and Python SDKs which also
nest _meta within the context rather than merging it at the top level.
@erickreutz erickreutz force-pushed the add-meta-extraction-support branch from 879856e to 4ca32c5 Compare March 4, 2026 14:43
@erickreutz
Copy link
Author

@koic feedback addressed

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.

3 participants