Skip to content

Commit 87c0764

Browse files
Refactor MCP primitives to use shared base class and enable class-based Resources
- Introduce `MCP::Primitive` to encapsulate common logic (name, title, description, icons, meta) for all MCP primitives. - Refactor `MCP::Tool`, `MCP::Resource`, `MCP::ResourceTemplate`, and `MCP::Prompt` to inherit from `MCP::Primitive`, reducing code duplication. - Enable class-based definitions for `MCP::Resource` and `MCP::ResourceTemplate`, aligning them with the `Tool` pattern. - Update `MCP::Server` to support registering class-based resources and templates. - Add tests for `Primitive`, `Resource`, and `ResourceTemplate` covering the new inheritance structure. - Update `README.md` with examples of the new class-based Resource and Resource Template APIs. - Preserve backward compatibility for instance-based creation of Resources and Templates.
1 parent ade3d1f commit 87c0764

File tree

11 files changed

+487
-170
lines changed

11 files changed

+487
-170
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,33 @@ end
849849

850850
otherwise `resources/read` requests will be a no-op.
851851

852+
Alternatively, you can define resources as classes, which allows them to be self-contained:
853+
854+
```ruby
855+
class MyResource < MCP::Resource
856+
uri "https://example.com/my_resource"
857+
resource_name "my-resource"
858+
title "My Resource"
859+
description "Lorem ipsum dolor sit amet"
860+
mime_type "text/html"
861+
862+
def self.contents
863+
[
864+
MCP::Resource::TextContents.new(
865+
uri: uri,
866+
mime_type: mime_type,
867+
text: "Hello from example resource!"
868+
)
869+
]
870+
end
871+
end
872+
873+
server = MCP::Server.new(
874+
name: "my_server",
875+
resources: [MyResource]
876+
)
877+
```
878+
852879
### Resource Templates
853880

854881
The `MCP::ResourceTemplate` class provides a way to register resource templates with the server.
@@ -868,6 +895,34 @@ server = MCP::Server.new(
868895
)
869896
```
870897

898+
Like Resources, you can also define Resource Templates as classes:
899+
900+
```ruby
901+
class MyResourceTemplate < MCP::ResourceTemplate
902+
uri_template "https://example.com/items/{item_id}"
903+
name "item-resource-template"
904+
title "Item Resource Template"
905+
description "Template for retrieving item details"
906+
mime_type "application/json"
907+
908+
def self.contents(params:)
909+
item_id = params[:item_id]
910+
[
911+
MCP::Resource::TextContents.new(
912+
uri: "https://example.com/items/#{item_id}",
913+
mime_type: "application/json",
914+
text: { id: item_id, name: "Item #{item_id}" }.to_json
915+
)
916+
]
917+
end
918+
end
919+
920+
server = MCP::Server.new(
921+
name: "my_server",
922+
resource_templates: [MyResourceTemplate]
923+
)
924+
```
925+
871926
## Building an MCP Client
872927

873928
The `MCP::Client` class provides an interface for interacting with MCP servers.

lib/mcp.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require_relative "mcp/icon"
77
require_relative "mcp/instrumentation"
88
require_relative "mcp/methods"
9+
require_relative "mcp/primitive"
910
require_relative "mcp/prompt"
1011
require_relative "mcp/prompt/argument"
1112
require_relative "mcp/prompt/message"

lib/mcp/primitive.rb

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# frozen_string_literal: true
2+
3+
module MCP
4+
class Primitive
5+
class << self
6+
NOT_SET = Object.new
7+
8+
attr_reader :title_value
9+
attr_reader :description_value
10+
attr_reader :icons_value
11+
attr_reader :meta_value
12+
13+
def name_value
14+
@name_value ||= StringUtils.handle_from_class_name(name)
15+
end
16+
17+
def primitive_name(value = NOT_SET)
18+
if value == NOT_SET
19+
@name_value
20+
else
21+
@name_value = value
22+
end
23+
end
24+
25+
def title(value = NOT_SET)
26+
if value == NOT_SET
27+
@title_value
28+
else
29+
@title_value = value
30+
end
31+
end
32+
33+
def description(value = NOT_SET)
34+
if value == NOT_SET
35+
@description_value
36+
else
37+
@description_value = value
38+
end
39+
end
40+
41+
def icons(value = NOT_SET)
42+
if value == NOT_SET
43+
@icons_value
44+
else
45+
@icons_value = value
46+
end
47+
end
48+
49+
def meta(value = NOT_SET)
50+
if value == NOT_SET
51+
@meta_value
52+
else
53+
@meta_value = value
54+
end
55+
end
56+
57+
def to_h
58+
{
59+
name: name_value,
60+
title: title_value,
61+
description: description_value,
62+
icons: icons_value&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
63+
_meta: meta_value,
64+
}.compact
65+
end
66+
67+
def inherited(subclass)
68+
super
69+
subclass.instance_variable_set(:@name_value, nil)
70+
subclass.instance_variable_set(:@title_value, nil)
71+
subclass.instance_variable_set(:@description_value, nil)
72+
subclass.instance_variable_set(:@icons_value, nil)
73+
subclass.instance_variable_set(:@meta_value, nil)
74+
end
75+
76+
def define(name: nil, title: nil, description: nil, icons: [], meta: nil, &block)
77+
Class.new(self) do
78+
primitive_name name
79+
title title
80+
description description
81+
icons icons
82+
meta meta
83+
class_exec(&block) if block
84+
end
85+
end
86+
end
87+
88+
attr_reader :name, :title, :description, :icons, :meta
89+
90+
def initialize(name:, title: nil, description: nil, icons: [], meta: nil)
91+
@name = name
92+
@title = title
93+
@description = description
94+
@icons = icons
95+
@meta = meta
96+
end
97+
98+
def to_h
99+
{
100+
name: name,
101+
title: title,
102+
description: description,
103+
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
104+
_meta: meta,
105+
}.compact
106+
end
107+
end
108+
end

lib/mcp/prompt.rb

Lines changed: 6 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,26 @@
11
# frozen_string_literal: true
22

33
module MCP
4-
class Prompt
4+
class Prompt < Primitive
55
class << self
6-
NOT_SET = Object.new
7-
8-
attr_reader :title_value
9-
attr_reader :description_value
10-
attr_reader :icons_value
116
attr_reader :arguments_value
12-
attr_reader :meta_value
137

148
def template(args, server_context: nil)
159
raise NotImplementedError, "Subclasses must implement template"
1610
end
1711

1812
def to_h
19-
{
20-
name: name_value,
21-
title: title_value,
22-
description: description_value,
23-
icons: icons_value&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
13+
super.merge(
2414
arguments: arguments_value&.map(&:to_h),
25-
_meta: meta_value,
26-
}.compact
15+
).compact
2716
end
2817

2918
def inherited(subclass)
3019
super
31-
subclass.instance_variable_set(:@name_value, nil)
32-
subclass.instance_variable_set(:@title_value, nil)
33-
subclass.instance_variable_set(:@description_value, nil)
34-
subclass.instance_variable_set(:@icons_value, nil)
3520
subclass.instance_variable_set(:@arguments_value, nil)
36-
subclass.instance_variable_set(:@meta_value, nil)
37-
end
38-
39-
def prompt_name(value = NOT_SET)
40-
if value == NOT_SET
41-
@name_value
42-
else
43-
@name_value = value
44-
end
45-
end
46-
47-
def name_value
48-
@name_value || StringUtils.handle_from_class_name(name)
4921
end
5022

51-
def title(value = NOT_SET)
52-
if value == NOT_SET
53-
@title_value
54-
else
55-
@title_value = value
56-
end
57-
end
58-
59-
def description(value = NOT_SET)
60-
if value == NOT_SET
61-
@description_value
62-
else
63-
@description_value = value
64-
end
65-
end
66-
67-
def icons(value = NOT_SET)
68-
if value == NOT_SET
69-
@icons_value
70-
else
71-
@icons_value = value
72-
end
73-
end
23+
alias_method :prompt_name, :primitive_name
7424

7525
def arguments(value = NOT_SET)
7626
if value == NOT_SET
@@ -80,25 +30,12 @@ def arguments(value = NOT_SET)
8030
end
8131
end
8232

83-
def meta(value = NOT_SET)
84-
if value == NOT_SET
85-
@meta_value
86-
else
87-
@meta_value = value
88-
end
89-
end
90-
9133
def define(name: nil, title: nil, description: nil, icons: [], arguments: [], meta: nil, &block)
92-
Class.new(self) do
93-
prompt_name name
94-
title title
95-
description description
96-
icons icons
97-
arguments arguments
34+
super(name: name, title: title, description: description, icons: icons, meta: meta) do
35+
arguments(arguments)
9836
define_singleton_method(:template) do |args, server_context: nil|
9937
instance_exec(args, server_context: server_context, &block)
10038
end
101-
meta meta
10239
end
10340
end
10441

lib/mcp/resource.rb

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,68 @@
11
# frozen_string_literal: true
22

33
module MCP
4-
class Resource
5-
attr_reader :uri, :name, :title, :description, :icons, :mime_type
4+
class Resource < Primitive
5+
class << self
6+
attr_reader :uri_value
7+
attr_reader :mime_type_value
68

7-
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil)
9+
def to_h
10+
super.merge(
11+
uri: uri_value,
12+
mimeType: mime_type_value,
13+
).compact
14+
end
15+
16+
def contents
17+
raise NotImplementedError, "Subclasses must implement contents"
18+
end
19+
20+
def inherited(subclass)
21+
super
22+
subclass.instance_variable_set(:@uri_value, nil)
23+
subclass.instance_variable_set(:@mime_type_value, nil)
24+
end
25+
26+
alias_method :resource_name, :primitive_name
27+
28+
def uri(value = NOT_SET)
29+
if value == NOT_SET
30+
@uri_value
31+
else
32+
@uri_value = value
33+
end
34+
end
35+
36+
def mime_type(value = NOT_SET)
37+
if value == NOT_SET
38+
@mime_type_value
39+
else
40+
@mime_type_value = value
41+
end
42+
end
43+
44+
def define(uri: nil, name: nil, title: nil, description: nil, icons: [], mime_type: nil, meta: nil, &block)
45+
super(name: name, title: title, description: description, icons: icons, meta: meta) do
46+
uri(uri)
47+
mime_type(mime_type)
48+
class_exec(&block) if block
49+
end
50+
end
51+
end
52+
53+
attr_reader :uri, :mime_type
54+
55+
def initialize(uri:, mime_type: nil, **kwargs)
56+
super(**kwargs)
857
@uri = uri
9-
@name = name
10-
@title = title
11-
@description = description
12-
@icons = icons
1358
@mime_type = mime_type
1459
end
1560

1661
def to_h
17-
{
62+
super.merge(
1863
uri: uri,
19-
name: name,
20-
title: title,
21-
description: description,
22-
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
2364
mimeType: mime_type,
24-
}.compact
65+
).compact
2566
end
2667
end
2768
end

0 commit comments

Comments
 (0)