mcp 0.17.0 → 0.18.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 325fab0f82b31b6a2614ef626ebecb12a7bf22c199ddf189dcd70b4fcf9acb58
4
- data.tar.gz: 7cb2b010e0808efce62e9d80f487c7cb6bff9604e13f6b2b68590ffa4e0fc784
3
+ metadata.gz: 7268234a54e4c0b422a916aabc08de04a4cde157b9a6b3909274ab4629885137
4
+ data.tar.gz: 1f48ca777ef0269c8c1f946a23efed1cd990e9ac1898568d8143d84e6c8d7fcb
5
5
  SHA512:
6
- metadata.gz: 6c675a999c460dd7a285203e949587ecf1faf43a0f80f67f6f00bbb06abaa31846382cf013a0590a22c237b341860bc9a880d2d6d5de2971c8f70cde8d9d78ee
7
- data.tar.gz: 84f4ca2c9a3a745976059655e2ea6f89ff5bae6637b32ab151894f12b3aea96ce5f11d9e50b3c908769f97e9bc923fd68f0e0b83060104964710d6ca70f06cb0
6
+ metadata.gz: bb859fc7e68f54f1a1d7c3d523a3487799c7b5ff8e3ddb5c01cd84101be79c9b5f81eeeb65b8fb06d620b87898f6c3466492e57ad921eac729c113cd5a609f8d
7
+ data.tar.gz: 0bf7b67aa29e8211c97168a9e9472e5fdfb1ea5a97db16d917ab1602f584db6076854e354abe0456384c70e712a797832c0949f12d5a5e76bea6761fcecab0c1
data/README.md CHANGED
@@ -1253,6 +1253,25 @@ A `ping` request has no parameters, and the receiver MUST respond promptly with
1253
1253
  Servers respond to incoming `ping` requests automatically - no setup is required.
1254
1254
  Any `MCP::Server` instance replies with an empty result.
1255
1255
 
1256
+ Servers can also send `ping` requests to the client via `ServerSession#ping`.
1257
+ Inside a tool handler that receives `server_context:`, call `ping` on it:
1258
+
1259
+ ```ruby
1260
+ class HealthCheckTool < MCP::Tool
1261
+ description "Verifies the client is still responsive"
1262
+
1263
+ def self.call(server_context:)
1264
+ server_context.ping # => {} on success
1265
+
1266
+ MCP::Tool::Response.new([{ type: "text", text: "client is alive" }])
1267
+ end
1268
+ end
1269
+ ```
1270
+
1271
+ `#ping` raises `MCP::Server::ValidationError` when the client returns a `result`
1272
+ that is not a Hash. Transport-level errors (e.g., the client returning a JSON-RPC error)
1273
+ propagate as exceptions raised by the transport layer.
1274
+
1256
1275
  #### Client-Side
1257
1276
 
1258
1277
  `MCP::Client` exposes `ping` to send a ping to the server:
@@ -1736,7 +1755,7 @@ This class supports:
1736
1755
 
1737
1756
  - Liveness check via the `ping` method (`MCP::Client#ping`)
1738
1757
  - Tool listing via the `tools/list` method (`MCP::Client#tools`)
1739
- - Tool invocation via the `tools/call` method (`MCP::Client#call_tools`)
1758
+ - Tool invocation via the `tools/call` method (`MCP::Client#call_tool`)
1740
1759
  - Resource listing via the `resources/list` method (`MCP::Client#resources`)
1741
1760
  - Resource template listing via the `resources/templates/list` method (`MCP::Client#resource_templates`)
1742
1761
  - Resource reading via the `resources/read` method (`MCP::Client#read_resource`)
@@ -134,7 +134,10 @@ module MCP
134
134
 
135
135
  def send_request(request:)
136
136
  start unless @started
137
- connect unless @initialized
137
+ unless @initialized
138
+ warn("Calling `MCP::Client::Stdio#send_request` without calling `MCP::Client#connect` is deprecated. Use `MCP::Client#connect` before sending requests instead.", uplevel: 1)
139
+ connect
140
+ end
138
141
 
139
142
  write_message(request)
140
143
  read_response(request)
@@ -426,7 +426,7 @@ module MCP
426
426
  return success_response
427
427
  end
428
428
 
429
- return missing_session_id_response unless (session_id = request.env["HTTP_MCP_SESSION_ID"])
429
+ return missing_session_id_response unless (session_id = extract_session_id(request))
430
430
  return session_not_found_response unless session_exists?(session_id)
431
431
 
432
432
  protocol_version_error = validate_protocol_version_header(request)
@@ -504,7 +504,7 @@ module MCP
504
504
 
505
505
  def parse_accept_header(header)
506
506
  header.split(",").map do |part|
507
- part.split(";").first.strip
507
+ part.split(";").first.strip.downcase
508
508
  end
509
509
  end
510
510
 
data/lib/mcp/server.rb CHANGED
@@ -67,6 +67,11 @@ module MCP
67
67
  end
68
68
  end
69
69
 
70
+ # Raised when a client response fails server-side validation, e.g., a success response
71
+ # whose `result` field is missing or has the wrong type. This is distinct from a
72
+ # client-returned JSON-RPC error.
73
+ class ValidationError < StandardError; end
74
+
70
75
  include Instrumentation
71
76
  include Pagination
72
77
 
@@ -59,6 +59,28 @@ module MCP
59
59
  end
60
60
  end
61
61
 
62
+ # Sends a `ping` request to the originating client to verify it is still responsive.
63
+ # Per the MCP spec, the client MUST respond promptly with an empty result.
64
+ #
65
+ # @return [Hash] An empty hash on success.
66
+ # @raise [Server::ValidationError] If the response `result` is not a Hash.
67
+ # @raise [NoMethodError] If the session does not support sending pings.
68
+ #
69
+ # @example
70
+ # def self.call(server_context:)
71
+ # server_context.ping # => {}
72
+ # # ...
73
+ # end
74
+ #
75
+ # @see https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping
76
+ def ping
77
+ if @notification_target.respond_to?(:ping)
78
+ @notification_target.ping(related_request_id: @related_request_id)
79
+ else
80
+ raise NoMethodError, "undefined method 'ping' for #{self}"
81
+ end
82
+ end
83
+
62
84
  # Delegates to the session so the request is scoped to the originating client.
63
85
  # Falls back to `@context` (via `method_missing`) when `@notification_target`
64
86
  # does not support sampling.
@@ -106,6 +106,14 @@ module MCP
106
106
  send_to_transport_request(Methods::ROOTS_LIST, nil, related_request_id: related_request_id)
107
107
  end
108
108
 
109
+ # Sends a `ping` request scoped to this session.
110
+ def ping(related_request_id: nil)
111
+ result = send_to_transport_request(Methods::PING, nil, related_request_id: related_request_id)
112
+ raise Server::ValidationError, "Response validation failed: invalid `result`" unless result.is_a?(Hash)
113
+
114
+ result
115
+ end
116
+
109
117
  # Sends a `sampling/createMessage` request scoped to this session.
110
118
  def create_sampling_message(related_request_id: nil, **kwargs)
111
119
  params = @server.build_sampling_params(client_capabilities, **kwargs)
@@ -1,10 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
3
4
  require "json-schema"
4
5
 
5
6
  module MCP
6
7
  class Tool
7
8
  class Schema
9
+ # Metaschema validation depends only on schema content, so a given schema
10
+ # never needs to be validated more than once. Caching the result lets repeated
11
+ # (e.g. dynamically rebuilt) schemas skip the costly traversal.
12
+ class ValidationCache
13
+ DEFAULT_MAX_SIZE = 1000
14
+
15
+ def initialize(max_size: DEFAULT_MAX_SIZE)
16
+ @max_size = max_size
17
+ @entries = {}
18
+ @mutex = Mutex.new
19
+ end
20
+
21
+ def validated?(key)
22
+ @mutex.synchronize { @entries.key?(key) }
23
+ end
24
+
25
+ def store(key)
26
+ @mutex.synchronize do
27
+ @entries.delete(key)
28
+ @entries[key] = true
29
+ @entries.shift while @entries.size > @max_size
30
+ end
31
+ end
32
+
33
+ def clear
34
+ @mutex.synchronize { @entries.clear }
35
+ end
36
+ end
37
+ VALIDATION_CACHE = ValidationCache.new
38
+
8
39
  # JSON Schema 2020-12 is the default dialect for MCP schema definitions
9
40
  # per MCP 2025-11-25 (SEP-1613). Note: emission only — runtime validation
10
41
  # is still performed against the JSON Schema draft-04 metaschema because
@@ -36,6 +67,14 @@ module MCP
36
67
  end
37
68
 
38
69
  def validate_schema!
70
+ target = schema_for_validation
71
+
72
+ # `max_nesting: false` because normalization uses `JSON.dump` (no nesting limit),
73
+ # so the default `JSON.generate` limit would raise on a deeply nested schema that
74
+ # the initializer already accepted.
75
+ key = Digest::SHA256.hexdigest(JSON.generate(target, max_nesting: false))
76
+ return if VALIDATION_CACHE.validated?(key)
77
+
39
78
  gem_path = File.realpath(Gem.loaded_specs["json-schema"].full_gem_path)
40
79
  schema_reader = JSON::Schema::Reader.new(
41
80
  accept_uri: false,
@@ -45,10 +84,12 @@ module MCP
45
84
  # Converts metaschema to a file URI for cross-platform compatibility
46
85
  metaschema_uri = JSON::Util::URI.file_uri(metaschema_path.expand_path.cleanpath.to_s.tr("\\", "/"))
47
86
  metaschema = metaschema_uri.to_s
48
- errors = JSON::Validator.fully_validate(metaschema, schema_for_validation, schema_reader: schema_reader)
87
+ errors = JSON::Validator.fully_validate(metaschema, target, schema_reader: schema_reader)
49
88
  if errors.any?
50
89
  raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
51
90
  end
91
+
92
+ VALIDATION_CACHE.store(key)
52
93
  end
53
94
 
54
95
  # The `json-schema` gem's draft-04 validator cannot resolve newer or unknown `$schema`
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.17.0"
4
+ VERSION = "0.18.0"
5
5
  end
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.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Model Context Protocol
@@ -87,7 +87,7 @@ licenses:
87
87
  - Apache-2.0
88
88
  metadata:
89
89
  allowed_push_host: https://rubygems.org
90
- changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.17.0
90
+ changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.18.0
91
91
  homepage_uri: https://ruby.sdk.modelcontextprotocol.io
92
92
  source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
93
93
  bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues