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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
if [[ ${{ matrix.php-version }} == '8.5' && ${{ matrix.os }} == 'ubuntu-latest' ]]; then
vendor/bin/phpunit --coverage-clover=coverage.xml --log-junit junit.xml
else
vendor/bin/phpunit
vendor/bin/phpunit --log-junit junit.xml
fi

- name: Validate prefer-lowest
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ The Model Context Protocol is an open protocol that enables seamless integration

- 🚀 **Rapid Integration**: Enable MCP support in your CakePHP app with minimal setup
- 🔍 **Attribute-Based Discovery**: Finds MCP tools, resources, and prompts using PHP 8+ attributes and class scanning
- 🛠️ **Built-in Tools**: Includes tools for system info, database schema exploration, route inspection, code execution, and more
- 🛠️ **Built-in Tools**: Includes tools for system info, database schema exploration, route inspection, code execution, log inspection, cache management, ORM introspection, and more
- 📚 **Advanced Documentation Search**: Full-text search with fuzzy matching and relevance ranking, powered by SQLite FTS5 (indexes official CakePHP markdown docs)
- 🧩 **Customizable & Extensible**: Easily define your own tools, resources, and prompts using attributes and configuration
- ⚡ **Prompt Workflows**: Built-in prompt flows for common development tasks, code review, debugging, and feature building
Expand Down Expand Up @@ -150,6 +150,16 @@ Synapse includes several built-in tools and resources for common operations:
| Documentation | `docs://content/{documentId}` | Retrieve full document content by document ID (format: `source::path`) |
| Commands | `list_commands` | List all available CakePHP console commands with optional filtering and sorting |
| Commands | `get_command_info` | Get detailed information about a specific console command (options, arguments, help) |
| Commands | `run_command` | Run a registered CakePHP console command in a subprocess with configurable timeout |
| Logs | `log_list` | List all log files in the application logs directory with size and modification time |
| Logs | `log_read` | Read the last N lines from a log file, optionally filtered by log level (error, warning, info, debug, …) |
| Cache | `cache_configs` | List all configured CakePHP cache configurations with engine information |
| Cache | `cache_read` | Read a value from a cache configuration by key |
| Cache | `cache_write` | Write a string value to a cache configuration |
| Cache | `cache_delete` | Delete a key from a cache configuration |
| Cache | `cache_clear` | Clear all entries from a cache configuration |
| ORM | `orm_describe` | Describe a CakePHP ORM Table: associations, behaviors, display field, primary key, entity class |
| ORM | `orm_find` | Run a find query (`all`, `first`, or `count`) on an ORM Table with a configurable row limit |

> [!WARNING]
> The `tinker` tool executes arbitrary code in your application. Use responsibly and avoid modifying data without explicit approval.
Expand Down
167 changes: 167 additions & 0 deletions src/Tools/CacheTools.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);

namespace Synapse\Tools;

use Cake\Cache\Cache;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Exception\ToolCallException;

/**
* Cache Tools
*
* MCP tools for inspecting and managing CakePHP cache configurations and entries.
*/
class CacheTools
{
/**
* List all configured cache configurations.
*
* Returns information about each configured cache engine including
* the engine class name and any configured prefix.
*
* @return array<int, array<string, mixed>> List of cache configurations
*/
#[McpTool(
name: 'cache_configs',
description: 'List all configured CakePHP cache configurations with engine information',
)]
public function listConfigs(): array
{
$configs = Cache::configured();
$result = [];

foreach ($configs as $name) {
$config = Cache::getConfig($name) ?? [];

$result[] = [
'name' => $name,
'className' => $config['className'] ?? $config['engine'] ?? 'Unknown',
'prefix' => $config['prefix'] ?? null,
'path' => $config['path'] ?? null,
'duration' => $config['duration'] ?? null,
];
}

return $result;
}

/**
* Read a value from the cache.
*
* Returns the stored value for the given key, or indicates the key was not found.
*
* @param string $config Cache configuration name (e.g. 'default')
* @param string $key Cache key to read
* @return array{key: string, value: mixed, found: bool} Cache read result
*/
#[McpTool(
name: 'cache_read',
description: 'Read a value from a CakePHP cache configuration by key',
)]
public function cacheRead(string $config, string $key): array
{
$this->assertConfigured($config);

$value = Cache::read($key, $config);
$found = $value !== null;

return [
'key' => $key,
'value' => $found ? $value : null,
'found' => $found,
];
}

/**
* Write a value to the cache.
*
* Stores the given string value under the specified key in the cache configuration.
*
* @param string $config Cache configuration name (e.g. 'default')
* @param string $key Cache key to write
* @param string $value Value to store
* @return array{key: string, stored: bool} Cache write result
*/
#[McpTool(
name: 'cache_write',
description: 'Write a string value to a CakePHP cache configuration',
)]
public function cacheWrite(string $config, string $key, string $value): array
{
$this->assertConfigured($config);

$stored = Cache::write($key, $value, $config);

return [
'key' => $key,
'stored' => $stored,
];
}

/**
* Delete a key from the cache.
*
* Removes the specified key from the given cache configuration.
*
* @param string $config Cache configuration name (e.g. 'default')
* @param string $key Cache key to delete
* @return array{key: string, deleted: bool} Cache delete result
*/
#[McpTool(
name: 'cache_delete',
description: 'Delete a key from a CakePHP cache configuration',
)]
public function cacheDelete(string $config, string $key): array
{
Comment thread
josbeir marked this conversation as resolved.
Resolved
$this->assertConfigured($config);

$deleted = Cache::delete($key, $config);

return [
'key' => $key,
'deleted' => $deleted,
];
}

/**
* Clear all entries from a cache configuration.
*
* Removes all cached entries from the specified cache configuration.
*
* @param string $config Cache configuration name (e.g. 'default')
* @return array{config: string, cleared: bool} Cache clear result
*/
#[McpTool(
name: 'cache_clear',
description: 'Clear all entries from a CakePHP cache configuration',
)]
public function cacheClear(string $config): array
{
$this->assertConfigured($config);

$cleared = Cache::clear($config);

return [
'config' => $config,
'cleared' => $cleared,
];
}

/**
* Assert that a cache configuration exists.
*
* @param string $config Cache configuration name to validate
* @throws \Mcp\Exception\ToolCallException If the configuration is not registered
*/
private function assertConfigured(string $config): void
{
if (!in_array($config, Cache::configured(), true)) {
throw new ToolCallException(sprintf(
'Cache config "%s" is not configured. Available: %s',
$config,
implode(', ', Cache::configured()),
));
}
}
}
63 changes: 63 additions & 0 deletions src/Tools/CommandTools.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Cake\Console\ConsoleOptionParser;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Exception\ToolCallException;
use Synapse\Utility\SubprocessRunner;
use Throwable;

/**
Expand All @@ -23,9 +24,11 @@ class CommandTools
* Constructor
*
* @param \Cake\Console\CommandCollection|null $commandCollection Command collection to inspect
* @param \Synapse\Utility\SubprocessRunner $runner Subprocess execution utility
*/
public function __construct(
protected ?CommandCollection $commandCollection = null,
private SubprocessRunner $runner = new SubprocessRunner(),
) {
$this->commandCollection ??= new CommandCollection();
}
Expand Down Expand Up @@ -176,6 +179,66 @@ public function getCommandInfo(string $name): array
];
}

/**
* Run a registered CakePHP console command in a subprocess.
*
* Only commands present in the application's command collection can be executed,
* preventing invocation of arbitrary shell commands.
*
* @param string $name Command name as registered (e.g. 'synapse search_docs')
* @param string $args Additional arguments/flags passed verbatim (e.g. '--help')
* @param int $timeout Maximum execution time in seconds (default: 60, max: 300)
* @return array{command: string, args: string, success: bool, output: string, stderr: string, exit_code: int}
*/
#[McpTool(
name: 'run_command',
description: 'Run a registered CakePHP console command. ' .
'Use list_commands to discover available commands. ' .
'Only commands registered in the application command collection can be executed.',
)]
public function runCommand(string $name, string $args = '', int $timeout = 60): array
{
if (!$this->getCommandCollection()->has($name)) {
throw new ToolCallException(
sprintf("Command '%s' not found. Use list_commands to see available commands.", $name),
);
}

$timeout = min(max(1, $timeout), 300);

$phpBinary = $this->runner->getPhpBinary();
if ($phpBinary === null) {
throw new ToolCallException(
'Could not find PHP binary. Configure Synapse.tinker.php_binary or ensure php is in PATH.',
);
}

// Command names may contain spaces (e.g. "synapse search_docs"); each
// word is a separate argv element when passed to bin/cake.php.
$nameParts = array_map('escapeshellarg', explode(' ', trim($name)));
$commandStr = escapeshellarg($phpBinary) . ' bin/cake.php ' . implode(' ', $nameParts);

if (trim($args) !== '') {
// Escape each argument token individually to prevent shell injection.
// Users who need a single argument with embedded spaces should pass it
// as one token without whitespace separating it from other args.
$argTokens = preg_split('/\s+/', trim($args), -1, PREG_SPLIT_NO_EMPTY) ?: [];
$commandStr .= ' ' . implode(' ', array_map('escapeshellarg', $argTokens));
}
Comment thread
josbeir marked this conversation as resolved.
Resolved

$cwd = dirname($this->runner->getBinPath());
$raw = $this->runner->run($commandStr, null, $timeout, $cwd);

return [
'command' => $name,
'args' => $args,
'success' => $raw['success'],
'output' => $raw['output'],
'stderr' => $raw['stderr'],
'exit_code' => $raw['exit_code'],
];
}

/**
* Extract the namespace from a fully qualified class name.
*
Expand Down
Loading
Loading