File Coverage

blib/lib/MCP/Run.pm
Criterion Covered Total %
statement 57 57 100.0
branch 12 12 100.0
condition 8 15 53.3
subroutine 7 7 100.0
pod 3 3 100.0
total 87 94 92.5


line stmt bran cond sub pod time code
1             package MCP::Run;
2             our $VERSION = '0.001';
3 4     4   629417 use Mojo::Base 'MCP::Server', -signatures;
  4         27935  
  4         51  
4              
5             # ABSTRACT: MCP server with a command execution tool
6              
7              
8             has allowed_commands => sub { undef };
9              
10              
11             has working_directory => sub { undef };
12              
13              
14             has timeout => 30;
15              
16              
17             has tool_name => 'run';
18              
19              
20             has tool_description => 'Execute a command and return stdout, stderr, and exit code';
21              
22              
23 16     16 1 1563500 sub new ($class, %args) {
  16         42  
  16         123  
  16         29  
24 16         186 my $self = $class->SUPER::new(%args);
25 16         253 $self->_register_run_tool;
26 16         2118 return $self;
27             }
28              
29 16     16   30 sub _register_run_tool ($self) {
  16         29  
  16         25  
30 16         45 my $server = $self;
31 4         14 $self->tool(
32             name => $self->tool_name,
33             description => $self->tool_description,
34             input_schema => {
35             type => 'object',
36             properties => {
37             command => { type => 'string', description => 'The command to execute' },
38             working_directory => { type => 'string', description => 'Working directory for the command' },
39             timeout => { type => 'integer', description => 'Timeout in seconds' },
40             },
41             required => ['command'],
42             },
43 4     4   79 code => sub ($tool, $args) { $server->_handle_run($tool, $args) },
  4         21701  
  4         10  
  4         14  
44 16         121 );
45             }
46              
47 4     4   13 sub _handle_run ($self, $tool, $args) {
  4         23  
  4         13  
  4         10  
  4         11  
48 4         14 my $command = $args->{command};
49              
50 4 100       66 if (my $allowed = $self->allowed_commands) {
51 2         41 my ($first_word) = $command =~ /^\s*(\S+)/;
52 2 100 66     26 unless ($first_word && grep { $_ eq $first_word } @$allowed) {
  3         19  
53 1         37 return $tool->text_result("Command not allowed: $first_word", 1);
54             }
55             }
56              
57 3   66     112 my $wd = $args->{working_directory} // $self->working_directory;
58 3   33     42 my $timeout = $args->{timeout} // $self->timeout;
59              
60 3         44 my $result = $self->execute($command, $wd, $timeout);
61 3         105 return $self->format_result($tool, $result);
62             }
63              
64 1     1 1 9 sub execute ($self, $command, $working_directory, $timeout) {
  1         2  
  1         2  
  1         2  
  1         2  
  1         2  
65 1         11 die "execute() must be implemented by a subclass";
66             }
67              
68              
69 5     5 1 62 sub format_result ($self, $tool, $result) {
  5         16  
  5         14  
  5         15  
  5         18  
70 5   50     39 my $exit_code = $result->{exit_code} // -1;
71 5   50     39 my $stdout = $result->{stdout} // '';
72 5   50     32 my $stderr = $result->{stderr} // '';
73 5         24 my $error = $result->{error};
74              
75 5         31 my $text = "Exit code: $exit_code\n";
76 5 100       28 $text .= "\n=== STDOUT ===\n$stdout\n" if length $stdout;
77 5 100       53 $text .= "\n=== STDERR ===\n$stderr\n" if length $stderr;
78 5 100       26 $text .= "\n=== ERROR ===\n$error\n" if defined $error;
79              
80 5 100       35 my $is_error = $exit_code != 0 ? 1 : 0;
81 5         116 return $tool->text_result($text, $is_error);
82             }
83              
84              
85              
86             1;
87              
88             __END__