File Coverage

blib/lib/MCP/Server.pm
Criterion Covered Total %
statement 206 211 97.6
branch 40 42 95.2
condition 15 27 55.5
subroutine 34 35 97.1
pod 6 6 100.0
total 301 321 93.7


line stmt bran cond sub pod time code
1             package MCP::Server;
2 2     2   409771 use Mojo::Base 'Mojo::EventEmitter', -signatures;
  2         5  
  2         13  
3              
4 2     2   588 use List::Util qw(first);
  2         4  
  2         151  
5 2     2   12 use Mojo::JSON qw(false true);
  2         3  
  2         129  
6 2     2   11 use MCP::Constants qw(INVALID_PARAMS INVALID_REQUEST METHOD_NOT_FOUND PARSE_ERROR PROTOCOL_VERSION RESOURCE_NOT_FOUND);
  2         15  
  2         143  
7 2     2   1032 use MCP::Prompt;
  2         6  
  2         11  
8 2     2   1039 use MCP::Resource;
  2         7  
  2         13  
9 2     2   1137 use MCP::Server::Transport::HTTP;
  2         5  
  2         19  
10 2     2   1066 use MCP::Server::Transport::Stdio;
  2         6  
  2         16  
11 2     2   1115 use MCP::Tool;
  2         8  
  2         15  
12 2     2   106 use Scalar::Util qw(blessed);
  2         3  
  2         5431  
13              
14             has name => 'PerlServer';
15             has prompts => sub { [] };
16             has resources => sub { [] };
17             has tools => sub { [] };
18             has 'transport';
19             has version => '1.0.0';
20              
21 52     52 1 474 sub handle ($self, $request, $context) {
  52         89  
  52         86  
  52         77  
  52         98  
22 52 50       193 return _jsonrpc_error(PARSE_ERROR, 'Invalid JSON-RPC request') unless ref $request eq 'HASH';
23 52 50       221 return _jsonrpc_error(INVALID_REQUEST, 'Missing JSON-RPC method') unless my $method = $request->{method};
24              
25             # Requests
26 52 100       188 if (defined(my $id = $request->{id})) {
27              
28 49 100       423 if ($method eq 'initialize') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
29 3   50     21 my $result = $self->_handle_initialize($request->{params} // {});
30 3         54 return _jsonrpc_response($result, $id);
31             }
32             elsif ($method eq 'tools/list') {
33 3         14 my $result = $self->_handle_tools_list($context);
34 3         13 return _jsonrpc_response($result, $id);
35             }
36             elsif ($method eq 'tools/call') {
37 17   50     169 return $self->_handle_tools_call($request->{params} // {}, $id, $context);
38             }
39             elsif ($method eq 'ping') {
40 1         3 return _jsonrpc_response({}, $id);
41             }
42             elsif ($method eq 'prompts/list') {
43 3         15 my $result = $self->_handle_prompts_list($context);
44 3         16 return _jsonrpc_response($result, $id);
45             }
46             elsif ($method eq 'prompts/get') {
47 10   50     98 return $self->_handle_prompts_get($request->{params} // {}, $id, $context);
48             }
49             elsif ($method eq 'resources/list') {
50 3         42 my $result = $self->_handle_resources_list($context);
51 3         13 return _jsonrpc_response($result, $id);
52             }
53             elsif ($method eq 'resources/read') {
54 8   50     56 return $self->_handle_resources_read($request->{params} // {}, $id, $context);
55             }
56              
57             # Method not found
58 1         9 return _jsonrpc_error(METHOD_NOT_FOUND, "Method '$method' not found", $id);
59             }
60              
61             # Notifications (ignored for now)
62 3         9 return undef;
63             }
64              
65 5     5 1 63 sub prompt ($self, %args) {
  5         11  
  5         17  
  5         27  
66 5         35 my $prompt = MCP::Prompt->new(%args);
67 5         41 push @{$self->prompts}, $prompt;
  5         16  
68 5         65 return $prompt;
69             }
70              
71 5     5 1 44 sub resource ($self, %args) {
  5         10  
  5         16  
  5         10  
72 5         59 my $resource = MCP::Resource->new(%args);
73 5         50 push @{$self->resources}, $resource;
  5         15  
74 5         25 return $resource;
75             }
76              
77 2     2 1 776 sub to_action ($self) {
  2         4  
  2         5  
78 2         29 $self->transport(my $http = MCP::Server::Transport::HTTP->new(server => $self));
79 2     53   67 return sub ($c) { $http->handle_request($c) };
  53         356  
  53         349410  
  53         144  
  53         145  
80             }
81              
82 0     0 1 0 sub to_stdio ($self) {
  0         0  
  0         0  
83 0         0 $self->transport(my $stdio = MCP::Server::Transport::Stdio->new(server => $self));
84 0         0 $self->transport->handle_requests;
85             }
86              
87 10     10 1 343877 sub tool ($self, %args) {
  10         17  
  10         30  
  10         14  
88 10         41 my $tool = MCP::Tool->new(%args);
89 10         76 push @{$self->tools}, $tool;
  10         24  
90 10         47 return $tool;
91             }
92              
93 3     3   6 sub _handle_initialize ($self, $params) {
  3         5  
  3         4  
  3         6  
94             return {
95 3         31 protocolVersion => PROTOCOL_VERSION,
96             capabilities => {prompts => {}, resources => {}, tools => {}},
97             serverInfo => {name => $self->name, version => $self->version}
98             };
99             }
100              
101 3     3   7 sub _handle_prompts_list ($self, $context) {
  3         6  
  3         7  
  3         7  
102 3         5 my @prompts;
103 3         7 for my $prompt (@{$self->_prompts($context)}) {
  3         15  
104 6         46 my $info = {name => $prompt->name, description => $prompt->description, arguments => $prompt->arguments};
105 6         62 push @prompts, $info;
106             }
107              
108 3         18 return {prompts => \@prompts};
109             }
110              
111 10     10   20 sub _handle_prompts_get ($self, $params, $id, $context) {
  10         18  
  10         16  
  10         18  
  10         17  
  10         15  
112 10   50     37 my $name = $params->{name} // '';
113 10   50     29 my $args = $params->{arguments} // {};
114             return _jsonrpc_error(METHOD_NOT_FOUND, "Prompt '$name' not found")
115 10 100   18   54 unless my $prompt = first { $_->name eq $name } @{$self->_prompts($context)};
  18         128  
  10         48  
116 8 100       129 return _jsonrpc_error(INVALID_PARAMS, 'Invalid arguments') if $prompt->validate_input($args);
117              
118 7         33 my $result = $prompt->call($args, $context);
119 7 100 66 2   233 return $result->then(sub { _jsonrpc_response($_[0], $id) }) if blessed($result) && $result->isa('Mojo::Promise');
  2         486  
120 5         17 return _jsonrpc_response($result, $id);
121             }
122              
123 3     3   9 sub _handle_resources_list ($self, $context) {
  3         5  
  3         10  
  3         6  
124 3         5 my @resources;
125 3         6 for my $resource (@{$self->_resources($context)}) {
  3         31  
126 6         26 my $info = {
127             uri => $resource->uri,
128             name => $resource->name,
129             description => $resource->description,
130             mimeType => $resource->mime_type
131             };
132 6         157 push @resources, $info;
133             }
134              
135 3         18 return {resources => \@resources};
136             }
137              
138 8     8   40 sub _handle_resources_read ($self, $params, $id, $context) {
  8         36  
  8         21  
  8         12  
  8         14  
  8         12  
139 8   50     31 my $uri = $params->{uri} // '';
140             return _jsonrpc_error(RESOURCE_NOT_FOUND, 'Resource not found')
141 8 100   14   47 unless my $resource = first { $_->uri eq $uri } @{$self->_resources($context)};
  14         78  
  8         34  
142              
143 6         81 my $result = $resource->call($context);
144 6 100 66 1   164 return $result->then(sub { _jsonrpc_response($_[0], $id) }) if blessed($result) && $result->isa('Mojo::Promise');
  1         168  
145 5         16 return _jsonrpc_response($result, $id);
146             }
147              
148 17     17   38 sub _handle_tools_call ($self, $params, $id, $context) {
  17         35  
  17         33  
  17         30  
  17         31  
  17         34  
149 17   50     72 my $name = $params->{name} // '';
150 17   50     87 my $args = $params->{arguments} // {};
151             return _jsonrpc_error(METHOD_NOT_FOUND, "Tool '$name' not found")
152 17 100   61   970 unless my $tool = first { $_->name eq $name } @{$self->_tools($context)};
  61         352  
  17         83  
153 15 100       231 return _jsonrpc_error(INVALID_PARAMS, 'Invalid arguments') if $tool->validate_input($args);
154              
155 14         89 my $result = $tool->call($args, $context);
156 14 100 66 2   362 return $result->then(sub { _jsonrpc_response($_[0], $id) }) if blessed($result) && $result->isa('Mojo::Promise');
  2         338  
157 12         66 return _jsonrpc_response($result, $id);
158             }
159              
160 3     3   8 sub _handle_tools_list ($self, $context) {
  3         19  
  3         7  
  3         5  
161 3         7 my @tools;
162 3         6 for my $tool (@{$self->_tools($context)}) {
  3         13  
163 11         32 my $info = {name => $tool->name, description => $tool->description, inputSchema => $tool->input_schema};
164 11 100       116 if (my $output_schema = $tool->output_schema) { $info->{outputSchema} = $output_schema }
  1         46  
165              
166 11         59 my $annotations = $tool->annotations;
167 11 100       37 $info->{annotations} = $annotations if keys %$annotations;
168 11         51 push @tools, $info;
169             }
170              
171 3         16 return {tools => \@tools};
172             }
173              
174 9     9   81 sub _jsonrpc_error ($code, $message, $id = undef) {
  9         18  
  9         20  
  9         21  
  9         19  
175 9         139 return {jsonrpc => '2.0', id => $id, error => {code => $code, message => $message}};
176             }
177              
178 40     40   93 sub _jsonrpc_response ($result, $id = undef) {
  40         76  
  40         74  
  40         75  
179 40         328 return {jsonrpc => '2.0', id => $id, result => $result};
180             }
181              
182 13     13   26 sub _prompts ($self, $context) {
  13         23  
  13         19  
  13         50  
183 13         25 my $prompts = [@{$self->prompts}];
  13         52  
184 13         170 $self->emit('prompts', $prompts, $context);
185 13         433 return $prompts;
186             }
187              
188 11     11   19 sub _resources ($self, $context) {
  11         20  
  11         21  
  11         44  
189 11         20 my $resources = [@{$self->resources}];
  11         3566  
190 11         133 $self->emit('resources', $resources, $context);
191 11         399 return $resources;
192             }
193              
194 20     20   35 sub _tools ($self, $context) {
  20         43  
  20         35  
  20         30  
195 20         49 my $tools = [@{$self->tools}];
  20         89  
196 20         271 $self->emit('tools', $tools, $context);
197 20         554 return $tools;
198             }
199              
200             1;
201              
202             =encoding utf8
203              
204             =head1 NAME
205              
206             MCP::Server - MCP server implementation
207              
208             =head1 SYNOPSIS
209              
210             use MCP::Server;
211              
212             my $server = MCP::Server->new(name => 'MyServer');
213              
214             $server->tool(
215             name => 'echo',
216             description => 'Echo the input text',
217             input_schema => {type => 'object', properties => {msg => {type => 'string'}}, required => ['msg']},
218             code => sub ($tool, $args) {
219             return "Echo: $args->{msg}";
220             }
221             );
222              
223             $server->prompt(
224             name => 'echo',
225             description => 'A prompt to demonstrate the echo tool',
226             code => sub ($prompt, $args) {
227             return 'Use the echo tool with the message "Hello, World!"';
228             }
229             );
230              
231             $server->resource(
232             uri => 'file:///example.txt',
233             name => 'example',
234             description => 'A simple text resource',
235             mime_type => 'text/plain',
236             code => sub ($resource) {
237             return 'This is an example resource content.';
238             }
239             );
240              
241             $server->to_stdio;
242              
243             =head1 DESCRIPTION
244              
245             L is an MCP (Model Context Protocol) server.
246              
247             =head1 EVENTS
248              
249             L inherits all events from L and emits the following new ones.
250              
251             =head2 prompts
252              
253             $server->on(prompts => sub ($server, $prompts, $context) { ... });
254              
255             Emitted whenever the list of prompts is accessed.
256              
257             =head2 resources
258              
259             $server->on(resources => sub ($server, $resources, $context) { ... });
260              
261             Emitted whenever the list of resources is accessed.
262              
263             =head2 tools
264              
265             $server->on(tools => sub ($server, $tools, $context) { ... });
266              
267             Emitted whenever the list of tools is accessed.
268              
269             =head1 ATTRIBUTES
270              
271             L implements the following attributes.
272              
273             =head2 name
274              
275             my $name = $server->name;
276             $server = $server->name('MyServer');
277              
278             The name of the server, used for identification.
279              
280             =head2 prompts
281              
282             my $prompts = $server->prompts;
283             $server = $server->prompts([MCP::Prompt->new]);
284              
285             An array reference containing registered prompts.
286              
287             =head2 resources
288              
289             my $resources = $server->resources;
290             $server = $server->resources([MCP::Resource->new]);
291              
292             An array reference containing registered resources.
293              
294             =head2 tools
295              
296             my $tools = $server->tools;
297             $server = $server->tools([MCP::Tool->new]);
298              
299             An array reference containing registered tools.
300              
301             =head2 transport
302              
303             my $transport = $server->transport;
304             $server = $server->transport(MCP::Server::Transport::HTTP->new);
305              
306             The transport layer used by the server, such as L or L.
307              
308             =head2 version
309              
310             my $version = $server->version;
311             $server = $server->version('1.0.0');
312              
313             The version of the server.
314              
315             =head1 METHODS
316              
317             L inherits all methods from L and implements the following new ones.
318              
319             =head2 handle
320              
321             my $response = $server->handle($request, $context);
322              
323             Handle a JSON-RPC request and return a response.
324              
325             =head2 prompt
326              
327             my $prompt = $server->prompt(
328             name => 'my_prompt',
329             description => 'A sample prompt',
330             arguments => [{name => 'foo', description => 'Whatever', required => 1}],
331             code => sub ($prompt, $args) { ... }
332             );
333              
334             Register a new prompt with the server.
335              
336             =head2 resource
337              
338             my $resource = $server->resource(
339             uri => 'file://my_resource',
340             name => 'sample_resource',
341             description => 'A sample resource',
342             mime_type => 'text/plain',
343             code => sub ($resource) { ... }
344             );
345              
346             Register a new resource with the server.
347              
348             =head2 to_action
349              
350             my $action = $server->to_action;
351              
352             Convert the server to a L action.
353              
354             =head2 to_stdio
355              
356             $server->to_stdio;
357              
358             Handles JSON-RPC requests over standard input/output.
359              
360             =head2 tool
361              
362             my $tool = $server->tool(
363             name => 'my_tool',
364             description => 'A sample tool',
365             input_schema => {type => 'object', properties => {foo => {type => 'string'}}},
366             code => sub ($tool, $args) { ... }
367             );
368              
369             Register a new tool with the server.
370              
371             =head1 SEE ALSO
372              
373             L, L, L.
374              
375             =cut