mcp 0.10.0 → 0.14.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.
@@ -7,11 +7,18 @@ module MCP
7
7
  LATEST_STABLE_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05",
8
8
  ]
9
9
 
10
- attr_writer :exception_reporter, :instrumentation_callback
10
+ attr_writer :exception_reporter, :around_request
11
11
 
12
- def initialize(exception_reporter: nil, instrumentation_callback: nil, protocol_version: nil,
12
+ # @deprecated Use {#around_request=} instead. `instrumentation_callback`
13
+ # fires only after a request completes and cannot wrap execution in a
14
+ # surrounding block (e.g. for Application Performance Monitoring (APM) spans).
15
+ # @see #around_request=
16
+ attr_writer :instrumentation_callback
17
+
18
+ def initialize(exception_reporter: nil, around_request: nil, instrumentation_callback: nil, protocol_version: nil,
13
19
  validate_tool_call_arguments: true)
14
20
  @exception_reporter = exception_reporter
21
+ @around_request = around_request
15
22
  @instrumentation_callback = instrumentation_callback
16
23
  @protocol_version = protocol_version
17
24
  if protocol_version
@@ -50,10 +57,24 @@ module MCP
50
57
  !@exception_reporter.nil?
51
58
  end
52
59
 
60
+ def around_request
61
+ @around_request || default_around_request
62
+ end
63
+
64
+ def around_request?
65
+ !@around_request.nil?
66
+ end
67
+
68
+ # @deprecated Use {#around_request} instead. `instrumentation_callback`
69
+ # fires only after a request completes and cannot wrap execution in a
70
+ # surrounding block (e.g. for Application Performance Monitoring (APM) spans).
71
+ # @see #around_request
53
72
  def instrumentation_callback
54
73
  @instrumentation_callback || default_instrumentation_callback
55
74
  end
56
75
 
76
+ # @deprecated Use {#around_request?} instead.
77
+ # @see #around_request?
57
78
  def instrumentation_callback?
58
79
  !@instrumentation_callback.nil?
59
80
  end
@@ -72,20 +93,30 @@ module MCP
72
93
  else
73
94
  @exception_reporter
74
95
  end
96
+
97
+ around_request = if other.around_request?
98
+ other.around_request
99
+ else
100
+ @around_request
101
+ end
102
+
75
103
  instrumentation_callback = if other.instrumentation_callback?
76
104
  other.instrumentation_callback
77
105
  else
78
106
  @instrumentation_callback
79
107
  end
108
+
80
109
  protocol_version = if other.protocol_version?
81
110
  other.protocol_version
82
111
  else
83
112
  @protocol_version
84
113
  end
114
+
85
115
  validate_tool_call_arguments = other.validate_tool_call_arguments
86
116
 
87
117
  Configuration.new(
88
118
  exception_reporter: exception_reporter,
119
+ around_request: around_request,
89
120
  instrumentation_callback: instrumentation_callback,
90
121
  protocol_version: protocol_version,
91
122
  validate_tool_call_arguments: validate_tool_call_arguments,
@@ -111,6 +142,11 @@ module MCP
111
142
  @default_exception_reporter ||= ->(exception, server_context) {}
112
143
  end
113
144
 
145
+ def default_around_request
146
+ @default_around_request ||= ->(_data, &request_handler) { request_handler.call }
147
+ end
148
+
149
+ # @deprecated Use {#default_around_request} instead.
114
150
  def default_instrumentation_callback
115
151
  @default_instrumentation_callback ||= ->(data) {}
116
152
  end
data/lib/mcp/content.rb CHANGED
@@ -3,56 +3,60 @@
3
3
  module MCP
4
4
  module Content
5
5
  class Text
6
- attr_reader :text, :annotations
6
+ attr_reader :text, :annotations, :meta
7
7
 
8
- def initialize(text, annotations: nil)
8
+ def initialize(text, annotations: nil, meta: nil)
9
9
  @text = text
10
10
  @annotations = annotations
11
+ @meta = meta
11
12
  end
12
13
 
13
14
  def to_h
14
- { text: text, annotations: annotations, type: "text" }.compact
15
+ { text: text, annotations: annotations, _meta: meta, type: "text" }.compact
15
16
  end
16
17
  end
17
18
 
18
19
  class Image
19
- attr_reader :data, :mime_type, :annotations
20
+ attr_reader :data, :mime_type, :annotations, :meta
20
21
 
21
- def initialize(data, mime_type, annotations: nil)
22
+ def initialize(data, mime_type, annotations: nil, meta: nil)
22
23
  @data = data
23
24
  @mime_type = mime_type
24
25
  @annotations = annotations
26
+ @meta = meta
25
27
  end
26
28
 
27
29
  def to_h
28
- { data: data, mimeType: mime_type, annotations: annotations, type: "image" }.compact
30
+ { data: data, mimeType: mime_type, annotations: annotations, _meta: meta, type: "image" }.compact
29
31
  end
30
32
  end
31
33
 
32
34
  class Audio
33
- attr_reader :data, :mime_type, :annotations
35
+ attr_reader :data, :mime_type, :annotations, :meta
34
36
 
35
- def initialize(data, mime_type, annotations: nil)
37
+ def initialize(data, mime_type, annotations: nil, meta: nil)
36
38
  @data = data
37
39
  @mime_type = mime_type
38
40
  @annotations = annotations
41
+ @meta = meta
39
42
  end
40
43
 
41
44
  def to_h
42
- { data: data, mimeType: mime_type, annotations: annotations, type: "audio" }.compact
45
+ { data: data, mimeType: mime_type, annotations: annotations, _meta: meta, type: "audio" }.compact
43
46
  end
44
47
  end
45
48
 
46
49
  class EmbeddedResource
47
- attr_reader :resource, :annotations
50
+ attr_reader :resource, :annotations, :meta
48
51
 
49
- def initialize(resource, annotations: nil)
52
+ def initialize(resource, annotations: nil, meta: nil)
50
53
  @resource = resource
51
54
  @annotations = annotations
55
+ @meta = meta
52
56
  end
53
57
 
54
58
  def to_h
55
- { resource: resource.to_h, annotations: annotations, type: "resource" }.compact
59
+ { resource: resource.to_h, annotations: annotations, _meta: meta, type: "resource" }.compact
56
60
  end
57
61
  end
58
62
  end
@@ -2,19 +2,40 @@
2
2
 
3
3
  module MCP
4
4
  module Instrumentation
5
- def instrument_call(method, &block)
5
+ def instrument_call(method, server_context: {}, exception_already_reported: nil, &block)
6
6
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
7
7
  begin
8
8
  @instrumentation_data = {}
9
9
  add_instrumentation_data(method: method)
10
10
 
11
- result = yield block
11
+ result = configuration.around_request.call(@instrumentation_data, &block)
12
12
 
13
13
  result
14
+ rescue => e
15
+ already_reported = begin
16
+ !!exception_already_reported&.call(e)
17
+ # rubocop:disable Lint/RescueException
18
+ rescue Exception
19
+ # rubocop:enable Lint/RescueException
20
+ # The predicate is expected to be side-effect-free and return a boolean.
21
+ # Any exception raised from it (including non-StandardError such as SystemExit)
22
+ # must not shadow the original exception.
23
+ false
24
+ end
25
+
26
+ unless already_reported
27
+ add_instrumentation_data(error: :internal_error) unless @instrumentation_data.key?(:error)
28
+ configuration.exception_reporter.call(e, server_context)
29
+ end
30
+
31
+ raise
14
32
  ensure
15
33
  end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
34
  add_instrumentation_data(duration: end_time - start_time)
17
35
 
36
+ # Backward compatibility: `instrumentation_callback` is soft-deprecated
37
+ # in favor of `around_request`, but existing callers still expect it
38
+ # to fire after every request until it is removed in a future version.
18
39
  configuration.instrumentation_callback.call(@instrumentation_data)
19
40
  end
20
41
  end
data/lib/mcp/methods.rb CHANGED
@@ -33,6 +33,7 @@ module MCP
33
33
  NOTIFICATIONS_MESSAGE = "notifications/message"
34
34
  NOTIFICATIONS_PROGRESS = "notifications/progress"
35
35
  NOTIFICATIONS_CANCELLED = "notifications/cancelled"
36
+ NOTIFICATIONS_ELICITATION_COMPLETE = "notifications/elicitation/complete"
36
37
 
37
38
  class MissingRequiredCapabilityError < StandardError
38
39
  attr_reader :method
@@ -72,15 +73,13 @@ module MCP
72
73
  require_capability!(method, capabilities, :completions)
73
74
  when ROOTS_LIST
74
75
  require_capability!(method, capabilities, :roots)
75
- when NOTIFICATIONS_ROOTS_LIST_CHANGED
76
- require_capability!(method, capabilities, :roots)
77
- require_capability!(method, capabilities, :roots, :listChanged)
78
76
  when SAMPLING_CREATE_MESSAGE
79
77
  require_capability!(method, capabilities, :sampling)
80
78
  when ELICITATION_CREATE
81
79
  require_capability!(method, capabilities, :elicitation)
82
- when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED
83
- # No specific capability required for initialize, ping, progress or cancelled
80
+ when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_ROOTS_LIST_CHANGED,
81
+ NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED, NOTIFICATIONS_ELICITATION_COMPLETE
82
+ # No specific capability required.
84
83
  end
85
84
  end
86
85
 
data/lib/mcp/progress.rb CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  module MCP
4
4
  class Progress
5
- def initialize(notification_target:, progress_token:)
5
+ def initialize(notification_target:, progress_token:, related_request_id: nil)
6
6
  @notification_target = notification_target
7
7
  @progress_token = progress_token
8
+ @related_request_id = related_request_id
8
9
  end
9
10
 
10
11
  def report(progress, total: nil, message: nil)
@@ -16,6 +17,7 @@ module MCP
16
17
  progress: progress,
17
18
  total: total,
18
19
  message: message,
20
+ related_request_id: @related_request_id,
19
21
  )
20
22
  end
21
23
  end
@@ -3,15 +3,16 @@
3
3
  module MCP
4
4
  class Prompt
5
5
  class Result
6
- attr_reader :description, :messages
6
+ attr_reader :description, :messages, :meta
7
7
 
8
- def initialize(description: nil, messages: [])
8
+ def initialize(description: nil, messages: [], meta: nil)
9
9
  @description = description
10
10
  @messages = messages
11
+ @meta = meta
11
12
  end
12
13
 
13
14
  def to_h
14
- { description: description, messages: messages.map(&:to_h) }.compact
15
+ { description: description, messages: messages.map(&:to_h), _meta: meta }.compact
15
16
  end
16
17
  end
17
18
  end
@@ -3,23 +3,24 @@
3
3
  module MCP
4
4
  class Resource
5
5
  class Contents
6
- attr_reader :uri, :mime_type
6
+ attr_reader :uri, :mime_type, :meta
7
7
 
8
- def initialize(uri:, mime_type: nil)
8
+ def initialize(uri:, mime_type: nil, meta: nil)
9
9
  @uri = uri
10
10
  @mime_type = mime_type
11
+ @meta = meta
11
12
  end
12
13
 
13
14
  def to_h
14
- { uri: uri, mimeType: mime_type }.compact
15
+ { uri: uri, mimeType: mime_type, _meta: meta }.compact
15
16
  end
16
17
  end
17
18
 
18
19
  class TextContents < Contents
19
20
  attr_reader :text
20
21
 
21
- def initialize(text:, uri:, mime_type:)
22
- super(uri: uri, mime_type: mime_type)
22
+ def initialize(text:, uri:, mime_type:, meta: nil)
23
+ super(uri: uri, mime_type: mime_type, meta: meta)
23
24
  @text = text
24
25
  end
25
26
 
@@ -31,8 +32,8 @@ module MCP
31
32
  class BlobContents < Contents
32
33
  attr_reader :data
33
34
 
34
- def initialize(data:, uri:, mime_type:)
35
- super(uri: uri, mime_type: mime_type)
35
+ def initialize(data:, uri:, mime_type:, meta: nil)
36
+ super(uri: uri, mime_type: mime_type, meta: meta)
36
37
  @data = data
37
38
  end
38
39
 
data/lib/mcp/resource.rb CHANGED
@@ -5,15 +5,16 @@ require_relative "resource/embedded"
5
5
 
6
6
  module MCP
7
7
  class Resource
8
- attr_reader :uri, :name, :title, :description, :icons, :mime_type
8
+ attr_reader :uri, :name, :title, :description, :icons, :mime_type, :meta
9
9
 
10
- def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil)
10
+ def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
11
11
  @uri = uri
12
12
  @name = name
13
13
  @title = title
14
14
  @description = description
15
15
  @icons = icons
16
16
  @mime_type = mime_type
17
+ @meta = meta
17
18
  end
18
19
 
19
20
  def to_h
@@ -24,6 +25,7 @@ module MCP
24
25
  description: description,
25
26
  icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
26
27
  mimeType: mime_type,
28
+ _meta: meta,
27
29
  }.compact
28
30
  end
29
31
  end
@@ -2,15 +2,16 @@
2
2
 
3
3
  module MCP
4
4
  class ResourceTemplate
5
- attr_reader :uri_template, :name, :title, :description, :icons, :mime_type
5
+ attr_reader :uri_template, :name, :title, :description, :icons, :mime_type, :meta
6
6
 
7
- def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil)
7
+ def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
8
8
  @uri_template = uri_template
9
9
  @name = name
10
10
  @title = title
11
11
  @description = description
12
12
  @icons = icons
13
13
  @mime_type = mime_type
14
+ @meta = meta
14
15
  end
15
16
 
16
17
  def to_h
@@ -21,6 +22,7 @@ module MCP
21
22
  description: description,
22
23
  icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
23
24
  mimeType: mime_type,
25
+ _meta: meta,
24
26
  }.compact
25
27
  end
26
28
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCP
4
+ class Server
5
+ module Pagination
6
+ private
7
+
8
+ def cursor_from(request)
9
+ return if request.nil?
10
+
11
+ unless request.is_a?(Hash)
12
+ raise RequestHandlerError.new("Invalid params", request, error_type: :invalid_params)
13
+ end
14
+
15
+ request[:cursor]
16
+ end
17
+
18
+ def paginate(items, cursor:, page_size:, request:, &block)
19
+ start_index = 0
20
+
21
+ if cursor
22
+ unless cursor.is_a?(String)
23
+ raise RequestHandlerError.new("Invalid cursor", request, error_type: :invalid_params)
24
+ end
25
+
26
+ start_index = Integer(cursor, exception: false)
27
+ if start_index.nil? || start_index < 0 || start_index >= items.size
28
+ raise RequestHandlerError.new("Invalid cursor", request, error_type: :invalid_params)
29
+ end
30
+ end
31
+
32
+ end_index = page_size ? start_index + page_size : items.size
33
+ page = items[start_index...end_index]
34
+ page = page.map(&block) if block
35
+
36
+ result = { items: page }
37
+ result[:next_cursor] = end_index.to_s if end_index < items.size
38
+ result
39
+ end
40
+ end
41
+ end
42
+ end
@@ -53,6 +53,41 @@ module MCP
53
53
  MCP.configuration.exception_reporter.call(e, { error: "Failed to send notification" })
54
54
  false
55
55
  end
56
+
57
+ def send_request(method, params = nil)
58
+ request_id = generate_request_id
59
+ request = { jsonrpc: "2.0", id: request_id, method: method }
60
+ request[:params] = params if params
61
+
62
+ begin
63
+ send_response(request)
64
+ rescue => e
65
+ MCP.configuration.exception_reporter.call(e, { error: "Failed to send request" })
66
+ raise
67
+ end
68
+
69
+ while @open && (line = $stdin.gets)
70
+ begin
71
+ parsed = JSON.parse(line.strip, symbolize_names: true)
72
+ rescue JSON::ParserError => e
73
+ MCP.configuration.exception_reporter.call(e, { error: "Failed to parse response" })
74
+ raise
75
+ end
76
+
77
+ if parsed[:id] == request_id && !parsed.key?(:method)
78
+ if parsed[:error]
79
+ raise StandardError, "Client returned an error for #{method} request (code: #{parsed[:error][:code]}): #{parsed[:error][:message]}"
80
+ end
81
+
82
+ return parsed[:result]
83
+ else
84
+ response = @session ? @session.handle(parsed) : @server.handle(parsed)
85
+ send_response(response) if response
86
+ end
87
+ end
88
+
89
+ raise "Transport closed while waiting for response to #{method} request."
90
+ end
56
91
  end
57
92
  end
58
93
  end