mcp 0.1.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 +7 -0
- data/.cursor/rules/release-changelogs.mdc +30 -0
- data/.gitattributes +4 -0
- data/.github/workflows/ci.yml +33 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +10 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +500 -0
- data/Rakefile +17 -0
- data/bin/console +15 -0
- data/bin/rake +31 -0
- data/bin/setup +8 -0
- data/dev.yml +31 -0
- data/examples/stdio_server.rb +95 -0
- data/lib/mcp/configuration.rb +75 -0
- data/lib/mcp/content.rb +33 -0
- data/lib/mcp/instrumentation.rb +26 -0
- data/lib/mcp/methods.rb +73 -0
- data/lib/mcp/prompt/argument.rb +21 -0
- data/lib/mcp/prompt/message.rb +19 -0
- data/lib/mcp/prompt/result.rb +19 -0
- data/lib/mcp/prompt.rb +82 -0
- data/lib/mcp/resource/contents.rb +45 -0
- data/lib/mcp/resource/embedded.rb +18 -0
- data/lib/mcp/resource.rb +24 -0
- data/lib/mcp/resource_template.rb +24 -0
- data/lib/mcp/server.rb +299 -0
- data/lib/mcp/string_utils.rb +26 -0
- data/lib/mcp/tool/annotations.rb +27 -0
- data/lib/mcp/tool/input_schema.rb +26 -0
- data/lib/mcp/tool/response.rb +18 -0
- data/lib/mcp/tool.rb +85 -0
- data/lib/mcp/transport.rb +33 -0
- data/lib/mcp/transports/stdio.rb +35 -0
- data/lib/mcp/version.rb +5 -0
- data/lib/mcp.rb +41 -0
- data/mcp.gemspec +33 -0
- metadata +126 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCP
|
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
|
data/lib/mcp/content.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module MCP
|
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 MCP
|
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
|
data/lib/mcp/methods.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCP
|
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
|
+
def ensure_capability!(method, capabilities)
|
38
|
+
case method
|
39
|
+
when PROMPTS_GET, PROMPTS_LIST
|
40
|
+
unless capabilities[:prompts]
|
41
|
+
raise MissingRequiredCapabilityError.new(method, :prompts)
|
42
|
+
end
|
43
|
+
when RESOURCES_LIST, RESOURCES_TEMPLATES_LIST, RESOURCES_READ, RESOURCES_SUBSCRIBE, RESOURCES_UNSUBSCRIBE
|
44
|
+
unless capabilities[:resources]
|
45
|
+
raise MissingRequiredCapabilityError.new(method, :resources)
|
46
|
+
end
|
47
|
+
|
48
|
+
if method == RESOURCES_SUBSCRIBE && !capabilities[:resources][:subscribe]
|
49
|
+
raise MissingRequiredCapabilityError.new(method, :resources_subscribe)
|
50
|
+
end
|
51
|
+
when TOOLS_CALL, TOOLS_LIST
|
52
|
+
unless capabilities[:tools]
|
53
|
+
raise MissingRequiredCapabilityError.new(method, :tools)
|
54
|
+
end
|
55
|
+
when SAMPLING_CREATE_MESSAGE
|
56
|
+
unless capabilities[:sampling]
|
57
|
+
raise MissingRequiredCapabilityError.new(method, :sampling)
|
58
|
+
end
|
59
|
+
when COMPLETION_COMPLETE
|
60
|
+
unless capabilities[:completions]
|
61
|
+
raise MissingRequiredCapabilityError.new(method, :completions)
|
62
|
+
end
|
63
|
+
when LOGGING_SET_LEVEL
|
64
|
+
# Logging is unsupported by the Server
|
65
|
+
unless capabilities[:logging]
|
66
|
+
raise MissingRequiredCapabilityError.new(method, :logging)
|
67
|
+
end
|
68
|
+
when INITIALIZE, PING
|
69
|
+
# No specific capability required for initialize or ping
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module MCP
|
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 MCP
|
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 MCP
|
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
|
data/lib/mcp/prompt.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module MCP
|
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 MCP::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 MCP
|
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 MCP
|
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
|
data/lib/mcp/resource.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module MCP
|
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
|
+
}.compact
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module MCP
|
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
|