oolCatalog $catalog,
private ExecutionRouter $router,
) {}
public function dispatch(Request $request): JsonResponse|Response
{
$payload = $request->json()->all();
$requestId = $payload['id'] ?? null;
$method = $payload['method'] ?? null;
$arguments = $payload['params'] ?? [];
if ($requestId === null && str_starts_with((string) $method, 'notifications/')) {
return response()->noContent();
}
try {
$output = match ($method) {
'initialize' => $this->handshake->negotiate($arguments),
'tools/list' => $this->catalog->publish(),
'tools/call' => $this->router->execute($arguments),
default => throw new ProtocolViolation(-32601, 'Method not recognized'),
};
} catch (ProtocolViolation $e) {
return response()->json([
'jsonrpc' => '2.0',
'id' => $requestId,
'error' => ['code' => $e->getCode(), 'message' => $e->getMessage()],
]);
}
return response()->json([
'jsonrpc' => '2.0',
'id' => $requestId,
'result' => $output,
]);
}
}
**Architecture Rationale:** A dedicated controller enables constructor injection, unit testability, and clean extension points. Notifications bypass response generation entirely, adhering to JSON-RPC 2.0 semantics. All protocol errors remain inside the JSON-RPC envelope over HTTP 200, preventing client-side transport confusion.
### Step 2: Protocol Negotiation & Capability Declaration
The `initialize` handshake establishes the session contract. The server must echo the exact protocol version it supports and declare available capabilities. Hardcoding these values creates version drift; configuration-driven negotiation ensures consistency across environments.
```php
// app/AgentBridge/Handlers/HandshakeProcessor.php
namespace App\AgentBridge\Handlers;
class HandshakeProcessor
{
public function negotiate(array $clientParams): array
{
$supportedVersion = config('agent_bridge.protocol_version');
$clientVersion = $clientParams['protocolVersion'] ?? null;
if ($clientVersion !== $supportedVersion) {
throw new \InvalidArgumentException('Protocol version mismatch');
}
return [
'protocolVersion' => $supportedVersion,
'capabilities' => [
'tools' => ['listChanged' => false],
],
'serverInfo' => [
'name' => config('agent_bridge.service_name'),
'version' => config('app.version', '1.0.0'),
],
];
}
}
Architecture Rationale: listChanged: false indicates a static tool registry. Setting it to true requires implementing push notifications for dynamic updates, which adds complexity. Version validation prevents silent schema mismatches. All metadata derives from environment configuration for deployment flexibility.
Tool definitions serve as the primary contract between the agent and the backend. Strict JSON Schema validation prevents malformed inputs and reduces execution failures. The schema enforces type safety, length constraints, pattern matching, and property restrictions.
// app/AgentBridge/Handlers/ToolCatalog.php
namespace App\AgentBridge\Handlers;
class ToolCatalog
{
public function publish(): array
{
return [
'tools' => [
[
'name' => 'v2__query_inventory',
'description' => 'Search warehouse stock by keyword. Returns paginated results with SKU, quantity, and location.',
'inputSchema' => [
'$schema' => 'http://json-schema.org/draft-07/schema#',
'type' => 'object',
'required' => ['search_term'],
'additionalProperties' => false,
'properties' => [
'search_term' => [
'type' => 'string',
'description' => 'Keyword or partial SKU to match.',
'minLength' => 2,
'maxLength' => 120,
],
'page' => [
'type' => 'integer',
'default' => 1,
'minimum' => 1,
'maximum' => 100,
],
],
],
],
[
'name' => 'v2__fetch_manifest',
'description' => 'Retrieve full shipment details using a tracking identifier.',
'inputSchema' => [
'$schema' => 'http://json-schema.org/draft-07/schema#',
'type' => 'object',
'required' => ['tracking_id'],
'additionalProperties' => false,
'properties' => [
'tracking_id' => [
'type' => 'string',
'description' => 'Alphanumeric tracking code.',
'pattern' => '^[A-Z0-9]{8,16}$',
],
],
],
],
],
];
}
}
Architecture Rationale: additionalProperties: false rejects unknown fields, preventing injection attacks and schema drift. Pattern validation enforces format constraints at the protocol layer. Version prefixes (v2__) enable backward compatibility during API evolution.
The tools/call method routes validated inputs to domain logic. Results must conform to the MCP content block format, supporting text, images, and structured data. Error handling maps domain exceptions to JSON-RPC error codes.
// app/AgentBridge/Handlers/ExecutionRouter.php
namespace App\AgentBridge\Handlers;
use App\AgentBridge\Exceptions\ProtocolViolation;
use App\Services\WarehouseService;
class ExecutionRouter
{
public function __construct(private WarehouseService $warehouse) {}
public function execute(array $arguments): array
{
$toolName = $arguments['name'] ?? null;
$input = $arguments['arguments'] ?? [];
$payload = match ($toolName) {
'v2__query_inventory' => $this->warehouse->search($input['search_term'], $input['page'] ?? 1),
'v2__fetch_manifest' => $this->warehouse->getShipment($input['tracking_id']),
default => throw new ProtocolViolation(-32602, 'Invalid tool invocation'),
};
return [
'content' => [
['type' => 'text', 'text' => json_encode($payload, JSON_THROW_ON_ERROR)],
],
'isError' => false,
];
}
}
Architecture Rationale: Domain services remain decoupled from protocol logic. JSON encoding enforces consistent output formatting. isError flags enable client-side fallback handling without HTTP status confusion.
Pitfall Guide
1. HTTP Status Code Confusion
Explanation: Returning 400 or 500 for JSON-RPC validation errors breaks MCP clients, which expect all protocol errors inside the JSON-RPC envelope over HTTP 200.
Fix: Reserve non-200 responses for transport failures (auth rejection, server crash). Map all schema/method errors to JSON-RPC codes (-32602, -32603) inside the response body.
2. Ignoring Notification Semantics
Explanation: Notifications like notifications/initialized lack an id field and require no response. Attempting to return a payload causes client-side parsing failures.
Fix: Check for id === null and notifications/ prefix. Return 204 No Content immediately without processing.
3. Protocol Version Drift
Explanation: Hardcoding version strings creates silent failures when clients negotiate newer revisions. Schema field names and error codes shift between spec releases.
Fix: Drive protocolVersion from environment configuration. Validate client version against supported values and reject mismatches explicitly.
4. Loose JSON Schema Validation
Explanation: Omitting additionalProperties: false or weak pattern matching allows agents to send unexpected fields, causing downstream parsing errors or security vulnerabilities.
Fix: Enforce strict schemas with additionalProperties: false, explicit required arrays, and regex pattern constraints. Validate inputs before domain execution.
5. Blocking SSE Streams
Explanation: Synchronous tool execution blocks the HTTP response, preventing streaming updates for long-running operations. MCP clients expect SSE channels for progress tracking.
Fix: Offload heavy execution to queued jobs. Use Laravel's Event::dispatch() with SSE-compatible listeners. Return immediate acknowledgment while streaming results asynchronously.
6. Missing initialized Acknowledgment
Explanation: Skipping the notifications/initialized handler leaves sessions in a half-open state. Some clients refuse to send tool requests until the notification is processed.
Fix: Explicitly handle notifications/initialized in the dispatcher. Log session activation and prepare internal state caches.
7. Over-Engineering the Router
Explanation: Dynamic method resolution via reflection or service containers adds latency and obscures error tracing. MCP method names are static and predictable.
Fix: Use explicit match statements for method routing. Keep the dispatcher lean and delegate complex logic to dedicated handlers.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Local IDE tooling | stdio transport | Zero network config, fast process spawning, single-client isolation | Minimal infrastructure |
| Multi-agent orchestration | HTTP+SSE transport | Persistent connections, concurrent clients, streaming support, auth middleware | Moderate (load balancer, SSE workers) |
| Static tool registry | listChanged: false | Simpler implementation, no push notification overhead, predictable caching | Lower maintenance |
| Dynamic tool updates | listChanged: true + push notifications | Real-time schema changes, adaptive agent behavior | Higher complexity, requires event bus |
| Strict validation | JSON Schema draft-07 with additionalProperties: false | Prevents injection, enforces contracts, reduces client errors | Slightly higher initial schema design time |
Configuration Template
// config/agent_bridge.php
return [
'protocol_version' => env('MCP_PROTOCOL_VERSION', '2025-11-25'),
'service_name' => env('MCP_SERVICE_NAME', 'enterprise-agent-gateway'),
'sse_heartbeat' => env('MCP_SSE_HEARTBEAT', 30),
'max_tool_payload' => env('MCP_MAX_PAYLOAD', 1024 * 1024),
'allowed_origins' => explode(',', env('MCP_ALLOWED_ORIGINS', '')),
];
// routes/api.php
use App\Http\Controllers\AgentGatewayController;
use Illuminate\Support\Facades\Route;
Route::prefix('agent')->middleware([
'auth:sanctum',
'ability:mcp:execute',
'throttle:agent_bridge'
])->group(function () {
Route::post('/bridge', [AgentGatewayController::class, 'dispatch']);
});
Quick Start Guide
- Install dependencies: Ensure Laravel 10+ with PHP 8.2+. Add
laravel/sanctum for token authentication and configure your rate limiting middleware.
- Publish configuration: Create
config/agent_bridge.php with protocol version, service name, and SSE heartbeat settings. Set environment variables for production overrides.
- Register handlers: Implement
HandshakeProcessor, ToolCatalog, and ExecutionRouter. Define strict JSON Schema contracts for each tool.
- Wire the gateway: Attach
AgentGatewayController to a single POST route. Apply authentication, capability scoping, and throttling middleware.
- Validate locally: Test with an MCP client using HTTP+SSE transport. Verify
initialize negotiation, tools/list schema compliance, and tools/call execution. Monitor SSE streams for long-running operations.