mcp-rb 0.2.0 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +27 -1
- data/lib/mcp/app/resource.rb +25 -2
- data/lib/mcp/app/resource_template.rb +142 -0
- data/lib/mcp/app/tool.rb +1 -1
- data/lib/mcp/app.rb +2 -0
- data/lib/mcp/client.rb +123 -0
- data/lib/mcp/constants.rb +1 -0
- data/lib/mcp/delegator.rb +1 -1
- data/lib/mcp/server.rb +27 -5
- data/lib/mcp/version.rb +1 -2
- data/lib/mcp.rb +1 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ff3f38b8c9fec5aac8fce9d0906f956a77f481ac0477c273e9b677948d0a8cb
|
4
|
+
data.tar.gz: 116def752685bfa0792c6d2b1dc6bd75e6b029cec03205fb2a67b00d84532cfc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3ed62e7606ff685a999c7f95118167e30d8ecb6a6ff35a260223b60fed2fc65599a79bd22424dfde3b0bfc4cd1b3bdf1cb46dcb6877c7720e8837021f456d8cf
|
7
|
+
data.tar.gz: 5536f506e8e2c89a73fe34acc6edc3981b444672ca8cb3730e089eea22fa9194cee33e6afe26a2442175441487283828aa29103e78b39c8387714d2f1c6a5a82
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [0.3.1] - 2025-03-05
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Add `resources/templates/list` method: https://github.com/funwarioisii/mcp-rb/pull/5
|
12
|
+
|
13
|
+
## [0.3.0] - 2025-02-19
|
14
|
+
|
15
|
+
- Allow specifying the version via DSL keyword: https://github.com/funwarioisii/mcp-rb/pull/2
|
16
|
+
- Add MCP Client: https://github.com/funwarioisii/mcp-rb/pull/3
|
17
|
+
|
18
|
+
### Breaking Changes
|
19
|
+
- `MCP::PROTOCOL_VERSION` is moved to `MCP::Constants::PROTOCOL_VERSION`
|
20
|
+
- https://github.com/funwarioisii/mcp-rb/pull/3/commits/caad65500935a8eebfe024dbd25de0d16868c44e
|
21
|
+
|
8
22
|
## [0.2.0] - 2025-02-14
|
9
23
|
|
10
24
|
### Breaking Changes
|
data/README.md
CHANGED
@@ -19,6 +19,8 @@ require 'mcp'
|
|
19
19
|
|
20
20
|
name "hello-world"
|
21
21
|
|
22
|
+
version "1.0.0"
|
23
|
+
|
22
24
|
# Define a resource
|
23
25
|
resource "hello://world" do
|
24
26
|
name "Hello World"
|
@@ -26,6 +28,13 @@ resource "hello://world" do
|
|
26
28
|
call { "Hello, World!" }
|
27
29
|
end
|
28
30
|
|
31
|
+
# Define a resource template
|
32
|
+
resource_template "hello://{user_name}" do
|
33
|
+
name "Hello User"
|
34
|
+
description "A simple hello user message"
|
35
|
+
call { |args| "Hello, #{args[:user_name]}!" }
|
36
|
+
end
|
37
|
+
|
29
38
|
# Define a tool
|
30
39
|
tool "greet" do
|
31
40
|
description "Greet someone by name"
|
@@ -36,6 +45,24 @@ tool "greet" do
|
|
36
45
|
end
|
37
46
|
```
|
38
47
|
|
48
|
+
## Supported specifications
|
49
|
+
|
50
|
+
Reference: [MCP 2024-11-05](https://spec.modelcontextprotocol.io/specification/2024-11-05/)
|
51
|
+
|
52
|
+
- [Base Protocol](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/)
|
53
|
+
- ping
|
54
|
+
- stdio transport
|
55
|
+
- [Server features](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/)
|
56
|
+
- Resources
|
57
|
+
- resources/read
|
58
|
+
- resources/list
|
59
|
+
- resources/templates/list
|
60
|
+
- Tools
|
61
|
+
- tools/list
|
62
|
+
- tools/call
|
63
|
+
|
64
|
+
Any capabilities are not supported yet.
|
65
|
+
|
39
66
|
## Testing
|
40
67
|
|
41
68
|
```bash
|
@@ -85,4 +112,3 @@ gem push mcp-rb-*.gem
|
|
85
112
|
## Changelog
|
86
113
|
|
87
114
|
See [CHANGELOG.md](CHANGELOG.md)
|
88
|
-
|
data/lib/mcp/app/resource.rb
CHANGED
@@ -69,11 +69,11 @@ module MCP
|
|
69
69
|
|
70
70
|
if page_size.nil?
|
71
71
|
paginated = values[start_index..]
|
72
|
-
next_cursor =
|
72
|
+
next_cursor = nil
|
73
73
|
else
|
74
74
|
paginated = values[start_index, page_size]
|
75
75
|
has_next = start_index + page_size < values.length
|
76
|
-
next_cursor = has_next ? (start_index + page_size).to_s :
|
76
|
+
next_cursor = has_next ? (start_index + page_size).to_s : nil
|
77
77
|
end
|
78
78
|
|
79
79
|
{
|
@@ -84,6 +84,29 @@ module MCP
|
|
84
84
|
|
85
85
|
def read_resource(uri)
|
86
86
|
resource = resources[uri]
|
87
|
+
|
88
|
+
# If no direct match, check if it matches a template
|
89
|
+
if resource.nil? && respond_to?(:find_matching_template)
|
90
|
+
template, variable_values = find_matching_template(uri)
|
91
|
+
|
92
|
+
if template
|
93
|
+
begin
|
94
|
+
# Call the template handler with the extracted variables
|
95
|
+
content = template[:handler].call(variable_values)
|
96
|
+
return {
|
97
|
+
contents: [{
|
98
|
+
uri: uri,
|
99
|
+
mimeType: template[:mime_type],
|
100
|
+
text: content
|
101
|
+
}]
|
102
|
+
}
|
103
|
+
rescue => e
|
104
|
+
raise ArgumentError, "Error reading resource from template: #{e.message}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# If we still don't have a resource, raise an error
|
87
110
|
raise ArgumentError, "Resource not found: #{uri}" unless resource
|
88
111
|
|
89
112
|
begin
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "resource"
|
4
|
+
|
5
|
+
module MCP
|
6
|
+
class App
|
7
|
+
module ResourceTemplate
|
8
|
+
def resource_templates
|
9
|
+
@resource_templates ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
class ResourceTemplateBuilder
|
13
|
+
attr_reader :uri_template, :name, :description, :mime_type, :handler
|
14
|
+
|
15
|
+
def initialize(uri_template)
|
16
|
+
raise ArgumentError, "Resource URI template cannot be nil or empty" if uri_template.nil? || uri_template.empty?
|
17
|
+
@uri_template = uri_template
|
18
|
+
@name = ""
|
19
|
+
@description = ""
|
20
|
+
@mime_type = "text/plain"
|
21
|
+
@handler = nil
|
22
|
+
@variables = extract_variables(uri_template)
|
23
|
+
end
|
24
|
+
|
25
|
+
# standard:disable Lint/DuplicateMethods,Style/TrivialAccessors
|
26
|
+
def name(value)
|
27
|
+
@name = value
|
28
|
+
end
|
29
|
+
# standard:enable Lint/DuplicateMethods,Style/TrivialAccessors
|
30
|
+
|
31
|
+
# standard:disable Lint/DuplicateMethods,Style/TrivialAccessors
|
32
|
+
def description(text)
|
33
|
+
@description = text
|
34
|
+
end
|
35
|
+
# standard:enable Lint/DuplicateMethods,Style/TrivialAccessors
|
36
|
+
|
37
|
+
# standard:disable Lint/DuplicateMethods,Style/TrivialAccessors
|
38
|
+
def mime_type(value)
|
39
|
+
@mime_type = value
|
40
|
+
end
|
41
|
+
# standard:enable Lint/DuplicateMethods,Style/TrivialAccessors
|
42
|
+
|
43
|
+
def call(&block)
|
44
|
+
@handler = block
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_resource_template_hash
|
48
|
+
raise ArgumentError, "Name must be provided" if @name.empty?
|
49
|
+
|
50
|
+
{
|
51
|
+
uri_template: @uri_template,
|
52
|
+
name: @name,
|
53
|
+
mime_type: @mime_type,
|
54
|
+
description: @description,
|
55
|
+
handler: @handler,
|
56
|
+
variables: @variables
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Extract variables from a URI template
|
61
|
+
# e.g., "channels://{channel_id}" => ["channel_id"]
|
62
|
+
def extract_variables(uri_template)
|
63
|
+
variables = []
|
64
|
+
uri_template.scan(/\{([^}]+)\}/) do |match|
|
65
|
+
variables << match[0]&.to_sym
|
66
|
+
end
|
67
|
+
variables
|
68
|
+
end
|
69
|
+
|
70
|
+
# Creates a pattern for matching URIs against this template
|
71
|
+
def to_pattern
|
72
|
+
pattern_string = Regexp.escape(@uri_template).gsub(/\\\{[^}]+\\\}/) do |match|
|
73
|
+
"([^/]+)"
|
74
|
+
end
|
75
|
+
Regexp.new("^#{pattern_string}$")
|
76
|
+
end
|
77
|
+
|
78
|
+
# Extract variable values from a concrete URI based on the template
|
79
|
+
# e.g., template: "channels://{channel_id}", uri: "channels://123" => {"channel_id" => "123"}
|
80
|
+
def extract_variable_values(uri)
|
81
|
+
pattern = to_pattern
|
82
|
+
match = pattern.match(uri)
|
83
|
+
return {} unless match
|
84
|
+
|
85
|
+
result = {}
|
86
|
+
@variables.each_with_index do |var_name, index|
|
87
|
+
result[var_name] = match[index + 1]
|
88
|
+
end
|
89
|
+
result
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def register_resource_template(uri_template, &block)
|
94
|
+
builder = ResourceTemplateBuilder.new(uri_template)
|
95
|
+
builder.instance_eval(&block)
|
96
|
+
template_hash = builder.to_resource_template_hash
|
97
|
+
resource_templates[uri_template] = template_hash
|
98
|
+
template_hash
|
99
|
+
end
|
100
|
+
|
101
|
+
# Find a template that matches the given URI and extract variable values
|
102
|
+
def find_matching_template(uri)
|
103
|
+
resource_templates.each do |template_uri, template|
|
104
|
+
builder = ResourceTemplateBuilder.new(template_uri)
|
105
|
+
variable_values = builder.extract_variable_values(uri)
|
106
|
+
return [template, variable_values] unless variable_values.empty?
|
107
|
+
end
|
108
|
+
[nil, {}]
|
109
|
+
end
|
110
|
+
|
111
|
+
def list_resource_templates(cursor: nil, page_size: nil)
|
112
|
+
start_index = cursor&.to_i || 0
|
113
|
+
values = resource_templates.values
|
114
|
+
|
115
|
+
if page_size.nil?
|
116
|
+
paginated = values[start_index..]
|
117
|
+
next_cursor = nil
|
118
|
+
else
|
119
|
+
paginated = values[start_index, page_size]
|
120
|
+
has_next = start_index + page_size < values.length
|
121
|
+
next_cursor = has_next ? (start_index + page_size).to_s : nil
|
122
|
+
end
|
123
|
+
|
124
|
+
{
|
125
|
+
resourceTemplates: paginated.map { |t| format_resource_template(t) },
|
126
|
+
nextCursor: next_cursor
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def format_resource_template(template)
|
133
|
+
{
|
134
|
+
uriTemplate: template[:uri_template],
|
135
|
+
name: template[:name],
|
136
|
+
description: template[:description],
|
137
|
+
mimeType: template[:mime_type]
|
138
|
+
}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
data/lib/mcp/app/tool.rb
CHANGED
@@ -77,7 +77,7 @@ module MCP
|
|
77
77
|
tool_values = tools.values
|
78
78
|
start_index = cursor ? cursor.to_i : 0
|
79
79
|
paginated = tool_values[start_index, page_size]
|
80
|
-
next_cursor = (start_index + page_size < tool_values.length) ? (start_index + page_size).to_s :
|
80
|
+
next_cursor = (start_index + page_size < tool_values.length) ? (start_index + page_size).to_s : nil
|
81
81
|
|
82
82
|
{
|
83
83
|
tools: paginated.map { |t| format_tool(t) },
|
data/lib/mcp/app.rb
CHANGED
data/lib/mcp/client.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "open3"
|
5
|
+
require "securerandom"
|
6
|
+
require_relative "constants"
|
7
|
+
|
8
|
+
module MCP
|
9
|
+
class Client
|
10
|
+
attr_reader :command, :args, :process, :stdin, :stdout, :stderr, :wait_thread
|
11
|
+
|
12
|
+
def initialize(command:, args: [], name: "mcp-client", version: VERSION)
|
13
|
+
@command = command
|
14
|
+
@args = args
|
15
|
+
@process = nil
|
16
|
+
@name = name
|
17
|
+
@version = version
|
18
|
+
end
|
19
|
+
|
20
|
+
def connect
|
21
|
+
return if @process
|
22
|
+
|
23
|
+
start_server
|
24
|
+
initialize_connection
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def running? = !@process.nil?
|
29
|
+
|
30
|
+
def list_tools
|
31
|
+
ensure_running
|
32
|
+
send_request({
|
33
|
+
jsonrpc: Constants::JSON_RPC_VERSION,
|
34
|
+
method: Constants::RequestMethods::TOOLS_LIST,
|
35
|
+
params: {},
|
36
|
+
id: SecureRandom.uuid
|
37
|
+
})
|
38
|
+
end
|
39
|
+
|
40
|
+
def call_tool(name:, args: {})
|
41
|
+
ensure_running
|
42
|
+
send_request({
|
43
|
+
jsonrpc: Constants::JSON_RPC_VERSION,
|
44
|
+
method: Constants::RequestMethods::TOOLS_CALL,
|
45
|
+
params: {
|
46
|
+
name: name,
|
47
|
+
arguments: args
|
48
|
+
},
|
49
|
+
id: SecureRandom.uuid
|
50
|
+
})
|
51
|
+
end
|
52
|
+
|
53
|
+
def close
|
54
|
+
return unless @process
|
55
|
+
|
56
|
+
@stdin.close
|
57
|
+
@stdout.close
|
58
|
+
@stderr.close
|
59
|
+
Process.kill("TERM", @process)
|
60
|
+
@wait_thread.join
|
61
|
+
@process = nil
|
62
|
+
rescue IOError, Errno::ESRCH
|
63
|
+
# プロセスが既に終了している場合は無視
|
64
|
+
@process = nil
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def ensure_running
|
70
|
+
raise "Server process not running. Call #start first." unless running?
|
71
|
+
end
|
72
|
+
|
73
|
+
def initialize_connection
|
74
|
+
response = send_request({
|
75
|
+
jsonrpc: Constants::JSON_RPC_VERSION,
|
76
|
+
method: "initialize",
|
77
|
+
params: {
|
78
|
+
protocolVersion: Constants::PROTOCOL_VERSION,
|
79
|
+
client: {
|
80
|
+
name: @name,
|
81
|
+
version: @version
|
82
|
+
}
|
83
|
+
},
|
84
|
+
id: SecureRandom.uuid
|
85
|
+
})
|
86
|
+
|
87
|
+
@stdin.puts(JSON.generate({
|
88
|
+
jsonrpc: Constants::JSON_RPC_VERSION,
|
89
|
+
method: "notifications/initialized"
|
90
|
+
}))
|
91
|
+
|
92
|
+
response
|
93
|
+
end
|
94
|
+
|
95
|
+
def start_server
|
96
|
+
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(@command, *@args)
|
97
|
+
@process = @wait_thread.pid
|
98
|
+
|
99
|
+
Thread.new do
|
100
|
+
while (line = @stderr.gets)
|
101
|
+
warn "[MCP Server] #{line}"
|
102
|
+
end
|
103
|
+
rescue IOError
|
104
|
+
# ignore when stream is closed
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def send_request(request)
|
109
|
+
@stdin.puts(JSON.generate(request))
|
110
|
+
response = @stdout.gets
|
111
|
+
raise "No response from server" unless response
|
112
|
+
|
113
|
+
result = JSON.parse(response, symbolize_names: true)
|
114
|
+
if result[:error]
|
115
|
+
raise "Server error: #{result[:error][:message]} (#{result[:error][:code]})"
|
116
|
+
end
|
117
|
+
|
118
|
+
result[:result]
|
119
|
+
rescue JSON::ParserError => e
|
120
|
+
raise "Invalid JSON response: #{e.message}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/lib/mcp/constants.rb
CHANGED
data/lib/mcp/delegator.rb
CHANGED
data/lib/mcp/server.rb
CHANGED
@@ -3,26 +3,34 @@
|
|
3
3
|
require "json"
|
4
4
|
require "English"
|
5
5
|
require "uri"
|
6
|
+
require_relative "constants"
|
6
7
|
|
7
8
|
module MCP
|
8
9
|
class Server
|
9
|
-
|
10
|
+
attr_writer :name, :version
|
10
11
|
attr_reader :initialized
|
11
12
|
|
12
|
-
def initialize(name:, version:
|
13
|
+
def initialize(name:, version: "0.1.0")
|
13
14
|
@name = name
|
14
15
|
@version = version
|
15
16
|
@app = App.new
|
16
17
|
@initialized = false
|
17
|
-
@supported_protocol_versions = [PROTOCOL_VERSION]
|
18
|
+
@supported_protocol_versions = [Constants::PROTOCOL_VERSION]
|
18
19
|
end
|
19
20
|
|
20
|
-
def name(value = nil)
|
21
|
+
def name(value = nil)
|
21
22
|
return @name if value.nil?
|
22
23
|
|
23
24
|
@name = value
|
24
25
|
end
|
25
26
|
|
27
|
+
def version(value = nil)
|
28
|
+
return @version if value.nil?
|
29
|
+
|
30
|
+
@version = value
|
31
|
+
@supported_protocol_versions << value
|
32
|
+
end
|
33
|
+
|
26
34
|
def tool(name, &block)
|
27
35
|
@app.register_tool(name, &block)
|
28
36
|
end
|
@@ -31,6 +39,10 @@ module MCP
|
|
31
39
|
@app.register_resource(uri, &block)
|
32
40
|
end
|
33
41
|
|
42
|
+
def resource_template(uri_template, &block)
|
43
|
+
@app.register_resource_template(uri_template, &block)
|
44
|
+
end
|
45
|
+
|
34
46
|
def run
|
35
47
|
while (input = $stdin.gets)
|
36
48
|
process_input(input)
|
@@ -49,6 +61,10 @@ module MCP
|
|
49
61
|
@app.list_resources[:resources]
|
50
62
|
end
|
51
63
|
|
64
|
+
def list_resource_templates
|
65
|
+
@app.list_resource_templates[:resourceTemplates]
|
66
|
+
end
|
67
|
+
|
52
68
|
def read_resource(uri)
|
53
69
|
@app.read_resource(uri).dig(:contents, 0, :text)
|
54
70
|
end
|
@@ -113,7 +129,7 @@ module MCP
|
|
113
129
|
jsonrpc: MCP::Constants::JSON_RPC_VERSION,
|
114
130
|
id: request[:id],
|
115
131
|
result: {
|
116
|
-
protocolVersion: PROTOCOL_VERSION,
|
132
|
+
protocolVersion: Constants::PROTOCOL_VERSION,
|
117
133
|
capabilities: {
|
118
134
|
logging: {},
|
119
135
|
prompts: {
|
@@ -165,6 +181,12 @@ module MCP
|
|
165
181
|
success_response(request[:id], result)
|
166
182
|
end
|
167
183
|
|
184
|
+
def handle_list_resources_templates(request)
|
185
|
+
cursor = request.dig(:params, :cursor)
|
186
|
+
result = @app.list_resource_templates(cursor:)
|
187
|
+
success_response(request[:id], result)
|
188
|
+
end
|
189
|
+
|
168
190
|
def handle_read_resource(request)
|
169
191
|
uri = request.dig(:params, :uri)
|
170
192
|
result = @app.read_resource(uri)
|
data/lib/mcp/version.rb
CHANGED
data/lib/mcp.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mcp-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- funwarioisii
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-03-04 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
12
|
description: MCP-RB is a Ruby framework that provides a Sinatra-like DSL for implementing
|
13
13
|
Model Context Protocol servers.
|
@@ -23,7 +23,9 @@ files:
|
|
23
23
|
- lib/mcp.rb
|
24
24
|
- lib/mcp/app.rb
|
25
25
|
- lib/mcp/app/resource.rb
|
26
|
+
- lib/mcp/app/resource_template.rb
|
26
27
|
- lib/mcp/app/tool.rb
|
28
|
+
- lib/mcp/client.rb
|
27
29
|
- lib/mcp/constants.rb
|
28
30
|
- lib/mcp/delegator.rb
|
29
31
|
- lib/mcp/server.rb
|
@@ -49,7 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
51
|
- !ruby/object:Gem::Version
|
50
52
|
version: '0'
|
51
53
|
requirements: []
|
52
|
-
rubygems_version: 3.6.
|
54
|
+
rubygems_version: 3.6.3
|
53
55
|
specification_version: 4
|
54
56
|
summary: A lightweight Ruby framework for implementing MCP (Model Context Protocol)
|
55
57
|
servers
|