Catalyst-Plugin-MCP
view release on metacpan or search on metacpan
t/server-verbs.t view on Meta::CPAN
use v5.36;
use Test::More;
use Test::Fatal;
use Catalyst::Plugin::MCP::Server;
# --- stub providers that record their args and exercise every path ------
package RecTools {
use Moo;
with 'Catalyst::Plugin::MCP::Role::ToolProvider';
has seen => ( is => 'ro', default => sub { {} } );
sub list ( $self, $cursor = undef ) {
$self->seen->{list_cursor} = $cursor;
return { tools => [ { name => 'echo' } ], nextCursor => 'NEXT' };
}
sub call ( $self, $name, $args ) {
$self->seen->{call} = [ $name, $args ];
return { isError => 1, content => [ { type => 'text', text => 'boom' } ] }
if $args->{fail};
return { content => [ { type => 'text', text => "ran $name" } ] };
}
}
package RecResources {
use Moo;
with 'Catalyst::Plugin::MCP::Role::ResourceProvider';
sub list ( $self, $cursor = undef ) { return { resources => [], nextCursor => $cursor } }
sub templates ( $self ) { return { resourceTemplates => [ { uriTemplate => 't' } ] } }
sub read ( $self, $uri ) {
return undef if $uri eq 'gone://x';
return { contents => [ { uri => $uri, text => 'hi' } ] };
}
}
package RecPrompts {
use Moo;
with 'Catalyst::Plugin::MCP::Role::PromptProvider';
sub list ( $self, $cursor = undef ) { return { prompts => [], nextCursor => $cursor } }
sub get ( $self, $name, $args ) {
return undef if $name eq 'nope';
return { messages => [ { role => 'user', content => $args } ] };
}
}
# ------------------------------------------------------------------------
my $tools = RecTools->new;
my $engine = Catalyst::Plugin::MCP::Server->new;
$engine->register_provider($tools);
$engine->register_provider( RecResources->new );
$engine->register_provider( RecPrompts->new );
my $h = $engine->handlers;
# pagination pass-through: cursor in -> provider sees it; nextCursor out
my $list = $h->{'tools/list'}->( { cursor => 'C1' } );
is( $tools->seen->{list_cursor}, 'C1', 'cursor passed to provider list()' );
is( $list->{nextCursor}, 'NEXT', 'nextCursor returned verbatim' );
# resources/list passes the cursor too
is( $h->{'resources/list'}->( { cursor => 'RC' } )->{nextCursor},
'RC', 'resources/list pass-through cursor' );
# resources/read happy path
is_deeply( $h->{'resources/read'}->( { uri => 'file://a' } ),
{ contents => [ { uri => 'file://a', text => 'hi' } ] },
'resources/read returns provider contents' );
( run in 0.697 second using v1.01-cache-2.11-cpan-54e63673c56 )