Internal Release 0.7.0
- add support for resource templates - improve capability negotiation on handlers - clean up readme - reduce default protocol version to "2024-11-05" - address spec compliance of resources - add debug gem to development - alias ModelContextProtocol to MCP - move prompt subclasses to subdir - improve tool and prompt definition - improve passing of server context to tools - improve tool input schema validation
This commit is contained in:
parent
62184957e0
commit
baeb99ffbc
23 changed files with 842 additions and 253 deletions
1
Gemfile
1
Gemfile
|
|
@ -12,3 +12,4 @@ gem "rubocop-shopify", require: false
|
||||||
|
|
||||||
gem "minitest-reporters"
|
gem "minitest-reporters"
|
||||||
gem "mocha"
|
gem "mocha"
|
||||||
|
gem "debug"
|
||||||
|
|
|
||||||
23
Gemfile.lock
23
Gemfile.lock
|
|
@ -1,7 +1,7 @@
|
||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
model_context_protocol (0.6.0)
|
model_context_protocol (0.7.0)
|
||||||
json_rpc_handler (~> 0.1)
|
json_rpc_handler (~> 0.1)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
|
|
@ -28,9 +28,18 @@ GEM
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
concurrent-ruby (1.3.5)
|
concurrent-ruby (1.3.5)
|
||||||
connection_pool (2.5.3)
|
connection_pool (2.5.3)
|
||||||
|
date (3.4.1)
|
||||||
|
debug (1.10.0)
|
||||||
|
irb (~> 1.10)
|
||||||
|
reline (>= 0.3.8)
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
i18n (1.14.7)
|
i18n (1.14.7)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
|
io-console (0.8.0)
|
||||||
|
irb (1.15.2)
|
||||||
|
pp (>= 0.6.0)
|
||||||
|
rdoc (>= 4.0.0)
|
||||||
|
reline (>= 0.4.2)
|
||||||
json (2.11.3)
|
json (2.11.3)
|
||||||
json_rpc_handler (0.1.1)
|
json_rpc_handler (0.1.1)
|
||||||
language_server-protocol (3.17.0.4)
|
language_server-protocol (3.17.0.4)
|
||||||
|
|
@ -48,11 +57,21 @@ GEM
|
||||||
parser (3.3.8.0)
|
parser (3.3.8.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
|
pp (0.6.2)
|
||||||
|
prettyprint
|
||||||
|
prettyprint (0.2.0)
|
||||||
prism (1.4.0)
|
prism (1.4.0)
|
||||||
|
psych (5.2.4)
|
||||||
|
date
|
||||||
|
stringio
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.2.1)
|
rake (13.2.1)
|
||||||
|
rdoc (6.13.1)
|
||||||
|
psych (>= 4.0.0)
|
||||||
regexp_parser (2.10.0)
|
regexp_parser (2.10.0)
|
||||||
|
reline (0.6.1)
|
||||||
|
io-console (~> 0.5)
|
||||||
rubocop (1.75.5)
|
rubocop (1.75.5)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
|
|
@ -72,6 +91,7 @@ GEM
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
|
stringio (3.1.7)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
unicode-display_width (3.1.4)
|
unicode-display_width (3.1.4)
|
||||||
|
|
@ -85,6 +105,7 @@ PLATFORMS
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
activesupport
|
activesupport
|
||||||
|
debug
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
minitest-reporters
|
minitest-reporters
|
||||||
mocha
|
mocha
|
||||||
|
|
|
||||||
211
README.md
211
README.md
|
|
@ -1,17 +1,6 @@
|
||||||
# Model Context Protocol
|
# Model Context Protocol
|
||||||
|
|
||||||
A Ruby gem implementing Model Context Protocol + JSON RPC 2.0 spec
|
A Ruby gem for implementing Model Context Protocol servers
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Model Context Protocol provides a standardized way to interact with language models through a JSON RPC 2.0 interface.
|
|
||||||
|
|
||||||
It handles request/response formatting, error handling, and tool integrations in a type-safe manner using Sorbet.
|
|
||||||
|
|
||||||
See https://spec.modelcontextprotocol.io/specification/
|
|
||||||
|
|
||||||
This implementation breaks from the transport examples in the spec as it implements a POST-based JSON RPC call rather
|
|
||||||
than a stateful SSE transport.
|
|
||||||
|
|
||||||
## MCP Server
|
## MCP Server
|
||||||
|
|
||||||
|
|
@ -19,31 +8,45 @@ The `ModelContextProtocol::Server` class is the core component that handles JSON
|
||||||
It implements the Model Context Protocol specification, handling model context requests and responses.
|
It implements the Model Context Protocol specification, handling model context requests and responses.
|
||||||
|
|
||||||
### Key Features
|
### Key Features
|
||||||
|
|
||||||
- Implements JSON-RPC 2.0 message handling
|
- Implements JSON-RPC 2.0 message handling
|
||||||
- Supports protocol initialization and capability negotiation
|
- Supports protocol initialization and capability negotiation
|
||||||
- Manages tool registration and invocation
|
- Manages tool registration and invocation
|
||||||
- Provides type-safe request/response handling using Sorbet
|
- Supports prompt registration and execution
|
||||||
|
- Supports resource registration and retrieval
|
||||||
|
|
||||||
### Supported Methods
|
### Supported Methods
|
||||||
|
|
||||||
- `initialize` - Initializes the protocol and returns server capabilities
|
- `initialize` - Initializes the protocol and returns server capabilities
|
||||||
- `ping` - Simple health check that returns "pong"
|
- `ping` - Simple health check
|
||||||
- `tools/list` - Lists all registered tools and their schemas
|
- `tools/list` - Lists all registered tools and their schemas
|
||||||
- `tools/call` - Invokes a specific tool with provided arguments
|
- `tools/call` - Invokes a specific tool with provided arguments
|
||||||
- `prompts/list` - Lists all registered prompts and their schemas
|
- `prompts/list` - Lists all registered prompts and their schemas
|
||||||
- `prompts/get` - Retrieves a specific prompt by name
|
- `prompts/get` - Retrieves a specific prompt by name
|
||||||
|
- `resources/list` - Lists all registered resources and their schemas
|
||||||
|
- `resources/read` - Retrieves a specific resource by name
|
||||||
|
- `resources/templates/list` - Lists all registered resource templates and their schemas
|
||||||
|
|
||||||
|
### Unsupported Features ( to be implemented in future versions )
|
||||||
|
|
||||||
|
- Notifications
|
||||||
|
- Log Level
|
||||||
|
- Resource subscriptions
|
||||||
|
- Completions
|
||||||
|
- Complete StreamableHTTP implementation with streaming responses
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
#### Rails Controller
|
#### Rails Controller
|
||||||
Implement an `ApplicationController` which calls the `Server#handle` method, eg
|
|
||||||
|
When added to a Rails controller on a route that handles POST requests, your server will be compliant with non-streaming
|
||||||
|
[StreamableHTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport
|
||||||
|
requests.
|
||||||
|
|
||||||
|
You can use the `Server#handle_json` method to handle requests.
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
module ModelContextProtocol
|
module ModelContextProtocol
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
sig { void }
|
|
||||||
def index
|
def index
|
||||||
server = ModelContextProtocol::Server.new(
|
server = ModelContextProtocol::Server.new(
|
||||||
name: "my_server",
|
name: "my_server",
|
||||||
|
|
@ -57,29 +60,9 @@ module ModelContextProtocol
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
or, if you want/need to parse the json yourself you can do the following
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
module ModelContextProtocol
|
|
||||||
class ApplicationController < ActionController::Base
|
|
||||||
|
|
||||||
sig { void }
|
|
||||||
def index
|
|
||||||
server = ModelContextProtocol::Server.new(
|
|
||||||
name: "my_server",
|
|
||||||
tools: [SomeTool, AnotherTool],
|
|
||||||
prompts: [MyPrompt],
|
|
||||||
server_context: { user_id: current_user.id },
|
|
||||||
)
|
|
||||||
render(json: server.handle(JSON.parse(request.body.read)).to_h)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Stdio Transport
|
#### Stdio Transport
|
||||||
|
|
||||||
If you want to build a simple command-line application, you can use the stdio transport:
|
If you want to build a local command-line application, you can use the stdio transport:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
|
|
@ -89,11 +72,12 @@ require "model_context_protocol/transports/stdio"
|
||||||
# Create a simple tool
|
# Create a simple tool
|
||||||
class ExampleTool < ModelContextProtocol::Tool
|
class ExampleTool < ModelContextProtocol::Tool
|
||||||
description "A simple example tool that echoes back its arguments"
|
description "A simple example tool that echoes back its arguments"
|
||||||
input_schema type: "object",
|
input_schema(
|
||||||
properties: {
|
properties: {
|
||||||
message: { type: "string" },
|
message: { type: "string" },
|
||||||
},
|
},
|
||||||
required: ["message"]
|
required: ["message"]
|
||||||
|
)
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def call(message:, server_context:)
|
def call(message:, server_context:)
|
||||||
|
|
@ -125,36 +109,13 @@ $ ./stdio_server.rb
|
||||||
{"jsonrpc":"2.0","id":"3","result":["ExampleTool"]}
|
{"jsonrpc":"2.0","id":"3","result":["ExampleTool"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Testing without a transport
|
|
||||||
|
|
||||||
To see sample responses without setting up a client to hit the server, you can simply run
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
server = ModelContextProtocol::Server.new(
|
|
||||||
name: "my_server",
|
|
||||||
tools: [SomeTool, AnotherTool],
|
|
||||||
prompts: [MyPrompt],
|
|
||||||
server_context: { user_id: current_user.id },
|
|
||||||
)
|
|
||||||
request = {
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
method: "tools/list",
|
|
||||||
params: {},
|
|
||||||
id: "1"
|
|
||||||
}
|
|
||||||
server.handle(request)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The gem can be configured using the `ModelContextProtocol.configure` block:
|
The gem can be configured using the `ModelContextProtocol.configure` block:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
ModelContextProtocol.configure do |config|
|
ModelContextProtocol.configure do |config|
|
||||||
config.exception_reporter = ->(exception, server_context) do
|
config.exception_reporter = do |exception, server_context|
|
||||||
# Your exception reporting logic here
|
# Your exception reporting logic here
|
||||||
# For example with Bugsnag:
|
# For example with Bugsnag:
|
||||||
Bugsnag.notify(exception) do |report|
|
Bugsnag.notify(exception) do |report|
|
||||||
|
|
@ -162,15 +123,19 @@ ModelContextProtocol.configure do |config|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
config.instrumentation_callback = -> (data) { puts "Got instrumentation data #{data.inspect}" }
|
config.instrumentation_callback = do |data|
|
||||||
|
puts "Got instrumentation data #{data.inspect}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
```
|
||||||
|
|
||||||
or by creating an explicit configuration and passing it into the server.
|
or by creating an explicit configuration and passing it into the server.
|
||||||
This is useful for systems where an application hosts more than one MCP server but
|
This is useful for systems where an application hosts more than one MCP server but
|
||||||
they might require different instrumentation callbacks.
|
they might require different instrumentation callbacks.
|
||||||
|
|
||||||
|
```ruby
|
||||||
configuration = ModelContextProtocol::Configuration.new
|
configuration = ModelContextProtocol::Configuration.new
|
||||||
configuration.exception_reporter = ->(exception, server_context) do
|
configuration.exception_reporter = do |exception, server_context|
|
||||||
# Your exception reporting logic here
|
# Your exception reporting logic here
|
||||||
# For example with Bugsnag:
|
# For example with Bugsnag:
|
||||||
Bugsnag.notify(exception) do |report|
|
Bugsnag.notify(exception) do |report|
|
||||||
|
|
@ -178,7 +143,9 @@ configuration.exception_reporter = ->(exception, server_context) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
configuration.instrumentation_callback = -> (data) { puts "Got instrumentation data #{data.inspect}" }
|
configuration.instrumentation_callback = do |data|
|
||||||
|
puts "Got instrumentation data #{data.inspect}"
|
||||||
|
end
|
||||||
|
|
||||||
server = ModelContextProtocol::Server.new(
|
server = ModelContextProtocol::Server.new(
|
||||||
# ... all other options
|
# ... all other options
|
||||||
|
|
@ -186,6 +153,65 @@ server = ModelContextProtocol::Server.new(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Server Context and Configuration Block Data
|
||||||
|
|
||||||
|
#### `server_context`
|
||||||
|
|
||||||
|
The `server_context` is a user-defined hash that is passed into the server instance and made available to tools, prompts, and exception/instrumentation callbacks. It can be used to provide contextual information such as authentication state, user IDs, or request-specific data.
|
||||||
|
|
||||||
|
**Type:**
|
||||||
|
```ruby
|
||||||
|
server_context: { [String, Symbol] => Any }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```ruby
|
||||||
|
server = ModelContextProtocol::Server.new(
|
||||||
|
name: "my_server",
|
||||||
|
server_context: { user_id: current_user.id, request_id: request.uuid }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
This hash is then passed as the `server_context` argument to tool and prompt calls, and is included in exception and instrumentation callbacks.
|
||||||
|
|
||||||
|
#### Configuration Block Data
|
||||||
|
|
||||||
|
##### Exception Reporter
|
||||||
|
|
||||||
|
The exception reporter receives:
|
||||||
|
|
||||||
|
- `exception`: The Ruby exception object that was raised
|
||||||
|
- `server_context`: The context hash provided to the server
|
||||||
|
|
||||||
|
**Signature:**
|
||||||
|
```ruby
|
||||||
|
exception_reporter = ->(exception, server_context) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Instrumentation Callback
|
||||||
|
|
||||||
|
The instrumentation callback receives a hash with the following possible keys:
|
||||||
|
|
||||||
|
- `method`: (String) The protocol method called (e.g., "ping", "tools/list")
|
||||||
|
- `tool_name`: (String, optional) The name of the tool called
|
||||||
|
- `prompt_name`: (String, optional) The name of the prompt called
|
||||||
|
- `resource_uri`: (String, optional) The URI of the resource called
|
||||||
|
- `error`: (String, optional) Error code if a lookup failed
|
||||||
|
- `duration`: (Float) Duration of the call in seconds
|
||||||
|
|
||||||
|
**Type:**
|
||||||
|
```ruby
|
||||||
|
instrumentation_callback = ->(data) { ... }
|
||||||
|
# where data is a Hash with keys as described above
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```ruby
|
||||||
|
config.instrumentation_callback = ->(data) do
|
||||||
|
puts "Instrumentation: #{data.inspect}"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
### Server Protocol Version
|
### Server Protocol Version
|
||||||
|
|
||||||
The server's protocol version can be overridden using the `protocol_version` class method:
|
The server's protocol version can be overridden using the `protocol_version` class method:
|
||||||
|
|
@ -233,7 +259,12 @@ This gem provides a `ModelContextProtocol::Tool` class that can be used to creat
|
||||||
```ruby
|
```ruby
|
||||||
class MyTool < ModelContextProtocol::Tool
|
class MyTool < ModelContextProtocol::Tool
|
||||||
description "This tool performs specific functionality..."
|
description "This tool performs specific functionality..."
|
||||||
input_schema [{ type: "text", text: "message" }]
|
input_schema(
|
||||||
|
properties: {
|
||||||
|
message: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["message"]
|
||||||
|
)
|
||||||
annotations(
|
annotations(
|
||||||
title: "My Tool",
|
title: "My Tool",
|
||||||
read_only_hint: true,
|
read_only_hint: true,
|
||||||
|
|
@ -242,12 +273,6 @@ class MyTool < ModelContextProtocol::Tool
|
||||||
open_world_hint: false
|
open_world_hint: false
|
||||||
)
|
)
|
||||||
|
|
||||||
input_schema type: 'object',
|
|
||||||
properties: {
|
|
||||||
message: { type: 'string' },
|
|
||||||
},
|
|
||||||
required: ['message']
|
|
||||||
|
|
||||||
def self.call(message:, server_context:)
|
def self.call(message:, server_context:)
|
||||||
Tool::Response.new([{ type: "text", text: "OK" }])
|
Tool::Response.new([{ type: "text", text: "OK" }])
|
||||||
end
|
end
|
||||||
|
|
@ -392,7 +417,9 @@ To register a handler pass a proc/lambda to as `instrumentation_callback` into t
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
ModelContextProtocol.configure do |config|
|
ModelContextProtocol.configure do |config|
|
||||||
config.instrumentation_callback = -> (data) { puts "Got instrumentation data #{data.inspect}" }
|
config.instrumentation_callback = do |data|
|
||||||
|
puts "Got instrumentation data #{data.inspect}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -407,6 +434,40 @@ The data contains the following keys:
|
||||||
`tool_name`, `prompt_name` and `resource_uri` are only populated if a matching handler is registered.
|
`tool_name`, `prompt_name` and `resource_uri` are only populated if a matching handler is registered.
|
||||||
This is to avoid potential issues with metric cardinality
|
This is to avoid potential issues with metric cardinality
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
MCP spec includes [Resources](https://modelcontextprotocol.io/docs/concepts/resources)
|
||||||
|
|
||||||
|
The `ModelContextProtocol::Resource` class provides a way to register resources with the server.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
resource = ModelContextProtocol::Resource.new(
|
||||||
|
uri: "example.com/my_resource",
|
||||||
|
mime_type: "text/plain",
|
||||||
|
text: "Lorem ipsum dolor sit amet"
|
||||||
|
)
|
||||||
|
|
||||||
|
server = ModelContextProtocol::Server.new(
|
||||||
|
name: "my_server",
|
||||||
|
resources: [resource],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The server must register a handler for the `resources/read` method to retrieve a resource dynamically.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
server.resources_read_handler do |params|
|
||||||
|
[{
|
||||||
|
uri: params[:uri],
|
||||||
|
mimeType: "text/plain",
|
||||||
|
text: "Hello, world!",
|
||||||
|
}]
|
||||||
|
end
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
otherwise 'resources/read' requests will be a no-op.
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
|
||||||
|
|
@ -6,30 +6,89 @@ require "model_context_protocol"
|
||||||
require "model_context_protocol/transports/stdio"
|
require "model_context_protocol/transports/stdio"
|
||||||
|
|
||||||
# Create a simple tool
|
# Create a simple tool
|
||||||
class ExampleTool < ModelContextProtocol::Tool
|
class ExampleTool < MCP::Tool
|
||||||
description "A simple example tool that echoes back its arguments"
|
description "A simple example tool that adds two numbers"
|
||||||
input_schema type: "object",
|
input_schema(
|
||||||
properties: {
|
properties: {
|
||||||
message: { type: "string" },
|
a: { type: "number" },
|
||||||
|
b: { type: "number" },
|
||||||
},
|
},
|
||||||
required: ["message"]
|
required: ["a", "b"],
|
||||||
|
)
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def call(message:, server_context:)
|
def call(a:, b:)
|
||||||
ModelContextProtocol::Tool::Response.new([{
|
MCP::Tool::Response.new([{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Hello from example tool! Message: #{message}",
|
text: "The sum of #{a} and #{b} is #{a + b}",
|
||||||
}])
|
}])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Create a simple prompt
|
||||||
|
class ExamplePrompt < MCP::Prompt
|
||||||
|
description "A simple example prompt that echoes back its arguments"
|
||||||
|
arguments [
|
||||||
|
MCP::Prompt::Argument.new(
|
||||||
|
name: "message",
|
||||||
|
description: "The message to echo back",
|
||||||
|
required: true,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def template(args, server_context:)
|
||||||
|
MCP::Prompt::Result.new(
|
||||||
|
messages: [
|
||||||
|
MCP::Prompt::Message.new(
|
||||||
|
role: "user",
|
||||||
|
content: MCP::Content::Text.new(args[:message]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Set up the server
|
# Set up the server
|
||||||
server = ModelContextProtocol::Server.new(
|
server = MCP::Server.new(
|
||||||
name: "example_server",
|
name: "example_server",
|
||||||
tools: [ExampleTool],
|
tools: [ExampleTool],
|
||||||
|
prompts: [ExamplePrompt],
|
||||||
|
resources: [
|
||||||
|
MCP::Resource.new(
|
||||||
|
uri: "test_resource",
|
||||||
|
name: "Test resource",
|
||||||
|
description: "Test resource that echoes back the uri as its content",
|
||||||
|
mime_type: "text/plain",
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
server.define_tool(
|
||||||
|
name: "echo",
|
||||||
|
description: "A simple example tool that echoes back its arguments",
|
||||||
|
input_schema: { properties: { message: { type: "string" } }, required: ["message"] },
|
||||||
|
) do |message:|
|
||||||
|
MCP::Tool::Response.new(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Hello from echo tool! Message: #{message}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
server.resources_read_handler do |params|
|
||||||
|
[{
|
||||||
|
uri: params[:uri],
|
||||||
|
mimeType: "text/plain",
|
||||||
|
text: "Hello, world!",
|
||||||
|
}]
|
||||||
|
end
|
||||||
|
|
||||||
# Create and start the transport
|
# Create and start the transport
|
||||||
transport = ModelContextProtocol::Transports::StdioTransport.new(server)
|
transport = MCP::Transports::StdioTransport.new(server)
|
||||||
transport.open
|
transport.open
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,18 @@
|
||||||
require "model_context_protocol/server"
|
require "model_context_protocol/server"
|
||||||
require "model_context_protocol/string_utils"
|
require "model_context_protocol/string_utils"
|
||||||
require "model_context_protocol/tool"
|
require "model_context_protocol/tool"
|
||||||
|
require "model_context_protocol/tool/input_schema"
|
||||||
|
require "model_context_protocol/tool/annotations"
|
||||||
|
require "model_context_protocol/tool/response"
|
||||||
require "model_context_protocol/content"
|
require "model_context_protocol/content"
|
||||||
require "model_context_protocol/resource"
|
require "model_context_protocol/resource"
|
||||||
|
require "model_context_protocol/resource/contents"
|
||||||
|
require "model_context_protocol/resource/embedded"
|
||||||
|
require "model_context_protocol/resource_template"
|
||||||
require "model_context_protocol/prompt"
|
require "model_context_protocol/prompt"
|
||||||
|
require "model_context_protocol/prompt/argument"
|
||||||
|
require "model_context_protocol/prompt/message"
|
||||||
|
require "model_context_protocol/prompt/result"
|
||||||
require "model_context_protocol/version"
|
require "model_context_protocol/version"
|
||||||
require "model_context_protocol/configuration"
|
require "model_context_protocol/configuration"
|
||||||
require "model_context_protocol/methods"
|
require "model_context_protocol/methods"
|
||||||
|
|
@ -31,3 +40,5 @@ module ModelContextProtocol
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
MCP = ModelContextProtocol
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
module ModelContextProtocol
|
module ModelContextProtocol
|
||||||
class Configuration
|
class Configuration
|
||||||
DEFAULT_PROTOCOL_VERSION = "2025-03-26"
|
DEFAULT_PROTOCOL_VERSION = "2024-11-05"
|
||||||
|
|
||||||
attr_writer :exception_reporter, :instrumentation_callback, :protocol_version
|
attr_writer :exception_reporter, :instrumentation_callback, :protocol_version
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,85 @@
|
||||||
|
|
||||||
module ModelContextProtocol
|
module ModelContextProtocol
|
||||||
module Methods
|
module Methods
|
||||||
RESOURCES_LIST = "resources/list"
|
|
||||||
RESOURCES_READ = "resources/read"
|
|
||||||
TOOLS_LIST = "tools/list"
|
|
||||||
TOOLS_CALL = "tools/call"
|
|
||||||
PROMPTS_LIST = "prompts/list"
|
|
||||||
PROMPTS_GET = "prompts/get"
|
|
||||||
INITIALIZE = "initialize"
|
INITIALIZE = "initialize"
|
||||||
PING = "ping"
|
PING = "ping"
|
||||||
|
LOGGING_SET_LEVEL = "logging/setLevel"
|
||||||
|
|
||||||
|
PROMPTS_GET = "prompts/get"
|
||||||
|
PROMPTS_LIST = "prompts/list"
|
||||||
|
COMPLETION_COMPLETE = "completion/complete"
|
||||||
|
|
||||||
|
RESOURCES_LIST = "resources/list"
|
||||||
|
RESOURCES_READ = "resources/read"
|
||||||
|
RESOURCES_TEMPLATES_LIST = "resources/templates/list"
|
||||||
|
RESOURCES_SUBSCRIBE = "resources/subscribe"
|
||||||
|
RESOURCES_UNSUBSCRIBE = "resources/unsubscribe"
|
||||||
|
|
||||||
|
TOOLS_CALL = "tools/call"
|
||||||
|
TOOLS_LIST = "tools/list"
|
||||||
|
|
||||||
|
SAMPLING_CREATE_MESSAGE = "sampling/createMessage"
|
||||||
|
|
||||||
|
class MissingRequiredCapabilityError < StandardError
|
||||||
|
attr_reader :method
|
||||||
|
attr_reader :capability
|
||||||
|
|
||||||
|
def initialize(method, capability)
|
||||||
|
super("Server does not support #{capability} (required for #{method})")
|
||||||
|
@method = method
|
||||||
|
@capability = capability
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
extend self
|
||||||
|
|
||||||
|
ALL_METHODS = [
|
||||||
|
SAMPLING_CREATE_MESSAGE,
|
||||||
|
INITIALIZE,
|
||||||
|
LOGGING_SET_LEVEL,
|
||||||
|
PING,
|
||||||
|
PROMPTS_GET,
|
||||||
|
PROMPTS_LIST,
|
||||||
|
COMPLETION_COMPLETE,
|
||||||
|
RESOURCES_LIST,
|
||||||
|
RESOURCES_READ,
|
||||||
|
RESOURCES_TEMPLATES_LIST,
|
||||||
|
RESOURCES_SUBSCRIBE,
|
||||||
|
RESOURCES_UNSUBSCRIBE,
|
||||||
|
TOOLS_CALL,
|
||||||
|
TOOLS_LIST,
|
||||||
|
]
|
||||||
|
|
||||||
|
def ensure_capability!(method, capabilities)
|
||||||
|
case method
|
||||||
|
when PROMPTS_GET, PROMPTS_LIST, COMPLETION_COMPLETE
|
||||||
|
unless capabilities[:prompts]
|
||||||
|
raise MissingRequiredCapabilityError.new(method, :prompts)
|
||||||
|
end
|
||||||
|
when RESOURCES_LIST, RESOURCES_TEMPLATES_LIST, RESOURCES_READ, RESOURCES_SUBSCRIBE, RESOURCES_UNSUBSCRIBE
|
||||||
|
unless capabilities[:resources]
|
||||||
|
raise MissingRequiredCapabilityError.new(method, :resources)
|
||||||
|
end
|
||||||
|
|
||||||
|
if method == RESOURCES_SUBSCRIBE && !capabilities[:resources][:subscribe]
|
||||||
|
raise MissingRequiredCapabilityError.new(method, :resources_subscribe)
|
||||||
|
end
|
||||||
|
when TOOLS_CALL, TOOLS_LIST
|
||||||
|
unless capabilities[:tools]
|
||||||
|
raise MissingRequiredCapabilityError.new(method, :tools)
|
||||||
|
end
|
||||||
|
when SAMPLING_CREATE_MESSAGE
|
||||||
|
unless capabilities[:sampling]
|
||||||
|
raise MissingRequiredCapabilityError.new(method, :sampling)
|
||||||
|
end
|
||||||
|
when LOGGING_SET_LEVEL
|
||||||
|
# Logging is unsupported by the Server
|
||||||
|
unless capabilities[:logging]
|
||||||
|
raise MissingRequiredCapabilityError.new(method, :logging)
|
||||||
|
end
|
||||||
|
when INITIALIZE, PING
|
||||||
|
# No specific capability required for initialize or ping
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,47 +3,6 @@
|
||||||
|
|
||||||
module ModelContextProtocol
|
module ModelContextProtocol
|
||||||
class Prompt
|
class Prompt
|
||||||
class Argument
|
|
||||||
attr_reader :name, :description, :required, :arguments
|
|
||||||
|
|
||||||
def initialize(name:, description: nil, required: false)
|
|
||||||
@name = name
|
|
||||||
@description = description
|
|
||||||
@required = required
|
|
||||||
@arguments = arguments
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_h
|
|
||||||
{ name:, description:, required: }.compact
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Message
|
|
||||||
attr_reader :role, :content
|
|
||||||
|
|
||||||
def initialize(role:, content:)
|
|
||||||
@role = role
|
|
||||||
@content = content
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_h
|
|
||||||
{ role:, content: content.to_h }.compact
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Result
|
|
||||||
attr_reader :description, :messages
|
|
||||||
|
|
||||||
def initialize(description: nil, messages: [])
|
|
||||||
@description = description
|
|
||||||
@messages = messages
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_h
|
|
||||||
{ description:, messages: messages.map(&:to_h) }.compact
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
NOT_SET = Object.new
|
NOT_SET = Object.new
|
||||||
|
|
||||||
|
|
|
||||||
21
lib/model_context_protocol/prompt/argument.rb
Normal file
21
lib/model_context_protocol/prompt/argument.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# typed: strict
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModelContextProtocol
|
||||||
|
class Prompt
|
||||||
|
class Argument
|
||||||
|
attr_reader :name, :description, :required, :arguments
|
||||||
|
|
||||||
|
def initialize(name:, description: nil, required: false)
|
||||||
|
@name = name
|
||||||
|
@description = description
|
||||||
|
@required = required
|
||||||
|
@arguments = arguments
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_h
|
||||||
|
{ name:, description:, required: }.compact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
19
lib/model_context_protocol/prompt/message.rb
Normal file
19
lib/model_context_protocol/prompt/message.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# typed: strict
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModelContextProtocol
|
||||||
|
class Prompt
|
||||||
|
class Message
|
||||||
|
attr_reader :role, :content
|
||||||
|
|
||||||
|
def initialize(role:, content:)
|
||||||
|
@role = role
|
||||||
|
@content = content
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_h
|
||||||
|
{ role:, content: content.to_h }.compact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
19
lib/model_context_protocol/prompt/result.rb
Normal file
19
lib/model_context_protocol/prompt/result.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# typed: strict
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModelContextProtocol
|
||||||
|
class Prompt
|
||||||
|
class Result
|
||||||
|
attr_reader :description, :messages
|
||||||
|
|
||||||
|
def initialize(description: nil, messages: [])
|
||||||
|
@description = description
|
||||||
|
@messages = messages
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_h
|
||||||
|
{ description:, messages: messages.map(&:to_h) }.compact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -3,14 +3,13 @@
|
||||||
|
|
||||||
module ModelContextProtocol
|
module ModelContextProtocol
|
||||||
class Resource
|
class Resource
|
||||||
attr_reader :uri, :name, :description, :mime_type, :contents
|
attr_reader :uri, :name, :description, :mime_type
|
||||||
|
|
||||||
def initialize(uri:, name:, description:, mime_type:, contents:)
|
def initialize(uri:, name:, description:, mime_type:)
|
||||||
@uri = uri
|
@uri = uri
|
||||||
@name = name
|
@name = name
|
||||||
@description = description
|
@description = description
|
||||||
@mime_type = mime_type
|
@mime_type = mime_type
|
||||||
@contents = contents
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_h
|
def to_h
|
||||||
|
|
@ -19,7 +18,6 @@ module ModelContextProtocol
|
||||||
name: @name,
|
name: @name,
|
||||||
description: @description,
|
description: @description,
|
||||||
mimeType: @mime_type,
|
mimeType: @mime_type,
|
||||||
contents: @contents.map(&:to_h),
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
24
lib/model_context_protocol/resource_template.rb
Normal file
24
lib/model_context_protocol/resource_template.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# typed: strict
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModelContextProtocol
|
||||||
|
class ResourceTemplate
|
||||||
|
attr_reader :uri_template, :name, :description, :mime_type
|
||||||
|
|
||||||
|
def initialize(uri_template:, name:, description: nil, mime_type: nil)
|
||||||
|
@uri_template = uri_template
|
||||||
|
@name = name
|
||||||
|
@description = description
|
||||||
|
@mime_type = mime_type
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_h
|
||||||
|
{
|
||||||
|
uriTemplate: @uri_template,
|
||||||
|
name: @name,
|
||||||
|
description: @description,
|
||||||
|
mimeType: @mime_type,
|
||||||
|
}.compact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -20,26 +20,43 @@ module ModelContextProtocol
|
||||||
|
|
||||||
include Instrumentation
|
include Instrumentation
|
||||||
|
|
||||||
attr_accessor :name, :tools, :prompts, :resources, :server_context, :configuration
|
attr_accessor :name, :tools, :prompts, :resources, :server_context, :configuration, :capabilities
|
||||||
|
|
||||||
def initialize(name: "model_context_protocol", tools: [], prompts: [], resources: [], server_context: nil,
|
def initialize(
|
||||||
configuration: nil)
|
name: "model_context_protocol",
|
||||||
|
tools: [],
|
||||||
|
prompts: [],
|
||||||
|
resources: [],
|
||||||
|
resource_templates: [],
|
||||||
|
server_context: nil,
|
||||||
|
configuration: nil,
|
||||||
|
capabilities: { prompts: {}, resources: {}, tools: {} }
|
||||||
|
)
|
||||||
@name = name
|
@name = name
|
||||||
@tools = tools.to_h { |t| [t.name_value, t] }
|
@tools = tools.to_h { |t| [t.name_value, t] }
|
||||||
@prompts = prompts.to_h { |p| [p.name_value, p] }
|
@prompts = prompts.to_h { |p| [p.name_value, p] }
|
||||||
@resources = resources
|
@resources = resources
|
||||||
|
@resource_templates = resource_templates
|
||||||
@resource_index = index_resources_by_uri(resources)
|
@resource_index = index_resources_by_uri(resources)
|
||||||
@server_context = server_context
|
@server_context = server_context
|
||||||
@configuration = ModelContextProtocol.configuration.merge(configuration)
|
@configuration = ModelContextProtocol.configuration.merge(configuration)
|
||||||
|
@capabilities = capabilities
|
||||||
|
|
||||||
@handlers = {
|
@handlers = {
|
||||||
Methods::RESOURCES_LIST => method(:list_resources),
|
Methods::RESOURCES_LIST => method(:list_resources),
|
||||||
Methods::RESOURCES_READ => method(:read_resource),
|
Methods::RESOURCES_READ => method(:read_resource_no_content),
|
||||||
|
Methods::RESOURCES_TEMPLATES_LIST => method(:list_resource_templates),
|
||||||
Methods::TOOLS_LIST => method(:list_tools),
|
Methods::TOOLS_LIST => method(:list_tools),
|
||||||
Methods::TOOLS_CALL => method(:call_tool),
|
Methods::TOOLS_CALL => method(:call_tool),
|
||||||
Methods::PROMPTS_LIST => method(:list_prompts),
|
Methods::PROMPTS_LIST => method(:list_prompts),
|
||||||
Methods::PROMPTS_GET => method(:get_prompt),
|
Methods::PROMPTS_GET => method(:get_prompt),
|
||||||
Methods::INITIALIZE => method(:init),
|
Methods::INITIALIZE => method(:init),
|
||||||
Methods::PING => ->(_) { {} },
|
Methods::PING => ->(_) { {} },
|
||||||
|
|
||||||
|
# No op handlers for currently unsupported methods
|
||||||
|
Methods::RESOURCES_SUBSCRIBE => ->(_) {},
|
||||||
|
Methods::RESOURCES_UNSUBSCRIBE => ->(_) {},
|
||||||
|
Methods::LOGGING_SET_LEVEL => ->(_) {},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -55,6 +72,16 @@ module ModelContextProtocol
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def define_tool(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
|
||||||
|
tool = Tool.define(name:, description:, input_schema:, annotations:, &block)
|
||||||
|
@tools[tool.name_value] = tool
|
||||||
|
end
|
||||||
|
|
||||||
|
def define_prompt(name: nil, description: nil, arguments: [], &block)
|
||||||
|
prompt = Prompt.define(name:, description:, arguments:, &block)
|
||||||
|
@prompts[prompt.name_value] = prompt
|
||||||
|
end
|
||||||
|
|
||||||
def resources_list_handler(&block)
|
def resources_list_handler(&block)
|
||||||
@handlers[Methods::RESOURCES_LIST] = block
|
@handlers[Methods::RESOURCES_LIST] = block
|
||||||
end
|
end
|
||||||
|
|
@ -63,6 +90,10 @@ module ModelContextProtocol
|
||||||
@handlers[Methods::RESOURCES_READ] = block
|
@handlers[Methods::RESOURCES_READ] = block
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def resources_templates_list_handler(&block)
|
||||||
|
@handlers[Methods::RESOURCES_TEMPLATES_LIST] = block
|
||||||
|
end
|
||||||
|
|
||||||
def tools_list_handler(&block)
|
def tools_list_handler(&block)
|
||||||
@handlers[Methods::TOOLS_LIST] = block
|
@handlers[Methods::TOOLS_LIST] = block
|
||||||
end
|
end
|
||||||
|
|
@ -88,6 +119,8 @@ module ModelContextProtocol
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Methods.ensure_capability!(method, capabilities)
|
||||||
|
|
||||||
->(params) {
|
->(params) {
|
||||||
instrument_call(method) do
|
instrument_call(method) do
|
||||||
case method
|
case method
|
||||||
|
|
@ -97,6 +130,10 @@ module ModelContextProtocol
|
||||||
{ prompts: @handlers[Methods::PROMPTS_LIST].call(params) }
|
{ prompts: @handlers[Methods::PROMPTS_LIST].call(params) }
|
||||||
when Methods::RESOURCES_LIST
|
when Methods::RESOURCES_LIST
|
||||||
{ resources: @handlers[Methods::RESOURCES_LIST].call(params) }
|
{ resources: @handlers[Methods::RESOURCES_LIST].call(params) }
|
||||||
|
when Methods::RESOURCES_READ
|
||||||
|
{ contents: @handlers[Methods::RESOURCES_READ].call(params) }
|
||||||
|
when Methods::RESOURCES_TEMPLATES_LIST
|
||||||
|
{ resourceTemplates: @handlers[Methods::RESOURCES_TEMPLATES_LIST].call(params) }
|
||||||
else
|
else
|
||||||
@handlers[method].call(params)
|
@handlers[method].call(params)
|
||||||
end
|
end
|
||||||
|
|
@ -113,14 +150,6 @@ module ModelContextProtocol
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def capabilities
|
|
||||||
@capabilities ||= {
|
|
||||||
prompts: {},
|
|
||||||
resources: {},
|
|
||||||
tools: {},
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def server_info
|
def server_info
|
||||||
@server_info ||= {
|
@server_info ||= {
|
||||||
name:,
|
name:,
|
||||||
|
|
@ -151,10 +180,25 @@ module ModelContextProtocol
|
||||||
raise RequestHandlerError.new("Tool not found #{tool_name}", request, error_type: :tool_not_found)
|
raise RequestHandlerError.new("Tool not found #{tool_name}", request, error_type: :tool_not_found)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
arguments = request[:arguments]
|
||||||
add_instrumentation_data(tool_name:)
|
add_instrumentation_data(tool_name:)
|
||||||
|
|
||||||
|
if tool.input_schema&.missing_required_arguments?(arguments)
|
||||||
|
add_instrumentation_data(error: :missing_required_arguments)
|
||||||
|
raise RequestHandlerError.new(
|
||||||
|
"Missing required arguments: #{tool.input_schema.missing_required_arguments(arguments).join(", ")}",
|
||||||
|
request,
|
||||||
|
error_type: :missing_required_arguments,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
tool.call(**request[:arguments], server_context:).to_h
|
call_params = tool.method(:call).parameters.flatten
|
||||||
|
if call_params.include?(:server_context)
|
||||||
|
tool.call(**arguments.transform_keys(&:to_sym), server_context:).to_h
|
||||||
|
else
|
||||||
|
tool.call(**arguments.transform_keys(&:to_sym)).to_h
|
||||||
|
end
|
||||||
rescue => e
|
rescue => e
|
||||||
raise RequestHandlerError.new("Internal error calling tool #{tool_name}", request, original_error: e)
|
raise RequestHandlerError.new("Internal error calling tool #{tool_name}", request, original_error: e)
|
||||||
end
|
end
|
||||||
|
|
@ -188,18 +232,17 @@ module ModelContextProtocol
|
||||||
@resources.map(&:to_h)
|
@resources.map(&:to_h)
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_resource(request)
|
# Server implementation should set read_resource_handler to override no-op default
|
||||||
|
def read_resource_no_content(request)
|
||||||
add_instrumentation_data(method: Methods::RESOURCES_READ)
|
add_instrumentation_data(method: Methods::RESOURCES_READ)
|
||||||
resource_uri = request[:uri]
|
add_instrumentation_data(resource_uri: request[:uri])
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
resource = @resource_index[resource_uri]
|
def list_resource_templates(request)
|
||||||
unless resource
|
add_instrumentation_data(method: Methods::RESOURCES_TEMPLATES_LIST)
|
||||||
add_instrumentation_data(error: :resource_not_found)
|
|
||||||
raise RequestHandlerError.new("Resource not found #{resource_uri}", request, error_type: :resource_not_found)
|
|
||||||
end
|
|
||||||
|
|
||||||
add_instrumentation_data(resource_uri:)
|
@resource_templates.map(&:to_h)
|
||||||
resource.to_h
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def report_exception(exception, server_context = {})
|
def report_exception(exception, server_context = {})
|
||||||
|
|
|
||||||
|
|
@ -2,41 +2,6 @@
|
||||||
|
|
||||||
module ModelContextProtocol
|
module ModelContextProtocol
|
||||||
class Tool
|
class Tool
|
||||||
class Response
|
|
||||||
attr_reader :content, :is_error
|
|
||||||
|
|
||||||
def initialize(content, is_error = false)
|
|
||||||
@content = content
|
|
||||||
@is_error = is_error
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_h
|
|
||||||
{ content:, isError: is_error }.compact
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Annotations
|
|
||||||
attr_reader :title, :read_only_hint, :destructive_hint, :idempotent_hint, :open_world_hint
|
|
||||||
|
|
||||||
def initialize(title: nil, read_only_hint: nil, destructive_hint: nil, idempotent_hint: nil, open_world_hint: nil)
|
|
||||||
@title = title
|
|
||||||
@read_only_hint = read_only_hint
|
|
||||||
@destructive_hint = destructive_hint
|
|
||||||
@idempotent_hint = idempotent_hint
|
|
||||||
@open_world_hint = open_world_hint
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_h
|
|
||||||
{
|
|
||||||
title:,
|
|
||||||
readOnlyHint: read_only_hint,
|
|
||||||
destructiveHint: destructive_hint,
|
|
||||||
idempotentHint: idempotent_hint,
|
|
||||||
openWorldHint: open_world_hint,
|
|
||||||
}.compact
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
NOT_SET = Object.new
|
NOT_SET = Object.new
|
||||||
|
|
||||||
|
|
@ -52,7 +17,7 @@ module ModelContextProtocol
|
||||||
result = {
|
result = {
|
||||||
name: name_value,
|
name: name_value,
|
||||||
description: description_value,
|
description: description_value,
|
||||||
inputSchema: input_schema_value,
|
inputSchema: input_schema_value.to_h,
|
||||||
}
|
}
|
||||||
result[:annotations] = annotations_value.to_h if annotations_value
|
result[:annotations] = annotations_value.to_h if annotations_value
|
||||||
result
|
result
|
||||||
|
|
@ -88,8 +53,12 @@ module ModelContextProtocol
|
||||||
|
|
||||||
def input_schema(value = NOT_SET)
|
def input_schema(value = NOT_SET)
|
||||||
if value == NOT_SET
|
if value == NOT_SET
|
||||||
@input_schema_value
|
input_schema_value
|
||||||
else
|
elsif value.is_a?(Hash)
|
||||||
|
properties = value[:properties] || value["properties"] || {}
|
||||||
|
required = value[:required] || value["required"] || []
|
||||||
|
@input_schema_value = InputSchema.new(properties:, required:)
|
||||||
|
elsif value.is_a?(InputSchema)
|
||||||
@input_schema_value = value
|
@input_schema_value = value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -108,9 +77,7 @@ module ModelContextProtocol
|
||||||
description description
|
description description
|
||||||
input_schema input_schema
|
input_schema input_schema
|
||||||
self.annotations(annotations) if annotations
|
self.annotations(annotations) if annotations
|
||||||
define_singleton_method(:call) do |*args, server_context:|
|
define_singleton_method(:call, &block) if block
|
||||||
instance_exec(*args, server_context:, &block)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
27
lib/model_context_protocol/tool/annotations.rb
Normal file
27
lib/model_context_protocol/tool/annotations.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModelContextProtocol
|
||||||
|
class Tool
|
||||||
|
class Annotations
|
||||||
|
attr_reader :title, :read_only_hint, :destructive_hint, :idempotent_hint, :open_world_hint
|
||||||
|
|
||||||
|
def initialize(title: nil, read_only_hint: nil, destructive_hint: nil, idempotent_hint: nil, open_world_hint: nil)
|
||||||
|
@title = title
|
||||||
|
@read_only_hint = read_only_hint
|
||||||
|
@destructive_hint = destructive_hint
|
||||||
|
@idempotent_hint = idempotent_hint
|
||||||
|
@open_world_hint = open_world_hint
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_h
|
||||||
|
{
|
||||||
|
title:,
|
||||||
|
readOnlyHint: read_only_hint,
|
||||||
|
destructiveHint: destructive_hint,
|
||||||
|
idempotentHint: idempotent_hint,
|
||||||
|
openWorldHint: open_world_hint,
|
||||||
|
}.compact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
26
lib/model_context_protocol/tool/input_schema.rb
Normal file
26
lib/model_context_protocol/tool/input_schema.rb
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModelContextProtocol
|
||||||
|
class Tool
|
||||||
|
class InputSchema
|
||||||
|
attr_reader :properties, :required
|
||||||
|
|
||||||
|
def initialize(properties: {}, required: [])
|
||||||
|
@properties = properties
|
||||||
|
@required = required.map(&:to_sym)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_h
|
||||||
|
{ type: "object", properties:, required: }
|
||||||
|
end
|
||||||
|
|
||||||
|
def missing_required_arguments?(arguments)
|
||||||
|
missing_required_arguments(arguments).any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def missing_required_arguments(arguments)
|
||||||
|
(required - arguments.keys.map(&:to_sym))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
18
lib/model_context_protocol/tool/response.rb
Normal file
18
lib/model_context_protocol/tool/response.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModelContextProtocol
|
||||||
|
class Tool
|
||||||
|
class Response
|
||||||
|
attr_reader :content, :is_error
|
||||||
|
|
||||||
|
def initialize(content, is_error = false)
|
||||||
|
@content = content
|
||||||
|
@is_error = is_error
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_h
|
||||||
|
{ content:, isError: is_error }.compact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ModelContextProtocol
|
module ModelContextProtocol
|
||||||
VERSION = "0.6.0"
|
VERSION = "0.7.0"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
68
test/model_context_protocol/methods_test.rb
Normal file
68
test/model_context_protocol/methods_test.rb
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# typed: strict
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
module ModelContextProtocol
|
||||||
|
class MethodsTest < ActiveSupport::TestCase
|
||||||
|
test "ensure_capability! for tools/list method raises an error if tools capability is not present" do
|
||||||
|
error = assert_raises(Methods::MissingRequiredCapabilityError) do
|
||||||
|
Methods.ensure_capability!(Methods::TOOLS_LIST, {})
|
||||||
|
end
|
||||||
|
assert_equal "Server does not support tools (required for tools/list)", error.message
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ensure_capability! for sampling/createMessage raises an error if sampling capability is not present" do
|
||||||
|
error = assert_raises(Methods::MissingRequiredCapabilityError) do
|
||||||
|
Methods.ensure_capability!(Methods::SAMPLING_CREATE_MESSAGE, {})
|
||||||
|
end
|
||||||
|
assert_equal "Server does not support sampling (required for sampling/createMessage)", error.message
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ensure_capability! for logging/setLevel raises an error if logging capability is not present" do
|
||||||
|
error = assert_raises(Methods::MissingRequiredCapabilityError) do
|
||||||
|
Methods.ensure_capability!(Methods::LOGGING_SET_LEVEL, {})
|
||||||
|
end
|
||||||
|
assert_equal "Server does not support logging (required for logging/setLevel)", error.message
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ensure_capability! for prompts/get and prompts/list raise an error if prompts capability is not present" do
|
||||||
|
[Methods::PROMPTS_GET, Methods::PROMPTS_LIST].each do |method|
|
||||||
|
error = assert_raises(Methods::MissingRequiredCapabilityError) do
|
||||||
|
Methods.ensure_capability!(method, {})
|
||||||
|
end
|
||||||
|
assert_equal "Server does not support prompts (required for #{method})", error.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ensure_capability! for resources/list, resources/templates/list, resources/read raise an error if resources capability is not present" do
|
||||||
|
[Methods::RESOURCES_LIST, Methods::RESOURCES_TEMPLATES_LIST, Methods::RESOURCES_READ].each do |method|
|
||||||
|
error = assert_raises(Methods::MissingRequiredCapabilityError) do
|
||||||
|
Methods.ensure_capability!(method, {})
|
||||||
|
end
|
||||||
|
assert_equal "Server does not support resources (required for #{method})", error.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ensure_capability! for tools/call and tools/list raise an error if tools capability is not present" do
|
||||||
|
[Methods::TOOLS_CALL, Methods::TOOLS_LIST].each do |method|
|
||||||
|
error = assert_raises(Methods::MissingRequiredCapabilityError) do
|
||||||
|
Methods.ensure_capability!(method, {})
|
||||||
|
end
|
||||||
|
assert_equal "Server does not support tools (required for #{method})", error.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ensure_capability! for resources/subscribe raises an error if resources subscribe capability is not present" do
|
||||||
|
error = assert_raises(Methods::MissingRequiredCapabilityError) do
|
||||||
|
Methods.ensure_capability!(Methods::RESOURCES_SUBSCRIBE, { resources: {} })
|
||||||
|
end
|
||||||
|
assert_equal "Server does not support resources_subscribe (required for resources/subscribe)", error.message
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ensure_capability! does not raise for ping and initialize methods" do
|
||||||
|
assert_nothing_raised { Methods.ensure_capability!(Methods::PING, {}) }
|
||||||
|
assert_nothing_raised { Methods.ensure_capability!(Methods::INITIALIZE, {}) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue