File Coverage

blib/lib/MCP/Run.pm
Criterion Covered Total %
statement 61 75 81.3
branch 13 16 81.2
condition 11 24 45.8
subroutine 7 9 77.7
pod 4 4 100.0
total 96 128 75.0


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