File Coverage

blib/lib/MCP/Run.pm
Criterion Covered Total %
statement 66 80 82.5
branch 19 22 86.3
condition 18 33 54.5
subroutine 7 9 77.7
pod 4 4 100.0
total 114 148 77.0


line stmt bran cond sub pod time code
1             package MCP::Run;
2             our $VERSION = '0.004';
3 4     4   371920 use Mojo::Base 'MCP::Server', -signatures;
  4         17025  
  4         48  
4              
5             # ABSTRACT: MCP server with a command execution tool
6              
7              
8             has allowed_commands => sub { undef };
9              
10              
11             has validator => sub { undef };
12              
13              
14             has working_directory => sub { undef };
15              
16              
17             has timeout => 30;
18              
19              
20             has tool_name => 'run';
21              
22              
23             has tool_description => sub {
24             my $self = shift;
25             my $base = 'Execute a command and return stdout, stderr, and exit code';
26             if ($self->compress) {
27             $base .= '. Output is compressed for token efficiency - set compress:false in arguments if you need full detail';
28             }
29             return $base;
30             };
31              
32              
33             has compress => 0;
34              
35              
36 19     19 1 858867 sub new ($class, %args) {
  19         47  
  19         73  
  19         23  
37 19         195 my $self = $class->SUPER::new(%args);
38 19         181 $self->_register_run_tool;
39 19         654 return $self;
40             }
41              
42 19     19   28 sub _register_run_tool ($self) {
  19         54  
  19         25  
43 19         25 my $server = $self;
44 7         16 $self->tool(
45             name => $self->tool_name,
46             description => $self->tool_description,
47             input_schema => {
48             type => 'object',
49             properties => {
50             command => { type => 'string', description => 'The command to execute' },
51             working_directory => { type => 'string', description => 'Working directory for the command' },
52             timeout => { type => 'integer', description => 'Timeout in seconds' },
53             compress => { type => 'boolean', description => 'Compress output for LLM efficiency' },
54             },
55             required => ['command'],
56             },
57 7     7   32 code => sub ($tool, $args) { $server->_handle_run($tool, $args) },
  7         13000  
  7         11  
  7         10  
58 19         72 );
59             }
60              
61 7     7   11 sub _handle_run ($self, $tool, $args) {
  7         8  
  7         13  
  7         10  
  7         9  
62 7         14 my $command = $args->{command};
63              
64 7 100       35 if (my $allowed = $self->allowed_commands) {
65 2         29 my ($first_word) = $command =~ /^\s*(\S+)/;
66 2 100 66     13 unless ($first_word && grep { $_ eq $first_word } @$allowed) {
  3         13  
67 1         7 return $tool->text_result("Command not allowed: $first_word", 1);
68             }
69             }
70              
71 6 100       21 if (my $validator = $self->validator) {
72 3         18 my $vresult = $validator->($command, $args->{working_directory});
73 3 100 66     57 unless (defined($vresult) && !ref($vresult) && $vresult eq '1') {
      100        
74 2 100 66     14 my $reason = defined($vresult) && length($vresult) ? $vresult : 'denied';
75 2         8 return $tool->text_result("Command $reason", 1);
76             }
77             }
78              
79 4   66     46 my $wd = $args->{working_directory} // $self->working_directory;
80 4   33     34 my $timeout = $args->{timeout} // $self->timeout;
81 4   33     50 my $compress = $args->{compress} // $self->compress;
82              
83 4         30 my $result = $self->execute($command, $wd, $timeout);
84 4         80 return $self->format_result($tool, $result, $compress);
85             }
86              
87 0     0 1 0 sub run_stdio ($class_or_self, %args) {
  0         0  
  0         0  
  0         0  
88 0 0       0 my $self = ref $class_or_self ? $class_or_self : $class_or_self->new(%args);
89 0         0 $self->to_stdio;
90 0         0 return $self;
91             }
92              
93              
94 1     1 1 5 sub execute ($self, $command, $working_directory, $timeout) {
  1         1  
  1         2  
  1         2  
  1         1  
  1         1  
95 1         26 die "execute() must be implemented by a subclass";
96             }
97              
98              
99 6     6 1 76 sub format_result ($self, $tool, $result, $compress = undef) {
  6         10  
  6         10  
  6         9  
  6         50  
  6         10  
100 6   50     30 my $exit_code = $result->{exit_code} // -1;
101 6   50     56 my $stdout = $result->{stdout} // '';
102 6   50     23 my $stderr = $result->{stderr} // '';
103 6         20 my $error = $result->{error};
104              
105 6   66     31 $compress //= $self->compress;
106              
107 6 50       26 if ($compress) {
108 0         0 (my $compressor) = $self->_get_compressor;
109 0         0 ($stdout, $stderr) = $compressor->compress('', $stdout, $stderr);
110             }
111              
112 6         28 my $text = "Exit code: $exit_code\n";
113 6 100       21 $text .= "\n=== STDOUT ===\n$stdout\n" if length $stdout;
114 6 100       22 $text .= "\n=== STDERR ===\n$stderr\n" if length $stderr;
115 6 100       17 $text .= "\n=== ERROR ===\n$error\n" if defined $error;
116              
117 6 100       21 my $is_error = $exit_code != 0 ? 1 : 0;
118 6         57 return $tool->text_result($text, $is_error);
119             }
120              
121             my $_compressor;
122              
123 0     0     sub _get_compressor ($self) {
  0            
  0            
124 0   0       $_compressor //= MCP::Run::Compress->new;
125 0           return $_compressor;
126             }
127              
128              
129              
130             1;
131              
132             __END__