mcp 0.8.0 → 0.10.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +176 -5
  3. data/lib/mcp/client/stdio.rb +222 -0
  4. data/lib/mcp/client.rb +21 -3
  5. data/lib/mcp/progress.rb +22 -0
  6. data/lib/mcp/prompt.rb +4 -0
  7. data/lib/mcp/resource.rb +3 -0
  8. data/lib/mcp/server/transports/stdio_transport.rb +6 -4
  9. data/lib/mcp/server/transports/streamable_http_transport.rb +140 -31
  10. data/lib/mcp/server/transports.rb +10 -0
  11. data/lib/mcp/server.rb +71 -39
  12. data/lib/mcp/server_context.rb +44 -0
  13. data/lib/mcp/server_session.rb +79 -0
  14. data/lib/mcp/tool.rb +5 -0
  15. data/lib/mcp/transport.rb +2 -2
  16. data/lib/mcp/version.rb +1 -1
  17. data/lib/mcp.rb +11 -24
  18. metadata +8 -36
  19. data/.gitattributes +0 -4
  20. data/.github/dependabot.yml +0 -6
  21. data/.github/workflows/ci.yml +0 -54
  22. data/.github/workflows/conformance.yml +0 -29
  23. data/.github/workflows/release.yml +0 -57
  24. data/.gitignore +0 -11
  25. data/.rubocop.yml +0 -15
  26. data/AGENTS.md +0 -107
  27. data/CHANGELOG.md +0 -168
  28. data/CODE_OF_CONDUCT.md +0 -74
  29. data/Gemfile +0 -29
  30. data/RELEASE.md +0 -12
  31. data/Rakefile +0 -56
  32. data/SECURITY.md +0 -21
  33. data/bin/console +0 -15
  34. data/bin/generate-gh-pages.sh +0 -119
  35. data/bin/rake +0 -31
  36. data/bin/setup +0 -8
  37. data/conformance/README.md +0 -103
  38. data/conformance/expected_failures.yml +0 -9
  39. data/conformance/runner.rb +0 -101
  40. data/conformance/server.rb +0 -547
  41. data/dev.yml +0 -30
  42. data/docs/_config.yml +0 -6
  43. data/docs/index.md +0 -7
  44. data/docs/latest/index.html +0 -19
  45. data/examples/README.md +0 -197
  46. data/examples/http_client.rb +0 -184
  47. data/examples/http_server.rb +0 -169
  48. data/examples/stdio_server.rb +0 -94
  49. data/examples/streamable_http_client.rb +0 -207
  50. data/examples/streamable_http_server.rb +0 -172
  51. data/mcp.gemspec +0 -35
@@ -1,547 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rackup"
4
- require "json"
5
- require "uri"
6
- require_relative "../lib/mcp"
7
-
8
- module Conformance
9
- # 1x1 red PNG pixel (matches TypeScript SDK and Python SDK)
10
- BASE64_1X1_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="
11
-
12
- # Minimal WAV file (matches TypeScript SDK and Python SDK)
13
- BASE64_MINIMAL_WAV = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA="
14
-
15
- module Tools
16
- class TestSimpleText < MCP::Tool
17
- tool_name "test_simple_text"
18
- description "A tool that returns simple text content"
19
-
20
- class << self
21
- def call(**_args)
22
- MCP::Tool::Response.new([MCP::Content::Text.new("This is a simple text response for testing.").to_h])
23
- end
24
- end
25
- end
26
-
27
- class TestImageContent < MCP::Tool
28
- tool_name "test_image_content"
29
- description "A tool that returns image content"
30
-
31
- class << self
32
- def call(**_args)
33
- MCP::Tool::Response.new([MCP::Content::Image.new(BASE64_1X1_PNG, "image/png").to_h])
34
- end
35
- end
36
- end
37
-
38
- class TestAudioContent < MCP::Tool
39
- tool_name "test_audio_content"
40
- description "A tool that returns audio content"
41
-
42
- class << self
43
- def call(**_args)
44
- MCP::Tool::Response.new([MCP::Content::Audio.new(BASE64_MINIMAL_WAV, "audio/wav").to_h])
45
- end
46
- end
47
- end
48
-
49
- class TestEmbeddedResource < MCP::Tool
50
- tool_name "test_embedded_resource"
51
- description "A tool that returns embedded resource content"
52
-
53
- class << self
54
- def call(**_args)
55
- text_contents = MCP::Resource::TextContents.new(
56
- uri: "test://embedded-resource",
57
- mime_type: "text/plain",
58
- text: "This is an embedded resource content.",
59
- )
60
- MCP::Tool::Response.new([MCP::Content::EmbeddedResource.new(text_contents).to_h])
61
- end
62
- end
63
- end
64
-
65
- class TestMultipleContentTypes < MCP::Tool
66
- tool_name "test_multiple_content_types"
67
- description "A tool that returns multiple content types"
68
-
69
- class << self
70
- def call(**_args)
71
- MCP::Tool::Response.new([
72
- MCP::Content::Text.new("Multiple content types test:").to_h,
73
- MCP::Content::Image.new(BASE64_1X1_PNG, "image/png").to_h,
74
- MCP::Content::EmbeddedResource.new(
75
- MCP::Resource::TextContents.new(
76
- uri: "test://mixed-content-resource",
77
- mime_type: "application/json",
78
- text: '{"test":"data","value":123}',
79
- ),
80
- ).to_h,
81
- ])
82
- end
83
- end
84
- end
85
-
86
- class TestErrorHandling < MCP::Tool
87
- tool_name "test_error_handling"
88
- description "A tool that intentionally returns an error for testing"
89
-
90
- class << self
91
- def call(**_args)
92
- MCP::Tool::Response.new(
93
- [MCP::Content::Text.new("This tool intentionally returns an error for testing").to_h],
94
- error: true,
95
- )
96
- end
97
- end
98
- end
99
-
100
- class JsonSchema202012Tool < MCP::Tool
101
- tool_name "json_schema_2020_12_tool"
102
- description "Tool with JSON Schema 2020-12 features"
103
- input_schema(
104
- "$schema": "https://json-schema.org/draft/2020-12/schema",
105
- "$defs": {
106
- address: {
107
- type: "object",
108
- properties: {
109
- street: { type: "string" },
110
- city: { type: "string" },
111
- },
112
- },
113
- },
114
- properties: {
115
- name: { type: "string" },
116
- address: { "$ref": "#/$defs/address" },
117
- },
118
- additionalProperties: false,
119
- )
120
-
121
- class << self
122
- def call(**_args)
123
- MCP::Tool::Response.new([MCP::Content::Text.new("Processed with JSON Schema 2020-12").to_h])
124
- end
125
- end
126
- end
127
-
128
- class TestToolWithLogging < MCP::Tool
129
- tool_name "test_tool_with_logging"
130
- description "A tool that sends log messages during execution"
131
-
132
- class << self
133
- def call(server_context:, **_args)
134
- server_context.notify_log_message(data: "Tool execution started", level: "info", logger: "test_logger")
135
- sleep(0.05) # Required by the conformance test to verify clients handle interleaved notifications (same as TypeScript SDK).
136
- server_context.notify_log_message(data: "Tool processing data", level: "info", logger: "test_logger")
137
- sleep(0.05) # Same as above.
138
- server_context.notify_log_message(data: "Tool execution completed", level: "info", logger: "test_logger")
139
- MCP::Tool::Response.new([MCP::Content::Text.new("Logging complete (3 messages sent)").to_h])
140
- end
141
- end
142
- end
143
-
144
- # test_tool_with_progress: the actual progress dispatch is in `tools_call_handler`
145
- class TestToolWithProgress < MCP::Tool
146
- tool_name "test_tool_with_progress"
147
- description "A tool that reports progress notifications"
148
-
149
- class << self
150
- def call(**_args)
151
- MCP::Tool::Response.new([MCP::Content::Text.new("Progress complete").to_h])
152
- end
153
- end
154
- end
155
-
156
- # TODO: Implement when `Transport` supports server-to-client requests.
157
- class TestSampling < MCP::Tool
158
- tool_name "test_sampling"
159
- description "A tool that requests LLM sampling from the client"
160
- input_schema(
161
- properties: { prompt: { type: "string" } },
162
- required: ["prompt"],
163
- )
164
-
165
- class << self
166
- def call(prompt:)
167
- MCP::Tool::Response.new(
168
- [MCP::Content::Text.new("Sampling not supported in this SDK version").to_h],
169
- error: true,
170
- )
171
- end
172
- end
173
- end
174
-
175
- # TODO: Implement when `Transport` supports server-to-client requests.
176
- class TestElicitation < MCP::Tool
177
- tool_name "test_elicitation"
178
- description "A tool that requests user input from the client"
179
- input_schema(
180
- properties: { message: { type: "string" } },
181
- required: ["message"],
182
- )
183
-
184
- class << self
185
- def call(message:)
186
- MCP::Tool::Response.new(
187
- [MCP::Content::Text.new("Elicitation not supported in this SDK version").to_h],
188
- error: true,
189
- )
190
- end
191
- end
192
- end
193
-
194
- # TODO: Implement when `Transport` supports server-to-client requests.
195
- class TestElicitationSep1034Defaults < MCP::Tool
196
- tool_name "test_elicitation_sep1034_defaults"
197
- description "A tool that tests elicitation with default values"
198
-
199
- class << self
200
- def call(**_args)
201
- MCP::Tool::Response.new(
202
- [MCP::Content::Text.new("Elicitation not supported in this SDK version").to_h],
203
- error: true,
204
- )
205
- end
206
- end
207
- end
208
-
209
- # TODO: Implement when `Transport` supports server-to-client requests.
210
- class TestElicitationSep1330Enums < MCP::Tool
211
- tool_name "test_elicitation_sep1330_enums"
212
- description "A tool that tests elicitation with enum schemas"
213
-
214
- class << self
215
- def call(**_args)
216
- MCP::Tool::Response.new(
217
- [MCP::Content::Text.new("Elicitation not supported in this SDK version").to_h],
218
- error: true,
219
- )
220
- end
221
- end
222
- end
223
-
224
- class TestReconnection < MCP::Tool
225
- tool_name "test_reconnection"
226
- description "A tool that triggers SSE stream closure to test client reconnection behavior"
227
-
228
- class << self
229
- def call(**_args)
230
- MCP::Tool::Response.new([MCP::Content::Text.new("Reconnection test completed").to_h])
231
- end
232
- end
233
- end
234
- end
235
-
236
- module Prompts
237
- class TestSimplePrompt < MCP::Prompt
238
- prompt_name "test_simple_prompt"
239
- description "A simple prompt for testing with no arguments"
240
-
241
- class << self
242
- def template(_args, server_context: nil)
243
- MCP::Prompt::Result.new(
244
- messages: [
245
- MCP::Prompt::Message.new(
246
- role: "user",
247
- content: MCP::Content::Text.new("This is a simple prompt for testing."),
248
- ),
249
- ],
250
- )
251
- end
252
- end
253
- end
254
-
255
- class TestPromptWithArguments < MCP::Prompt
256
- prompt_name "test_prompt_with_arguments"
257
- description "A prompt with required arguments for testing"
258
- arguments [
259
- MCP::Prompt::Argument.new(name: "arg1", description: "First test argument", required: true),
260
- MCP::Prompt::Argument.new(name: "arg2", description: "Second test argument", required: true),
261
- ]
262
-
263
- class << self
264
- def template(args, server_context: nil)
265
- arg1 = args.dig(:arg1) || args.dig("arg1") || ""
266
- arg2 = args.dig(:arg2) || args.dig("arg2") || ""
267
- MCP::Prompt::Result.new(
268
- messages: [
269
- MCP::Prompt::Message.new(
270
- role: "user",
271
- content: MCP::Content::Text.new("Prompt with arguments: arg1='#{arg1}', arg2='#{arg2}'"),
272
- ),
273
- ],
274
- )
275
- end
276
- end
277
- end
278
-
279
- class TestPromptWithEmbeddedResource < MCP::Prompt
280
- prompt_name "test_prompt_with_embedded_resource"
281
- description "A prompt with an embedded resource for testing"
282
- arguments [
283
- MCP::Prompt::Argument.new(name: "resourceUri", description: "URI of the resource to embed", required: true),
284
- ]
285
-
286
- class << self
287
- def template(args, server_context: nil)
288
- resource_uri = args.dig(:resourceUri) || args.dig("resourceUri") || "test://example-resource"
289
- MCP::Prompt::Result.new(
290
- messages: [
291
- MCP::Prompt::Message.new(
292
- role: "user",
293
- content: MCP::Content::EmbeddedResource.new(
294
- MCP::Resource::TextContents.new(
295
- uri: resource_uri,
296
- mime_type: "text/plain",
297
- text: "Embedded resource content for testing.",
298
- ),
299
- ),
300
- ),
301
- MCP::Prompt::Message.new(
302
- role: "user",
303
- content: MCP::Content::Text.new("Please process the embedded resource above."),
304
- ),
305
- ],
306
- )
307
- end
308
- end
309
- end
310
-
311
- class TestPromptWithImage < MCP::Prompt
312
- prompt_name "test_prompt_with_image"
313
- description "A prompt with image content for testing"
314
-
315
- class << self
316
- def template(_args, server_context: nil)
317
- MCP::Prompt::Result.new(
318
- messages: [
319
- MCP::Prompt::Message.new(
320
- role: "user",
321
- content: MCP::Content::Image.new(BASE64_1X1_PNG, "image/png"),
322
- ),
323
- MCP::Prompt::Message.new(
324
- role: "user",
325
- content: MCP::Content::Text.new("Please analyze the image above."),
326
- ),
327
- ],
328
- )
329
- end
330
- end
331
- end
332
- end
333
-
334
- class Server
335
- DEFAULT_PORT = 9292
336
-
337
- class DnsRebindingProtection
338
- LOCALHOST_PATTERNS = /\A(localhost|127\.0\.0\.1|\[::1\]|::1)(:\d+)?\z/i.freeze
339
-
340
- def initialize(app)
341
- @app = app
342
- end
343
-
344
- def call(env)
345
- host = env["HTTP_HOST"] || env["SERVER_NAME"] || ""
346
-
347
- unless localhost?(host)
348
- return [
349
- 403,
350
- { "Content-Type" => "application/json" },
351
- [{ error: "Forbidden: DNS rebinding protection - invalid Host header '#{host}'" }.to_json],
352
- ]
353
- end
354
-
355
- origin = env["HTTP_ORIGIN"]
356
- if origin && !origin.empty?
357
- begin
358
- origin_host = URI.parse(origin).host.to_s
359
- unless localhost?(origin_host)
360
- return [
361
- 403,
362
- { "Content-Type" => "application/json" },
363
- [{ error: "Forbidden: DNS rebinding protection - invalid Origin '#{origin}'" }.to_json],
364
- ]
365
- end
366
- rescue URI::InvalidURIError
367
- return [
368
- 403,
369
- { "Content-Type" => "application/json" },
370
- [{ error: "Forbidden: invalid Origin header" }.to_json],
371
- ]
372
- end
373
- end
374
-
375
- @app.call(env)
376
- end
377
-
378
- private
379
-
380
- def localhost?(host)
381
- host.empty? || host.match?(LOCALHOST_PATTERNS)
382
- end
383
- end
384
-
385
- def initialize(port: DEFAULT_PORT)
386
- @port = port
387
- end
388
-
389
- def start
390
- server = build_server
391
- transport = build_transport(server)
392
- configure_handlers(server)
393
- rack_app = build_rack_app(transport)
394
-
395
- puts <<~MESSAGE
396
- MCP Conformance Server starting on http://localhost:#{@port}/mcp
397
- Use Ctrl-C to stop
398
- MESSAGE
399
-
400
- Rackup::Handler.get("puma").run(rack_app, Port: @port, Host: "localhost", Silent: true)
401
- end
402
-
403
- private
404
-
405
- def build_server
406
- MCP::Server.new(
407
- name: "ruby-sdk-conformance-server",
408
- version: MCP::VERSION,
409
- tools: [
410
- Tools::TestSimpleText,
411
- Tools::TestImageContent,
412
- Tools::TestAudioContent,
413
- Tools::TestEmbeddedResource,
414
- Tools::TestMultipleContentTypes,
415
- Tools::TestErrorHandling,
416
- Tools::JsonSchema202012Tool,
417
- Tools::TestToolWithLogging,
418
- Tools::TestToolWithProgress,
419
- Tools::TestSampling,
420
- Tools::TestElicitation,
421
- Tools::TestElicitationSep1034Defaults,
422
- Tools::TestElicitationSep1330Enums,
423
- Tools::TestReconnection,
424
- ],
425
- prompts: [
426
- Prompts::TestSimplePrompt,
427
- Prompts::TestPromptWithArguments,
428
- Prompts::TestPromptWithEmbeddedResource,
429
- Prompts::TestPromptWithImage,
430
- ],
431
- resources: resources,
432
- resource_templates: resource_templates,
433
- capabilities: {
434
- tools: { listChanged: true },
435
- prompts: { listChanged: true },
436
- resources: { listChanged: true, subscribe: true },
437
- logging: {},
438
- completions: {},
439
- },
440
- )
441
- end
442
-
443
- def resources
444
- [
445
- MCP::Resource.new(
446
- uri: "test://static-text",
447
- name: "static-text",
448
- description: "A static text resource for testing",
449
- mime_type: "text/plain",
450
- ),
451
- MCP::Resource.new(
452
- uri: "test://static-binary",
453
- name: "static-binary",
454
- description: "A static binary (PNG) resource for testing",
455
- mime_type: "image/png",
456
- ),
457
- MCP::Resource.new(
458
- uri: "test://watched-resource",
459
- name: "watched-resource",
460
- description: "A resource for subscription testing",
461
- mime_type: "text/plain",
462
- ),
463
- ]
464
- end
465
-
466
- def resource_templates
467
- [
468
- MCP::ResourceTemplate.new(
469
- uri_template: "test://template/{id}/data",
470
- name: "template-resource",
471
- description: "A parameterized resource template for testing",
472
- mime_type: "application/json",
473
- ),
474
- ]
475
- end
476
-
477
- def build_transport(server)
478
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
479
- server.transport = transport
480
- transport
481
- end
482
-
483
- def configure_handlers(server)
484
- server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "debug")
485
- server.server_context = server
486
-
487
- configure_resources_read_handler(server)
488
- end
489
-
490
- def configure_resources_read_handler(server)
491
- server.resources_read_handler do |params|
492
- uri = params[:uri].to_s
493
-
494
- case uri
495
- when "test://static-text"
496
- [
497
- MCP::Resource::TextContents.new(
498
- text: "This is the content of the static text resource.",
499
- uri: uri,
500
- mime_type: "text/plain",
501
- ).to_h,
502
- ]
503
- when "test://static-binary"
504
- [
505
- MCP::Resource::BlobContents.new(
506
- data: BASE64_1X1_PNG,
507
- uri: uri,
508
- mime_type: "image/png",
509
- ).to_h,
510
- ]
511
- when %r{\Atest://template/(.+)/data\z}
512
- id = Regexp.last_match(1)
513
- content = { id: id, templateTest: true, data: "Data for ID: #{id}" }.to_json
514
-
515
- [
516
- MCP::Resource::TextContents.new(
517
- text: content,
518
- uri: uri,
519
- mime_type: "application/json",
520
- ).to_h,
521
- ]
522
- else
523
- []
524
- end
525
- end
526
- end
527
-
528
- def build_rack_app(transport)
529
- mcp_app = proc do |env|
530
- request = Rack::Request.new(env)
531
-
532
- if request.path_info == "/health"
533
- [200, { "Content-Type" => "application/json" }, ['{"status":"ok"}']]
534
- elsif request.path_info == "/mcp" || request.path_info == "/"
535
- transport.handle_request(request)
536
- else
537
- [404, { "Content-Type" => "application/json" }, ['{"error":"Not found"}']]
538
- end
539
- end
540
-
541
- Rack::Builder.new do
542
- use(DnsRebindingProtection)
543
- run(mcp_app)
544
- end
545
- end
546
- end
547
- end
data/dev.yml DELETED
@@ -1,30 +0,0 @@
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/rake rubocop
data/docs/_config.yml DELETED
@@ -1,6 +0,0 @@
1
- # Use package name as site title
2
- title: "MCP Ruby SDK"
3
-
4
- # Include generated files and directories which may start with underscores
5
- include:
6
- - "_*"
data/docs/index.md DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- # Empty Jekyll front matter to enable Liquid templating (see {{ ... }} below)
3
- ---
4
-
5
- {% for version in site.data.versions -%}
6
- - [v{{ version }}](https://rubydoc.info/gems/mcp/{{ version }})
7
- {% endfor %}
@@ -1,19 +0,0 @@
1
- ---
2
- # Empty Jekyll front matter to enable Liquid templating (see {{ ... }} below)
3
- ---
4
-
5
- <!DOCTYPE html>
6
- <html>
7
- <head>
8
- <meta charset="utf-8">
9
- <title>Redirecting to latest documentation...</title>
10
- <meta http-equiv="refresh" content="0; url=https://rubydoc.info/gems/mcp">
11
- <link rel="canonical" href="https://rubydoc.info/gems/mcp">
12
- </head>
13
- <body>
14
- <p>Redirecting to <a href="https://rubydoc.info/gems/mcp">latest documentation</a>...</p>
15
- <script>
16
- window.location.href = "https://rubydoc.info/gems/mcp";
17
- </script>
18
- </body>
19
- </html>