-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathServerCommand.php
More file actions
248 lines (214 loc) · 7.74 KB
/
ServerCommand.php
File metadata and controls
248 lines (214 loc) · 7.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
<?php
declare(strict_types=1);
namespace Synapse\Command;
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\CommandFactoryInterface;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\Core\Configure;
use Cake\Core\ContainerInterface;
use Cake\Log\Log;
use Mcp\Server\Transport\StdioTransport;
use Psr\Log\NullLogger;
use Synapse\Builder\ServerBuilder;
use Throwable;
/**
* MCP Server Command
*
* Starts the Model Context Protocol server with the specified transport.
* The server exposes CakePHP functionality (Tools, Resources, Prompts) to MCP clients.
*/
class ServerCommand extends Command
{
/**
* @inheritDoc
*/
public static function defaultName(): string
{
return 'synapse server';
}
/**
* @inheritDoc
*/
public static function getDescription(): string
{
return 'Start the MCP (Model Context Protocol) server';
}
/**
* Constructor
*
* @param \Cake\Core\ContainerInterface $container CakePHP DI container
* @param \Cake\Console\CommandFactoryInterface|null $factory Command factory
*/
public function __construct(
private ContainerInterface $container,
?CommandFactoryInterface $factory = null,
) {
parent::__construct($factory);
}
/**
* Configure command options
*
* @param \Cake\Console\ConsoleOptionParser $parser Option parser
*/
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser
->setDescription('Start the MCP (Model Context Protocol) server')
->addOption('transport', [
'short' => 't',
'help' => 'Transport type (currently only stdio is supported)',
'default' => 'stdio',
'choices' => ['stdio'],
])
->addOption('no-cache', [
'short' => 'n',
'help' => 'Disable discovery caching for this run',
'boolean' => true,
])
->addOption('clear-cache', [
'short' => 'c',
'help' => 'Clear discovery cache before starting',
'boolean' => true,
])
->addOption('inspect', [
'short' => 'i',
'help' => 'Launch MCP Inspector to test the server interactively (requires Node.js/npx)',
'boolean' => true,
]);
return $parser;
}
/**
* Execute the command
*
* @param \Cake\Console\Arguments $args Command arguments
* @param \Cake\Console\ConsoleIo $io Console I/O
* @return int Exit code
*/
public function execute(Arguments $args, ConsoleIo $io): int
{
// If --inspect flag is present, launch inspector
if ($args->getOption('inspect')) {
return $this->launchInspector($io);
}
$config = Configure::read('Synapse', []);
$logEngine = $config['logger'];
$logger = new NullLogger();
// Use stderr in verbose mode so we can see what's happening.
// Note that this overrides the configured log engine.
if ($args->getOption('verbose')) {
$logEngine = 'stderr';
}
if ($logEngine !== null && is_string($logEngine)) {
$logger = Log::engine($logEngine) ?: $logger;
}
try {
// Handle cache clearing if requested
if ($args->getOption('clear-cache')) {
$cacheEngine = $config['discovery']['cache'] ?? ServerBuilder::DEFAULT_CACHE_ENGINE;
if (ServerBuilder::clearCache($cacheEngine)) {
$logger->info(
sprintf('Discovery cache cleared (cache engine: %s)', $cacheEngine),
);
} else {
$logger->warning(
sprintf('Failed to clear discovery cache (cache engine: %s)', $cacheEngine),
);
}
}
$logger->info('Building MCP server...');
// Build server using ServerBuilder
$builder = (new ServerBuilder($config))
->setContainer($this->container)
->setLogger($logger)
->withPluginTools();
// Disable cache if --no-cache flag
if ($args->getOption('no-cache')) {
$builder->withoutCache();
$logger->debug('Discovery caching disabled via --no-cache');
} else {
$cacheEngine = $config['discovery']['cache'] ?? ServerBuilder::DEFAULT_CACHE_ENGINE;
$logger->debug(
sprintf('Discovery caching enabled (cache engine: %s)', $cacheEngine),
);
}
// Log discovery configuration
$logger->debug(
sprintf(
'Discovery: scanning %s, excluding %s',
implode(', ', $builder->getScanDirs()),
implode(', ', $builder->getExcludeDirs()),
),
);
$logger->info('Discovering MCP elements...');
$server = $builder->build();
$logger->info('Discovery complete');
$logger->info('MCP server started with stdio transport');
$logger->info('Listening for MCP requests...');
// Start server (blocking call)
$stdioTransport = new StdioTransport();
$server->run($stdioTransport);
return static::CODE_SUCCESS;
} catch (Throwable $throwable) {
$logger->error('MCP Server error: ' . $throwable->getMessage());
return static::CODE_ERROR;
}
}
/**
* Launch MCP Inspector to test the server
*
* @param \Cake\Console\ConsoleIo $io Console I/O
* @return int Exit code
*/
private function launchInspector(ConsoleIo $io): int
{
$io->out('<info>Launching MCP Inspector...</info>');
$io->out('');
// Check if npx is available
$npxPath = $this->findExecutable('npx');
if ($npxPath === null) {
$io->error('npx not found. Please install Node.js to use the MCP Inspector.');
$io->out('Visit: https://nodejs.org/');
return static::CODE_ERROR;
}
// Build command - inspector will launch the actual server
$command = sprintf(
'%s @modelcontextprotocol/inspector %s',
escapeshellarg($npxPath),
'bin/cake synapse server',
);
$io->out('<info>Command:</info> ' . $command);
$io->out('');
$io->out('The inspector will open in your browser...');
$io->out('Press <warning>Ctrl+C</warning> to stop');
$io->out('');
// Execute and return exit code
passthru($command, $exitCode);
return $exitCode === 0 ? static::CODE_SUCCESS : static::CODE_ERROR;
}
/**
* Find executable in system PATH
*
* @param string $name Executable name
* @return string|null Full path to executable or null if not found
*/
private function findExecutable(string $name): ?string
{
// Try which command first (Unix/Linux/macOS)
$which = trim((string)shell_exec(sprintf('which %s 2>/dev/null', escapeshellarg($name))));
if ($which !== '' && is_executable($which)) {
return $which;
}
// Try where command (Windows)
$where = trim((string)shell_exec(sprintf('where %s 2>nul', escapeshellarg($name))));
if ($where !== '') {
$paths = explode("\n", $where);
$firstPath = trim($paths[0]);
if ($firstPath !== '' && is_executable($firstPath)) {
return $firstPath;
}
}
return null;
}
}