---
name: waaseyaa:mcp-endpoint
description: Use when working with the MCP server endpoint, JSON-RPC handling, tool registry, authentication, or files in packages/mcp/
---

# MCP Endpoint Specialist

## Scope

This skill covers the MCP (Model Context Protocol) package:

- `packages/mcp/src/` -- McpEndpoint, McpResponse, McpRouteProvider, McpServerCard
- `packages/mcp/src/Auth/` -- McpAuthInterface, BearerTokenAuth
- `packages/mcp/src/Bridge/` -- ToolRegistryInterface, ToolExecutorInterface

Use this skill when:
- Adding or modifying the MCP HTTP endpoint
- Working with JSON-RPC protocol handling
- Changing MCP authentication
- Adding new JSON-RPC methods or tool capabilities
- Wiring MCP routes into the front controller
- Working with the tool registry or executor bridge

## Key Interfaces

### McpEndpoint (packages/mcp/src/McpEndpoint.php)

```php
final readonly class McpEndpoint
{
    public function __construct(
        private McpAuthInterface $auth,
        private ToolRegistryInterface $registry,
        private ToolExecutorInterface $executor,
    ) {}

    public function handle(
        string $method,
        string $body,
        ?string $authorizationHeader,
    ): McpResponse;
}
```

Single entry point for all MCP traffic. Authenticates, parses JSON-RPC, dispatches to internal handlers.

### McpResponse (packages/mcp/src/McpResponse.php)

```php
final readonly class McpResponse
{
    public function __construct(
        public string $body,
        public int $statusCode = 200,
        public string $contentType = 'application/json',
    ) {}
}
```

### McpAuthInterface (packages/mcp/src/Auth/McpAuthInterface.php)

```php
interface McpAuthInterface
{
    public function authenticate(?string $authorizationHeader): ?AccountInterface;
}
```

Returns `AccountInterface` on success, `null` on failure. `BearerTokenAuth` is the MVP implementation mapping opaque tokens to accounts.

### ToolRegistryInterface (packages/mcp/src/Bridge/ToolRegistryInterface.php)

```php
interface ToolRegistryInterface
{
    /** @return McpToolDefinition[] */
    public function getTools(): array;
    public function getTool(string $name): ?McpToolDefinition;
}
```

### ToolExecutorInterface (packages/mcp/src/Bridge/ToolExecutorInterface.php)

```php
interface ToolExecutorInterface
{
    public function execute(string $toolName, array $arguments): array;
}
```

Returns MCP tool result format: `{content: [{type: "text", text: "..."}], isError?: bool}`.

## Architecture

### JSON-RPC Dispatch Flow

```
POST /mcp
  -> McpEndpoint::handle()
    -> authenticate($authorizationHeader) -> AccountInterface | null (401)
    -> json_decode($body) -> parse error (-32700) | invalid request (-32600)
    -> match $request['method']:
        'initialize' -> protocol version, capabilities, server info
        'ping'       -> empty result
        'tools/list' -> ToolRegistryInterface::getTools() -> toArray()
        'tools/call' -> ToolExecutorInterface::execute($name, $arguments)
        default      -> method not found (-32601)
```

### JSON-RPC Error Codes

| Code | Meaning |
|------|---------|
| `-32700` | Parse error (invalid JSON) |
| `-32600` | Invalid request (missing `method` field) |
| `-32601` | Method not found |
| `-32602` | Invalid params (missing tool name, unknown tool) |
| `-32001` | Unauthorized (auth failure) |

### Routes

`McpRouteProvider` registers two routes:

| Route | Path | Methods | Auth |
|-------|------|---------|------|
| `mcp.endpoint` | `/mcp` | POST, GET | Required (bearer token) |
| `mcp.server_card` | `/.well-known/mcp.json` | GET | Public |

**Note:** Routes are defined in `McpRouteProvider` but not yet wired into `public/index.php`. This is a known gap.

### Package Dependencies

- **Layer 6** (Interfaces) -- can import from layers 0-5
- Depends on: `waaseyaa/ai-schema` (McpToolDefinition), `waaseyaa/ai-agent` (AgentExecutor), `waaseyaa/routing`, `waaseyaa/access` (AccountInterface)

## Common Mistakes

### JSON symmetry

Always pair `json_encode(..., JSON_THROW_ON_ERROR)` with `json_decode(..., JSON_THROW_ON_ERROR)`. The endpoint already does this correctly -- maintain it.

### AccountInterface, not concrete User

The auth interface returns `?AccountInterface` (from `waaseyaa/access`), not a concrete `User` class (from `waaseyaa/user`). The MCP package must not depend on `waaseyaa/user`.

### Tool result format

Tool executor must return `{content: [{type: "text", text: "..."}]}`. The `isError` key is optional (defaults to false). Don't return raw strings or arrays -- wrap in the content block format.

### php://input is single-read

`HttpRequest::createFromGlobals()` consumes `php://input`. The front controller must pass the body via `$httpRequest->getContent()`, not re-read `php://input`.

### Final classes cannot be mocked

All concrete classes are `final readonly class`. Use real instances in tests:

```php
// Auth: use BearerTokenAuth with known tokens
$auth = new BearerTokenAuth(['test-token' => $account]);

// Registry/Executor: use anonymous classes implementing the interfaces
$registry = new class implements ToolRegistryInterface { ... };
$executor = new class implements ToolExecutorInterface { ... };

$endpoint = new McpEndpoint($auth, $registry, $executor);
$response = $endpoint->handle('POST', $body, 'Bearer test-token');
```

## Testing Patterns

### Unit Testing McpEndpoint

```php
$auth = new BearerTokenAuth(['tok' => $account]);
$registry = new class implements ToolRegistryInterface {
    public function getTools(): array { return []; }
    public function getTool(string $name): ?McpToolDefinition { return null; }
};
$executor = new class implements ToolExecutorInterface {
    public function execute(string $toolName, array $arguments): array {
        return ['content' => [['type' => 'text', 'text' => 'ok']]];
    }
};

$endpoint = new McpEndpoint($auth, $registry, $executor);

// Test auth failure
$response = $endpoint->handle('POST', '{}', null);
assert($response->statusCode === 401);

// Test ping
$body = json_encode(['jsonrpc' => '2.0', 'method' => 'ping', 'id' => 1]);
$response = $endpoint->handle('POST', $body, 'Bearer tok');
assert($response->statusCode === 200);
```

### Testing BearerTokenAuth

```php
$auth = new BearerTokenAuth(['secret' => $account]);

assert($auth->authenticate(null) === null);
assert($auth->authenticate('Bearer wrong') === null);
assert($auth->authenticate('Bearer secret') === $account);
assert($auth->authenticate('bearer secret') === $account); // case-insensitive
```

### Testing McpServerCard

```php
$card = new McpServerCard(name: 'Test', version: '1.0.0', endpoint: '/mcp');
$json = $card->toJson();
$decoded = json_decode($json, true);
assert($decoded['endpoint'] === '/mcp');
assert($decoded['transport'] === 'streamable-http');
```

## Related Specs

- `docs/specs/mcp-endpoint.md` -- Full MCP endpoint specification
- `docs/specs/ai-integration.md` -- AI layer that provides McpToolDefinition and AgentExecutor
- `CLAUDE.md` -- Project-wide gotchas including JSON symmetry and AccountInterface vs User
