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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28e6b1fc53252ecd7717e71d06f0c124c61a8fbb9e389fa9514f3249b056930e
4
- data.tar.gz: 454af20d33b8cbe80dad61214b0ca3680a8715bef024d4d26dd77a5b800e37ba
3
+ metadata.gz: 4ff3f38b8c9fec5aac8fce9d0906f956a77f481ac0477c273e9b677948d0a8cb
4
+ data.tar.gz: 116def752685bfa0792c6d2b1dc6bd75e6b029cec03205fb2a67b00d84532cfc
5
5
  SHA512:
6
- metadata.gz: 6aca66d612f5851304248561e0eb73402ef30d30da6fd45c35aea89b43d6499c29da58a37f6deae55931a3ede9eb56de4cf447595adaee8f227836e8923f1823
7
- data.tar.gz: cde9c8f9b18b6945d5e5e478ea18e1592c372393b1263f329b35bc0510df58110feda73ae1fcd785d93af0e701fe6bfaab200296845d7b601ba33525337f654c
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
-
@@ -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
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "app/resource"
4
+ require_relative "app/resource_template"
4
5
  require_relative "app/tool"
5
6
 
6
7
  module MCP
7
8
  class App
8
9
  include Resource
10
+ include ResourceTemplate
9
11
  include Tool
10
12
  end
11
13
  end
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
@@ -3,6 +3,7 @@
3
3
  module MCP
4
4
  module Constants
5
5
  JSON_RPC_VERSION = "2.0"
6
+ PROTOCOL_VERSION = "2024-11-05"
6
7
 
7
8
  module ErrorCodes
8
9
  NOT_INITIALIZED = -32_002
data/lib/mcp/delegator.rb CHANGED
@@ -15,6 +15,6 @@ module MCP
15
15
  end
16
16
  end
17
17
 
18
- delegate :name, :resource, :tool
18
+ delegate :name, :version, :resource, :resource_template, :tool
19
19
  end
20
20
  end
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
- attr_accessor :name
10
+ attr_writer :name, :version
10
11
  attr_reader :initialized
11
12
 
12
- def initialize(name:, version: 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) # standard:disable Lint/DuplicateMethods
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
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MCP
4
- PROTOCOL_VERSION = "2024-11-05"
5
- VERSION = "0.2.0"
4
+ VERSION = "0.3.1"
6
5
  end
data/lib/mcp.rb CHANGED
@@ -8,6 +8,7 @@ require_relative "mcp/constants"
8
8
  require_relative "mcp/app"
9
9
  require_relative "mcp/server"
10
10
  require_relative "mcp/delegator"
11
+ require_relative "mcp/client"
11
12
 
12
13
  module MCP
13
14
  class << self
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.2.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-02-14 00:00:00.000000000 Z
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.2
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