{"id":36987463,"url":"https://github.com/code-wheel/mcp-tool-gateway-php","last_synced_at":"2026-01-16T01:28:43.284Z","repository":{"id":331564391,"uuid":"1131372242","full_name":"code-wheel/mcp-tool-gateway-php","owner":"code-wheel","description":"Generic tool gateway pattern for MCP servers - discover, introspect, and execute tools dynamically","archived":false,"fork":false,"pushed_at":"2026-01-10T06:23:30.000Z","size":76,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-01-11T01:18:23.602Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/code-wheel.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-09T22:33:49.000Z","updated_at":"2026-01-10T06:23:34.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/code-wheel/mcp-tool-gateway-php","commit_stats":null,"previous_names":["code-wheel/mcp-tool-gateway"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/code-wheel/mcp-tool-gateway-php","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-wheel%2Fmcp-tool-gateway-php","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-wheel%2Fmcp-tool-gateway-php/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-wheel%2Fmcp-tool-gateway-php/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-wheel%2Fmcp-tool-gateway-php/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/code-wheel","download_url":"https://codeload.github.com/code-wheel/mcp-tool-gateway-php/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-wheel%2Fmcp-tool-gateway-php/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28405141,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T21:51:37.118Z","status":"ssl_error","status_checked_at":"2026-01-13T21:45:14.585Z","response_time":56,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-13T23:07:18.635Z","updated_at":"2026-01-13T23:07:19.440Z","avatar_url":"https://github.com/code-wheel.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MCP Tool Gateway\n\n[![CI](https://github.com/code-wheel/mcp-tool-gateway/actions/workflows/ci.yml/badge.svg)](https://github.com/code-wheel/mcp-tool-gateway/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/gh/code-wheel/mcp-tool-gateway/graph/badge.svg)](https://codecov.io/gh/code-wheel/mcp-tool-gateway)\n[![Latest Stable Version](https://poser.pugx.org/code-wheel/mcp-tool-gateway/v)](https://packagist.org/packages/code-wheel/mcp-tool-gateway)\n[![License](https://poser.pugx.org/code-wheel/mcp-tool-gateway/license)](https://packagist.org/packages/code-wheel/mcp-tool-gateway)\n\nA production-ready framework for PHP MCP (Model Context Protocol) servers. Features middleware pipeline, input validation, tool composition, caching, and event dispatching.\n\n## Installation\n\n```bash\ncomposer require code-wheel/mcp-tool-gateway\n```\n\n## Features\n\n- **Middleware Pipeline** - Chain validation, auth, logging, and custom middleware\n- **Input Validation** - Reject malformed LLM inputs before execution\n- **Tool Composition** - Combine multiple tool providers with namespacing\n- **Caching** - PSR-16 cache support for discovery and read-only results\n- **Events** - PSR-14 event dispatching for tool lifecycle\n- **Framework Agnostic** - Works with Drupal, Laravel, Symfony, or vanilla PHP\n\n## Quick Start\n\n### Basic Usage\n\n```php\nuse CodeWheel\\McpToolGateway\\ArrayToolProvider;\nuse CodeWheel\\McpToolGateway\\ToolInfo;\nuse CodeWheel\\McpToolGateway\\ToolResult;\n\n// Create a tool provider\n$provider = new ArrayToolProvider([\n    'greet' =\u003e new ToolInfo(\n        name: 'greet',\n        label: 'Greet User',\n        description: 'Says hello to a user',\n        inputSchema: [\n            'type' =\u003e 'object',\n            'properties' =\u003e [\n                'name' =\u003e ['type' =\u003e 'string'],\n            ],\n            'required' =\u003e ['name'],\n        ],\n    ),\n]);\n\n// Register handler\n$provider-\u003esetHandler('greet', function (array $args): ToolResult {\n    return ToolResult::success(\"Hello, {$args['name']}!\");\n});\n\n// Execute\n$result = $provider-\u003eexecute('greet', ['name' =\u003e 'World']);\necho $result-\u003emessage; // \"Hello, World!\"\n```\n\n### Middleware Pipeline\n\n```php\nuse CodeWheel\\McpToolGateway\\Middleware\\MiddlewarePipeline;\nuse CodeWheel\\McpToolGateway\\Middleware\\ValidatingMiddleware;\nuse CodeWheel\\McpToolGateway\\Middleware\\LoggingMiddleware;\nuse CodeWheel\\McpSchemaBuilder\\SchemaValidator;\n\n$pipeline = new MiddlewarePipeline($provider);\n\n// Add validation (rejects malformed inputs)\n$validator = new SchemaValidator();\n$pipeline-\u003eadd(new ValidatingMiddleware($provider, $validator));\n\n// Add logging (PSR-3)\n$pipeline-\u003eadd(new LoggingMiddleware($logger));\n\n// Execute through pipeline\n$result = $pipeline-\u003eexecute('create_user', [\n    'email' =\u003e 'invalid',  // Will be rejected by validation\n    'name' =\u003e 'John',\n]);\n```\n\n### Composing Multiple Providers\n\n```php\nuse CodeWheel\\McpToolGateway\\CompositeToolProvider;\n\n// Combine providers with prefixes\n$composite = new CompositeToolProvider([\n    'drupal' =\u003e $drupalProvider,   // drupal/get_users, drupal/create_node\n    'custom' =\u003e $customProvider,    // custom/my_tool\n    'api' =\u003e $externalProvider,     // api/fetch_data\n]);\n\n// Or without prefixes (tools must have unique names)\n$composite = new CompositeToolProvider([\n    $provider1,\n    $provider2,\n], prefixed: false);\n\n$allTools = $composite-\u003egetTools();\n$result = $composite-\u003eexecute('drupal/get_users', ['limit' =\u003e 10]);\n```\n\n### Caching\n\n```php\nuse CodeWheel\\McpToolGateway\\CachingToolProvider;\n\n$cached = new CachingToolProvider(\n    $provider,\n    $cache,              // PSR-16 CacheInterface\n    discoveryTtl: 3600,  // Cache tool list for 1 hour\n    resultTtl: 300,      // Cache read-only tool results for 5 minutes\n    cacheableTools: ['get_config', 'list_users'],  // Tools to cache results\n);\n\n// First call hits provider, subsequent calls use cache\n$tools = $cached-\u003egetTools();\n```\n\n### Events\n\n```php\nuse CodeWheel\\McpToolGateway\\Middleware\\EventMiddleware;\nuse CodeWheel\\McpToolGateway\\Event\\ToolExecutionStarted;\nuse CodeWheel\\McpToolGateway\\Event\\ToolExecutionSucceeded;\nuse CodeWheel\\McpToolGateway\\Event\\ToolExecutionFailed;\n\n// Add event dispatching (PSR-14)\n$pipeline-\u003eadd(new EventMiddleware($eventDispatcher));\n\n// Listen for events\n$dispatcher-\u003eaddListener(ToolExecutionStarted::class, function ($event) {\n    $this-\u003emetrics-\u003eincrement(\"tool.{$event-\u003etoolName}.started\");\n});\n\n$dispatcher-\u003eaddListener(ToolExecutionSucceeded::class, function ($event) {\n    $this-\u003emetrics-\u003etiming(\"tool.{$event-\u003etoolName}.duration\", $event-\u003eduration);\n});\n\n$dispatcher-\u003eaddListener(ToolExecutionFailed::class, function ($event) {\n    $this-\u003ealerting-\u003enotify(\"Tool {$event-\u003etoolName} failed: {$event-\u003eexception-\u003egetMessage()}\");\n});\n```\n\n## Middleware\n\n### Built-in Middleware\n\n| Middleware | Purpose | Requires |\n|------------|---------|----------|\n| `ValidatingMiddleware` | Validates inputs against JSON Schema | `mcp-schema-builder` |\n| `LoggingMiddleware` | Logs execution with timing | PSR-3 Logger |\n| `EventMiddleware` | Dispatches lifecycle events | PSR-14 EventDispatcher |\n\n### Custom Middleware\n\n```php\nuse CodeWheel\\McpToolGateway\\Middleware\\MiddlewareInterface;\nuse CodeWheel\\McpToolGateway\\ExecutionContext;\nuse CodeWheel\\McpToolGateway\\ToolResult;\n\nclass AuthorizationMiddleware implements MiddlewareInterface\n{\n    public function __construct(\n        private readonly AccessChecker $access,\n    ) {}\n\n    public function process(\n        string $toolName,\n        array $arguments,\n        ExecutionContext $context,\n        callable $next,\n    ): ToolResult {\n        // Check access before execution\n        if (!$this-\u003eaccess-\u003ecanExecute($context-\u003euserId, $toolName)) {\n            return ToolResult::error(\"Access denied to tool: {$toolName}\");\n        }\n\n        return $next($toolName, $arguments, $context);\n    }\n}\n\nclass RateLimitMiddleware implements MiddlewareInterface\n{\n    public function process(\n        string $toolName,\n        array $arguments,\n        ExecutionContext $context,\n        callable $next,\n    ): ToolResult {\n        $key = \"{$context-\u003euserId}:{$toolName}\";\n\n        if ($this-\u003elimiter-\u003eisLimited($key)) {\n            return ToolResult::error(\"Rate limit exceeded for {$toolName}\");\n        }\n\n        $this-\u003elimiter-\u003ehit($key);\n        return $next($toolName, $arguments, $context);\n    }\n}\n\nclass AuditMiddleware implements MiddlewareInterface\n{\n    public function process(\n        string $toolName,\n        array $arguments,\n        ExecutionContext $context,\n        callable $next,\n    ): ToolResult {\n        $start = microtime(true);\n\n        try {\n            $result = $next($toolName, $arguments, $context);\n\n            $this-\u003eaudit-\u003elog([\n                'tool' =\u003e $toolName,\n                'user' =\u003e $context-\u003euserId,\n                'success' =\u003e $result-\u003esuccess,\n                'duration' =\u003e microtime(true) - $start,\n            ]);\n\n            return $result;\n        } catch (\\Throwable $e) {\n            $this-\u003eaudit-\u003elogError($toolName, $e);\n            throw $e;\n        }\n    }\n}\n```\n\n### Production Pipeline Example\n\n```php\n$pipeline = new MiddlewarePipeline($provider);\n\n// Order matters: outer middleware wraps inner\n$pipeline-\u003eadd(new AuditMiddleware($auditor));              // 1. Audit everything\n$pipeline-\u003eadd(new RateLimitMiddleware($limiter));          // 2. Rate limiting\n$pipeline-\u003eadd(new AuthorizationMiddleware($access));       // 3. Access control\n$pipeline-\u003eadd(new ValidatingMiddleware($provider, $validator)); // 4. Input validation\n$pipeline-\u003eadd(new LoggingMiddleware($logger));             // 5. Execution logging\n$pipeline-\u003eadd(new EventMiddleware($dispatcher));           // 6. Lifecycle events\n\n$result = $pipeline-\u003eexecute('delete_user', ['user_id' =\u003e 123], $context);\n```\n\n## Execution Context\n\n```php\nuse CodeWheel\\McpToolGateway\\ExecutionContext;\n\n$context = ExecutionContext::create(\n    userId: 'user-123',\n    requestId: 'req-abc',\n    scopes: ['read', 'write', 'admin'],\n    metadata: ['client' =\u003e 'claude-desktop'],\n);\n\n// Check scopes\n$context-\u003ehasScope('write');           // true\n$context-\u003ehasAnyScope(['admin', 'superuser']); // true\n\n// Use in middleware\nif (!$context-\u003ehasScope('write')) {\n    return ToolResult::error('Write scope required');\n}\n```\n\n## Custom Tool Provider\n\n```php\nuse CodeWheel\\McpToolGateway\\ToolProviderInterface;\nuse CodeWheel\\McpToolGateway\\ToolInfo;\nuse CodeWheel\\McpToolGateway\\ToolResult;\nuse CodeWheel\\McpToolGateway\\ExecutionContext;\n\nclass DrupalToolProvider implements ToolProviderInterface\n{\n    public function __construct(\n        private readonly ToolPluginManager $pluginManager,\n    ) {}\n\n    public function getTools(): array\n    {\n        $tools = [];\n        foreach ($this-\u003epluginManager-\u003egetDefinitions() as $id =\u003e $def) {\n            $tools[$id] = new ToolInfo(\n                name: $id,\n                label: $def['label'],\n                description: $def['description'],\n                inputSchema: $def['input_schema'],\n                annotations: $def['annotations'] ?? [],\n                provider: $def['provider'],\n            );\n        }\n        return $tools;\n    }\n\n    public function getTool(string $toolName): ?ToolInfo\n    {\n        $tools = $this-\u003egetTools();\n        return $tools[$toolName] ?? null;\n    }\n\n    public function execute(\n        string $toolName,\n        array $arguments,\n        ?ExecutionContext $context = null,\n    ): ToolResult {\n        $plugin = $this-\u003epluginManager-\u003ecreateInstance($toolName);\n        $result = $plugin-\u003eexecute($arguments);\n\n        return new ToolResult(\n            success: $result['success'],\n            message: $result['message'],\n            data: $result['data'] ?? [],\n        );\n    }\n}\n```\n\n## Gateway Pattern\n\nFor MCP servers with many tools, use the gateway pattern to reduce context window usage:\n\n```php\nuse CodeWheel\\McpToolGateway\\ToolGateway;\n\n$gateway = new ToolGateway($provider);\n\n// Register just 3 gateway tools instead of 100+ individual tools\nforeach ($gateway-\u003egetGatewayTools() as $tool) {\n    $mcpServer-\u003eregisterTool($tool['name'], $tool['handler'], $tool['inputSchema']);\n}\n\n// LLM uses:\n// - gateway/discover-tools { \"query\": \"user\" }\n// - gateway/get-tool-info { \"tool_name\": \"create-user\" }\n// - gateway/execute-tool { \"tool_name\": \"create-user\", \"arguments\": {...} }\n```\n\n## Integration with mcp-schema-builder\n\n```php\nuse CodeWheel\\McpSchemaBuilder\\SchemaBuilder;\nuse CodeWheel\\McpSchemaBuilder\\McpSchema;\nuse CodeWheel\\McpSchemaBuilder\\SchemaValidator;\n\n// Build schema with presets\n$schema = SchemaBuilder::object()\n    -\u003eproperty('user_id', McpSchema::entityId('user')-\u003erequired())\n    -\u003eproperty('status', SchemaBuilder::string()-\u003eenum(['active', 'blocked']))\n    -\u003emerge(McpSchema::pagination())\n    -\u003ebuild();\n\n// Validate in middleware\n$validator = new SchemaValidator();\n$pipeline-\u003eadd(new ValidatingMiddleware($provider, $validator));\n```\n\n## Integration with mcp-error-codes\n\n```php\nuse CodeWheel\\McpErrorCodes\\McpError;\n\n// In your tool handler\npublic function execute(array $args): ToolResult\n{\n    $user = $this-\u003eusers-\u003efind($args['user_id']);\n\n    if (!$user) {\n        return McpError::notFound('user', $args['user_id'])\n            -\u003ewithSuggestion('Check if the user ID is correct')\n            -\u003etoToolResult();\n    }\n\n    // ...\n}\n```\n\n## License\n\nMIT License - see [LICENSE](LICENSE) file.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcode-wheel%2Fmcp-tool-gateway-php","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcode-wheel%2Fmcp-tool-gateway-php","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcode-wheel%2Fmcp-tool-gateway-php/lists"}