mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,500 @@
1
+ # Model Context Protocol
2
+
3
+ A Ruby gem for implementing Model Context Protocol servers
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'mcp'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ $ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```bash
22
+ $ gem install mcp
23
+ ```
24
+
25
+ ## MCP Server
26
+
27
+ The `MCP::Server` class is the core component that handles JSON-RPC requests and responses.
28
+ It implements the Model Context Protocol specification, handling model context requests and responses.
29
+
30
+ ### Key Features
31
+ - Implements JSON-RPC 2.0 message handling
32
+ - Supports protocol initialization and capability negotiation
33
+ - Manages tool registration and invocation
34
+ - Supports prompt registration and execution
35
+ - Supports resource registration and retrieval
36
+
37
+ ### Supported Methods
38
+ - `initialize` - Initializes the protocol and returns server capabilities
39
+ - `ping` - Simple health check
40
+ - `tools/list` - Lists all registered tools and their schemas
41
+ - `tools/call` - Invokes a specific tool with provided arguments
42
+ - `prompts/list` - Lists all registered prompts and their schemas
43
+ - `prompts/get` - Retrieves a specific prompt by name
44
+ - `resources/list` - Lists all registered resources and their schemas
45
+ - `resources/read` - Retrieves a specific resource by name
46
+ - `resources/templates/list` - Lists all registered resource templates and their schemas
47
+
48
+ ### Unsupported Features ( to be implemented in future versions )
49
+
50
+ - Notifications
51
+ - Log Level
52
+ - Resource subscriptions
53
+ - Completions
54
+ - Complete StreamableHTTP implementation with streaming responses
55
+
56
+ ### Usage
57
+
58
+ #### Rails Controller
59
+
60
+ When added to a Rails controller on a route that handles POST requests, your server will be compliant with non-streaming
61
+ [StreamableHTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport
62
+ requests.
63
+
64
+ You can use the `Server#handle_json` method to handle requests.
65
+
66
+ ```ruby
67
+ class ApplicationController < ActionController::Base
68
+
69
+ def index
70
+ server = MCP::Server.new(
71
+ name: "my_server",
72
+ version: "1.0.0",
73
+ tools: [SomeTool, AnotherTool],
74
+ prompts: [MyPrompt],
75
+ server_context: { user_id: current_user.id },
76
+ )
77
+ render(json: server.handle_json(request.body.read))
78
+ end
79
+ end
80
+ ```
81
+
82
+ #### Stdio Transport
83
+
84
+ If you want to build a local command-line application, you can use the stdio transport:
85
+
86
+ ```ruby
87
+ #!/usr/bin/env ruby
88
+ require "mcp"
89
+ require "mcp/transports/stdio"
90
+
91
+ # Create a simple tool
92
+ class ExampleTool < MCP::Tool
93
+ description "A simple example tool that echoes back its arguments"
94
+ input_schema(
95
+ properties: {
96
+ message: { type: "string" },
97
+ },
98
+ required: ["message"]
99
+ )
100
+
101
+ class << self
102
+ def call(message:, server_context:)
103
+ MCP::Tool::Response.new([{
104
+ type: "text",
105
+ text: "Hello from example tool! Message: #{message}",
106
+ }])
107
+ end
108
+ end
109
+ end
110
+
111
+ # Set up the server
112
+ server = MCP::Server.new(
113
+ name: "example_server",
114
+ tools: [ExampleTool],
115
+ )
116
+
117
+ # Create and start the transport
118
+ transport = MCP::Transports::StdioTransport.new(server)
119
+ transport.open
120
+ ```
121
+
122
+ You can run this script and then type in requests to the server at the command line.
123
+
124
+ ```
125
+ $ ./stdio_server.rb
126
+ {"jsonrpc":"2.0","id":"1","result":"pong"}
127
+ {"jsonrpc":"2.0","id":"2","result":["ExampleTool"]}
128
+ {"jsonrpc":"2.0","id":"3","result":["ExampleTool"]}
129
+ ```
130
+
131
+ ## Configuration
132
+
133
+ The gem can be configured using the `MCP.configure` block:
134
+
135
+ ```ruby
136
+ MCP.configure do |config|
137
+ config.exception_reporter = ->(exception, server_context) {
138
+ # Your exception reporting logic here
139
+ # For example with Bugsnag:
140
+ Bugsnag.notify(exception) do |report|
141
+ report.add_metadata(:model_context_protocol, server_context)
142
+ end
143
+ }
144
+
145
+ config.instrumentation_callback = ->(data) {
146
+ puts "Got instrumentation data #{data.inspect}"
147
+ }
148
+ end
149
+ ```
150
+
151
+ or by creating an explicit configuration and passing it into the server.
152
+ This is useful for systems where an application hosts more than one MCP server but
153
+ they might require different instrumentation callbacks.
154
+
155
+ ```ruby
156
+ configuration = MCP::Configuration.new
157
+ configuration.exception_reporter = ->(exception, server_context) {
158
+ # Your exception reporting logic here
159
+ # For example with Bugsnag:
160
+ Bugsnag.notify(exception) do |report|
161
+ report.add_metadata(:model_context_protocol, server_context)
162
+ end
163
+ }
164
+
165
+ configuration.instrumentation_callback = ->(data) {
166
+ puts "Got instrumentation data #{data.inspect}"
167
+ }
168
+
169
+ server = MCP::Server.new(
170
+ # ... all other options
171
+ configuration:,
172
+ )
173
+ ```
174
+
175
+ ### Server Context and Configuration Block Data
176
+
177
+ #### `server_context`
178
+
179
+ 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.
180
+
181
+ **Type:**
182
+ ```ruby
183
+ server_context: { [String, Symbol] => Any }
184
+ ```
185
+
186
+ **Example:**
187
+ ```ruby
188
+ server = MCP::Server.new(
189
+ name: "my_server",
190
+ server_context: { user_id: current_user.id, request_id: request.uuid }
191
+ )
192
+ ```
193
+
194
+ This hash is then passed as the `server_context` argument to tool and prompt calls, and is included in exception and instrumentation callbacks.
195
+
196
+ #### Configuration Block Data
197
+
198
+ ##### Exception Reporter
199
+
200
+ The exception reporter receives:
201
+
202
+ - `exception`: The Ruby exception object that was raised
203
+ - `server_context`: The context hash provided to the server
204
+
205
+ **Signature:**
206
+ ```ruby
207
+ exception_reporter = ->(exception, server_context) { ... }
208
+ ```
209
+
210
+ ##### Instrumentation Callback
211
+
212
+ The instrumentation callback receives a hash with the following possible keys:
213
+
214
+ - `method`: (String) The protocol method called (e.g., "ping", "tools/list")
215
+ - `tool_name`: (String, optional) The name of the tool called
216
+ - `prompt_name`: (String, optional) The name of the prompt called
217
+ - `resource_uri`: (String, optional) The URI of the resource called
218
+ - `error`: (String, optional) Error code if a lookup failed
219
+ - `duration`: (Float) Duration of the call in seconds
220
+
221
+ **Type:**
222
+ ```ruby
223
+ instrumentation_callback = ->(data) { ... }
224
+ # where data is a Hash with keys as described above
225
+ ```
226
+
227
+ **Example:**
228
+ ```ruby
229
+ config.instrumentation_callback = ->(data) {
230
+ puts "Instrumentation: #{data.inspect}"
231
+ }
232
+ ```
233
+
234
+ ### Server Protocol Version
235
+
236
+ The server's protocol version can be overridden using the `protocol_version` class method:
237
+
238
+ ```ruby
239
+ MCP::Server.protocol_version = "2024-11-05"
240
+ ```
241
+
242
+ This will make all new server instances use the specified protocol version instead of the default version. The protocol version can be reset to the default by setting it to `nil`:
243
+
244
+ ```ruby
245
+ MCP::Server.protocol_version = nil
246
+ ```
247
+
248
+ Be sure to check the [MCP spec](https://spec.modelcontextprotocol.io/specification/2024-11-05/) for the protocol version to understand the supported features for the version being set.
249
+
250
+ ### Exception Reporting
251
+
252
+ The exception reporter receives two arguments:
253
+
254
+ - `exception`: The Ruby exception object that was raised
255
+ - `server_context`: A hash containing contextual information about where the error occurred
256
+
257
+ The server_context hash includes:
258
+
259
+ - For tool calls: `{ tool_name: "name", arguments: { ... } }`
260
+ - For general request handling: `{ request: { ... } }`
261
+
262
+ When an exception occurs:
263
+
264
+ 1. The exception is reported via the configured reporter
265
+ 2. For tool calls, a generic error response is returned to the client: `{ error: "Internal error occurred", isError: true }`
266
+ 3. For other requests, the exception is re-raised after reporting
267
+
268
+ If no exception reporter is configured, a default no-op reporter is used that silently ignores exceptions.
269
+
270
+ ## Tools
271
+
272
+ MCP spec includes [Tools](https://modelcontextprotocol.io/docs/concepts/tools) which provide functionality to LLM apps.
273
+
274
+ This gem provides a `MCP::Tool` class that can be used to create tools in two ways:
275
+
276
+ 1. As a class definition:
277
+
278
+ ```ruby
279
+ class MyTool < MCP::Tool
280
+ description "This tool performs specific functionality..."
281
+ input_schema(
282
+ properties: {
283
+ message: { type: "string" },
284
+ },
285
+ required: ["message"]
286
+ )
287
+ annotations(
288
+ title: "My Tool",
289
+ read_only_hint: true,
290
+ destructive_hint: false,
291
+ idempotent_hint: true,
292
+ open_world_hint: false
293
+ )
294
+
295
+ def self.call(message:, server_context:)
296
+ MCP::Tool::Response.new([{ type: "text", text: "OK" }])
297
+ end
298
+ end
299
+
300
+ tool = MyTool
301
+ ```
302
+
303
+ 2. By using the `MCP::Tool.define` method with a block:
304
+
305
+ ```ruby
306
+ tool = MCP::Tool.define(
307
+ name: "my_tool",
308
+ description: "This tool performs specific functionality...",
309
+ annotations: {
310
+ title: "My Tool",
311
+ read_only_hint: true
312
+ }
313
+ ) do |args, server_context|
314
+ Tool::Response.new([{ type: "text", text: "OK" }])
315
+ end
316
+ ```
317
+
318
+ The server_context parameter is the server_context passed into the server and can be used to pass per request information,
319
+ e.g. around authentication state.
320
+
321
+ ### Tool Annotations
322
+
323
+ Tools can include annotations that provide additional metadata about their behavior. The following annotations are supported:
324
+
325
+ - `title`: A human-readable title for the tool
326
+ - `read_only_hint`: Indicates if the tool only reads data (doesn't modify state)
327
+ - `destructive_hint`: Indicates if the tool performs destructive operations
328
+ - `idempotent_hint`: Indicates if the tool's operations are idempotent
329
+ - `open_world_hint`: Indicates if the tool operates in an open world context
330
+
331
+ Annotations can be set either through the class definition using the `annotations` class method or when defining a tool using the `define` method.
332
+
333
+ ## Prompts
334
+
335
+ MCP spec includes [Prompts](https://modelcontextprotocol.io/docs/concepts/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs.
336
+
337
+ The `MCP::Prompt` class provides two ways to create prompts:
338
+
339
+ 1. As a class definition with metadata:
340
+
341
+ ```ruby
342
+ class MyPrompt < MCP::Prompt
343
+ prompt_name "my_prompt" # Optional - defaults to underscored class name
344
+ description "This prompt performs specific functionality..."
345
+ arguments [
346
+ Prompt::Argument.new(
347
+ name: "message",
348
+ description: "Input message",
349
+ required: true
350
+ )
351
+ ]
352
+
353
+ class << self
354
+ def template(args, server_context:)
355
+ Prompt::Result.new(
356
+ description: "Response description",
357
+ messages: [
358
+ Prompt::Message.new(
359
+ role: "user",
360
+ content: Content::Text.new("User message")
361
+ ),
362
+ Prompt::Message.new(
363
+ role: "assistant",
364
+ content: Content::Text.new(args["message"])
365
+ )
366
+ ]
367
+ )
368
+ end
369
+ end
370
+ end
371
+
372
+ prompt = MyPrompt
373
+ ```
374
+
375
+ 2. Using the `MCP::Prompt.define` method:
376
+
377
+ ```ruby
378
+ prompt = MCP::Prompt.define(
379
+ name: "my_prompt",
380
+ description: "This prompt performs specific functionality...",
381
+ arguments: [
382
+ Prompt::Argument.new(
383
+ name: "message",
384
+ description: "Input message",
385
+ required: true
386
+ )
387
+ ]
388
+ ) do |args, server_context:|
389
+ Prompt::Result.new(
390
+ description: "Response description",
391
+ messages: [
392
+ Prompt::Message.new(
393
+ role: "user",
394
+ content: Content::Text.new("User message")
395
+ ),
396
+ Prompt::Message.new(
397
+ role: "assistant",
398
+ content: Content::Text.new(args["message"])
399
+ )
400
+ ]
401
+ )
402
+ end
403
+ ```
404
+
405
+ The server_context parameter is the server_context passed into the server and can be used to pass per request information,
406
+ e.g. around authentication state or user preferences.
407
+
408
+ ### Key Components
409
+
410
+ - `Prompt::Argument` - Defines input parameters for the prompt template
411
+ - `Prompt::Message` - Represents a message in the conversation with a role and content
412
+ - `Prompt::Result` - The output of a prompt template containing description and messages
413
+ - `Content::Text` - Text content for messages
414
+
415
+ ### Usage
416
+
417
+ Register prompts with the MCP server:
418
+
419
+ ```ruby
420
+ server = MCP::Server.new(
421
+ name: "my_server",
422
+ prompts: [MyPrompt],
423
+ server_context: { user_id: current_user.id },
424
+ )
425
+ ```
426
+
427
+ The server will handle prompt listing and execution through the MCP protocol methods:
428
+
429
+ - `prompts/list` - Lists all registered prompts and their schemas
430
+ - `prompts/get` - Retrieves and executes a specific prompt with arguments
431
+
432
+ ### Instrumentation
433
+
434
+ The server allows registering a callback to receive information about instrumentation.
435
+ To register a handler pass a proc/lambda to as `instrumentation_callback` into the server constructor.
436
+
437
+ ```ruby
438
+ MCP.configure do |config|
439
+ config.instrumentation_callback = ->(data) {
440
+ puts "Got instrumentation data #{data.inspect}"
441
+ end
442
+ }
443
+ ```
444
+
445
+ The data contains the following keys:
446
+ `method`: the metod called, e.g. `ping`, `tools/list`, `tools/call` etc
447
+ `tool_name`: the name of the tool called
448
+ `prompt_name`: the name of the prompt called
449
+ `resource_uri`: the uri of the resource called
450
+ `error`: if looking up tools/prompts etc failed, e.g. `tool_not_found`
451
+ `duration`: the duration of the call in seconds
452
+
453
+ `tool_name`, `prompt_name` and `resource_uri` are only populated if a matching handler is registered.
454
+ This is to avoid potential issues with metric cardinality
455
+
456
+ ## Resources
457
+
458
+ MCP spec includes [Resources](https://modelcontextprotocol.io/docs/concepts/resources)
459
+
460
+ The `MCP::Resource` class provides a way to register resources with the server.
461
+
462
+ ```ruby
463
+ resource = MCP::Resource.new(
464
+ uri: "example.com/my_resource",
465
+ mime_type: "text/plain",
466
+ text: "Lorem ipsum dolor sit amet"
467
+ )
468
+
469
+ server = MCP::Server.new(
470
+ name: "my_server",
471
+ resources: [resource],
472
+ )
473
+ ```
474
+
475
+ The server must register a handler for the `resources/read` method to retrieve a resource dynamically.
476
+
477
+ ```ruby
478
+ server.resources_read_handler do |params|
479
+ [{
480
+ uri: params[:uri],
481
+ mimeType: "text/plain",
482
+ text: "Hello, world!",
483
+ }]
484
+ end
485
+
486
+ ```
487
+
488
+ otherwise 'resources/read' requests will be a no-op.
489
+
490
+ ## Releases
491
+
492
+ This gem is published to [RubyGems.org](https://rubygems.org/gems/mcp)
493
+
494
+ Releases are triggered by PRs to the `main` branch updating the version number in `lib/mcp/version.rb`.
495
+
496
+ 1. **Update the version number** in `lib/mcp/version.rb`, following [semver](https://semver.org/)
497
+ 2. **Create A PR and get approval from a maintainer**
498
+ 3. **Merge your PR to the main branch** - This will automatically trigger the release workflow via GitHub Actions
499
+
500
+ When changes are merged to the `main` branch, the GitHub Actions workflow (`.github/workflows/release.yml`) is triggered and the gem is published to RubyGems.
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.ruby_opts = ["-W0", "-W:deprecated"]
8
+ t.libs << "test"
9
+ t.libs << "lib"
10
+ t.test_files = FileList["test/**/*_test.rb"]
11
+ end
12
+
13
+ require "rubocop/rake_task"
14
+
15
+ RuboCop::RakeTask.new
16
+
17
+ task default: [:test, :rubocop]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "mcp"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/rake ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path(
13
+ "../../Gemfile",
14
+ Pathname.new(__FILE__).realpath,
15
+ )
16
+
17
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
18
+
19
+ if File.file?(bundle_binstub)
20
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
21
+ load(bundle_binstub)
22
+ else
23
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
24
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
25
+ end
26
+ end
27
+
28
+ require "rubygems"
29
+ require "bundler/setup"
30
+
31
+ load Gem.bin_path("rake", "rake")
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/dev.yml ADDED
@@ -0,0 +1,31 @@
1
+ name: mcp-ruby
2
+
3
+ type: ruby
4
+
5
+ up:
6
+ - ruby
7
+ - bundler
8
+
9
+ commands:
10
+ console:
11
+ desc: Open console with the gem loaded
12
+ run: bin/console
13
+ build:
14
+ desc: Build the gem using rake build
15
+ run: bin/rake build
16
+ test:
17
+ desc: Run tests
18
+ syntax:
19
+ argument: file
20
+ optional: args...
21
+ run: |
22
+ if [[ $# -eq 0 ]]; then
23
+ bin/rake test
24
+ else
25
+ bin/rake -I test "$@"
26
+ fi
27
+ style:
28
+ desc: Run rubocop
29
+ aliases: [rubocop, lint]
30
+ run: bin/rubocop
31
+
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+ require "mcp"
6
+ require "mcp/transports/stdio"
7
+
8
+ # Create a simple tool
9
+ class ExampleTool < MCP::Tool
10
+ description "A simple example tool that adds two numbers"
11
+ input_schema(
12
+ properties: {
13
+ a: { type: "number" },
14
+ b: { type: "number" },
15
+ },
16
+ required: ["a", "b"],
17
+ )
18
+
19
+ class << self
20
+ def call(a:, b:)
21
+ MCP::Tool::Response.new([{
22
+ type: "text",
23
+ text: "The sum of #{a} and #{b} is #{a + b}",
24
+ }])
25
+ end
26
+ end
27
+ end
28
+
29
+ # Create a simple prompt
30
+ class ExamplePrompt < MCP::Prompt
31
+ description "A simple example prompt that echoes back its arguments"
32
+ arguments [
33
+ MCP::Prompt::Argument.new(
34
+ name: "message",
35
+ description: "The message to echo back",
36
+ required: true,
37
+ ),
38
+ ]
39
+
40
+ class << self
41
+ def template(args, server_context:)
42
+ MCP::Prompt::Result.new(
43
+ messages: [
44
+ MCP::Prompt::Message.new(
45
+ role: "user",
46
+ content: MCP::Content::Text.new(args[:message]),
47
+ ),
48
+ ],
49
+ )
50
+ end
51
+ end
52
+ end
53
+
54
+ # Set up the server
55
+ server = MCP::Server.new(
56
+ name: "example_server",
57
+ version: "1.0.0",
58
+ tools: [ExampleTool],
59
+ prompts: [ExamplePrompt],
60
+ resources: [
61
+ MCP::Resource.new(
62
+ uri: "test_resource",
63
+ name: "Test resource",
64
+ description: "Test resource that echoes back the uri as its content",
65
+ mime_type: "text/plain",
66
+ ),
67
+ ],
68
+ )
69
+
70
+ server.define_tool(
71
+ name: "echo",
72
+ description: "A simple example tool that echoes back its arguments",
73
+ input_schema: { properties: { message: { type: "string" } }, required: ["message"] },
74
+ ) do |message:|
75
+ MCP::Tool::Response.new(
76
+ [
77
+ {
78
+ type: "text",
79
+ text: "Hello from echo tool! Message: #{message}",
80
+ },
81
+ ],
82
+ )
83
+ end
84
+
85
+ server.resources_read_handler do |params|
86
+ [{
87
+ uri: params[:uri],
88
+ mimeType: "text/plain",
89
+ text: "Hello, world!",
90
+ }]
91
+ end
92
+
93
+ # Create and start the transport
94
+ transport = MCP::Transports::StdioTransport.new(server)
95
+ transport.open