File Coverage

blib/lib/MCP/Client.pm
Criterion Covered Total %
statement 119 122 97.5
branch 21 28 75.0
condition 4 9 44.4
subroutine 20 20 100.0
pod 12 12 100.0
total 176 191 92.1


line stmt bran cond sub pod time code
1             package MCP::Client;
2 4     4   2514194 use Mojo::Base -base, -signatures;
  4         9  
  4         24  
3              
4 4     4   1012 use Carp qw(croak);
  4         9  
  4         226  
5 4     4   1526 use MCP::Constants qw(PROTOCOL_VERSION);
  4         12  
  4         246  
6 4     4   21 use Mojo::JSON qw(from_json);
  4         4  
  4         132  
7 4     4   14 use Mojo::UserAgent;
  4         10  
  4         24  
8 4     4   127 use Scalar::Util qw(weaken);
  4         6  
  4         5663  
9              
10             has headers => sub { {} };
11             has name => 'PerlClient';
12             has 'session_id';
13             has ua => sub { Mojo::UserAgent->new };
14             has url => sub {'http://localhost:3000/mcp'};
15             has version => '1.0.0';
16              
17 88     88 1 9796 sub build_request ($self, $method, $params = {}) {
  88         168  
  88         116  
  88         113  
  88         110  
18 88         203 my $request = $self->build_notification($method, $params);
19 88 100       322 $request->{id} = $self->{id} = $self->{id} ? $self->{id} + 1 : 1;
20 88         244 return $request;
21             }
22              
23 105     105 1 116 sub build_notification ($self, $method, $params = {}) {
  105         157  
  105         129  
  105         136  
  105         117  
24 105         381 return {jsonrpc => '2.0', method => $method, params => $params};
25             }
26              
27 25     25 1 91404 sub call_tool ($self, $name, $args = {}) {
  25         43  
  25         36  
  25         42  
  25         32  
28 25         107 my $request = $self->build_request('tools/call', {name => $name, arguments => $args});
29 25         70 return _result($self->send_request($request));
30             }
31              
32 10     10 1 963078 sub delete_session ($self) {
  10         21  
  10         15  
33 10 50       57 return undef unless my $session_id = $self->session_id;
34 10         91 my $tx = $self->ua->build_tx(DELETE => $self->url => {%{$self->headers}, 'Mcp-Session-Id' => $session_id});
  10         92  
35 10         1856 $tx = $self->ua->start($tx);
36 10 50       23236 if (my $err = $tx->error) {
37 0 0       0 croak "$err->{code} response: $err->{message}" if $err->{code};
38 0         0 croak "Connection error: $err->{message}";
39             }
40 10         189 $self->session_id(undef);
41 10         245 return 1;
42             }
43              
44 13     13 1 41993 sub get_prompt ($self, $name, $args = {}) {
  13         22  
  13         18  
  13         23  
  13         19  
45 13         57 my $request = $self->build_request('prompts/get', {name => $name, arguments => $args});
46 13         35 return _result($self->send_request($request));
47             }
48              
49 17     17 1 106657 sub initialize_session ($self) {
  17         33  
  17         28  
50 17         80 my $request = $self->build_request(
51             initialize => {
52             protocolVersion => PROTOCOL_VERSION,
53             capabilities => {},
54             clientInfo => {name => $self->name, version => $self->version,},
55             }
56             );
57 17         64 my $result = _result($self->send_request($request));
58 17         51 $self->send_request($self->build_notification('notifications/initialized'));
59 17         234 return $result;
60             }
61              
62 5     5 1 19719 sub list_prompts ($self) { _result($self->send_request($self->build_request('prompts/list'))) }
  5         9  
  5         7  
  5         17  
63 5     5 1 20592 sub list_resources ($self) { _result($self->send_request($self->build_request('resources/list'))) }
  5         10  
  5         8  
  5         17  
64 6     6 1 7206 sub list_tools ($self) { _result($self->send_request($self->build_request('tools/list'))) }
  6         12  
  6         7  
  6         21  
65 3     3 1 11871 sub ping ($self) { _result($self->send_request($self->build_request('ping'))) }
  3         7  
  3         5  
  3         14  
66              
67 11     11 1 40077 sub read_resource ($self, $uri) {
  11         19  
  11         20  
  11         14  
68 11         67 my $request = $self->build_request('resources/read', {uri => $uri});
69 11         34 return _result($self->send_request($request));
70             }
71              
72 105     105 1 124 sub send_request ($self, $request) {
  105         116  
  105         121  
  105         110  
73             my $headers
74 105         143 = {%{$self->headers}, Accept => 'application/json, text/event-stream', 'Content-Type' => 'application/json'};
  105         333  
75 105 100       686 if (my $session_id = $self->session_id) { $headers->{'Mcp-Session-Id'} = $session_id }
  88         451  
76 105         294 my $ua = $self->ua;
77 105         526 my $tx = $ua->build_tx(POST => $self->url => $headers => json => $request);
78              
79             # SSE handling
80 105         23692 my $id = $request->{id};
81 105         165 my $response;
82             $tx->res->content->on(
83             sse => sub {
84 12     12   22010 my ($content, $event) = @_;
85 12 100 66     64 return unless $event->{text} && (my $res = eval { from_json($event->{text}) });
  6         36  
86 6 50 33     193 return unless defined($res->{id}) && defined($id) && $res->{id} eq $id;
      33        
87 6         14 $response = $res;
88 6         29 $tx->res->error({message => 'Interrupted'});
89             }
90 105         243 );
91              
92 105         3479 $tx = $ua->start($tx);
93              
94 105 100       257585 if (my $session_id = $tx->res->headers->header('Mcp-Session-Id')) { $self->session_id($session_id) }
  103         2156  
95              
96             # Request or notification accepted without a response
97 105 100       674 return undef if $tx->res->code eq '202';
98              
99 88 100       720 if (my $err = $tx->error) {
100 11 100       231 return $response if $err->{message} eq 'Interrupted';
101 5 50       1197 croak "$err->{code} response: $err->{message}" if $err->{code};
102 0         0 croak "Connection error: $err->{message}";
103             }
104              
105 77         1019 return $tx->res->json;
106             }
107              
108 80     80   3586 sub _result ($res) {
  80         112  
  80         93  
109 80 50       173 croak 'No response' unless $res;
110 80 100       233 if (my $err = $res->{error}) { croak "Error $err->{code}: $err->{message}" }
  9         1864  
111 71         316 return $res->{result};
112             }
113              
114             1;
115              
116             =encoding utf8
117              
118             =head1 NAME
119              
120             MCP::Server::Transport::HTTP - HTTP transport for MCP servers
121              
122             =head1 SYNOPSIS
123              
124             use MCP::Client;
125              
126             my $client = MCP::Client->new(url => 'http://localhost:3000/mcp');
127             $client->initialize_session;
128             my $tools = $client->list_tools;
129              
130             =head1 DESCRIPTION
131              
132             L is a client for MCP (Model Context Protocol) that communicates with MCP servers over HTTP.
133              
134             =head1 ATTRIBUTES
135              
136             L inherits all attributes from L and implements the following new ones.
137              
138             =head2 headers
139              
140             my $headers = $client->headers;
141             $client = $client->headers({Authorization => 'Bearer abc123'});
142              
143             Extra HTTP headers to send with every request, as a hash reference. Useful for passing an C header to
144             an MCP server that requires OAuth bearer authentication. Defaults to an empty hash reference.
145              
146             =head2 name
147              
148             my $name = $client->name;
149             $client = $client->name('PerlClient');
150              
151             The name of the client, defaults to C.
152              
153             =head2 session_id
154              
155             my $session_id = $client->session_id;
156             $client = $client->session_id('12345');
157              
158             The session ID for the client, used to maintain state across requests.
159              
160             =head2 ua
161              
162             my $ua = $client->ua;
163             $client = $client->ua(Mojo::UserAgent->new);
164              
165             The user agent used for making HTTP requests, defaults to a new instance of L.
166              
167             =head2 url
168              
169             my $url = $client->url;
170             $client = $client->url('http://localhost:3000/mcp');
171              
172             The URL of the MCP server, defaults to C.
173              
174             =head2 version
175              
176             my $version = $client->version;
177             $client = $client->version('1.0.0');
178              
179             The version of the client, defaults to C<1.0.0>.
180              
181             =head1 METHODS
182              
183             L inherits all methods from L and implements the following new ones.
184              
185             =head2 build_request
186              
187             my $request = $client->build_request('method_name', {param1 => 'value1'});
188              
189             Builds a JSON-RPC request with the given method name and parameters.
190              
191             =head2 build_notification
192              
193             my $notification = $client->build_notification('method_name', {param1 => 'value1'});
194              
195             Builds a JSON-RPC notification with the given method name and parameters.
196              
197             =head2 call_tool
198              
199             my $result = $client->call_tool('tool_name');
200             my $result = $client->call_tool('tool_name', {arg1 => 'value1'});
201              
202             Calls a tool on the MCP server with the specified name and arguments, returning the result.
203              
204             =head2 delete_session
205              
206             my $bool = $client->delete_session;
207              
208             Send a C request to terminate the current session on the MCP server, and clear the local
209             L. Returns true on success, or C if no session is active. The server only honors this when it
210             was configured with C<< streaming => 1 >>.
211              
212             =head2 get_prompt
213              
214             my $result = $client->get_prompt('prompt_name');
215             my $result = $client->get_prompt('prompt_name', {arg1 => 'value1'});
216              
217             Get a prompt from the MCP server with the specified name and arguments, returning the result.
218              
219             =head2 initialize_session
220              
221             my $result = $client->initialize_session;
222              
223             Initializes a session with the MCP server, setting up the protocol version and client information.
224              
225             =head2 list_prompts
226              
227             my $prompts = $client->list_prompts;
228              
229             Lists all available prompts on the MCP server.
230              
231             =head2 list_resources
232              
233             my $resources = $client->list_resources;
234              
235             Lists all available resources on the MCP server.
236              
237             =head2 list_tools
238              
239             my $tools = $client->list_tools;
240              
241             Lists all available tools on the MCP server.
242              
243             =head2 ping
244              
245             my $result = $client->ping;
246              
247             Sends a ping request to the MCP server to check connectivity.
248              
249             =head2 read_resource
250              
251             my $result = $client->read_resource('file:///path/to/resource.txt');
252              
253             Reads a resource from the MCP server with the specified URI, returning the result.
254              
255             =head2 send_request
256              
257             my $response = $client->send_request($request);
258              
259             Sends a JSON-RPC request to the MCP server and returns the response.
260              
261             =head1 SEE ALSO
262              
263             L, L, L.
264              
265             =cut