Skip to content
Open
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 src/McpServerBootloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Butschster\ContextGenerator\McpServer;

use Butschster\ContextGenerator\Application\Bootloader\ConsoleBootloader;
use Butschster\ContextGenerator\McpServer\Interceptor\McpServerInterceptorBootloader;
use Butschster\ContextGenerator\McpServer\McpConfig\McpConfigBootloader;
use Butschster\ContextGenerator\McpServer\Projects\McpProjectsBootloader;
Expand All @@ -13,6 +12,7 @@
use Butschster\ContextGenerator\McpServer\Routing\McpResponseStrategy;
use Butschster\ContextGenerator\McpServer\Routing\RouteRegistrar;
use Butschster\ContextGenerator\McpServer\Tool\McpToolBootloader;
use Butschster\ContextGenerator\McpServer\Watcher\ConfigWatcherBootloader;
use League\Route\Router;
use League\Route\Strategy\StrategyInterface;
use Psr\Container\ContainerInterface;
Expand All @@ -31,6 +31,7 @@ public function defineDependencies(): array
McpProjectsBootloader::class,
McpConfigBootloader::class,
McpServerCoreBootloader::class,
ConfigWatcherBootloader::class,
];
}

Expand Down
15 changes: 15 additions & 0 deletions src/Prompt/PromptRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ public function has(string $name): bool
return isset($this->prompts[$name]);
}

public function remove(string $id): bool
{
if (!$this->has($id)) {
return false;
}

unset($this->prompts[$id]);
return true;
}

public function clear(): void
{
$this->prompts = [];
}

public function all(): array
{
return $this->prompts;
Expand Down
19 changes: 18 additions & 1 deletion src/Prompt/PromptRegistryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,24 @@
interface PromptRegistryInterface
{
/**
* Registers a prompt in the registry.
* Registers a prompt in the registry (upsert semantics - replaces if exists).
*/
public function register(PromptDefinition $prompt): void;

/**
* Removes a prompt from the registry by ID.
*
* @return bool True if the prompt existed and was removed, false otherwise
*/
public function remove(string $id): bool;

/**
* Checks if a prompt with the given ID exists.
*/
public function has(string $id): bool;

/**
* Removes all prompts from the registry.
*/
public function clear(): void;
}
22 changes: 21 additions & 1 deletion src/ServerRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

namespace Butschster\ContextGenerator\McpServer;

use Butschster\ContextGenerator\Application\Logger\HasPrefixLoggerInterface;
use Butschster\ContextGenerator\McpServer\Registry\McpItemsRegistry;
use Butschster\ContextGenerator\McpServer\Routing\RouteRegistrar;
use Butschster\ContextGenerator\McpServer\Transport\StdioTransport;
use Butschster\ContextGenerator\McpServer\Watcher\ConfigWatcherConfig;
use Butschster\ContextGenerator\McpServer\Watcher\ConfigWatcherInterface;
use Mcp\Server\Contracts\ServerTransportInterface;
use Mcp\Server\Server;
use Spiral\Core\Attribute\Proxy;
Expand Down Expand Up @@ -49,17 +51,35 @@ public function run(string $name): void
ExceptionReporterInterface $reporter,
Server $server,
ServerTransportInterface $transport,
ConfigWatcherInterface $configWatcher,
?ConfigWatcherConfig $watcherConfig,
) use ($name): void {
// Register all classes with MCP item attributes. Should be before registering controllers!
$registry->registerMany($this->actions);

// Register all controllers for routing
$registrar->registerControllers($this->actions);

// Initialize config watcher with paths (if config provided)
if ($watcherConfig !== null) {
$configWatcher->start(
mainConfigPath: $watcherConfig->mainConfigPath,
importPaths: $watcherConfig->importPaths,
);

// Inject watcher into transport (if StdioTransport)
if ($transport instanceof StdioTransport) {
$transport->setConfigWatcher($configWatcher);
}
}

try {
$server->listen($transport);
} catch (\Throwable $e) {
$reporter->report($e);
} finally {
// Ensure watcher is stopped on exit
$configWatcher->stop();
}
},
);
Expand Down
15 changes: 15 additions & 0 deletions src/Tool/ToolRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ public function has(string $id): bool
return isset($this->tools[$id]);
}

public function remove(string $id): bool
{
if (!$this->has($id)) {
return false;
}

unset($this->tools[$id]);
return true;
}

public function clear(): void
{
$this->tools = [];
}

public function all(): array
{
return $this->getItems();
Expand Down
21 changes: 18 additions & 3 deletions src/Tool/ToolRegistryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,24 @@
interface ToolRegistryInterface
{
/**
* Registers a tool in the registry.
*
* @throws \InvalidArgumentException If a tool with the same ID already exists
* Registers a tool in the registry (upsert semantics - replaces if exists).
*/
public function register(ToolDefinition $tool): void;

/**
* Removes a tool from the registry by ID.
*
* @return bool True if the tool existed and was removed, false otherwise
*/
public function remove(string $id): bool;

/**
* Checks if a tool with the given ID exists.
*/
public function has(string $id): bool;

/**
* Removes all tools from the registry.
*/
public function clear(): void;
}
39 changes: 38 additions & 1 deletion src/Transport/StdioTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Butschster\ContextGenerator\McpServer\Transport;

use Butschster\ContextGenerator\McpServer\Watcher\ConfigWatcherInterface;
use Evenement\EventEmitter;
use Mcp\Server\Contracts\ServerTransportInterface;
use Mcp\Server\Exception\TransportException;
Expand Down Expand Up @@ -35,6 +36,8 @@ final class StdioTransport extends EventEmitter implements ServerTransportInterf
protected bool $closing = false;
protected bool $listening = false;

private ?ConfigWatcherInterface $configWatcher = null;

/**
* @param resource $input
* @param resource $output
Expand All @@ -45,6 +48,14 @@ public function __construct(
private readonly LoggerInterface $logger = new NullLogger(),
) {}

/**
* Set the config watcher for hot-reload support.
*/
public function setConfigWatcher(ConfigWatcherInterface $watcher): void
{
$this->configWatcher = $watcher;
}

public function listen(): void
{
if ($this->listening) {
Expand Down Expand Up @@ -78,7 +89,9 @@ public function listen(): void
}

if ($result === 0) {
// Timeout - no data available, check closing flag and continue
// Timeout - no data available
// Perfect time to check for config changes!
$this->tickConfigWatcher();
continue;
}

Expand Down Expand Up @@ -176,6 +189,12 @@ public function sendMessage(Message $message, string $sessionId, array $context

public function close(): void
{
// Stop config watcher on transport close
if ($this->configWatcher !== null) {
$this->configWatcher->stop();
$this->configWatcher = null;
}

if (\is_resource($this->input)) {
\fclose($this->input);
}
Expand All @@ -187,6 +206,24 @@ public function close(): void
$this->removeAllListeners();
}

/**
* Non-blocking config watcher tick.
*/
private function tickConfigWatcher(): void
{
if ($this->configWatcher === null) {
return;
}

try {
$this->configWatcher->tick();
} catch (\Throwable $e) {
$this->logger->error('Config watcher error', [
'error' => $e->getMessage(),
]);
}
}

/**
* Processes the internal buffer to find complete lines/frames.
*/
Expand Down
Loading