mcp 0.7.1 → 0.9.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.
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../transport"
4
3
  require "json"
5
4
  require "securerandom"
5
+ require_relative "../../transport"
6
6
 
7
7
  module MCP
8
8
  class Server
@@ -19,6 +19,7 @@ module MCP
19
19
 
20
20
  REQUIRED_POST_ACCEPT_TYPES = ["application/json", "text/event-stream"].freeze
21
21
  REQUIRED_GET_ACCEPT_TYPES = ["text/event-stream"].freeze
22
+ STREAM_WRITE_ERRORS = [IOError, Errno::EPIPE, Errno::ECONNRESET].freeze
22
23
 
23
24
  def handle_request(request)
24
25
  case request.env["REQUEST_METHOD"]
@@ -58,7 +59,7 @@ module MCP
58
59
  begin
59
60
  send_to_stream(session[:stream], notification)
60
61
  true
61
- rescue IOError, Errno::EPIPE => e
62
+ rescue *STREAM_WRITE_ERRORS => e
62
63
  MCP.configuration.exception_reporter.call(
63
64
  e,
64
65
  { session_id: session_id, error: "Failed to send notification" },
@@ -77,7 +78,7 @@ module MCP
77
78
  begin
78
79
  send_to_stream(session[:stream], notification)
79
80
  sent_count += 1
80
- rescue IOError, Errno::EPIPE => e
81
+ rescue *STREAM_WRITE_ERRORS => e
81
82
  MCP.configuration.exception_reporter.call(
82
83
  e,
83
84
  { session_id: sid, error: "Failed to send notification" },
@@ -153,13 +154,7 @@ module MCP
153
154
  return success_response
154
155
  end
155
156
 
156
- session_id = request.env["HTTP_MCP_SESSION_ID"]
157
-
158
- return [
159
- 400,
160
- { "Content-Type" => "application/json" },
161
- [{ error: "Missing session ID" }.to_json],
162
- ] unless session_id
157
+ return missing_session_id_response unless (session_id = request.env["HTTP_MCP_SESSION_ID"])
163
158
 
164
159
  cleanup_session(session_id)
165
160
  success_response
@@ -192,6 +187,8 @@ module MCP
192
187
  return not_acceptable_response(required_types) unless accept_header
193
188
 
194
189
  accepted_types = parse_accept_header(accept_header)
190
+ return if accepted_types.include?("*/*")
191
+
195
192
  missing_types = required_types - accepted_types
196
193
  return not_acceptable_response(required_types) unless missing_types.empty?
197
194
 
@@ -256,31 +253,23 @@ module MCP
256
253
 
257
254
  def handle_regular_request(body_string, session_id)
258
255
  unless @stateless
259
- # If session ID is provided, but not in the sessions hash, return an error
260
- if session_id && !@sessions.key?(session_id)
261
- return [400, { "Content-Type" => "application/json" }, [{ error: "Invalid session ID" }.to_json]]
256
+ if session_id && !session_exists?(session_id)
257
+ return session_not_found_response
262
258
  end
263
259
  end
264
260
 
265
- response = @server.handle_json(body_string) || ""
261
+ response = @server.handle_json(body_string)
266
262
 
267
263
  # Stream can be nil since stateless mode doesn't retain streams
268
264
  stream = get_session_stream(session_id) if session_id
269
265
 
270
266
  if stream
271
267
  send_response_to_stream(stream, response, session_id)
272
- elsif response.nil? && notification_request?(body_string)
273
- [202, { "Content-Type" => "application/json" }, [response]]
274
268
  else
275
269
  [200, { "Content-Type" => "application/json" }, [response]]
276
270
  end
277
271
  end
278
272
 
279
- def notification_request?(body_string)
280
- body = parse_request_body(body_string)
281
- body.is_a?(Hash) && body["method"].start_with?("notifications/")
282
- end
283
-
284
273
  def get_session_stream(session_id)
285
274
  @mutex.synchronize { @sessions[session_id]&.fetch(:stream, nil) }
286
275
  end
@@ -288,8 +277,8 @@ module MCP
288
277
  def send_response_to_stream(stream, response, session_id)
289
278
  message = JSON.parse(response)
290
279
  send_to_stream(stream, message)
291
- [200, { "Content-Type" => "application/json" }, [{ accepted: true }.to_json]]
292
- rescue IOError, Errno::EPIPE => e
280
+ handle_accepted
281
+ rescue *STREAM_WRITE_ERRORS => e
293
282
  MCP.configuration.exception_reporter.call(
294
283
  e,
295
284
  { session_id: session_id, error: "Stream closed during response" },
@@ -366,7 +355,7 @@ module MCP
366
355
  send_ping_to_stream(@sessions[session_id][:stream])
367
356
  end
368
357
  end
369
- rescue IOError, Errno::EPIPE => e
358
+ rescue *STREAM_WRITE_ERRORS => e
370
359
  MCP.configuration.exception_reporter.call(
371
360
  e,
372
361
  { session_id: session_id, error: "Stream closed" },
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCP
4
+ class Server
5
+ module Transports
6
+ autoload :StdioTransport, "mcp/server/transports/stdio_transport"
7
+ autoload :StreamableHTTPTransport, "mcp/server/transports/streamable_http_transport"
8
+ end
9
+ end
10
+ end
data/lib/mcp/server.rb CHANGED
@@ -4,6 +4,9 @@ require_relative "../json_rpc_handler"
4
4
  require_relative "instrumentation"
5
5
  require_relative "methods"
6
6
  require_relative "logging_message_notification"
7
+ require_relative "progress"
8
+ require_relative "server_context"
9
+ require_relative "server/transports"
7
10
 
8
11
  module MCP
9
12
  class ToolNotUnique < StandardError
@@ -96,12 +99,13 @@ module MCP
96
99
  Methods::INITIALIZE => method(:init),
97
100
  Methods::PING => ->(_) { {} },
98
101
  Methods::NOTIFICATIONS_INITIALIZED => ->(_) {},
102
+ Methods::NOTIFICATIONS_PROGRESS => ->(_) {},
99
103
  Methods::LOGGING_SET_LEVEL => method(:configure_logging_level),
100
104
 
101
105
  # No op handlers for currently unsupported methods
102
- Methods::RESOURCES_SUBSCRIBE => ->(_) {},
103
- Methods::RESOURCES_UNSUBSCRIBE => ->(_) {},
104
- Methods::COMPLETION_COMPLETE => ->(_) {},
106
+ Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
107
+ Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
108
+ Methods::COMPLETION_COMPLETE => ->(_) { { completion: { values: [], hasMore: false } } },
105
109
  Methods::ELICITATION_CREATE => ->(_) {},
106
110
  }
107
111
  @transport = transport
@@ -168,6 +172,21 @@ module MCP
168
172
  report_exception(e, { notification: "resources_list_changed" })
169
173
  end
170
174
 
175
+ def notify_progress(progress_token:, progress:, total: nil, message: nil)
176
+ return unless @transport
177
+
178
+ params = {
179
+ "progressToken" => progress_token,
180
+ "progress" => progress,
181
+ "total" => total,
182
+ "message" => message,
183
+ }.compact
184
+
185
+ @transport.send_notification(Methods::NOTIFICATIONS_PROGRESS, params)
186
+ rescue => e
187
+ report_exception(e, notification: "progress")
188
+ end
189
+
171
190
  def notify_log_message(data:, level:, logger: nil)
172
191
  return unless @transport
173
192
  return unless logging_message_notification&.should_notify?(level)
@@ -219,6 +238,14 @@ module MCP
219
238
  message = "Error occurred in server_info. `description` is not supported in protocol version 2025-06-18 or earlier"
220
239
  raise ArgumentError, message
221
240
  end
241
+
242
+ tools_with_ref = @tools.each_with_object([]) do |(tool_name, tool), names|
243
+ names << tool_name if schema_contains_ref?(tool.input_schema_value.to_h)
244
+ end
245
+ unless tools_with_ref.empty?
246
+ message = "Error occurred in #{tools_with_ref.join(", ")}. `$ref` in input schemas is supported by protocol version 2025-11-25 or higher"
247
+ raise ArgumentError, message
248
+ end
222
249
  end
223
250
 
224
251
  if @configuration.protocol_version <= "2025-03-26"
@@ -259,6 +286,17 @@ module MCP
259
286
  raise ToolNotUnique, duplicated_tool_names unless duplicated_tool_names.empty?
260
287
  end
261
288
 
289
+ def schema_contains_ref?(schema)
290
+ case schema
291
+ when Hash
292
+ schema.any? { |key, value| key.to_s == "$ref" || schema_contains_ref?(value) }
293
+ when Array
294
+ schema.any? { |element| schema_contains_ref?(element) }
295
+ else
296
+ false
297
+ end
298
+ end
299
+
262
300
  def handle_request(request, method)
263
301
  handler = @handlers[method]
264
302
  unless handler
@@ -400,7 +438,9 @@ module MCP
400
438
  end
401
439
  end
402
440
 
403
- call_tool_with_args(tool, arguments)
441
+ progress_token = request.dig(:_meta, :progressToken)
442
+
443
+ call_tool_with_args(tool, arguments, server_context_with_meta(request), progress_token: progress_token)
404
444
  rescue RequestHandlerError
405
445
  raise
406
446
  rescue => e
@@ -426,7 +466,7 @@ module MCP
426
466
  prompt_args = request[:arguments]
427
467
  prompt.validate_arguments!(prompt_args)
428
468
 
429
- call_prompt_template_with_args(prompt, prompt_args)
469
+ call_prompt_template_with_args(prompt, prompt_args, server_context_with_meta(request))
430
470
  end
431
471
 
432
472
  def list_resources(request)
@@ -469,22 +509,37 @@ module MCP
469
509
  parameters.any? { |type, name| type == :keyrest || name == :server_context }
470
510
  end
471
511
 
472
- def call_tool_with_args(tool, arguments)
512
+ def call_tool_with_args(tool, arguments, context, progress_token: nil)
473
513
  args = arguments&.transform_keys(&:to_sym) || {}
474
514
 
475
515
  if accepts_server_context?(tool.method(:call))
516
+ progress = Progress.new(server: self, progress_token: progress_token)
517
+ server_context = ServerContext.new(context, progress: progress)
476
518
  tool.call(**args, server_context: server_context).to_h
477
519
  else
478
520
  tool.call(**args).to_h
479
521
  end
480
522
  end
481
523
 
482
- def call_prompt_template_with_args(prompt, args)
524
+ def call_prompt_template_with_args(prompt, args, server_context)
483
525
  if accepts_server_context?(prompt.method(:template))
484
526
  prompt.template(args, server_context: server_context).to_h
485
527
  else
486
528
  prompt.template(args).to_h
487
529
  end
488
530
  end
531
+
532
+ def server_context_with_meta(request)
533
+ meta = request[:_meta]
534
+ if meta && @server_context.is_a?(Hash)
535
+ context = @server_context.dup
536
+ context[:_meta] = meta
537
+ context
538
+ elsif meta && @server_context.nil?
539
+ { _meta: meta }
540
+ else
541
+ @server_context
542
+ end
543
+ end
489
544
  end
490
545
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCP
4
+ class ServerContext
5
+ def initialize(context, progress:)
6
+ @context = context
7
+ @progress = progress
8
+ end
9
+
10
+ def report_progress(progress, total: nil, message: nil)
11
+ @progress.report(progress, total: total, message: message)
12
+ end
13
+
14
+ def method_missing(name, ...)
15
+ if @context.respond_to?(name)
16
+ @context.public_send(name, ...)
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ def respond_to_missing?(name, include_private = false)
23
+ @context.respond_to?(name) || super
24
+ end
25
+ end
26
+ end
@@ -31,10 +31,6 @@ module MCP
31
31
  case schema
32
32
  when Hash
33
33
  schema.each_with_object({}) do |(key, value), result|
34
- if key.casecmp?("$ref")
35
- raise ArgumentError, "Invalid JSON Schema: $ref is not allowed in tool schemas"
36
- end
37
-
38
34
  result[yield(key)] = deep_transform_keys(value, &block)
39
35
  end
40
36
  when Array
data/lib/mcp/tool.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "tool/annotations"
4
+ require_relative "tool/input_schema"
5
+ require_relative "tool/output_schema"
6
+ require_relative "tool/response"
7
+
3
8
  module MCP
4
9
  class Tool
5
10
  class << self
data/lib/mcp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MCP
4
- VERSION = "0.7.1"
4
+ VERSION = "0.9.0"
5
5
  end
data/lib/mcp.rb CHANGED
@@ -1,36 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "json_rpc_handler"
4
- require_relative "mcp/annotations"
5
4
  require_relative "mcp/configuration"
6
- require_relative "mcp/content"
7
- require_relative "mcp/icon"
8
- require_relative "mcp/instrumentation"
9
- require_relative "mcp/methods"
10
- require_relative "mcp/prompt"
11
- require_relative "mcp/prompt/argument"
12
- require_relative "mcp/prompt/message"
13
- require_relative "mcp/prompt/result"
14
- require_relative "mcp/resource"
15
- require_relative "mcp/resource/contents"
16
- require_relative "mcp/resource/embedded"
17
- require_relative "mcp/resource_template"
18
- require_relative "mcp/server"
19
- require_relative "mcp/server/transports/streamable_http_transport"
20
- require_relative "mcp/server/transports/stdio_transport"
21
5
  require_relative "mcp/string_utils"
22
- require_relative "mcp/tool"
23
- require_relative "mcp/tool/input_schema"
24
- require_relative "mcp/tool/output_schema"
25
- require_relative "mcp/tool/response"
26
- require_relative "mcp/tool/annotations"
27
6
  require_relative "mcp/transport"
28
7
  require_relative "mcp/version"
29
- require_relative "mcp/client"
30
- require_relative "mcp/client/http"
31
- require_relative "mcp/client/tool"
32
8
 
33
9
  module MCP
10
+ autoload :Annotations, "mcp/annotations"
11
+ autoload :Client, "mcp/client"
12
+ autoload :Content, "mcp/content"
13
+ autoload :Icon, "mcp/icon"
14
+ autoload :Prompt, "mcp/prompt"
15
+ autoload :Resource, "mcp/resource"
16
+ autoload :ResourceTemplate, "mcp/resource_template"
17
+ autoload :Server, "mcp/server"
18
+ autoload :Tool, "mcp/tool"
19
+
34
20
  class << self
35
21
  def configure
36
22
  yield(configuration)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Model Context Protocol
@@ -30,40 +30,14 @@ executables: []
30
30
  extensions: []
31
31
  extra_rdoc_files: []
32
32
  files:
33
- - ".gitattributes"
34
- - ".github/dependabot.yml"
35
- - ".github/workflows/ci.yml"
36
- - ".github/workflows/release.yml"
37
- - ".gitignore"
38
- - ".rubocop.yml"
39
- - AGENTS.md
40
- - CHANGELOG.md
41
- - CODE_OF_CONDUCT.md
42
- - Gemfile
43
33
  - LICENSE
44
34
  - README.md
45
- - RELEASE.md
46
- - Rakefile
47
- - SECURITY.md
48
- - bin/console
49
- - bin/generate-gh-pages.sh
50
- - bin/rake
51
- - bin/setup
52
- - dev.yml
53
- - docs/_config.yml
54
- - docs/index.md
55
- - docs/latest/index.html
56
- - examples/README.md
57
- - examples/http_client.rb
58
- - examples/http_server.rb
59
- - examples/stdio_server.rb
60
- - examples/streamable_http_client.rb
61
- - examples/streamable_http_server.rb
62
35
  - lib/json_rpc_handler.rb
63
36
  - lib/mcp.rb
64
37
  - lib/mcp/annotations.rb
65
38
  - lib/mcp/client.rb
66
39
  - lib/mcp/client/http.rb
40
+ - lib/mcp/client/stdio.rb
67
41
  - lib/mcp/client/tool.rb
68
42
  - lib/mcp/configuration.rb
69
43
  - lib/mcp/content.rb
@@ -71,6 +45,7 @@ files:
71
45
  - lib/mcp/instrumentation.rb
72
46
  - lib/mcp/logging_message_notification.rb
73
47
  - lib/mcp/methods.rb
48
+ - lib/mcp/progress.rb
74
49
  - lib/mcp/prompt.rb
75
50
  - lib/mcp/prompt/argument.rb
76
51
  - lib/mcp/prompt/message.rb
@@ -81,8 +56,10 @@ files:
81
56
  - lib/mcp/resource_template.rb
82
57
  - lib/mcp/server.rb
83
58
  - lib/mcp/server/capabilities.rb
59
+ - lib/mcp/server/transports.rb
84
60
  - lib/mcp/server/transports/stdio_transport.rb
85
61
  - lib/mcp/server/transports/streamable_http_transport.rb
62
+ - lib/mcp/server_context.rb
86
63
  - lib/mcp/string_utils.rb
87
64
  - lib/mcp/tool.rb
88
65
  - lib/mcp/tool/annotations.rb
@@ -93,13 +70,12 @@ files:
93
70
  - lib/mcp/transport.rb
94
71
  - lib/mcp/transports/stdio.rb
95
72
  - lib/mcp/version.rb
96
- - mcp.gemspec
97
73
  homepage: https://github.com/modelcontextprotocol/ruby-sdk
98
74
  licenses:
99
75
  - Apache-2.0
100
76
  metadata:
101
77
  allowed_push_host: https://rubygems.org
102
- changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.7.1
78
+ changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.9.0
103
79
  homepage_uri: https://github.com/modelcontextprotocol/ruby-sdk
104
80
  source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
105
81
  bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues
@@ -118,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
94
  - !ruby/object:Gem::Version
119
95
  version: '0'
120
96
  requirements: []
121
- rubygems_version: 4.0.3
97
+ rubygems_version: 4.0.6
122
98
  specification_version: 4
123
99
  summary: The official Ruby SDK for Model Context Protocol servers and clients
124
100
  test_files: []
data/.gitattributes DELETED
@@ -1,4 +0,0 @@
1
- # See https://git-scm.com/docs/gitattributes for more about git attribute files.
2
-
3
- # Mark any vendored files as having been vendored.
4
- vendor/* linguist-vendored
@@ -1,6 +0,0 @@
1
- version: 2
2
- updates:
3
- - package-ecosystem: 'github-actions'
4
- directory: '/'
5
- schedule:
6
- interval: 'weekly'
@@ -1,54 +0,0 @@
1
- name: CI
2
- on: [push, pull_request]
3
-
4
- jobs:
5
- test:
6
- runs-on: ubuntu-latest
7
- permissions:
8
- contents: read
9
- strategy:
10
- matrix:
11
- entry:
12
- - { ruby: '2.7', allowed-failure: false }
13
- - { ruby: '3.0', allowed-failure: false }
14
- - { ruby: '3.1', allowed-failure: false }
15
- - { ruby: '3.2', allowed-failure: false }
16
- - { ruby: '3.3', allowed-failure: false }
17
- - { ruby: '3.4', allowed-failure: false }
18
- - { ruby: '4.0', allowed-failure: false }
19
- - { ruby: 'head', allowed-failure: true }
20
- name: Test Ruby ${{ matrix.entry.ruby }}
21
- steps:
22
- - uses: actions/checkout@v6
23
- - uses: ruby/setup-ruby@v1
24
- with:
25
- ruby-version: ${{ matrix.entry.ruby }}
26
- bundler-cache: true
27
- - run: bundle exec rake test
28
- continue-on-error: ${{ matrix.entry.allowed-failure }}
29
-
30
- rubocop:
31
- runs-on: ubuntu-latest
32
- permissions:
33
- contents: read
34
- name: RuboCop
35
- steps:
36
- - uses: actions/checkout@v6
37
- - uses: ruby/setup-ruby@v1
38
- with:
39
- ruby-version: 4.0 # Specify the latest supported Ruby version.
40
- bundler-cache: true
41
- - run: bundle exec rake rubocop
42
-
43
- yard:
44
- runs-on: ubuntu-latest
45
- permissions:
46
- contents: read
47
- name: YARD Documentation
48
- steps:
49
- - uses: actions/checkout@v6
50
- - uses: ruby/setup-ruby@v1
51
- with:
52
- ruby-version: 4.0 # Specify the latest supported Ruby version.
53
- bundler-cache: true
54
- - run: bundle exec yard --no-output
@@ -1,57 +0,0 @@
1
- name: Release new version
2
- on:
3
- push:
4
- branches: [main]
5
- paths:
6
- - "lib/mcp/version.rb"
7
- jobs:
8
- publish_gem:
9
- if: github.repository_owner == 'modelcontextprotocol'
10
- name: Release Gem Version to RubyGems.org
11
- runs-on: ubuntu-latest
12
-
13
- environment: release
14
-
15
- permissions:
16
- id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
17
- contents: write # IMPORTANT: this permission is required for `rake release` to push the release tag
18
- steps:
19
- - uses: actions/checkout@v6
20
- - name: Set up Ruby
21
- uses: ruby/setup-ruby@v1
22
- with:
23
- bundler-cache: true
24
- ruby-version: 4.0
25
- - uses: rubygems/release-gem@v1
26
-
27
- publish_gh_pages:
28
- if: github.repository_owner == 'modelcontextprotocol'
29
- name: Publish Documentation to GitHub Pages
30
- runs-on: ubuntu-latest
31
- needs: [publish_gem]
32
-
33
- permissions:
34
- contents: write
35
-
36
- steps:
37
- - uses: actions/checkout@v6
38
- with:
39
- fetch-depth: 0 # Fetch all history for all branches and tags
40
-
41
- - name: Configure Git
42
- run: |
43
- git config --global user.name "github-actions[bot]"
44
- git config --global user.email "github-actions[bot]@users.noreply.github.com"
45
-
46
- - name: Get version tag
47
- id: version
48
- run: |
49
- git fetch --tags
50
- TAG=$(git describe --tags --exact-match HEAD)
51
- echo "tag=${TAG}" >> $GITHUB_OUTPUT
52
-
53
- - name: Generate GitHub Pages
54
- run: ./bin/generate-gh-pages.sh ${{ steps.version.outputs.tag }}
55
-
56
- - name: Push to gh-pages
57
- run: git push origin gh-pages
data/.gitignore DELETED
@@ -1,10 +0,0 @@
1
- .ruby-version
2
- /.bundle/
3
- /.yardoc
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
- Gemfile.lock
data/.rubocop.yml DELETED
@@ -1,15 +0,0 @@
1
- inherit_gem:
2
- rubocop-shopify: rubocop.yml
3
-
4
- plugins:
5
- - rubocop-minitest
6
- - rubocop-rake
7
-
8
- AllCops:
9
- TargetRubyVersion: 2.7
10
-
11
- Gemspec/DevelopmentDependencies:
12
- Enabled: true
13
-
14
- Minitest/LiteralAsActualArgument:
15
- Enabled: true