Skip to content

Commit 6004f42

Browse files
committed
Internal Release 0.2.0
- fix `lib/mcp-ruby` auto-import path - implement server resources - add basic listing of resources to Server - allow setting handlers on Server - wrap handler method results - make tool definition more declarative - make prompt definition more declarative - bump dependency versions
1 parent a4df3b8 commit 6004f42

File tree

15 files changed

+708
-164
lines changed

15 files changed

+708
-164
lines changed

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
inherit_gem:
22
rubocop-shopify: rubocop.yml
3+
Naming/FileName:
4+
Exclude:
5+
- 'lib/mcp-ruby.rb'

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
model_context_protocol (0.1.1)
4+
model_context_protocol (0.2.2)
55
activesupport
66

77
GEM

README.md

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,43 +59,117 @@ end
5959

6060
MCP spec includes [Tools](https://modelcontextprotocol.io/docs/concepts/tools) which provide functionality to LLM apps.
6161

62-
This gem provides a `ModelContextProtocol::Tool` class that can be instantiated to create tools.
62+
This gem provides a `ModelContextProtocol::Tool` class that can be used to create tools in two ways:
6363

64-
Tools can be passed into the `ModelContextProtocol::Server` constructor to register them with the server.
64+
1. As a class definition:
65+
```ruby
66+
class MyTool < ModelContextProtocol::Tool
67+
tool_name "my_tool"
68+
tool_description "This tool performs specific functionality..."
69+
tool_input_schema [{ type: "text", name: "message" }]
6570

66-
### Example Tool Implementation
71+
def call(message)
72+
Tool::Response.new([{ type: "text", content: "OK" }])
73+
end
74+
end
6775

68-
The `Tool` class allows creating tools that can be used within the model context.
69-
To create a tool, instantiate the class with the required parameters and an optional block:
76+
tool = MyTool.new
77+
```
7078

79+
2. By using the `ModelContextProtocol::Tool.define` method with a block:
7180
```ruby
72-
tool = ModelContextProtocol::Tool.new(name: "my_tool", description: "This tool performs specific functionality...") do |args|
73-
# Implement the tool's functionality here
74-
result = process_something(args["parameter_name"])
75-
ModelContextProtocol::Tool::Response.new([{ type: "text", text: result }], false )
81+
tool = ModelContextProtocol::Tool.define(name: "my_tool", description: "This tool performs specific functionality...") do |args|
82+
Tool::Response.new([{ type: "text", content: "OK" }])
7683
end
7784
```
7885

7986
## Prompts
8087

81-
MCP spec includes
82-
[Prompts](https://modelcontextprotocol.io/docs/concepts/prompts), `Prompts` enable servers to define reusable prompt
83-
templates and workflows that clients can easily surface to users and LLMs
88+
MCP spec includes [Prompts](https://modelcontextprotocol.io/docs/concepts/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs.
8489

85-
The `Prompt` class allows creating prompts that can be used within the model context.
90+
The `ModelContextProtocol::Prompt` class provides two ways to create prompts:
8691

87-
To create a prompt, instantiate the class with the required parameters and an optional block:
92+
1. As a class definition with metadata:
93+
```ruby
94+
class MyPrompt < ModelContextProtocol::Prompt
95+
prompt_name "my_prompt" # Optional - defaults to underscored class name
96+
description "This prompt performs specific functionality..."
97+
arguments [
98+
Prompt::Argument.new(
99+
name: "message",
100+
description: "Input message",
101+
required: true
102+
)
103+
]
104+
105+
def template(args)
106+
Prompt::Result.new(
107+
description: "Response description",
108+
messages: [
109+
Prompt::Message.new(
110+
role: "user",
111+
content: Content::Text.new("User message")
112+
),
113+
Prompt::Message.new(
114+
role: "assistant",
115+
content: Content::Text.new(args["message"])
116+
)
117+
]
118+
)
119+
end
120+
end
121+
```
88122

123+
2. Using the `ModelContextProtocol::Prompt.define` method:
89124
```ruby
90-
prompt = ModelContextProtocol::Prompt.new(name: "my_prompt", description: "This prompt performs specific functionality...") do |args|
91-
# Implement the prompt's functionality here
92-
result = template_something(args["parameter_name"])
93-
ModelContextProtocol::Prompt::Response.new([{ type: "text", text: result }], false )
125+
prompt = ModelContextProtocol::Prompt.define(
126+
name: "my_prompt",
127+
description: "This prompt performs specific functionality...",
128+
arguments: [
129+
Prompt::Argument.new(
130+
name: "message",
131+
description: "Input message",
132+
required: true
133+
)
134+
]
135+
) do |args|
136+
Prompt::Result.new(
137+
description: "Response description",
138+
messages: [
139+
Prompt::Message.new(
140+
role: "user",
141+
content: Content::Text.new("User message")
142+
),
143+
Prompt::Message.new(
144+
role: "assistant",
145+
content: Content::Text.new(args["message"])
146+
)
147+
]
148+
)
94149
end
95150
```
96151

152+
### Key Components
153+
154+
- `Prompt::Argument` - Defines input parameters for the prompt template
155+
- `Prompt::Message` - Represents a message in the conversation with a role and content
156+
- `Prompt::Result` - The output of a prompt template containing description and messages
157+
- `Content::Text` - Text content for messages
158+
159+
### Usage
160+
161+
Register prompts with the MCP server:
97162

163+
```ruby
164+
server = ModelContextProtocol::Server.new(
165+
name: "my_server",
166+
prompts: [MyPrompt.new]
167+
)
168+
```
98169

170+
The server will handle prompt listing and execution through the MCP protocol methods:
171+
- `prompts/list` - Lists all registered prompts and their schemas
172+
- `prompts/get` - Retrieves and executes a specific prompt with arguments
99173

100174
## Releases
101175

lib/mcp-ruby.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# frozen_string_literal: true
2+
3+
require "model_context_protocol"

lib/model_context_protocol.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require "model_context_protocol/content"
88
require "model_context_protocol/resource"
99
require "model_context_protocol/prompt"
10+
require "model_context_protocol/version"
1011

1112
module ModelContextProtocol
1213
class Annotations

lib/model_context_protocol/prompt.rb

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,59 @@ def to_h
4444
end
4545
end
4646

47+
class << self
48+
attr_reader :description_value
49+
attr_reader :arguments_value
50+
51+
def inherited(subclass)
52+
super
53+
subclass.instance_variable_set(:@name_value, nil)
54+
subclass.instance_variable_set(:@description_value, nil)
55+
subclass.instance_variable_set(:@arguments_value, nil)
56+
end
57+
58+
def prompt_name(value)
59+
@name_value = value
60+
end
61+
62+
def name_value
63+
@name_value || name.demodulize.underscore
64+
end
65+
66+
def description(value)
67+
@description_value = value
68+
end
69+
70+
def arguments(value)
71+
@arguments_value = value
72+
end
73+
74+
def define(name: nil, description: nil, arguments: [], &block)
75+
new(name:, description:, arguments:).tap do |prompt|
76+
prompt.define_singleton_method(:template) do |args|
77+
instance_exec(args, &block)
78+
end
79+
end
80+
end
81+
end
82+
4783
attr_reader :name, :description, :arguments
4884

49-
def initialize(name:, description: nil, arguments: [], &block)
50-
@name = name
51-
@description = description
52-
@arguments = arguments
53-
@template_block = block
85+
def initialize(name: nil, description: nil, arguments: nil)
86+
@name = name || self.class.name_value
87+
@description = description || self.class.description_value
88+
@arguments = arguments || self.class.arguments_value
5489
end
5590

5691
def template(args)
57-
validate_args!(args)
58-
result = @template_block.call(args)
59-
result
92+
raise NotImplementedError, "Prompt subclasses must implement template"
93+
end
94+
95+
def validate_arguments!(args)
96+
missing = required_args - args.keys
97+
return if missing.empty?
98+
99+
raise ArgumentError, "Missing required arguments: #{missing.join(", ")}"
60100
end
61101

62102
def to_h
@@ -68,12 +108,5 @@ def to_h
68108
def required_args
69109
arguments.filter_map { |arg| arg.name if arg.required }
70110
end
71-
72-
def validate_args!(args)
73-
missing = required_args - args.keys
74-
return if missing.empty?
75-
76-
raise ArgumentError, "Missing required arguments: #{missing.join(", ")}"
77-
end
78111
end
79112
end

lib/model_context_protocol/resource.rb

Lines changed: 17 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,25 @@
22
# frozen_string_literal: true
33

44
module ModelContextProtocol
5-
module Resource
6-
class Embedded
7-
attr_reader :resource, :annotations
8-
9-
def initialize(resource:, annotations: nil)
10-
@resource = resource
11-
@annotations = annotations
12-
end
13-
14-
def to_h
15-
{ resource: resource.to_h, annotations: }.compact
16-
end
17-
end
18-
19-
class Contents
20-
attr_reader :uri, :mime_type
21-
22-
def initialize(uri:, mime_type: nil)
23-
@uri = uri
24-
@mime_type = mime_type
25-
end
26-
27-
def to_h
28-
{ uri:, mime_type: }.compact
29-
end
5+
class Resource
6+
attr_reader :uri, :name, :description, :mime_type, :contents
7+
8+
def initialize(uri:, name:, description:, mime_type:, contents:)
9+
@uri = uri
10+
@name = name
11+
@description = description
12+
@mime_type = mime_type
13+
@contents = contents
3014
end
3115

32-
class TextContents < Contents
33-
attr_reader :text
34-
35-
def initialize(text:, uri:, mime_type:)
36-
super(uri: uri, mime_type: mime_type)
37-
@text = text
38-
end
39-
40-
def to_h
41-
super.merge(text: text)
42-
end
43-
end
44-
45-
class BlobContents < Contents
46-
attr_reader :data
47-
48-
def initialize(data:, uri:, mime_type:)
49-
super(uri: uri, mime_type: mime_type)
50-
@data = data
51-
end
52-
53-
def to_h
54-
super.merge(data: data)
55-
end
16+
def to_h
17+
{
18+
uri: @uri,
19+
name: @name,
20+
description: @description,
21+
mimeType: @mime_type,
22+
contents: @contents.map(&:to_h),
23+
}
5624
end
5725
end
5826
end
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
module ModelContextProtocol
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
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
module ModelContextProtocol
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

0 commit comments

Comments
 (0)