diff --git a/AGENTS.md b/AGENTS.md index db7ac0c..084fae9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,31 @@ - Meta-tools return JSON with `success` and `message` fields. Read `message` to adapt decisions (e.g., policy denial, already active, limits exceeded). -## HTTP (debugging only) +## HTTP endpoints -- Endpoints: `GET /healthz`, `GET /tools`, `POST/GET/DELETE /mcp`, `GET /.well-known/mcp-config`. -- Headers: use `mcp-client-id` to reuse per-client server bundles; `mcp-session-id` is managed by the MCP transport after initialize. +### Built-in MCP endpoints + +- `GET /healthz` - Health check +- `GET /tools` - List available toolsets and tools +- `POST /mcp` - MCP JSON-RPC requests +- `GET /mcp` - Server-sent events stream +- `DELETE /mcp` - Close session +- `GET /.well-known/mcp-config` - Configuration schema + +### Custom HTTP endpoints + +Servers may expose custom REST-like endpoints alongside MCP protocol endpoints. These are defined by the server implementer and provide direct HTTP access to functionality. + +- Custom endpoints use standard HTTP methods (GET, POST, PUT, DELETE, PATCH) +- Request/response validation via Zod schemas +- Access to client ID via `mcp-client-id` header +- Permission-aware endpoints receive client's allowed toolsets +- Standard error format with `VALIDATION_ERROR`, `INTERNAL_ERROR`, or `RESPONSE_VALIDATION_ERROR` codes + +Check `GET /tools` or server documentation to discover available custom endpoints. + +### Headers + +- `mcp-client-id`: Client identifier (reuse for per-client sessions) +- `mcp-session-id`: Session identifier (managed by MCP transport after initialize) +- `mcp-toolset-permissions`: Comma-separated toolset list (permission-based servers with header-based permissions) diff --git a/README.md b/README.md index 38f5c03..f5a7c6a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ - [Static startup](#static-startup) - [Permission-based starter guide](#permission-based-starter-guide) - [Permission configuration approaches](#permission-configuration-approaches) +- [Custom HTTP endpoints](#custom-http-endpoints) - [API](#api) - [createMcpServer](#createmcpserveroptions) - [createPermissionBasedMcpServer](#createpermissionbasedmcpserveroptions) @@ -589,6 +590,135 @@ await start(); **Note:** Resolver functions must be synchronous. If you need to fetch permissions from external sources, do so before server creation and cache the results. +## Custom HTTP endpoints + +Toolception supports custom HTTP endpoints alongside MCP protocol endpoints, enabling REST-like APIs with Zod validation and type inference. + +### Basic usage + +```ts +import { createMcpServer, defineEndpoint } from "toolception"; +import { z } from "zod"; + +const { start } = await createMcpServer({ + // ... standard options + http: { + port: 3000, + customEndpoints: [ + defineEndpoint({ + method: "GET", + path: "/api/users", + querySchema: z.object({ + limit: z.coerce.number().int().positive().default(10), + role: z.enum(["admin", "user"]).optional(), + }), + responseSchema: z.object({ + users: z.array(z.object({ id: z.string(), name: z.string() })), + }), + handler: async (req) => { + // req.query is typed: { limit: number, role?: "admin" | "user" } + // req.clientId is available from mcp-client-id header + return { users: [{ id: "1", name: "Alice" }] }; + }, + }), + ], + }, +}); +``` + +### Validation schemas + +- **querySchema**: Validates URL query parameters (use `z.coerce` for type conversion) +- **bodySchema**: Validates request body (POST/PUT/PATCH) +- **paramsSchema**: Validates path parameters (e.g., `/users/:userId`) +- **responseSchema**: Validates handler response (prevents invalid data leakage) + +### Request context + +Handlers receive a typed request object: + +```ts +{ + body: TBody, // Validated from bodySchema + query: TQuery, // Validated from querySchema + params: TParams, // Validated from paramsSchema + headers: Record, + clientId: string, // From mcp-client-id header or auto-generated +} +``` + +### Permission-aware endpoints + +Use `definePermissionAwareEndpoint` in permission-based servers to access client permissions: + +```ts +import { createPermissionBasedMcpServer, definePermissionAwareEndpoint } from "toolception"; + +const { start } = await createPermissionBasedMcpServer({ + // ... permission config + http: { + customEndpoints: [ + definePermissionAwareEndpoint({ + method: "GET", + path: "/api/me", + responseSchema: z.object({ + clientId: z.string(), + allowedToolsets: z.array(z.string()), + isAdmin: z.boolean(), + }), + handler: async (req) => { + // req.allowedToolsets and req.failedToolsets are available + return { + clientId: req.clientId, + allowedToolsets: req.allowedToolsets, + isAdmin: req.allowedToolsets.includes("admin-tools"), + }; + }, + }), + ], + }, +}); +``` + +### Error handling + +Validation failures return standardized error responses: + +- **400 VALIDATION_ERROR**: Request validation failed (body, query, or params) +- **500 INTERNAL_ERROR**: Handler threw an exception +- **500 RESPONSE_VALIDATION_ERROR**: Response validation failed + +Example error response: + +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "Validation failed for query", + "details": [ + { + "code": "invalid_type", + "path": ["limit"], + "message": "Expected number, received string" + } + ] + } +} +``` + +### Reserved paths + +Custom endpoints cannot override built-in MCP paths: + +- `/mcp` - MCP JSON-RPC endpoint +- `/healthz` - Health check +- `/tools` - Tool listing +- `/.well-known/mcp-config` - Configuration schema + +### Complete example + +See `examples/custom-endpoints-demo.ts` for a full working example with GET, POST, PUT, DELETE endpoints, pagination, and permission-aware handlers. + ## API ### createMcpServer(options) @@ -736,9 +866,10 @@ const moduleLoaders = { #### options.http (optional) -`{ host?: string; port?: number; basePath?: string; cors?: boolean; logger?: boolean }` +`{ host?: string; port?: number; basePath?: string; cors?: boolean; logger?: boolean; customEndpoints?: CustomEndpointDefinition[] }` - Fastify transport configuration. Defaults: host `0.0.0.0`, port `3000`, basePath `/`, CORS enabled, logger disabled. +- `customEndpoints`: Optional array of custom HTTP endpoints to register alongside MCP protocol endpoints. See [Custom HTTP endpoints](#custom-http-endpoints) for details. #### options.createServer (optional) diff --git a/examples/custom-endpoints-demo.ts b/examples/custom-endpoints-demo.ts index ece2275..b563b27 100644 --- a/examples/custom-endpoints-demo.ts +++ b/examples/custom-endpoints-demo.ts @@ -1,21 +1,32 @@ #!/usr/bin/env tsx /** - * Example server demonstrating custom endpoint functionality. + * Custom Endpoints Demo - Comprehensive examples of HTTP endpoint integration * - * This example shows: - * - Basic GET/POST/PUT/DELETE endpoints with Zod validation - * - Query parameter validation and coercion - * - Path parameter validation - * - Request body validation - * - Response validation - * - Client ID extraction - * - Permission-aware endpoints + * This example demonstrates: * - * Run this example with: - * npx tsx examples/custom-endpoints-demo.ts + * VALIDATION: + * - Query parameter validation with Zod (coercion, defaults, enums) + * - Path parameter validation (/users/:userId) + * - Request body validation (POST/PUT) + * - Response schema validation * - * Then test the endpoints with curl: + * FEATURES: + * - All HTTP methods: GET, POST, PUT, DELETE + * - Client ID extraction from mcp-client-id header + * - Permission-aware endpoints (allowedToolsets, failedToolsets) + * - Type-safe handlers with automatic inference + * - Standard error responses (VALIDATION_ERROR, INTERNAL_ERROR) + * + * EXAMPLES: + * 1. Standard server with custom endpoints (port 3000) + * 2. Permission-based server with permission-aware endpoints (port 3001) + * + * Run this example: + * npx tsx examples/custom-endpoints-demo.ts # Standard server + * npx tsx examples/custom-endpoints-demo.ts permission # Permission-based server + * + * Quick test: * curl "http://localhost:3000/api/users?limit=5" * curl -X POST http://localhost:3000/api/users -H 'Content-Type: application/json' -d '{"name":"Alice","email":"alice@example.com"}' */