File Coverage

blib/lib/MCP/Client.pm
Criterion Covered Total %
statement 107 109 98.1
branch 16 22 72.7
condition 4 9 44.4
subroutine 19 19 100.0
pod 11 11 100.0
total 157 170 92.3


line stmt bran cond sub pod time code
1             package MCP::Client;
2 2     2   1806012 use Mojo::Base -base, -signatures;
  2         6  
  2         18  
3              
4 2     2   839 use Carp qw(croak);
  2         7  
  2         191  
5 2     2   1195 use MCP::Constants qw(PROTOCOL_VERSION);
  2         8  
  2         210  
6 2     2   18 use Mojo::JSON qw(from_json);
  2         5  
  2         111  
7 2     2   15 use Mojo::UserAgent;
  2         8  
  2         15  
8 2     2   98 use Scalar::Util qw(weaken);
  2         4  
  2         4075  
9              
10             has name => 'PerlClient';
11             has 'session_id';
12             has ua => sub { Mojo::UserAgent->new };
13             has url => sub {'http://localhost:3000/mcp'};
14             has version => '1.0.0';
15              
16 49     49 1 12804 sub build_request ($self, $method, $params = {}) {
  49         125  
  49         94  
  49         107  
  49         81  
17 49         191 my $request = $self->build_notification($method, $params);
18 49 100       295 $request->{id} = $self->{id} = $self->{id} ? $self->{id} + 1 : 1;
19 49         138 return $request;
20             }
21              
22 52     52 1 101 sub build_notification ($self, $method, $params = {}) {
  52         85  
  52         94  
  52         87  
  52         79  
23 52         331 return {jsonrpc => '2.0', method => $method, params => $params};
24             }
25              
26 17     17 1 143315 sub call_tool ($self, $name, $args = {}) {
  17         47  
  17         52  
  17         49  
  17         40  
27 17         2889 my $request = $self->build_request('tools/call', {name => $name, arguments => $args});
28 17         87 return _result($self->send_request($request));
29             }
30              
31 10     10 1 48563 sub get_prompt ($self, $name, $args = {}) {
  10         22  
  10         22  
  10         19  
  10         20  
32 10         63 my $request = $self->build_request('prompts/get', {name => $name, arguments => $args});
33 10         67 return _result($self->send_request($request));
34             }
35              
36 3     3 1 56527 sub initialize_session ($self) {
  3         8  
  3         7  
37 3         19 my $request = $self->build_request(
38             initialize => {
39             protocolVersion => PROTOCOL_VERSION,
40             capabilities => {},
41             clientInfo => {name => $self->name, version => $self->version,},
42             }
43             );
44 3         13 my $result = _result($self->send_request($request));
45 3         13 $self->send_request($self->build_notification('notifications/initialized'));
46 3         59 return $result;
47             }
48              
49 3     3 1 23138 sub list_prompts ($self) { _result($self->send_request($self->build_request('prompts/list'))) }
  3         8  
  3         7  
  3         15  
50 3     3 1 16547 sub list_resources ($self) { _result($self->send_request($self->build_request('resources/list'))) }
  3         7  
  3         25  
  3         14  
51 3     3 1 10821 sub list_tools ($self) { _result($self->send_request($self->build_request('tools/list'))) }
  3         6  
  3         7  
  3         15  
52 1     1 1 7639 sub ping ($self) { _result($self->send_request($self->build_request('ping'))) }
  1         2  
  1         2  
  1         4  
53              
54 8     8 1 65381 sub read_resource ($self, $uri) {
  8         19  
  8         18  
  8         13  
55 8         42 my $request = $self->build_request('resources/read', {uri => $uri});
56 8         30 return _result($self->send_request($request));
57             }
58              
59 52     52 1 96 sub send_request ($self, $request) {
  52         93  
  52         95  
  52         82  
60 52         279 my $headers = {Accept => 'application/json, text/event-stream', 'Content-Type' => 'application/json'};
61 52 100       288 if (my $session_id = $self->session_id) { $headers->{'Mcp-Session-Id'} = $session_id }
  49         485  
62 52         218 my $ua = $self->ua;
63 52         493 my $tx = $ua->build_tx(POST => $self->url => $headers => json => $request);
64              
65             # SSE handling
66 52         19409 my $id = $request->{id};
67 52         147 my $response;
68             $tx->res->content->on(
69             sse => sub {
70 10     10   35476 my ($content, $event) = @_;
71 10 100 66     69 return unless $event->{text} && (my $res = eval { from_json($event->{text}) });
  5         33  
72 5 50 33     192 return unless defined($res->{id}) && defined($id) && $res->{id} eq $id;
      33        
73 5         13 $response = $res;
74 5         21 $tx->res->error({message => 'Interrupted'});
75             }
76 52         222 );
77              
78 52         3448 $tx = $ua->start($tx);
79              
80 52 50       213334 if (my $session_id = $tx->res->headers->header('Mcp-Session-Id')) { $self->session_id($session_id) }
  52         1768  
81              
82             # Request or notification accepted without a response
83 52 100       455 return undef if $tx->res->code eq '202';
84              
85 49 100       568 if (my $err = $tx->error) {
86 5 50       151 return $response if $err->{message} eq 'Interrupted';
87 0 0       0 croak "$err->{code} response: $err->{message}" if $err->{code};
88 0         0 croak "Connection error: $err->{message}";
89             }
90              
91 44         894 return $tx->res->json;
92             }
93              
94 48     48   3262 sub _result ($res) {
  48         133  
  48         102  
95 48 50       144 croak 'No response' unless $res;
96 48 100       211 if (my $err = $res->{error}) { croak "Error $err->{code}: $err->{message}" }
  8         2130  
97 40         312 return $res->{result};
98             }
99              
100             1;
101              
102             =encoding utf8
103              
104             =head1 NAME
105              
106             MCP::Server::Transport::HTTP - HTTP transport for MCP servers
107              
108             =head1 SYNOPSIS
109              
110             use MCP::Client;
111              
112             my $client = MCP::Client->new(url => 'http://localhost:3000/mcp');
113             $client->initialize_session;
114             my $tools = $client->list_tools;
115              
116             =head1 DESCRIPTION
117              
118             L is a client for MCP (Model Context Protocol) that communicates with MCP servers over HTTP.
119              
120             =head1 ATTRIBUTES
121              
122             L inherits all attributes from L and implements the following new ones.
123              
124             =head2 name
125              
126             my $name = $client->name;
127             $client = $client->name('PerlClient');
128              
129             The name of the client, defaults to C.
130              
131             =head2 session_id
132              
133             my $session_id = $client->session_id;
134             $client = $client->session_id('12345');
135              
136             The session ID for the client, used to maintain state across requests.
137              
138             =head2 ua
139              
140             my $ua = $client->ua;
141             $client = $client->ua(Mojo::UserAgent->new);
142              
143             The user agent used for making HTTP requests, defaults to a new instance of L.
144              
145             =head2 url
146              
147             my $url = $client->url;
148             $client = $client->url('http://localhost:3000/mcp');
149              
150             The URL of the MCP server, defaults to C.
151              
152             =head2 version
153              
154             my $version = $client->version;
155             $client = $client->version('1.0.0');
156              
157             The version of the client, defaults to C<1.0.0>.
158              
159             =head1 METHODS
160              
161             L inherits all methods from L and implements the following new ones.
162              
163             =head2 build_request
164              
165             my $request = $client->build_request('method_name', {param1 => 'value1'});
166              
167             Builds a JSON-RPC request with the given method name and parameters.
168              
169             =head2 build_notification
170              
171             my $notification = $client->build_notification('method_name', {param1 => 'value1'});
172              
173             Builds a JSON-RPC notification with the given method name and parameters.
174              
175             =head2 call_tool
176              
177             my $result = $client->call_tool('tool_name');
178             my $result = $client->call_tool('tool_name', {arg1 => 'value1'});
179              
180             Calls a tool on the MCP server with the specified name and arguments, returning the result.
181              
182             =head2 get_prompt
183              
184             my $result = $client->get_prompt('prompt_name');
185             my $result = $client->get_prompt('prompt_name', {arg1 => 'value1'});
186              
187             Get a prompt from the MCP server with the specified name and arguments, returning the result.
188              
189             =head2 initialize_session
190              
191             my $result = $client->initialize_session;
192              
193             Initializes a session with the MCP server, setting up the protocol version and client information.
194              
195             =head2 list_prompts
196              
197             my $prompts = $client->list_prompts;
198              
199             Lists all available prompts on the MCP server.
200              
201             =head2 list_resources
202              
203             my $resources = $client->list_resources;
204              
205             Lists all available resources on the MCP server.
206              
207             =head2 list_tools
208              
209             my $tools = $client->list_tools;
210              
211             Lists all available tools on the MCP server.
212              
213             =head2 ping
214              
215             my $result = $client->ping;
216              
217             Sends a ping request to the MCP server to check connectivity.
218              
219             =head2 read_resource
220              
221             my $result = $client->read_resource('file:///path/to/resource.txt');
222              
223             Reads a resource from the MCP server with the specified URI, returning the result.
224              
225             =head2 send_request
226              
227             my $response = $client->send_request($request);
228              
229             Sends a JSON-RPC request to the MCP server and returns the response.
230              
231             =head1 SEE ALSO
232              
233             L, L, L.
234              
235             =cut