code-wheel/mcp-tool-gateway

MCP Tool Gateway - Dynamic tool discovery, middleware pipeline, and caching for MCP servers

Installs: 25

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/code-wheel/mcp-tool-gateway

v1.1.2 2026-01-10 04:11 UTC

This package is auto-updated.

Last update: 2026-01-10 06:23:33 UTC


README

CI codecov Latest Stable Version License

A production-ready framework for PHP MCP (Model Context Protocol) servers. Features middleware pipeline, input validation, tool composition, caching, and event dispatching.

Installation

composer require code-wheel/mcp-tool-gateway

Features

  • Middleware Pipeline - Chain validation, auth, logging, and custom middleware
  • Input Validation - Reject malformed LLM inputs before execution
  • Tool Composition - Combine multiple tool providers with namespacing
  • Caching - PSR-16 cache support for discovery and read-only results
  • Events - PSR-14 event dispatching for tool lifecycle
  • Framework Agnostic - Works with Drupal, Laravel, Symfony, or vanilla PHP

Quick Start

Basic Usage

use CodeWheel\McpToolGateway\ArrayToolProvider;
use CodeWheel\McpToolGateway\ToolInfo;
use CodeWheel\McpToolGateway\ToolResult;

// Create a tool provider
$provider = new ArrayToolProvider([
    'greet' => new ToolInfo(
        name: 'greet',
        label: 'Greet User',
        description: 'Says hello to a user',
        inputSchema: [
            'type' => 'object',
            'properties' => [
                'name' => ['type' => 'string'],
            ],
            'required' => ['name'],
        ],
    ),
]);

// Register handler
$provider->setHandler('greet', function (array $args): ToolResult {
    return ToolResult::success("Hello, {$args['name']}!");
});

// Execute
$result = $provider->execute('greet', ['name' => 'World']);
echo $result->message; // "Hello, World!"

Middleware Pipeline

use CodeWheel\McpToolGateway\Middleware\MiddlewarePipeline;
use CodeWheel\McpToolGateway\Middleware\ValidatingMiddleware;
use CodeWheel\McpToolGateway\Middleware\LoggingMiddleware;
use CodeWheel\McpSchemaBuilder\SchemaValidator;

$pipeline = new MiddlewarePipeline($provider);

// Add validation (rejects malformed inputs)
$validator = new SchemaValidator();
$pipeline->add(new ValidatingMiddleware($provider, $validator));

// Add logging (PSR-3)
$pipeline->add(new LoggingMiddleware($logger));

// Execute through pipeline
$result = $pipeline->execute('create_user', [
    'email' => 'invalid',  // Will be rejected by validation
    'name' => 'John',
]);

Composing Multiple Providers

use CodeWheel\McpToolGateway\CompositeToolProvider;

// Combine providers with prefixes
$composite = new CompositeToolProvider([
    'drupal' => $drupalProvider,   // drupal/get_users, drupal/create_node
    'custom' => $customProvider,    // custom/my_tool
    'api' => $externalProvider,     // api/fetch_data
]);

// Or without prefixes (tools must have unique names)
$composite = new CompositeToolProvider([
    $provider1,
    $provider2,
], prefixed: false);

$allTools = $composite->getTools();
$result = $composite->execute('drupal/get_users', ['limit' => 10]);

Caching

use CodeWheel\McpToolGateway\CachingToolProvider;

$cached = new CachingToolProvider(
    $provider,
    $cache,              // PSR-16 CacheInterface
    discoveryTtl: 3600,  // Cache tool list for 1 hour
    resultTtl: 300,      // Cache read-only tool results for 5 minutes
    cacheableTools: ['get_config', 'list_users'],  // Tools to cache results
);

// First call hits provider, subsequent calls use cache
$tools = $cached->getTools();

Events

use CodeWheel\McpToolGateway\Middleware\EventMiddleware;
use CodeWheel\McpToolGateway\Event\ToolExecutionStarted;
use CodeWheel\McpToolGateway\Event\ToolExecutionSucceeded;
use CodeWheel\McpToolGateway\Event\ToolExecutionFailed;

// Add event dispatching (PSR-14)
$pipeline->add(new EventMiddleware($eventDispatcher));

// Listen for events
$dispatcher->addListener(ToolExecutionStarted::class, function ($event) {
    $this->metrics->increment("tool.{$event->toolName}.started");
});

$dispatcher->addListener(ToolExecutionSucceeded::class, function ($event) {
    $this->metrics->timing("tool.{$event->toolName}.duration", $event->duration);
});

$dispatcher->addListener(ToolExecutionFailed::class, function ($event) {
    $this->alerting->notify("Tool {$event->toolName} failed: {$event->exception->getMessage()}");
});

Middleware

Built-in Middleware

Middleware Purpose Requires
ValidatingMiddleware Validates inputs against JSON Schema mcp-schema-builder
LoggingMiddleware Logs execution with timing PSR-3 Logger
EventMiddleware Dispatches lifecycle events PSR-14 EventDispatcher

Custom Middleware

use CodeWheel\McpToolGateway\Middleware\MiddlewareInterface;
use CodeWheel\McpToolGateway\ExecutionContext;
use CodeWheel\McpToolGateway\ToolResult;

class AuthorizationMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly AccessChecker $access,
    ) {}

    public function process(
        string $toolName,
        array $arguments,
        ExecutionContext $context,
        callable $next,
    ): ToolResult {
        // Check access before execution
        if (!$this->access->canExecute($context->userId, $toolName)) {
            return ToolResult::error("Access denied to tool: {$toolName}");
        }

        return $next($toolName, $arguments, $context);
    }
}

class RateLimitMiddleware implements MiddlewareInterface
{
    public function process(
        string $toolName,
        array $arguments,
        ExecutionContext $context,
        callable $next,
    ): ToolResult {
        $key = "{$context->userId}:{$toolName}";

        if ($this->limiter->isLimited($key)) {
            return ToolResult::error("Rate limit exceeded for {$toolName}");
        }

        $this->limiter->hit($key);
        return $next($toolName, $arguments, $context);
    }
}

class AuditMiddleware implements MiddlewareInterface
{
    public function process(
        string $toolName,
        array $arguments,
        ExecutionContext $context,
        callable $next,
    ): ToolResult {
        $start = microtime(true);

        try {
            $result = $next($toolName, $arguments, $context);

            $this->audit->log([
                'tool' => $toolName,
                'user' => $context->userId,
                'success' => $result->success,
                'duration' => microtime(true) - $start,
            ]);

            return $result;
        } catch (\Throwable $e) {
            $this->audit->logError($toolName, $e);
            throw $e;
        }
    }
}

Production Pipeline Example

$pipeline = new MiddlewarePipeline($provider);

// Order matters: outer middleware wraps inner
$pipeline->add(new AuditMiddleware($auditor));              // 1. Audit everything
$pipeline->add(new RateLimitMiddleware($limiter));          // 2. Rate limiting
$pipeline->add(new AuthorizationMiddleware($access));       // 3. Access control
$pipeline->add(new ValidatingMiddleware($provider, $validator)); // 4. Input validation
$pipeline->add(new LoggingMiddleware($logger));             // 5. Execution logging
$pipeline->add(new EventMiddleware($dispatcher));           // 6. Lifecycle events

$result = $pipeline->execute('delete_user', ['user_id' => 123], $context);

Execution Context

use CodeWheel\McpToolGateway\ExecutionContext;

$context = ExecutionContext::create(
    userId: 'user-123',
    requestId: 'req-abc',
    scopes: ['read', 'write', 'admin'],
    metadata: ['client' => 'claude-desktop'],
);

// Check scopes
$context->hasScope('write');           // true
$context->hasAnyScope(['admin', 'superuser']); // true

// Use in middleware
if (!$context->hasScope('write')) {
    return ToolResult::error('Write scope required');
}

Custom Tool Provider

use CodeWheel\McpToolGateway\ToolProviderInterface;
use CodeWheel\McpToolGateway\ToolInfo;
use CodeWheel\McpToolGateway\ToolResult;
use CodeWheel\McpToolGateway\ExecutionContext;

class DrupalToolProvider implements ToolProviderInterface
{
    public function __construct(
        private readonly ToolPluginManager $pluginManager,
    ) {}

    public function getTools(): array
    {
        $tools = [];
        foreach ($this->pluginManager->getDefinitions() as $id => $def) {
            $tools[$id] = new ToolInfo(
                name: $id,
                label: $def['label'],
                description: $def['description'],
                inputSchema: $def['input_schema'],
                annotations: $def['annotations'] ?? [],
                provider: $def['provider'],
            );
        }
        return $tools;
    }

    public function getTool(string $toolName): ?ToolInfo
    {
        $tools = $this->getTools();
        return $tools[$toolName] ?? null;
    }

    public function execute(
        string $toolName,
        array $arguments,
        ?ExecutionContext $context = null,
    ): ToolResult {
        $plugin = $this->pluginManager->createInstance($toolName);
        $result = $plugin->execute($arguments);

        return new ToolResult(
            success: $result['success'],
            message: $result['message'],
            data: $result['data'] ?? [],
        );
    }
}

Gateway Pattern

For MCP servers with many tools, use the gateway pattern to reduce context window usage:

use CodeWheel\McpToolGateway\ToolGateway;

$gateway = new ToolGateway($provider);

// Register just 3 gateway tools instead of 100+ individual tools
foreach ($gateway->getGatewayTools() as $tool) {
    $mcpServer->registerTool($tool['name'], $tool['handler'], $tool['inputSchema']);
}

// LLM uses:
// - gateway/discover-tools { "query": "user" }
// - gateway/get-tool-info { "tool_name": "create-user" }
// - gateway/execute-tool { "tool_name": "create-user", "arguments": {...} }

Integration with mcp-schema-builder

use CodeWheel\McpSchemaBuilder\SchemaBuilder;
use CodeWheel\McpSchemaBuilder\McpSchema;
use CodeWheel\McpSchemaBuilder\SchemaValidator;

// Build schema with presets
$schema = SchemaBuilder::object()
    ->property('user_id', McpSchema::entityId('user')->required())
    ->property('status', SchemaBuilder::string()->enum(['active', 'blocked']))
    ->merge(McpSchema::pagination())
    ->build();

// Validate in middleware
$validator = new SchemaValidator();
$pipeline->add(new ValidatingMiddleware($provider, $validator));

Integration with mcp-error-codes

use CodeWheel\McpErrorCodes\McpError;

// In your tool handler
public function execute(array $args): ToolResult
{
    $user = $this->users->find($args['user_id']);

    if (!$user) {
        return McpError::notFound('user', $args['user_id'])
            ->withSuggestion('Check if the user ID is correct')
            ->toToolResult();
    }

    // ...
}

License

MIT License - see LICENSE file.