Skip to content

Commit 1ed3139

Browse files
committed
Add define_custom_method to define custom methods
1 parent 189d5fc commit 1ed3139

File tree

3 files changed

+117
-0
lines changed

3 files changed

+117
-0
lines changed

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The `MCP::Server` class is the core component that handles JSON-RPC requests and
2828
It implements the Model Context Protocol specification, handling model context requests and responses.
2929

3030
### Key Features
31+
3132
- Implements JSON-RPC 2.0 message handling
3233
- Supports protocol initialization and capability negotiation
3334
- Manages tool registration and invocation
@@ -37,6 +38,7 @@ It implements the Model Context Protocol specification, handling model context r
3738
- Supports notifications for list changes (tools, prompts, resources)
3839

3940
### Supported Methods
41+
4042
- `initialize` - Initializes the protocol and returns server capabilities
4143
- `ping` - Simple health check
4244
- `tools/list` - Lists all registered tools and their schemas
@@ -47,20 +49,73 @@ It implements the Model Context Protocol specification, handling model context r
4749
- `resources/read` - Retrieves a specific resource by name
4850
- `resources/templates/list` - Lists all registered resource templates and their schemas
4951

52+
### Custom Methods
53+
54+
The server allows you to define custom JSON-RPC methods beyond the standard MCP protocol methods using the `define_custom_method` method:
55+
56+
```ruby
57+
server = MCP::Server.new(name: "my_server")
58+
59+
# Define a custom method that returns a result
60+
server.define_custom_method(method_name: "add") do |params|
61+
params[:a] + params[:b]
62+
end
63+
64+
# Define a custom notification method (returns nil)
65+
server.define_custom_method(method_name: "notify") do |params|
66+
# Process notification
67+
nil
68+
end
69+
```
70+
71+
**Key Features:**
72+
73+
- Accepts any method name as a string
74+
- Block receives the request parameters as a hash
75+
- Can handle both regular methods (with responses) and notifications
76+
- Prevents overriding existing MCP protocol methods
77+
- Supports instrumentation callbacks for monitoring
78+
79+
**Usage Example:**
80+
81+
```ruby
82+
# Client request
83+
{
84+
"jsonrpc": "2.0",
85+
"id": 1,
86+
"method": "add",
87+
"params": { "a": 5, "b": 3 }
88+
}
89+
90+
# Server response
91+
{
92+
"jsonrpc": "2.0",
93+
"id": 1,
94+
"result": 8
95+
}
96+
```
97+
98+
**Error Handling:**
99+
100+
- Raises `MCP::Server::MethodAlreadyDefinedError` if trying to override an existing method
101+
- Supports the same exception reporting and instrumentation as standard methods
102+
50103
### Notifications
51104

52105
The server supports sending notifications to clients when lists of tools, prompts, or resources change. This enables real-time updates without polling.
53106

54107
#### Notification Methods
55108

56109
The server provides three notification methods:
110+
57111
- `notify_tools_list_changed()` - Send a notification when the tools list changes
58112
- `notify_prompts_list_changed()` - Send a notification when the prompts list changes
59113
- `notify_resources_list_changed()` - Send a notification when the resources list changes
60114

61115
#### Notification Format
62116

63117
Notifications follow the JSON-RPC 2.0 specification and use these method names:
118+
64119
- `notifications/tools/list_changed`
65120
- `notifications/prompts/list_changed`
66121
- `notifications/resources/list_changed`
@@ -212,11 +267,13 @@ server = MCP::Server.new(
212267
The `server_context` is a user-defined hash that is passed into the server instance and made available to tools, prompts, and exception/instrumentation callbacks. It can be used to provide contextual information such as authentication state, user IDs, or request-specific data.
213268

214269
**Type:**
270+
215271
```ruby
216272
server_context: { [String, Symbol] => Any }
217273
```
218274

219275
**Example:**
276+
220277
```ruby
221278
server = MCP::Server.new(
222279
name: "my_server",
@@ -236,6 +293,7 @@ The exception reporter receives:
236293
- `server_context`: The context hash provided to the server
237294

238295
**Signature:**
296+
239297
```ruby
240298
exception_reporter = ->(exception, server_context) { ... }
241299
```
@@ -252,12 +310,14 @@ The instrumentation callback receives a hash with the following possible keys:
252310
- `duration`: (Float) Duration of the call in seconds
253311

254312
**Type:**
313+
255314
```ruby
256315
instrumentation_callback = ->(data) { ... }
257316
# where data is a Hash with keys as described above
258317
```
259318

260319
**Example:**
320+
261321
```ruby
262322
config.instrumentation_callback = ->(data) {
263323
puts "Instrumentation: #{data.inspect}"

lib/mcp/server.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ def initialize(message, request, error_type: :internal_error, original_error: ni
2020
end
2121
end
2222

23+
class MethodAlreadyDefinedError < StandardError
24+
attr_reader :method_name
25+
26+
def initialize(method_name)
27+
super("Method #{method_name} already defined")
28+
@method_name = method_name
29+
end
30+
end
31+
2332
include Instrumentation
2433

2534
attr_accessor :name, :version, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport
@@ -89,6 +98,14 @@ def define_prompt(name: nil, description: nil, arguments: [], &block)
8998
@prompts[prompt.name_value] = prompt
9099
end
91100

101+
def define_custom_method(method_name:, &block)
102+
if @handlers.key?(method_name)
103+
raise MethodAlreadyDefinedError, method_name
104+
end
105+
106+
@handlers[method_name] = block
107+
end
108+
92109
def notify_tools_list_changed
93110
return unless @transport
94111

test/mcp/server_test.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,46 @@ def call(message:, server_context: nil)
653653
assert_instrumentation_data({ method: "unsupported_method" })
654654
end
655655

656+
test "#handle handles custom methods" do
657+
@server.define_custom_method(method_name: "add") do |params|
658+
params[:a] + params[:b]
659+
end
660+
661+
request = {
662+
jsonrpc: "2.0",
663+
id: 1,
664+
method: "add",
665+
params: { a: 1, b: 2 },
666+
}
667+
668+
response = @server.handle(request)
669+
assert_equal 3, response[:result]
670+
assert_instrumentation_data({ method: "add" })
671+
end
672+
673+
test "#handle handles custom notifications" do
674+
@server.define_custom_method(method_name: "notify") do
675+
nil
676+
end
677+
678+
request = {
679+
jsonrpc: "2.0",
680+
method: "notify",
681+
}
682+
683+
response = @server.handle(request)
684+
assert_nil response
685+
assert_instrumentation_data({ method: "notify" })
686+
end
687+
688+
test "#define_custom_method raises an error if the method is already defined" do
689+
assert_raises(Server::MethodAlreadyDefinedError) do
690+
@server.define_custom_method(method_name: "tools/call") do
691+
nil
692+
end
693+
end
694+
end
695+
656696
test "the global configuration is used if no configuration is passed to the server" do
657697
server = Server.new(name: "test_server")
658698
assert_equal MCP.configuration.instrumentation_callback,

0 commit comments

Comments
 (0)