Skip to content

Make server_context optional for Tools and Prompts #54

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

julianojulio
Copy link

This PR makes the server_context parameter truly optional for both tools and prompts, improving type checker compatibility and addressing developer experience concerns.

Motivation and Context

The previous implementation required server_context as a keyword argument in the base Tool and Prompt classes, which caused several issues:

  1. Type checking conflicts: Tools and prompts that didn't need server context were forced to accept it, creating type signature mismatches when using type checkers like Sorbet
  2. Inflexible tool/prompt definitions: Every tool and prompt had to declare server_context: in its method signature, even when unused
  3. Unintuitive parameter handling: As reported in issue Unintuitive use of meta programming for :server context in tool call #39, the implementation used meta-programming patterns that were difficult to understand and predict

How Has This Been Tested?

Added comprehensive test coverage in test/mcp/server_context_test.rb covering all scenarios:

For Tools:

  • Tools without server_context parameter
  • Tools with optional server_context
  • Tools with required server_context
  • Tools using **kwargs for flexible parameter handling

For Prompts:

  • Prompts without server_context parameter
  • Prompts with optional server_context
  • Prompts with required server_context
  • Prompts with flexible parameter signatures

All existing tests have been updated and continue to pass.

Breaking Changes

No breaking changes. This change maintains full backward compatibility:

  • Tools and prompts with explicit server_context parameters continue to work unchanged
  • Tools and prompts can now omit the parameter entirely if they don't need it
  • The server intelligently detects what each tool/prompt expects

Implementation Details

1. Made server_context optional in base classes

# In Tool class
def call(*args, server_context: nil)
  # ...
end

# In Prompt class  
def template(args, server_context: nil)
  # ...
end

2. Improved parameter detection

The new implementation properly inspects method parameters to determine if a tool/prompt accepts server_context:

def call_tool_with_args(tool, arguments)
  args = arguments.transform_keys(&:to_sym)
  
  # Check if the tool accepts server_context or has **kwargs
  parameters = tool.method(:call).parameters
  accepts_server_context = parameters.any? { |_type, name| name == :server_context }
  has_kwargs = parameters.any? { |type, _| type == :keyrest }
  
  if accepts_server_context || has_kwargs
    tool.call(**args, server_context: server_context).to_h
  else
    tool.call(**args).to_h
  end
end

This approach:

  • Preserves parameter type information for accurate detection
  • Handles tools/prompts with explicit server_context parameters
  • Supports tools/prompts using **kwargs for maximum flexibility
  • Makes behavior predictable and transparent

3. Usage Examples

Now you can define tools and prompts more flexibly:

# Simple tool - no server_context needed
class SimpleTool < Tool
  def self.call(message:)
    Tool::Response.new([{ type: "text", content: message }])
  end
end

# Optional context tool
class OptionalContextTool < Tool  
  def self.call(message:, server_context: nil)
    user = server_context&.dig(:user) || "anonymous"
    Tool::Response.new([{ type: "text", content: "#{message} from #{user}" }])
  end
end

# Flexible tool with kwargs
class FlexibleTool < Tool
  def self.call(**kwargs)
    # Can access both regular args and server_context if provided
    Tool::Response.new([{ type: "text", content: "Received: #{kwargs.inspect}" }])
  end
end

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

This PR addresses issue #39: Unintuitive use of meta programming for :server context in tool call by making the server_context handling explicit and transparent.

The change improves compatibility with Ruby type checkers (like Sorbet) and makes the gem more accessible to teams using static typing, while maintaining full backward compatibility for existing implementations.

Comment on lines 290 to 295
def call_tool_with_args(tool, arguments)
args = arguments.transform_keys(&:to_sym)

# Check if the tool accepts server_context or has **kwargs
parameters = tool.method(:call).parameters
accepts_server_context = parameters.any? { |_type, name| name == :server_context }
has_kwargs = parameters.any? { |type, _| type == :keyrest }

if accepts_server_context || has_kwargs
tool.call(**args, server_context: server_context).to_h
else
tool.call(**args).to_h
end
end

def call_prompt_template_with_args(prompt, args)
# Check if the prompt accepts server_context or has **kwargs
parameters = prompt.method(:template).parameters
accepts_server_context = parameters.any? { |_type, name| name == :server_context }
has_kwargs = parameters.any? { |type, _| type == :keyrest }

if accepts_server_context || has_kwargs
prompt.template(args, server_context: server_context).to_h
else
prompt.template(args).to_h
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this could be simplified into a single function like

def accepts_server_context?(method_object)
  parameters = method_object.parameters
  # ...
  accepts_server_context || has_kwargs
end

To have the signature checking logic be cleanly contained in a single place.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also independent of that an old function tool_call_parameters still exists which becomes unused by the current version of the change. So either deleting or adjusting that to include these changes would be good.

Copy link
Collaborator

@sambostock sambostock left a comment

Choose a reason for hiding this comment

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

#56 explores the idea of making tools & prompts into simple data/config/value objects, and gets rid of the inheritance based approach in favour of the define based approach.

One of the benefits of this is that the implementation of the tool/prompt always uses a block, and blocks implicitly handle omitting arguments, meaning we can get rid of all the logic to do with conditionally passing server_context:.

- Updated `call` method in `MCP::Tool` and `MCP::Prompt` to accept `server_context` as an optional parameter.
- Refactored `MCP::Server` to streamline argument handling for tool and prompt calls, introducing helper methods `call_tool_with_args` and `call_prompt_template_with_args`.
- Adjusted tests to reflect changes in method signatures and ensure compatibility with optional `server_context` parameter.
@julianojulio julianojulio force-pushed the make_server_context_optional_and_sorbet_friendly branch from 8fcd48e to c10c0dd Compare June 10, 2025 19:40
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