Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion demo/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"symfony-ai-mate": {
"command": "./vendor/bin/mate",
"args": [
"serve"
"serve",
"--force-keep-alive"
]
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/mate/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
CHANGELOG
=========

0.2
---

* Add `StopCommand` to stop a running server
* Add `--force-keep-alive` option to `ServeCommand` to restart server if it was stopped

0.1
---

Expand Down
61 changes: 60 additions & 1 deletion src/mate/bin/mate
Original file line number Diff line number Diff line change
@@ -1,4 +1,63 @@
#!/usr/bin/env php
<?php

include __DIR__ . '/mate.php';
/*
* This script will decide if we should try to keep the process alive or not.
* When running ./vendor/bin/mate serve --force-keep-alive, it will spawn a child process.
*/

$keepAlive = false;
if (($argv[1] ?? false) === 'serve') {
$keepAlive = $argv;
// try to detect "--force-keep-alive
for ($i = 2; $i < $argc; ++$i) {
if ($argv[$i] === '--force-keep-alive') {
unset($keepAlive[$i]);
}
}
}

if (false === $keepAlive || $argv === $keepAlive) {
include __DIR__ . '/mate.php';
return;
}

// Build the command as an array to avoid shell interpretation
$command = $keepAlive;

while (true) {
// Run child attached to parent's STDIN/STDOUT/STDERR so it can read from STDIN
// and interact as if it was run directly.
$descriptorSpec = [
0 => ['file', 'php://stdin', 'r'],
1 => ['file', 'php://stdout', 'w'],
2 => ['file', 'php://stderr', 'w'],
];

$process = proc_open($command, $descriptorSpec, $pipes, null, null, [
'bypass_shell' => true,
]);

if (!\is_resource($process)) {
fwrite(STDERR, "[mate][keep-alive] Failed to start process.\n");
exit(70); // EX_SOFTWARE
}

// Wait for the process to terminate and get its exit code
$exitCode = proc_close($process);

if ($exitCode === 0) {
fwrite(STDERR, "[mate][keep-alive] Process exited with code 0, restarting...\n");
sleep(1);
continue;
}

if ($exitCode >= 129 && $exitCode <= 192) {
$signal = $exitCode - 128;
fwrite(STDERR, sprintf("[mate][keep-alive] Process terminated by signal %d (exit %d), not restarting.\n", $signal, $exitCode));
} else {
fwrite(STDERR, sprintf("[mate][keep-alive] Process exited with code %d, not restarting.\n", $exitCode));
}

exit($exitCode);
}
3 changes: 2 additions & 1 deletion src/mate/resources/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"symfony-ai-mate": {
"command": "./vendor/bin/mate",
"args": [
"serve"
"serve",
"--force-keep-alive"
]
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/mate/src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@

namespace Symfony\AI\Mate;

use Mcp\Server\Transport\Stdio\RunnerControl;
use Mcp\Server\Transport\Stdio\RunnerState;
use Psr\Log\LoggerInterface;
use Symfony\AI\Mate\Command\ClearCacheCommand;
use Symfony\AI\Mate\Command\DiscoverCommand;
use Symfony\AI\Mate\Command\InitCommand;
use Symfony\AI\Mate\Command\ServeCommand;
use Symfony\AI\Mate\Command\StopCommand;
use Symfony\AI\Mate\Exception\UnsupportedVersionException;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
Expand Down Expand Up @@ -46,8 +49,15 @@ public static function build(ContainerBuilder $container): Application
self::addCommand($application, new InitCommand($rootDir));
self::addCommand($application, new ServeCommand($logger, $container));
self::addCommand($application, new DiscoverCommand($rootDir, $logger));
self::addCommand($application, new StopCommand((string) $container->getParameter('mate.cache_dir')));
self::addCommand($application, new ClearCacheCommand($cacheDir));

if (\defined('SIGUSR1') && class_exists(RunnerControl::class)) {
$application->getSignalRegistry()->register(\SIGUSR1, function () {
RunnerControl::$state = RunnerState::STOP;
});
}

return $application;
}

Expand Down
21 changes: 20 additions & 1 deletion src/mate/src/Command/ServeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

Expand Down Expand Up @@ -59,8 +60,19 @@ public static function getDefaultDescription(): string
return 'Starts the MCP server with stdio transport';
}

protected function configure(): void
{
$this->addOption('force-keep-alive', null, InputOption::VALUE_NONE, 'Force a restart of the server if it stops.');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($input->getOption('force-keep-alive')) {
$output->writeln('The option --force-keep-alive requires using the "bin/mate" file. Try running "./vendor/bin/mate serve --force-keep-alive"');

return Command::INVALID;
}

$rootDir = $this->container->getParameter('mate.root_dir');
\assert(\is_string($rootDir));

Expand Down Expand Up @@ -100,7 +112,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
->setLogger($this->logger)
->build();

$server->run(new StdioTransport());
$pidFileName = \sprintf('%s/server_%d.pid', $cacheDir, getmypid());
file_put_contents($pidFileName, getmypid());

try {
$server->run(new StdioTransport());
} finally {
unlink($pidFileName);
}

return Command::SUCCESS;
}
Expand Down
74 changes: 74 additions & 0 deletions src/mate/src/Command/StopCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Mate\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Finder\Finder;

/**
* Stop all running servers. This will force the AI to restart the server. Can be combined with
* the "--force-keep-alive" option on the "serve" command to make sure the server is restarted
* and not killed.
*
* @author Tobias Nyholm <[email protected]>
*/
#[AsCommand('stop', 'Stop running servers to allow them to be restarted with new configuration')]
class StopCommand extends Command
{
public function __construct(private string $cacheDir)
{
parent::__construct(self::getDefaultName());
}

public static function getDefaultName(): string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

symfony/console": "^7.4|^8.0",

Any reason not to use invokable command instead? These static methods are deprecated since 7.3 btw

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. The version constraint has been bumped. I intentionally set that to SF 5.4.

I'll make a PR to lower it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: #1292

{
return 'stop';
}

public static function getDefaultDescription(): string
{
return 'Stop running servers to allow them to be restarted with new configuration';
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

if (!\function_exists('posix_kill')) {
$io->error('The "stop" command require the posix php extension.');

return Command::FAILURE;
}

if (!\defined('SIGUSR1')) {
$io->error('The "stop" command require the pcntl php extension.');

return Command::FAILURE;
}

$finder = new Finder();
$finder->files()
->in($this->cacheDir)
->name('server_*.pid');

foreach ($finder as $file) {
$pid = (int) file_get_contents($file->getRealPath());
posix_kill($pid, \SIGUSR1);
}

return Command::SUCCESS;
}
}