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
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ Be aware that if you are exposing your endpoint publicly, you are exposing your
'streamable_http' => [
'middleware' => ['auth:sanctum'],
],

'sse' => [
'middleware' => ['auth:sanctum'],
],
Expand Down Expand Up @@ -265,6 +265,50 @@ Please note that not all clients support direct SSE connections. For those situa
}
```

## Dynamic Tools

Laravel Loop supports registering and removing tools during STDIO sessions. When tools are added or removed, MCP clients automatically receive `tools/list_changed` notifications.

**Important:** This feature is only available for the STDIO transport.

### Example Usage in STDIO

```php
// Tools can be dynamically managed during STDIO session
// Clients will receive notifications automatically
Loop::tool(new CustomTool());
Loop::removeTool('tool-name');
```

#### Adding Tools Dynamically

```php
// Add a single tool at runtime
Loop::tool(new MyCustomTool());

// Method chaining is supported
Loop::tool(new ToolOne())
->tool(new ToolTwo());
```

#### Removing Tools Dynamically

```php
// Remove a tool by name
Loop::removeTool('my-custom-tool');

// Method chaining is supported
Loop::removeTool('tool-one')
->removeTool('tool-two');
```

#### Clearing All Tools

```php
// Remove all registered tools and toolkits
Loop::clear();
```

***

## Troubleshooting
Expand Down
17 changes: 17 additions & 0 deletions src/Commands/LoopMcpServerStartCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Console\Command;
use Kirschbaum\Loop\Commands\Concerns\AuthenticateUsers;
use Kirschbaum\Loop\Enums\ErrorCode;
use Kirschbaum\Loop\LoopTools;
use Kirschbaum\Loop\McpHandler;
use React\EventLoop\Loop;
use React\Stream\ReadableResourceStream;
Expand Down Expand Up @@ -52,6 +53,8 @@ public function handle(McpHandler $mcpHandler): int
$this->processData($data);
});

$this->registerToolChangeCallback();

if ($this->option('debug')) {
$this->debug('Laravel Loop MCP server running. Press Ctrl+C or send SIGTERM to stop.');
}
Expand Down Expand Up @@ -134,6 +137,20 @@ protected function processData(string $data): void
}
}

protected function registerToolChangeCallback(): void
{
$loopTools = app(LoopTools::class);

$loopTools->onToolsChanged(function () {
if ($this->option('debug')) {
$this->debug('Tools list changed, sending notification');
}

$notification = $this->mcpHandler->createToolsChangedNotification();
$this->stdout->write(json_encode($notification).PHP_EOL);
});
}

protected function debug(string $message): void
{
$this->getOutput()->getOutput()->getErrorOutput()->writeln($message); // @phpstan-ignore method.notFound
Expand Down
20 changes: 20 additions & 0 deletions src/Loop.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,24 @@ public function getPrismTool(string $name): PrismTool
->getTool($name)
->build();
}

/**
* Remove a tool dynamically (persists only within STDIO session)
*/
public function removeTool(string $name): static
{
$this->loopTools->removeTool($name);

return $this;
}

/**
* Clear all tools and toolkits
*/
public function clear(): static
{
$this->loopTools->clear();

return $this;
}
}
75 changes: 74 additions & 1 deletion src/LoopTools.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,54 @@ class LoopTools
/** @var array<int, Toolkit> */
protected array $toolkits = [];

/** @var callable|null */
protected $changeCallback = null;

protected ?string $toolsHash = null;

public function __construct()
{
$this->tools = new ToolCollection;
}

/**
* Register a tool if not already present (prevents duplicates)
*/
public function registerTool(Tool $tool): void
{
$this->tools->push($tool);
$toolName = $tool->getName();

if (! $this->tools->contains(function ($existingTool) use ($toolName) {
return $existingTool->getName() === $toolName;
})) {
$this->tools->push($tool);
}

$this->notifyIfChanged();
}

/**
* Remove a tool by name
*/
public function removeTool(string $name): void
{
$originalCount = $this->tools->count();

$this->tools = $this->tools->reject(function ($tool) use ($name) {
return $tool->getName() === $name;
});

if ($this->tools->count() !== $originalCount) {
$this->notifyIfChanged();
}
}

/**
* Register a callback to be called when tools change
*/
public function onToolsChanged(callable $callback): void
{
$this->changeCallback = $callback;
}

public function registerToolkit(Toolkit $toolkit): void
Expand Down Expand Up @@ -57,5 +97,38 @@ public function clear(): void
{
$this->tools = new ToolCollection;
$this->toolkits = [];
$this->notifyIfChanged();
}

/**
* Check if tools have changed and notify callback if so
*/
protected function notifyIfChanged(): void
{
$currentHash = $this->computeToolsHash();

if ($currentHash !== $this->toolsHash) {
$this->toolsHash = $currentHash;

if ($this->changeCallback) {
($this->changeCallback)();
}
}
}

/**
* Compute a hash of the current tools for change detection
*/
protected function computeToolsHash(): string
{
$toolNames = $this->tools
->map(fn ($tool) => $tool->getName())
->sort()
->values()
->toArray();

$json = json_encode($toolNames);

return md5($json !== false ? $json : '[]');
}
}
19 changes: 17 additions & 2 deletions src/McpHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ public function __construct(
];

$this->serverCapabilities = $this->config['capabilities'] ?? [
'tools' => $this->listTools(),
'tools' => [
'listChanged' => true,
],
];
}

Expand Down Expand Up @@ -108,7 +110,7 @@ public function listTools(): array
'additionalProperties' => false,
],
];
})->toArray(),
})->values()->toArray(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixes bug where the array keys are invalid when removing a tool from the collection.

];
}

Expand Down Expand Up @@ -171,6 +173,19 @@ public function ping(): array
return [];
}

/**
* Create a tools/list_changed notification
*
* @return array<string, string>
*/
public function createToolsChangedNotification(): array
{
return [
'jsonrpc' => '2.0',
'method' => 'notifications/tools/list_changed',
];
}

public function handle(array $message): array
{
if (! isset($message['jsonrpc']) || $message['jsonrpc'] !== '2.0') {
Expand Down
Loading