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,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModelContextProtocol
4
+ class Configuration
5
+ DEFAULT_PROTOCOL_VERSION = "2024-11-05"
6
+
7
+ attr_writer :exception_reporter, :instrumentation_callback, :protocol_version
8
+
9
+ def initialize(exception_reporter: nil, instrumentation_callback: nil, protocol_version: nil)
10
+ @exception_reporter = exception_reporter
11
+ @instrumentation_callback = instrumentation_callback
12
+ @protocol_version = protocol_version
13
+ end
14
+
15
+ def protocol_version
16
+ @protocol_version || DEFAULT_PROTOCOL_VERSION
17
+ end
18
+
19
+ def protocol_version?
20
+ !@protocol_version.nil?
21
+ end
22
+
23
+ def exception_reporter
24
+ @exception_reporter || default_exception_reporter
25
+ end
26
+
27
+ def exception_reporter?
28
+ !@exception_reporter.nil?
29
+ end
30
+
31
+ def instrumentation_callback
32
+ @instrumentation_callback || default_instrumentation_callback
33
+ end
34
+
35
+ def instrumentation_callback?
36
+ !@instrumentation_callback.nil?
37
+ end
38
+
39
+ def merge(other)
40
+ return self if other.nil?
41
+
42
+ exception_reporter = if other.exception_reporter?
43
+ other.exception_reporter
44
+ else
45
+ @exception_reporter
46
+ end
47
+ instrumentation_callback = if other.instrumentation_callback?
48
+ other.instrumentation_callback
49
+ else
50
+ @instrumentation_callback
51
+ end
52
+ protocol_version = if other.protocol_version?
53
+ other.protocol_version
54
+ else
55
+ @protocol_version
56
+ end
57
+
58
+ Configuration.new(
59
+ exception_reporter:,
60
+ instrumentation_callback:,
61
+ protocol_version:,
62
+ )
63
+ end
64
+
65
+ private
66
+
67
+ def default_exception_reporter
68
+ @default_exception_reporter ||= ->(exception, server_context) {}
69
+ end
70
+
71
+ def default_instrumentation_callback
72
+ @default_instrumentation_callback ||= ->(data) {}
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,33 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module ModelContextProtocol
5
+ module Content
6
+ class Text
7
+ attr_reader :text, :annotations
8
+
9
+ def initialize(text, annotations: nil)
10
+ @text = text
11
+ @annotations = annotations
12
+ end
13
+
14
+ def to_h
15
+ { text:, annotations:, type: "text" }.compact
16
+ end
17
+ end
18
+
19
+ class Image
20
+ attr_reader :data, :mime_type, :annotations
21
+
22
+ def initialize(data, mime_type, annotations: nil)
23
+ @data = data
24
+ @mime_type = mime_type
25
+ @annotations = annotations
26
+ end
27
+
28
+ def to_h
29
+ { data:, mime_type:, annotations:, type: "image" }.compact
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModelContextProtocol
4
+ module Instrumentation
5
+ def instrument_call(method, &block)
6
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
7
+ begin
8
+ @instrumentation_data = {}
9
+ add_instrumentation_data(method:)
10
+
11
+ result = yield block
12
+
13
+ result
14
+ ensure
15
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
+ add_instrumentation_data(duration: end_time - start_time)
17
+
18
+ configuration.instrumentation_callback.call(@instrumentation_data)
19
+ end
20
+ end
21
+
22
+ def add_instrumentation_data(**kwargs)
23
+ @instrumentation_data.merge!(kwargs)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "model_context_protocol/json_rpc/error"
4
+ require "model_context_protocol/json_rpc/request"
5
+ require "model_context_protocol/json_rpc/response"
6
+
7
+ module ModelContextProtocol
8
+ module JsonRPC
9
+ VERSION = "2.0"
10
+ end
11
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModelContextProtocol
4
+ module Methods
5
+ INITIALIZE = "initialize"
6
+ PING = "ping"
7
+ LOGGING_SET_LEVEL = "logging/setLevel"
8
+
9
+ PROMPTS_GET = "prompts/get"
10
+ PROMPTS_LIST = "prompts/list"
11
+ COMPLETION_COMPLETE = "completion/complete"
12
+
13
+ RESOURCES_LIST = "resources/list"
14
+ RESOURCES_READ = "resources/read"
15
+ RESOURCES_TEMPLATES_LIST = "resources/templates/list"
16
+ RESOURCES_SUBSCRIBE = "resources/subscribe"
17
+ RESOURCES_UNSUBSCRIBE = "resources/unsubscribe"
18
+
19
+ TOOLS_CALL = "tools/call"
20
+ TOOLS_LIST = "tools/list"
21
+
22
+ SAMPLING_CREATE_MESSAGE = "sampling/createMessage"
23
+
24
+ class MissingRequiredCapabilityError < StandardError
25
+ attr_reader :method
26
+ attr_reader :capability
27
+
28
+ def initialize(method, capability)
29
+ super("Server does not support #{capability} (required for #{method})")
30
+ @method = method
31
+ @capability = capability
32
+ end
33
+ end
34
+
35
+ extend self
36
+
37
+ ALL_METHODS = [
38
+ SAMPLING_CREATE_MESSAGE,
39
+ INITIALIZE,
40
+ LOGGING_SET_LEVEL,
41
+ PING,
42
+ PROMPTS_GET,
43
+ PROMPTS_LIST,
44
+ COMPLETION_COMPLETE,
45
+ RESOURCES_LIST,
46
+ RESOURCES_READ,
47
+ RESOURCES_TEMPLATES_LIST,
48
+ RESOURCES_SUBSCRIBE,
49
+ RESOURCES_UNSUBSCRIBE,
50
+ TOOLS_CALL,
51
+ TOOLS_LIST,
52
+ ]
53
+
54
+ def ensure_capability!(method, capabilities)
55
+ case method
56
+ when PROMPTS_GET, PROMPTS_LIST, COMPLETION_COMPLETE
57
+ unless capabilities[:prompts]
58
+ raise MissingRequiredCapabilityError.new(method, :prompts)
59
+ end
60
+ when RESOURCES_LIST, RESOURCES_TEMPLATES_LIST, RESOURCES_READ, RESOURCES_SUBSCRIBE, RESOURCES_UNSUBSCRIBE
61
+ unless capabilities[:resources]
62
+ raise MissingRequiredCapabilityError.new(method, :resources)
63
+ end
64
+
65
+ if method == RESOURCES_SUBSCRIBE && !capabilities[:resources][:subscribe]
66
+ raise MissingRequiredCapabilityError.new(method, :resources_subscribe)
67
+ end
68
+ when TOOLS_CALL, TOOLS_LIST
69
+ unless capabilities[:tools]
70
+ raise MissingRequiredCapabilityError.new(method, :tools)
71
+ end
72
+ when SAMPLING_CREATE_MESSAGE
73
+ unless capabilities[:sampling]
74
+ raise MissingRequiredCapabilityError.new(method, :sampling)
75
+ end
76
+ when LOGGING_SET_LEVEL
77
+ # Logging is unsupported by the Server
78
+ unless capabilities[:logging]
79
+ raise MissingRequiredCapabilityError.new(method, :logging)
80
+ end
81
+ when INITIALIZE, PING
82
+ # No specific capability required for initialize or ping
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,21 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module ModelContextProtocol
5
+ class Prompt
6
+ class Argument
7
+ attr_reader :name, :description, :required, :arguments
8
+
9
+ def initialize(name:, description: nil, required: false)
10
+ @name = name
11
+ @description = description
12
+ @required = required
13
+ @arguments = arguments
14
+ end
15
+
16
+ def to_h
17
+ { name:, description:, required: }.compact
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module ModelContextProtocol
5
+ class Prompt
6
+ class Message
7
+ attr_reader :role, :content
8
+
9
+ def initialize(role:, content:)
10
+ @role = role
11
+ @content = content
12
+ end
13
+
14
+ def to_h
15
+ { role:, content: content.to_h }.compact
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module ModelContextProtocol
5
+ class Prompt
6
+ class Result
7
+ attr_reader :description, :messages
8
+
9
+ def initialize(description: nil, messages: [])
10
+ @description = description
11
+ @messages = messages
12
+ end
13
+
14
+ def to_h
15
+ { description:, messages: messages.map(&:to_h) }.compact
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,82 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module ModelContextProtocol
5
+ class Prompt
6
+ class << self
7
+ NOT_SET = Object.new
8
+
9
+ attr_reader :description_value
10
+ attr_reader :arguments_value
11
+
12
+ def template(args, server_context:)
13
+ raise NotImplementedError, "Subclasses must implement template"
14
+ end
15
+
16
+ def to_h
17
+ { name: name_value, description: description_value, arguments: arguments_value.map(&:to_h) }.compact
18
+ end
19
+
20
+ def inherited(subclass)
21
+ super
22
+ subclass.instance_variable_set(:@name_value, nil)
23
+ subclass.instance_variable_set(:@description_value, nil)
24
+ subclass.instance_variable_set(:@arguments_value, nil)
25
+ end
26
+
27
+ def prompt_name(value = NOT_SET)
28
+ if value == NOT_SET
29
+ @name_value
30
+ else
31
+ @name_value = value
32
+ end
33
+ end
34
+
35
+ def name_value
36
+ @name_value || StringUtils.handle_from_class_name(name)
37
+ end
38
+
39
+ def description(value = NOT_SET)
40
+ if value == NOT_SET
41
+ @description_value
42
+ else
43
+ @description_value = value
44
+ end
45
+ end
46
+
47
+ def arguments(value = NOT_SET)
48
+ if value == NOT_SET
49
+ @arguments_value
50
+ else
51
+ @arguments_value = value
52
+ end
53
+ end
54
+
55
+ def define(name: nil, description: nil, arguments: [], &block)
56
+ Class.new(self) do
57
+ prompt_name name
58
+ description description
59
+ arguments arguments
60
+ define_singleton_method(:template) do |args, server_context:|
61
+ instance_exec(args, server_context:, &block)
62
+ end
63
+ end
64
+ end
65
+
66
+ def validate_arguments!(args)
67
+ missing = required_args - args.keys
68
+ return if missing.empty?
69
+
70
+ raise ModelContextProtocol::Server::RequestHandlerError.new(
71
+ "Missing required arguments: #{missing.join(", ")}", nil, error_type: :missing_required_arguments
72
+ )
73
+ end
74
+
75
+ private
76
+
77
+ def required_args
78
+ arguments_value.filter_map { |arg| arg.name.to_sym if arg.required }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,45 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module ModelContextProtocol
5
+ class Resource
6
+ class Contents
7
+ attr_reader :uri, :mime_type
8
+
9
+ def initialize(uri:, mime_type: nil)
10
+ @uri = uri
11
+ @mime_type = mime_type
12
+ end
13
+
14
+ def to_h
15
+ { uri:, mime_type: }.compact
16
+ end
17
+ end
18
+
19
+ class TextContents < Contents
20
+ attr_reader :text
21
+
22
+ def initialize(text:, uri:, mime_type:)
23
+ super(uri: uri, mime_type: mime_type)
24
+ @text = text
25
+ end
26
+
27
+ def to_h
28
+ super.merge(text: text)
29
+ end
30
+ end
31
+
32
+ class BlobContents < Contents
33
+ attr_reader :data
34
+
35
+ def initialize(data:, uri:, mime_type:)
36
+ super(uri: uri, mime_type: mime_type)
37
+ @data = data
38
+ end
39
+
40
+ def to_h
41
+ super.merge(data: data)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,18 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module ModelContextProtocol
5
+ class Resource
6
+ class Embedded
7
+ attr_reader :resource, :annotations
8
+
9
+ def initialize(resource:, annotations: nil)
10
+ @annotations = annotations
11
+ end
12
+
13
+ def to_h
14
+ { resource: resource.to_h, annotations: }.compact
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module ModelContextProtocol
5
+ class Resource
6
+ attr_reader :uri, :name, :description, :mime_type
7
+
8
+ def initialize(uri:, name:, description:, mime_type:)
9
+ @uri = uri
10
+ @name = name
11
+ @description = description
12
+ @mime_type = mime_type
13
+ end
14
+
15
+ def to_h
16
+ {
17
+ uri: @uri,
18
+ name: @name,
19
+ description: @description,
20
+ mimeType: @mime_type,
21
+ }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module ModelContextProtocol
5
+ class ResourceTemplate
6
+ attr_reader :uri_template, :name, :description, :mime_type
7
+
8
+ def initialize(uri_template:, name:, description: nil, mime_type: nil)
9
+ @uri_template = uri_template
10
+ @name = name
11
+ @description = description
12
+ @mime_type = mime_type
13
+ end
14
+
15
+ def to_h
16
+ {
17
+ uriTemplate: @uri_template,
18
+ name: @name,
19
+ description: @description,
20
+ mimeType: @mime_type,
21
+ }.compact
22
+ end
23
+ end
24
+ end