File Coverage

blib/lib/Net/Async/MCP/Server.pm
Criterion Covered Total %
statement 77 83 92.7
branch 19 26 73.0
condition 12 18 66.6
subroutine 22 23 95.6
pod 9 10 90.0
total 139 160 86.8


line stmt bran cond sub pod time code
1             package Net::Async::MCP::Server;
2             # ABSTRACT: Async MCP server base class
3              
4 2     2   531870 use strict;
  2         3  
  2         59  
5 2     2   7 use warnings;
  2         3  
  2         100  
6              
7 2     2   376 use parent 'IO::Async::Notifier';
  2         227  
  2         11  
8              
9 2     2   30526 use Future::AsyncAwait;
  2         6486  
  2         7  
10              
11             our $VERSION = '0.001';
12              
13              
14             sub _init {
15 15     15   269515 my ( $self, $params ) = @_;
16 15         40 $self->SUPER::_init($params);
17             }
18              
19             sub configure {
20 15     15 1 60 my ( $self, %params ) = @_;
21 15 100       44 if (exists $params{name}) {
22 2         5 $self->{name} = delete $params{name};
23             }
24             }
25              
26             sub _add_to_loop {
27 0     0   0 my ( $self, $loop ) = @_;
28 0         0 $self->SUPER::_add_to_loop($loop);
29             }
30              
31 5   100 5 1 404 sub name { shift->{name} // 'NetAsyncMCPServer' }
32              
33              
34             sub server_info {
35 3     3 1 8 my ( $self ) = @_;
36             return {
37 3         5 name => $self->name,
38             version => $VERSION,
39             };
40             }
41              
42              
43             sub server_capabilities {
44 3     3 1 6 my ( $self ) = @_;
45 3   100     12 return $self->{server_capabilities} // {};
46             }
47              
48              
49 2     2 1 8 async sub initialize {
50 2         3 my ( $self ) = @_;
51              
52 2         5 $self->{server_capabilities} = $self->_build_capabilities;
53 2         4 $self->{_initialized} = 1;
54              
55             return {
56 2         6 protocolVersion => '2025-11-25',
57             capabilities => $self->server_capabilities,
58             serverInfo => $self->server_info,
59             };
60             }
61              
62              
63             sub _build_capabilities {
64 2     2   2 my ( $self ) = @_;
65 2         5 return { tools => {} };
66             }
67              
68             sub tools {
69 3     3 1 6 my ( $self ) = @_;
70 3   50     16 return $self->{tools} // [];
71             }
72              
73              
74             sub register_tool {
75 6     6 1 38 my ( $self, %tool ) = @_;
76 6   100     24 $self->{tools} //= [];
77 6         7 push @{ $self->{tools} }, \%tool;
  6         13  
78             }
79              
80              
81 2     2 1 5 async sub list_tools {
82 2         10 my ( $self ) = @_;
83 2         5 return $self->tools;
84             }
85              
86              
87 1     1 1 2 async sub call_tool {
88 1         2 my ( $self, $name, $arguments ) = @_;
89              
90 1         4 my $tool = $self->_find_tool($name);
91 1 50       3 die "No tool registered: $name" unless $tool;
92              
93 1 50       3 if (my $code = $tool->{code}) {
94 1         3 return $code->($arguments);
95             }
96              
97 0         0 die "Tool '$name' has no implementation";
98             }
99              
100             sub _find_tool {
101 1     1   2 my ( $self, $name ) = @_;
102 1   50     2 for my $tool (@{ $self->{tools} // [] }) {
  1         3  
103 1 50       5 return $tool if $tool->{name} eq $name;
104             }
105 0         0 return undef;
106             }
107              
108              
109             sub handle {
110 8     8 0 65 my ( $self, $request, $context ) = @_;
111              
112 8 50       18 return _jsonrpc_error( -32700, 'Invalid JSON-RPC request' )
113             unless ref $request eq 'HASH';
114              
115 8         37 my $method = $request->{method};
116 8         9 my $id = $request->{id};
117              
118 8 100       15 return _jsonrpc_error( -32600, 'Missing JSON-RPC method', $id )
119             unless defined $method;
120              
121 6 100       13 if ( defined $id ) {
122 5 100       20 if ( $method eq 'initialize' ) {
    100          
    100          
    100          
123 1   50     5 return $self->_handle_initialize( $request->{params} // {}, $id );
124             }
125             elsif ( $method eq 'tools/list' ) {
126 1         3 return $self->_handle_tools_list( $id );
127             }
128             elsif ( $method eq 'tools/call' ) {
129 1   50     5 return $self->_handle_tools_call( $request->{params} // {}, $id );
130             }
131             elsif ( $method eq 'ping' ) {
132 1         2 return _jsonrpc_response( {}, $id );
133             }
134              
135 1         6 return _jsonrpc_error( -32601, "Method '$method' not found", $id );
136             }
137              
138 1 50       3 if ( $method eq 'notifications/initialized' ) {
    0          
139 1         2 return undef;
140             }
141             elsif ( $method eq 'shutdown' ) {
142 0         0 return undef;
143             }
144              
145 0         0 return undef;
146             }
147              
148             sub _handle_initialize {
149 1     1   2 my ( $self, $params, $id ) = @_;
150              
151 1         3 my $result = $self->initialize->get;
152              
153 1         30 return _jsonrpc_response( $result, $id );
154             }
155              
156             sub _handle_tools_list {
157 1     1   2 my ( $self, $id ) = @_;
158              
159 1         2 my $tools = $self->list_tools->get;
160 1         29 return _jsonrpc_response( { tools => $tools }, $id );
161             }
162              
163             sub _handle_tools_call {
164 1     1   3 my ( $self, $params, $id ) = @_;
165              
166 1   50     4 my $name = $params->{name} // '';
167 1   50     2 my $args = $params->{arguments} // {};
168              
169 1         3 my $result = $self->call_tool( $name, $args )->get;
170 1         36 return _jsonrpc_response( $result, $id );
171             }
172              
173             sub _jsonrpc_error {
174 3     3   5 my ( $code, $message, $id ) = @_;
175             return {
176 3         14 jsonrpc => '2.0',
177             id => $id,
178             error => { code => $code, message => $message },
179             };
180             }
181              
182             sub _jsonrpc_response {
183 4     4   6 my ( $result, $id ) = @_;
184 4         15 return { jsonrpc => '2.0', id => $id, result => $result };
185             }
186              
187             1;
188              
189             __END__