mcp-rb 0.3.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: 131c29522f8eb44f7f613f51d92db433370d99950dcfcd27340ab9917b306dd3
4
- data.tar.gz: a676110b09851f7018205bc012b09a6f22d1c7730555ba569b9b7ae475c06009
3
+ metadata.gz: 4ff3f38b8c9fec5aac8fce9d0906f956a77f481ac0477c273e9b677948d0a8cb
4
+ data.tar.gz: 116def752685bfa0792c6d2b1dc6bd75e6b029cec03205fb2a67b00d84532cfc
5
5
  SHA512:
6
- metadata.gz: ebe914ec0fa9ba0ef97650f83454d0fe5bf5c924717365d5cea3bdd73bfb06b5e0ede7d49c6da2caa8d38c5a337bae3c98c22b86d5f1c25740785c66ac195509
7
- data.tar.gz: 719f1f95950ca24c0695316af978866e1b8834b5112c962701324d8279faa9e5705151157d23a66e7f9e6572313df18f7c4acfc247037965d059f12bdcacb883
6
+ metadata.gz: 3ed62e7606ff685a999c7f95118167e30d8ecb6a6ff35a260223b60fed2fc65599a79bd22424dfde3b0bfc4cd1b3bdf1cb46dcb6877c7720e8837021f456d8cf
7
+ data.tar.gz: 5536f506e8e2c89a73fe34acc6edc3981b444672ca8cb3730e089eea22fa9194cee33e6afe26a2442175441487283828aa29103e78b39c8387714d2f1c6a5a82
data/CHANGELOG.md CHANGED
@@ -5,6 +5,11 @@ 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
+
8
13
  ## [0.3.0] - 2025-02-19
9
14
 
10
15
  - Allow specifying the version via DSL keyword: https://github.com/funwarioisii/mcp-rb/pull/2
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"
@@ -47,6 +56,7 @@ Reference: [MCP 2024-11-05](https://spec.modelcontextprotocol.io/specification/2
47
56
  - Resources
48
57
  - resources/read
49
58
  - resources/list
59
+ - resources/templates/list
50
60
  - Tools
51
61
  - tools/list
52
62
  - tools/call
@@ -102,4 +112,3 @@ gem push mcp-rb-*.gem
102
112
  ## Changelog
103
113
 
104
114
  See [CHANGELOG.md](CHANGELOG.md)
105
-
@@ -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/delegator.rb CHANGED
@@ -15,6 +15,6 @@ module MCP
15
15
  end
16
16
  end
17
17
 
18
- delegate :name, :version, :resource, :tool
18
+ delegate :name, :version, :resource, :resource_template, :tool
19
19
  end
20
20
  end
data/lib/mcp/server.rb CHANGED
@@ -28,6 +28,7 @@ module MCP
28
28
  return @version if value.nil?
29
29
 
30
30
  @version = value
31
+ @supported_protocol_versions << value
31
32
  end
32
33
 
33
34
  def tool(name, &block)
@@ -38,6 +39,10 @@ module MCP
38
39
  @app.register_resource(uri, &block)
39
40
  end
40
41
 
42
+ def resource_template(uri_template, &block)
43
+ @app.register_resource_template(uri_template, &block)
44
+ end
45
+
41
46
  def run
42
47
  while (input = $stdin.gets)
43
48
  process_input(input)
@@ -56,6 +61,10 @@ module MCP
56
61
  @app.list_resources[:resources]
57
62
  end
58
63
 
64
+ def list_resource_templates
65
+ @app.list_resource_templates[:resourceTemplates]
66
+ end
67
+
59
68
  def read_resource(uri)
60
69
  @app.read_resource(uri).dig(:contents, 0, :text)
61
70
  end
@@ -172,6 +181,12 @@ module MCP
172
181
  success_response(request[:id], result)
173
182
  end
174
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
+
175
190
  def handle_read_resource(request)
176
191
  uri = request.dig(:params, :uri)
177
192
  result = @app.read_resource(uri)
data/lib/mcp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MCP
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
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.3.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-19 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,6 +23,7 @@ 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
27
28
  - lib/mcp/client.rb
28
29
  - lib/mcp/constants.rb