model_context_protocol_riccardo 0.7.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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/rules/release-changelogs.mdc +32 -0
  3. data/.gitattributes +4 -0
  4. data/.github/workflows/ci.yml +22 -0
  5. data/.gitignore +8 -0
  6. data/.rubocop.yml +5 -0
  7. data/.ruby-version +1 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +15 -0
  10. data/Gemfile.lock +117 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +473 -0
  13. data/Rakefile +17 -0
  14. data/bin/console +15 -0
  15. data/bin/rake +31 -0
  16. data/bin/setup +8 -0
  17. data/dev.yml +31 -0
  18. data/examples/stdio_server.rb +94 -0
  19. data/lib/mcp-ruby.rb +3 -0
  20. data/lib/model_context_protocol/configuration.rb +75 -0
  21. data/lib/model_context_protocol/content.rb +33 -0
  22. data/lib/model_context_protocol/instrumentation.rb +26 -0
  23. data/lib/model_context_protocol/json_rpc.rb +11 -0
  24. data/lib/model_context_protocol/methods.rb +86 -0
  25. data/lib/model_context_protocol/prompt/argument.rb +21 -0
  26. data/lib/model_context_protocol/prompt/message.rb +19 -0
  27. data/lib/model_context_protocol/prompt/result.rb +19 -0
  28. data/lib/model_context_protocol/prompt.rb +82 -0
  29. data/lib/model_context_protocol/resource/contents.rb +45 -0
  30. data/lib/model_context_protocol/resource/embedded.rb +18 -0
  31. data/lib/model_context_protocol/resource.rb +24 -0
  32. data/lib/model_context_protocol/resource_template.rb +24 -0
  33. data/lib/model_context_protocol/server.rb +258 -0
  34. data/lib/model_context_protocol/string_utils.rb +26 -0
  35. data/lib/model_context_protocol/tool/annotations.rb +27 -0
  36. data/lib/model_context_protocol/tool/input_schema.rb +26 -0
  37. data/lib/model_context_protocol/tool/response.rb +18 -0
  38. data/lib/model_context_protocol/tool.rb +85 -0
  39. data/lib/model_context_protocol/transport.rb +33 -0
  40. data/lib/model_context_protocol/transports/stdio.rb +33 -0
  41. data/lib/model_context_protocol/version.rb +5 -0
  42. data/lib/model_context_protocol.rb +44 -0
  43. data/model_context_protocol.gemspec +32 -0
  44. metadata +116 -0
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json_rpc_handler"
4
+ require_relative "instrumentation"
5
+ require_relative "methods"
6
+
7
+ module ModelContextProtocol
8
+ class Server
9
+ class RequestHandlerError < StandardError
10
+ attr_reader :error_type
11
+ attr_reader :original_error
12
+
13
+ def initialize(message, request, error_type: :internal_error, original_error: nil)
14
+ super(message)
15
+ @request = request
16
+ @error_type = error_type
17
+ @original_error = original_error
18
+ end
19
+ end
20
+
21
+ include Instrumentation
22
+
23
+ attr_accessor :name, :tools, :prompts, :resources, :server_context, :configuration, :capabilities
24
+
25
+ def initialize(
26
+ name: "model_context_protocol",
27
+ tools: [],
28
+ prompts: [],
29
+ resources: [],
30
+ resource_templates: [],
31
+ server_context: nil,
32
+ configuration: nil,
33
+ capabilities: { prompts: {}, resources: {}, tools: {} }
34
+ )
35
+ @name = name
36
+ @tools = tools.to_h { |t| [t.name_value, t] }
37
+ @prompts = prompts.to_h { |p| [p.name_value, p] }
38
+ @resources = resources
39
+ @resource_templates = resource_templates
40
+ @resource_index = index_resources_by_uri(resources)
41
+ @server_context = server_context
42
+ @configuration = ModelContextProtocol.configuration.merge(configuration)
43
+ @capabilities = capabilities
44
+
45
+ @handlers = {
46
+ Methods::RESOURCES_LIST => method(:list_resources),
47
+ Methods::RESOURCES_READ => method(:read_resource_no_content),
48
+ Methods::RESOURCES_TEMPLATES_LIST => method(:list_resource_templates),
49
+ Methods::TOOLS_LIST => method(:list_tools),
50
+ Methods::TOOLS_CALL => method(:call_tool),
51
+ Methods::PROMPTS_LIST => method(:list_prompts),
52
+ Methods::PROMPTS_GET => method(:get_prompt),
53
+ Methods::INITIALIZE => method(:init),
54
+ Methods::PING => ->(_) { {} },
55
+
56
+ # No op handlers for currently unsupported methods
57
+ Methods::RESOURCES_SUBSCRIBE => ->(_) {},
58
+ Methods::RESOURCES_UNSUBSCRIBE => ->(_) {},
59
+ Methods::LOGGING_SET_LEVEL => ->(_) {},
60
+ }
61
+ end
62
+
63
+ def handle(request)
64
+ JsonRpcHandler.handle(request) do |method|
65
+ handle_request(request, method)
66
+ end
67
+ end
68
+
69
+ def handle_json(request)
70
+ JsonRpcHandler.handle_json(request) do |method|
71
+ handle_request(request, method)
72
+ end
73
+ end
74
+
75
+ def define_tool(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
76
+ tool = Tool.define(name:, description:, input_schema:, annotations:, &block)
77
+ @tools[tool.name_value] = tool
78
+ end
79
+
80
+ def define_prompt(name: nil, description: nil, arguments: [], &block)
81
+ prompt = Prompt.define(name:, description:, arguments:, &block)
82
+ @prompts[prompt.name_value] = prompt
83
+ end
84
+
85
+ def resources_list_handler(&block)
86
+ @handlers[Methods::RESOURCES_LIST] = block
87
+ end
88
+
89
+ def resources_read_handler(&block)
90
+ @handlers[Methods::RESOURCES_READ] = block
91
+ end
92
+
93
+ def resources_templates_list_handler(&block)
94
+ @handlers[Methods::RESOURCES_TEMPLATES_LIST] = block
95
+ end
96
+
97
+ def tools_list_handler(&block)
98
+ @handlers[Methods::TOOLS_LIST] = block
99
+ end
100
+
101
+ def tools_call_handler(&block)
102
+ @handlers[Methods::TOOLS_CALL] = block
103
+ end
104
+
105
+ def prompts_list_handler(&block)
106
+ @handlers[Methods::PROMPTS_LIST] = block
107
+ end
108
+
109
+ def prompts_get_handler(&block)
110
+ @handlers[Methods::PROMPTS_GET] = block
111
+ end
112
+
113
+ private
114
+
115
+ def handle_request(request, method)
116
+ handler = @handlers[method]
117
+ unless handler
118
+ instrument_call("unsupported_method") {}
119
+ return
120
+ end
121
+
122
+ Methods.ensure_capability!(method, capabilities)
123
+
124
+ ->(params) {
125
+ instrument_call(method) do
126
+ case method
127
+ when Methods::TOOLS_LIST
128
+ { tools: @handlers[Methods::TOOLS_LIST].call(params) }
129
+ when Methods::PROMPTS_LIST
130
+ { prompts: @handlers[Methods::PROMPTS_LIST].call(params) }
131
+ when Methods::RESOURCES_LIST
132
+ { resources: @handlers[Methods::RESOURCES_LIST].call(params) }
133
+ when Methods::RESOURCES_READ
134
+ { contents: @handlers[Methods::RESOURCES_READ].call(params) }
135
+ when Methods::RESOURCES_TEMPLATES_LIST
136
+ { resourceTemplates: @handlers[Methods::RESOURCES_TEMPLATES_LIST].call(params) }
137
+ else
138
+ @handlers[method].call(params)
139
+ end
140
+ rescue => e
141
+ report_exception(e, { request: request })
142
+ if e.is_a?(RequestHandlerError)
143
+ add_instrumentation_data(error: e.error_type)
144
+ raise e
145
+ end
146
+
147
+ add_instrumentation_data(error: :internal_error)
148
+ raise RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
149
+ end
150
+ }
151
+ end
152
+
153
+ def server_info
154
+ @server_info ||= {
155
+ name:,
156
+ version: ModelContextProtocol::VERSION,
157
+ }
158
+ end
159
+
160
+ def init(request)
161
+ add_instrumentation_data(method: Methods::INITIALIZE)
162
+ {
163
+ protocolVersion: configuration.protocol_version,
164
+ capabilities: capabilities,
165
+ serverInfo: server_info,
166
+ }
167
+ end
168
+
169
+ def list_tools(request)
170
+ add_instrumentation_data(method: Methods::TOOLS_LIST)
171
+ @tools.map { |_, tool| tool.to_h }
172
+ end
173
+
174
+ def call_tool(request)
175
+ add_instrumentation_data(method: Methods::TOOLS_CALL)
176
+ tool_name = request[:name]
177
+ tool = tools[tool_name]
178
+ unless tool
179
+ add_instrumentation_data(error: :tool_not_found)
180
+ raise RequestHandlerError.new("Tool not found #{tool_name}", request, error_type: :tool_not_found)
181
+ end
182
+
183
+ arguments = request[:arguments]
184
+ add_instrumentation_data(tool_name:)
185
+
186
+ if tool.input_schema&.missing_required_arguments?(arguments)
187
+ add_instrumentation_data(error: :missing_required_arguments)
188
+ raise RequestHandlerError.new(
189
+ "Missing required arguments: #{tool.input_schema.missing_required_arguments(arguments).join(", ")}",
190
+ request,
191
+ error_type: :missing_required_arguments,
192
+ )
193
+ end
194
+
195
+ begin
196
+ call_params = tool.method(:call).parameters.flatten
197
+ if call_params.include?(:server_context)
198
+ tool.call(**arguments.transform_keys(&:to_sym), server_context:).to_h
199
+ else
200
+ tool.call(**arguments.transform_keys(&:to_sym)).to_h
201
+ end
202
+ rescue => e
203
+ raise RequestHandlerError.new("Internal error calling tool #{tool_name}", request, original_error: e)
204
+ end
205
+ end
206
+
207
+ def list_prompts(request)
208
+ add_instrumentation_data(method: Methods::PROMPTS_LIST)
209
+ @prompts.map { |_, prompt| prompt.to_h }
210
+ end
211
+
212
+ def get_prompt(request)
213
+ add_instrumentation_data(method: Methods::PROMPTS_GET)
214
+ prompt_name = request[:name]
215
+ prompt = @prompts[prompt_name]
216
+ unless prompt
217
+ add_instrumentation_data(error: :prompt_not_found)
218
+ raise RequestHandlerError.new("Prompt not found #{prompt_name}", request, error_type: :prompt_not_found)
219
+ end
220
+
221
+ add_instrumentation_data(prompt_name:)
222
+
223
+ prompt_args = request[:arguments]
224
+ prompt.validate_arguments!(prompt_args)
225
+
226
+ prompt.template(prompt_args, server_context:).to_h
227
+ end
228
+
229
+ def list_resources(request)
230
+ add_instrumentation_data(method: Methods::RESOURCES_LIST)
231
+
232
+ @resources.map(&:to_h)
233
+ end
234
+
235
+ # Server implementation should set read_resource_handler to override no-op default
236
+ def read_resource_no_content(request)
237
+ add_instrumentation_data(method: Methods::RESOURCES_READ)
238
+ add_instrumentation_data(resource_uri: request[:uri])
239
+ []
240
+ end
241
+
242
+ def list_resource_templates(request)
243
+ add_instrumentation_data(method: Methods::RESOURCES_TEMPLATES_LIST)
244
+
245
+ @resource_templates.map(&:to_h)
246
+ end
247
+
248
+ def report_exception(exception, server_context = {})
249
+ configuration.exception_reporter.call(exception, server_context)
250
+ end
251
+
252
+ def index_resources_by_uri(resources)
253
+ resources.each_with_object({}) do |resource, hash|
254
+ hash[resource.uri] = resource
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,26 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module ModelContextProtocol
5
+ module StringUtils
6
+ extend self
7
+
8
+ def handle_from_class_name(class_name)
9
+ underscore(demodulize(class_name))
10
+ end
11
+
12
+ private
13
+
14
+ def demodulize(path)
15
+ path.to_s.split("::").last || path.to_s
16
+ end
17
+
18
+ def underscore(camel_cased_word)
19
+ camel_cased_word.dup
20
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
21
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
22
+ .tr("-", "_")
23
+ .downcase
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModelContextProtocol
4
+ class Tool
5
+ class Annotations
6
+ attr_reader :title, :read_only_hint, :destructive_hint, :idempotent_hint, :open_world_hint
7
+
8
+ def initialize(title: nil, read_only_hint: nil, destructive_hint: nil, idempotent_hint: nil, open_world_hint: nil)
9
+ @title = title
10
+ @read_only_hint = read_only_hint
11
+ @destructive_hint = destructive_hint
12
+ @idempotent_hint = idempotent_hint
13
+ @open_world_hint = open_world_hint
14
+ end
15
+
16
+ def to_h
17
+ {
18
+ title:,
19
+ readOnlyHint: read_only_hint,
20
+ destructiveHint: destructive_hint,
21
+ idempotentHint: idempotent_hint,
22
+ openWorldHint: open_world_hint,
23
+ }.compact
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModelContextProtocol
4
+ class Tool
5
+ class InputSchema
6
+ attr_reader :properties, :required
7
+
8
+ def initialize(properties: {}, required: [])
9
+ @properties = properties
10
+ @required = required.map(&:to_sym)
11
+ end
12
+
13
+ def to_h
14
+ { type: "object", properties:, required: }
15
+ end
16
+
17
+ def missing_required_arguments?(arguments)
18
+ missing_required_arguments(arguments).any?
19
+ end
20
+
21
+ def missing_required_arguments(arguments)
22
+ (required - arguments.keys.map(&:to_sym))
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModelContextProtocol
4
+ class Tool
5
+ class Response
6
+ attr_reader :content, :is_error
7
+
8
+ def initialize(content, is_error = false)
9
+ @content = content
10
+ @is_error = is_error
11
+ end
12
+
13
+ def to_h
14
+ { content:, isError: is_error }.compact
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModelContextProtocol
4
+ class Tool
5
+ class << self
6
+ NOT_SET = Object.new
7
+
8
+ attr_reader :description_value
9
+ attr_reader :input_schema_value
10
+ attr_reader :annotations_value
11
+
12
+ def call(*args, server_context:)
13
+ raise NotImplementedError, "Subclasses must implement call"
14
+ end
15
+
16
+ def to_h
17
+ result = {
18
+ name: name_value,
19
+ description: description_value,
20
+ inputSchema: input_schema_value.to_h,
21
+ }
22
+ result[:annotations] = annotations_value.to_h if annotations_value
23
+ result
24
+ end
25
+
26
+ def inherited(subclass)
27
+ super
28
+ subclass.instance_variable_set(:@name_value, nil)
29
+ subclass.instance_variable_set(:@description_value, nil)
30
+ subclass.instance_variable_set(:@input_schema_value, nil)
31
+ subclass.instance_variable_set(:@annotations_value, nil)
32
+ end
33
+
34
+ def tool_name(value = NOT_SET)
35
+ if value == NOT_SET
36
+ name_value
37
+ else
38
+ @name_value = value
39
+ end
40
+ end
41
+
42
+ def name_value
43
+ @name_value || StringUtils.handle_from_class_name(name)
44
+ end
45
+
46
+ def description(value = NOT_SET)
47
+ if value == NOT_SET
48
+ @description_value
49
+ else
50
+ @description_value = value
51
+ end
52
+ end
53
+
54
+ def input_schema(value = NOT_SET)
55
+ if value == NOT_SET
56
+ input_schema_value
57
+ elsif value.is_a?(Hash)
58
+ properties = value[:properties] || value["properties"] || {}
59
+ required = value[:required] || value["required"] || []
60
+ @input_schema_value = InputSchema.new(properties:, required:)
61
+ elsif value.is_a?(InputSchema)
62
+ @input_schema_value = value
63
+ end
64
+ end
65
+
66
+ def annotations(hash = NOT_SET)
67
+ if hash == NOT_SET
68
+ @annotations_value
69
+ else
70
+ @annotations_value = Annotations.new(**hash)
71
+ end
72
+ end
73
+
74
+ def define(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
75
+ Class.new(self) do
76
+ tool_name name
77
+ description description
78
+ input_schema input_schema
79
+ self.annotations(annotations) if annotations
80
+ define_singleton_method(:call, &block) if block
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModelContextProtocol
4
+ class Transport
5
+ def initialize(server)
6
+ @server = server
7
+ end
8
+
9
+ def send_response(response)
10
+ raise NotImplementedError, "Subclasses must implement send_response"
11
+ end
12
+
13
+ def open
14
+ raise NotImplementedError, "Subclasses must implement open"
15
+ end
16
+
17
+ def close
18
+ raise NotImplementedError, "Subclasses must implement close"
19
+ end
20
+
21
+ private
22
+
23
+ def handle_request(request)
24
+ response = @server.handle(request)
25
+ send_response(response) if response
26
+ end
27
+
28
+ def handle_json_request(request)
29
+ response = @server.handle_json(request)
30
+ send_response(response) if response
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../transport"
4
+ require "json"
5
+
6
+ module ModelContextProtocol
7
+ module Transports
8
+ class StdioTransport < Transport
9
+ def initialize(server)
10
+ @server = server
11
+ @open = false
12
+ super
13
+ end
14
+
15
+ def open
16
+ @open = true
17
+ while @open && (line = $stdin.gets)
18
+ handle_json_request(line.strip)
19
+ end
20
+ end
21
+
22
+ def close
23
+ @open = false
24
+ end
25
+
26
+ def send_response(message)
27
+ json_message = message.is_a?(String) ? message : JSON.generate(message)
28
+ $stdout.puts(json_message)
29
+ $stdout.flush
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModelContextProtocol
4
+ VERSION = "0.7.0"
5
+ end
@@ -0,0 +1,44 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "model_context_protocol/server"
5
+ require "model_context_protocol/string_utils"
6
+ require "model_context_protocol/tool"
7
+ require "model_context_protocol/tool/input_schema"
8
+ require "model_context_protocol/tool/annotations"
9
+ require "model_context_protocol/tool/response"
10
+ require "model_context_protocol/content"
11
+ require "model_context_protocol/resource"
12
+ require "model_context_protocol/resource/contents"
13
+ require "model_context_protocol/resource/embedded"
14
+ require "model_context_protocol/resource_template"
15
+ require "model_context_protocol/prompt"
16
+ require "model_context_protocol/prompt/argument"
17
+ require "model_context_protocol/prompt/message"
18
+ require "model_context_protocol/prompt/result"
19
+ require "model_context_protocol/version"
20
+ require "model_context_protocol/configuration"
21
+ require "model_context_protocol/methods"
22
+
23
+ module ModelContextProtocol
24
+ class << self
25
+ def configure
26
+ yield(configuration)
27
+ end
28
+
29
+ def configuration
30
+ @configuration ||= Configuration.new
31
+ end
32
+ end
33
+
34
+ class Annotations
35
+ attr_reader :audience, :priority
36
+
37
+ def initialize(audience: nil, priority: nil)
38
+ @audience = audience
39
+ @priority = priority
40
+ end
41
+ end
42
+ end
43
+
44
+ MCP = ModelContextProtocol
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/model_context_protocol/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "model_context_protocol_riccardo"
7
+ spec.version = ModelContextProtocol::VERSION
8
+ spec.authors = ["Model Context Protocol"]
9
+ spec.email = ["mcp-support@anthropic.com"]
10
+
11
+ spec.summary = "The official Ruby SDK for Model Context Protocol servers and clients"
12
+ spec.description = spec.summary
13
+ spec.homepage = "https://github.com/modelcontextprotocol/ruby-sdk"
14
+ spec.license = "MIT"
15
+
16
+ spec.required_ruby_version = ">= 3.2.0"
17
+
18
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = spec.homepage
21
+
22
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
23
+ %x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency("json_rpc_handler", "~> 0.1")
31
+ spec.add_development_dependency("activesupport")
32
+ end