File Coverage

blib/lib/Acme/MCP.pm
Criterion Covered Total %
statement 14 14 100.0
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 19 19 100.0


line stmt bran cond sub pod time code
1 1     1   246189 use v5.42;
  1         5  
2 1     1   11 use feature 'class';
  1         2  
  1         239  
3 1     1   8 no warnings 'experimental::class';
  1         4  
  1         115  
4             #
5             class Acme::MCP v1.0.1 {
6 1     1   913 use JSON::PP;
  1         22441  
  1         139  
7 1     1   12 use Carp qw[carp croak];
  1         2  
  1         1780  
8             #
9             field $name : param : reader : writer = 'Generic MCP Server';
10             field $version : param : reader : writer = '1.0.0';
11             field %tools : reader;
12             field $json = JSON::PP->new->utf8(1);
13             #
14             method add_tool (%params) {
15             my $name = $params{name} or croak 'Tool name required';
16             my $code = $params{code} or croak 'Tool code (sub) required';
17             $tools{$name}
18             = { description => $params{description} // '', inputSchema => $params{schema} // { type => 'object', properties => {} }, code => $code };
19             }
20              
21             method run () {
22             $| = 1; # Autoflush for stdio communication
23             carp "$name started, listening on STDIN";
24             while ( my $line = ) {
25             chomp $line;
26             my $request;
27             try { $request = $json->decode($line) }
28             catch ($e) {
29             $self->_send_error( undef, -32700, 'Parse error' );
30             next;
31             }
32             $self->_handle_request($request);
33             }
34             }
35              
36             method _handle_request ($req) {
37             my $id = $req->{id};
38             my $method = $req->{method} // '';
39             if ( $method eq 'initialize' ) {
40             $self->_send_response(
41             $id,
42             { protocolVersion => '2024-11-05',
43             capabilities => { tools => { listChanged => JSON::PP::true }, },
44             serverInfo => { name => $name, version => $version }
45             }
46             );
47             }
48             elsif ( $method eq 'tools/list' ) {
49             my @tool_list;
50             for my $t_name ( sort keys %tools ) {
51             push @tool_list, { name => $t_name, description => $tools{$t_name}{description}, inputSchema => $tools{$t_name}{inputSchema} };
52             }
53             $self->_send_response( $id, { tools => \@tool_list } );
54             }
55             elsif ( $method eq 'tools/call' ) {
56             $self->_handle_tool_call( $id, $req->{params} );
57             }
58             else {
59             $self->_send_error( $id, -32601, 'Method not found: ' . $method );
60             }
61             }
62              
63             method _handle_tool_call ( $id, $params ) {
64             my $t_name = $params->{name};
65             my $args = $params->{arguments} // {};
66             if ( my $tool = $tools{$t_name} ) {
67             try {
68             my $result = $tool->{code}->($args);
69             $self->_send_tool_result( $id, $result );
70             }
71             catch ($e) {
72             $self->_send_tool_result( $id, { error => $e }, 1 );
73             }
74             }
75             else {
76             $self->_send_error( $id, -32602, 'Unknown tool: ' . $t_name );
77             }
78             }
79              
80             method _send_response ( $id, $result ) {
81             print STDOUT $json->encode( { jsonrpc => '2.0', id => $id, result => $result } ) . "\n";
82             }
83              
84             method _send_tool_result ( $id, $content, $is_error = 0 ) {
85             $self->_send_response(
86             $id,
87             { content => [ { type => 'text', text => ref($content) ? $json->encode($content) : $content } ],
88             isError => $is_error ? JSON::PP::true : JSON::PP::false
89             }
90             );
91             }
92              
93             method _send_error ( $id, $code, $message ) {
94             print STDOUT $json->encode( { jsonrpc => '2.0', id => $id, error => { code => $code, message => $message } } ) . "\n";
95             }
96             };
97             #
98             1;