From 4cf885779fdbbd584a95af53f6d67ca9f910e1db Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 20 Dec 2025 18:29:06 -0800 Subject: [PATCH 1/5] docs: Add TypeScript SDK reference documentation ## Summary Complete reference documentation for the TypeScript MCP Server SDK (arcade-mcp), providing feature parity with the Python SDK while using TypeScript-idiomatic patterns. ## New Files - `typescript/overview/` - MCPApp API, tool registration, Zod 4 schemas - `typescript/types/` - ToolContext, ContentItem, protocol types, schema utilities - `typescript/errors/` - Complete error hierarchy with constructor signatures - `typescript/middleware/` - MCP middleware and HTTP hooks via app.elysia - `typescript/server/` - Low-level MCPServer class for custom transports - `typescript/transports/` - stdio and HTTP transport modes - `typescript/settings/` - MCPSettings, environment variables, configuration ## Design Decisions ### Options Object Pattern TypeScript uses options objects for error constructors (vs Python positional args): ```typescript throw new ContextRequiredToolError('Message', { additionalPromptContent: 'Please specify...', // required developerMessage: 'debug info', // optional }); ``` ### HTTP Hooks via app.elysia Clean separation - MCPApp handles MCP, app.elysia handles HTTP: ```typescript app.elysia.onRequest(({ request }) => { ... }); ``` ### Runtime Tool Registration materializeTool() creates tool objects for runtime addition: ```typescript const tool = materializeTool({ name, input, handler }); await app.tools.add(tool); ``` ## Stack Bun 1.3.x + Elysia 1.3.x + Zod 4.x + TypeScript 5.9.x ## Related - Python SDK docs: /references/mcp/python/ - Depends on: #XX (Python error hierarchy improvements) - for consistent error docs --- app/en/references/mcp/_meta.tsx | 3 + app/en/references/mcp/typescript/_meta.tsx | 35 ++ .../references/mcp/typescript/errors/page.mdx | 424 +++++++++++++++ .../mcp/typescript/middleware/page.mdx | 288 ++++++++++ .../mcp/typescript/overview/page.mdx | 491 ++++++++++++++++++ .../references/mcp/typescript/server/page.mdx | 273 ++++++++++ .../mcp/typescript/settings/page.mdx | 335 ++++++++++++ .../mcp/typescript/transports/page.mdx | 324 ++++++++++++ .../references/mcp/typescript/types/page.mdx | 484 +++++++++++++++++ 9 files changed, 2657 insertions(+) create mode 100644 app/en/references/mcp/typescript/_meta.tsx create mode 100644 app/en/references/mcp/typescript/errors/page.mdx create mode 100644 app/en/references/mcp/typescript/middleware/page.mdx create mode 100644 app/en/references/mcp/typescript/overview/page.mdx create mode 100644 app/en/references/mcp/typescript/server/page.mdx create mode 100644 app/en/references/mcp/typescript/settings/page.mdx create mode 100644 app/en/references/mcp/typescript/transports/page.mdx create mode 100644 app/en/references/mcp/typescript/types/page.mdx diff --git a/app/en/references/mcp/_meta.tsx b/app/en/references/mcp/_meta.tsx index 5832a5e47..a7a81a612 100644 --- a/app/en/references/mcp/_meta.tsx +++ b/app/en/references/mcp/_meta.tsx @@ -11,6 +11,9 @@ const meta: MetaRecord = { python: { title: "Python", }, + typescript: { + title: "TypeScript", + }, telemetry: { title: "Telemetry", }, diff --git a/app/en/references/mcp/typescript/_meta.tsx b/app/en/references/mcp/typescript/_meta.tsx new file mode 100644 index 000000000..9f25b0ab0 --- /dev/null +++ b/app/en/references/mcp/typescript/_meta.tsx @@ -0,0 +1,35 @@ +import type { MetaRecord } from "nextra"; + +const meta: MetaRecord = { + "*": { + theme: { + breadcrumb: true, + layout: "full", + toc: true, + copyPage: true, + }, + }, + overview: { + title: "Overview", + }, + transports: { + title: "Transports", + }, + server: { + title: "Server", + }, + middleware: { + title: "Middleware", + }, + types: { + title: "Types", + }, + errors: { + title: "Errors", + }, + settings: { + title: "Settings", + }, +}; + +export default meta; diff --git a/app/en/references/mcp/typescript/errors/page.mdx b/app/en/references/mcp/typescript/errors/page.mdx new file mode 100644 index 000000000..6d9de4fd7 --- /dev/null +++ b/app/en/references/mcp/typescript/errors/page.mdx @@ -0,0 +1,424 @@ +--- +title: "Errors - Arcade MCP TypeScript Reference" +description: "Error types for MCP servers" +--- + +import { Callout } from "nextra/components"; + +# Errors + +The SDK provides specific error types for different failure scenarios. Use these in your tools to return meaningful errors to AI clients. + +## Common Errors + +These are the errors you'll use most often in your tools: + +### `NotFoundError` + +Requested resource doesn't exist. + +```typescript +import { MCPApp, NotFoundError } from 'arcade-mcp'; +import { z } from 'zod'; + +const app = new MCPApp({ name: 'user-service' }); + +app.tool('getUser', { + input: z.object({ id: z.string() }), + handler: async ({ input }) => { + const user = await db.findUser(input.id); + if (!user) { + throw new NotFoundError(`User ${input.id} not found`); + } + return user; + }, +}); +``` + +### `AuthorizationError` + +User lacks permission for the requested action. + +```typescript +import { MCPApp, AuthorizationError } from 'arcade-mcp'; +import { Google } from 'arcade-mcp/auth'; +import { z } from 'zod'; + +const app = new MCPApp({ name: 'project-service' }); + +app.tool('deleteProject', { + input: z.object({ projectId: z.string() }), + requiresAuth: Google({ scopes: ['profile'] }), + handler: async ({ input, authorization }) => { + const project = await db.findProject(input.projectId); + const userEmail = await fetchUserEmail(authorization.token); + + if (project.ownerEmail !== userEmail) { + throw new AuthorizationError('Only the owner can delete this project'); + } + + await db.deleteProject(input.projectId); + return { deleted: true }; + }, +}); +``` + +### `ToolExecutionError` + +Base class for tool execution errors. Use one of the specific subclasses: + +- `RetryableToolError` — can be retried +- `FatalToolError` — unrecoverable, don't retry +- `ContextRequiredToolError` — needs user input before retry +- `UpstreamError` — external API failure + +See [Retry-Aware Errors](#retry-aware-errors) below. + +### `ResourceError` + +Error in resource management. + +```typescript +import { MCPApp, ResourceError } from 'arcade-mcp'; +import { z } from 'zod'; + +const app = new MCPApp({ name: 'file-service' }); + +app.tool('readFile', { + input: z.object({ path: z.string() }), + handler: async ({ input }) => { + try { + return await Bun.file(input.path).text(); + } catch { + throw new ResourceError(`Cannot read file: ${input.path}`); + } + }, +}); +``` + +### `PromptError` + +Error in prompt management. + +```typescript +import { PromptError } from 'arcade-mcp'; + +// Thrown when a prompt is not found or invalid +throw new PromptError('Prompt template not found: greeting'); +``` + +### `UpstreamError` + +External API or service failure. Typically thrown by error adapters. + +```typescript +import { UpstreamError } from 'arcade-mcp'; + +throw new UpstreamError('Slack API error: channel_not_found', { + statusCode: 404, // required +}); +``` + +### `UpstreamRateLimitError` + +Rate limit from an external API. Includes retry information. + +```typescript +import { UpstreamRateLimitError } from 'arcade-mcp'; + +throw new UpstreamRateLimitError('Rate limited by Slack', { + retryAfterMs: 60_000, // required +}); +``` + + + You rarely throw `UpstreamError` manually. Use [error adapters](/references/mcp/typescript/types#error-adapter-types) to automatically translate SDK errors. + + +## Retry-Aware Errors + +These errors include metadata that helps AI orchestrators decide whether and how to retry. + +### `RetryableToolError` + +The operation failed but can be retried, optionally with guidance for the AI. + +```typescript +import { RetryableToolError } from 'arcade-mcp'; + +// Simple retry +throw new RetryableToolError('Service temporarily unavailable'); + +// With retry delay +throw new RetryableToolError('Rate limited', { + retryAfterMs: 5000, +}); + +// With guidance for the AI on how to retry differently +throw new RetryableToolError('Search returned no results', { + additionalPromptContent: 'Try broader search terms or check spelling.', +}); +``` + +### `FatalToolError` + +Unrecoverable error — the AI should not retry this operation. + +```typescript +import { FatalToolError } from 'arcade-mcp'; + +throw new FatalToolError('Account has been permanently deleted'); + +// With developer-only details (not shown to AI) +throw new FatalToolError('Configuration error', { + developerMessage: 'Missing required API key in environment', +}); +``` + +### `ContextRequiredToolError` + +The operation needs additional context from the user before it can proceed. + +```typescript +import { ContextRequiredToolError } from 'arcade-mcp'; + +throw new ContextRequiredToolError('Multiple users found matching "John"', { + additionalPromptContent: 'Please specify: John Smith (john@work.com) or John Doe (john@home.com)', // required +}); +``` + + + For most tool errors, use `RetryableToolError` (transient failures) or `FatalToolError` (unrecoverable). + Use `ContextRequiredToolError` when the AI needs to ask the user for clarification. + + +## Constructor Signatures + +All tool errors use an options object pattern. Required fields are marked. + +```typescript +// RetryableToolError — all options are optional +new RetryableToolError(message: string, options?: { + developerMessage?: string; + additionalPromptContent?: string; + retryAfterMs?: number; + extra?: Record; + cause?: unknown; +}); + +// FatalToolError — all options are optional +new FatalToolError(message: string, options?: { + developerMessage?: string; + extra?: Record; + cause?: unknown; +}); + +// ContextRequiredToolError — additionalPromptContent is REQUIRED +new ContextRequiredToolError(message: string, options: { + additionalPromptContent: string; // required + developerMessage?: string; + extra?: Record; + cause?: unknown; +}); + +// UpstreamError — statusCode is REQUIRED +new UpstreamError(message: string, options: { + statusCode: number; // required + developerMessage?: string; + extra?: Record; + cause?: unknown; +}); + +// UpstreamRateLimitError — retryAfterMs is REQUIRED +new UpstreamRateLimitError(message: string, options: { + retryAfterMs: number; // required + developerMessage?: string; + extra?: Record; + cause?: unknown; +}); +``` + +| Option | Purpose | +|--------|---------| +| `developerMessage` | Debug info (not shown to AI) | +| `extra` | Structured metadata for telemetry/adapters | +| `cause` | ES2022 error chaining for stack traces | + +## Error Hierarchy + +All errors extend from `MCPError`: + +```text +MCPError (base) +├── MCPContextError (user/input caused the error) +│ ├── AuthorizationError +│ ├── NotFoundError +│ ├── PromptError +│ └── ResourceError +└── MCPRuntimeError (server/infrastructure caused the error) + ├── ProtocolError + ├── TransportError + └── ServerError + ├── RequestError + │ └── ServerRequestError (server-to-client requests) + ├── ResponseError + ├── SessionError + └── LifespanError + +ToolkitError (base for tool errors) +└── ToolError + └── ToolRuntimeError + └── ToolExecutionError + ├── RetryableToolError (can retry) + ├── FatalToolError (do not retry) + ├── ContextRequiredToolError (needs user input) + └── UpstreamError (external API failure) + └── UpstreamRateLimitError +``` + +### When to Use Each Category + +| Error Type | Use when... | HTTP Analogy | +|------------|-------------|--------------| +| `MCPContextError` | User/input caused the error | 4xx errors | +| `MCPRuntimeError` | Server/infrastructure caused the error | 5xx errors | + +## Creating Custom Errors + +Extend `ToolExecutionError` for domain-specific tool errors: + +```typescript +import { ToolExecutionError } from 'arcade-mcp'; + +interface QuotaExceededOptions { + limitType: string; + developerMessage?: string; + extra?: Record; +} + +class QuotaExceededError extends ToolExecutionError { + readonly limitType: string; + + constructor(message: string, options: QuotaExceededOptions) { + super(message, options); + this.name = 'QuotaExceededError'; + this.limitType = options.limitType; + } +} + +// Usage +throw new QuotaExceededError('Monthly API quota exceeded', { + limitType: 'api_calls', + extra: { currentUsage: 10000, limit: 10000 }, +}); +``` + +For MCP-level errors (resources, prompts), extend `MCPContextError`: + +```typescript +import { MCPContextError } from 'arcade-mcp'; + +class InvalidResourceFormatError extends MCPContextError { + constructor(uri: string) { + super(`Invalid resource format: ${uri}`); + this.name = 'InvalidResourceFormatError'; + } +} +``` + +## Best Practices + + + Never expose internal error details to clients. The SDK's `ErrorHandlingMiddleware` + masks stack traces by default. In development, set `maskErrorDetails: false` to debug. + + +### Do: Use Specific Error Types + +```typescript +// ✅ Good: Specific error type +throw new NotFoundError('User not found'); + +// ✅ Good: Retryable with guidance +throw new RetryableToolError('Search returned no results', { + additionalPromptContent: 'Try broader search terms.', +}); +``` + +### Don't: Expose Internals + +```typescript +// ❌ Bad: Leaks implementation details +throw new Error(`Database error: ${dbError.stack}`); + +// ❌ Bad: Leaks SQL +throw new Error(`Query failed: SELECT * FROM users WHERE...`); +``` + +### Error Messages for AI Clients + +Write actionable messages that help AI clients understand what went wrong: + +```typescript +// ❌ Vague +throw new RetryableToolError('Invalid input'); + +// ✅ Actionable with context +throw new RetryableToolError(`Invalid email: "${input.email}"`, { + additionalPromptContent: 'Use format: user@domain.com', +}); +``` + +### Wrapping External Errors + +When calling external services, wrap their errors with cause chaining for debugging: + +```typescript +import { RetryableToolError } from 'arcade-mcp'; +import { z } from 'zod'; + +app.tool('fetchWeather', { + input: z.object({ city: z.string() }), + handler: async ({ input }) => { + try { + return await weatherApi.get(input.city); + } catch (error) { + // Wrap with context and preserve the original error for debugging + throw new RetryableToolError( + `Failed to fetch weather for ${input.city}. Please try again.`, + { cause: error } // Preserves stack trace for debugging + ); + } + }, +}); +``` + +## Error Handling in Middleware + +```typescript +import { + Middleware, + MCPError, + type MiddlewareContext, + type CallNext, +} from 'arcade-mcp'; + +class ErrorEnrichmentMiddleware extends Middleware { + async onCallTool(context: MiddlewareContext, next: CallNext) { + try { + return await next(context); + } catch (error) { + if (error instanceof MCPError) { + // Log structured error info + console.error({ + type: error.name, + message: error.message, + tool: context.message.params?.name, + session: context.session?.id, + }); + } + throw error; // Re-throw for ErrorHandlingMiddleware + } + } +} +``` diff --git a/app/en/references/mcp/typescript/middleware/page.mdx b/app/en/references/mcp/typescript/middleware/page.mdx new file mode 100644 index 000000000..7e042fd9d --- /dev/null +++ b/app/en/references/mcp/typescript/middleware/page.mdx @@ -0,0 +1,288 @@ +--- +title: "Middleware - Arcade MCP TypeScript Reference" +description: "Intercept and modify MCP requests and responses" +--- + +import { Callout } from "nextra/components"; + +# Middleware + + + Most users don't need custom middleware. The SDK includes logging and error handling by default. + Use middleware when you need request/response interception, custom authentication, or metrics. + + +Middleware intercepts MCP messages before they reach your tools and after responses are generated. + +## Built-in Middleware + +### `LoggingMiddleware` + +Logs all MCP messages with timing. Enabled by default. + +```typescript +import { LoggingMiddleware } from 'arcade-mcp'; + +new LoggingMiddleware({ logLevel: 'INFO' }) +``` + +### `ErrorHandlingMiddleware` + +Catches errors and returns safe error responses. Enabled by default. + +```typescript +import { ErrorHandlingMiddleware } from 'arcade-mcp'; + +new ErrorHandlingMiddleware({ maskErrorDetails: true }) +``` + +Set `maskErrorDetails: false` in development to see full stack traces. + +## Custom Middleware + +Extend the `Middleware` class and override handler methods: + +```typescript +import { Middleware, type MiddlewareContext, type CallNext } from 'arcade-mcp'; + +class TimingMiddleware extends Middleware { + async onMessage(context: MiddlewareContext, next: CallNext) { + const start = performance.now(); + const result = await next(context); + const elapsed = performance.now() - start; + console.error(`Request took ${elapsed.toFixed(2)}ms`); + return result; + } +} +``` + + + With stdio transport, use `console.error()` for logging. All `stdout` is protocol data. + + +### Available Hooks + +| Hook | When it runs | +|------|--------------| +| `onMessage` | Every message (use for logging, timing) | +| `onRequest` | All request messages | +| `onNotification` | All notification messages | +| `onCallTool` | Tool invocations | +| `onListTools` | Tool listing requests | +| `onListResources` | Resource listing requests | +| `onReadResource` | Resource read requests | +| `onListResourceTemplates` | Resource template listing requests | +| `onListPrompts` | Prompt listing requests | +| `onGetPrompt` | Prompt retrieval requests | + +### Hook Signatures + +All hooks follow the same pattern: + +```typescript +async onHookName( + context: MiddlewareContext, + next: CallNext +): Promise +``` + +- Call `next(context)` to continue the chain +- Modify `context.message` before calling `next` to alter the request +- Modify the result after calling `next` to alter the response +- Throw an error to abort processing +- Return early (without calling `next`) to short-circuit + +## Composing Middleware + +Combine multiple middleware into a single handler: + +```typescript +import { + composeMiddleware, + LoggingMiddleware, + ErrorHandlingMiddleware, +} from 'arcade-mcp'; + +const composed = composeMiddleware( + new ErrorHandlingMiddleware({ maskErrorDetails: false }), + new LoggingMiddleware({ logLevel: 'DEBUG' }), + new TimingMiddleware() +); +``` + +Pass middleware directly to `MCPServer`: + +```typescript +import { MCPServer, ToolCatalog } from 'arcade-mcp'; + +const server = new MCPServer({ + catalog: new ToolCatalog(), + middleware: [ + new ErrorHandlingMiddleware({ maskErrorDetails: false }), + new LoggingMiddleware({ logLevel: 'DEBUG' }), + ], +}); +``` + +Use `composeMiddleware` when you need to combine middleware into reusable units: + +```typescript +const authAndLogging = composeMiddleware( + new AuthMiddleware(), + new LoggingMiddleware() +); + +const server = new MCPServer({ + catalog: new ToolCatalog(), + middleware: [authAndLogging, new MetricsMiddleware()], +}); +``` + + + Middleware runs in order. The first wraps the second, which wraps the third. + `ErrorHandlingMiddleware` first means it catches errors from all subsequent middleware. + + +## MiddlewareContext + +Context passed to all middleware handlers: + +```typescript +interface MiddlewareContext { + /** The MCP message being processed */ + message: T; + + /** Mutable metadata to pass between middleware */ + metadata: Record; + + /** Client session info (if available) */ + session?: ServerSession; +} +``` + +### Sharing Data Between Middleware + +Use `metadata` to pass data between middleware: + +```typescript +class AuthMiddleware extends Middleware { + async onMessage(context: MiddlewareContext, next: CallNext) { + const userId = await validateToken(context.message); + context.metadata.userId = userId; // Available to subsequent middleware + return next(context); + } +} + +class AuditMiddleware extends Middleware { + async onCallTool(context: MiddlewareContext, next: CallNext) { + const userId = context.metadata.userId; // From AuthMiddleware + await logToolCall(userId, context.message); + return next(context); + } +} +``` + +### Creating Modified Context + +Use object spread to create a modified context: + +```typescript +class TransformMiddleware extends Middleware { + async onMessage(context: MiddlewareContext, next: CallNext) { + const modifiedContext = { + ...context, + metadata: { ...context.metadata, transformed: true }, + }; + return next(modifiedContext); + } +} +``` + +## Example: Auth Middleware + +```typescript +import { + Middleware, + AuthorizationError, + type MiddlewareContext, + type CallNext, +} from 'arcade-mcp'; + +class ApiKeyAuthMiddleware extends Middleware { + constructor(private validKeys: Set) { + super(); + } + + async onCallTool(context: MiddlewareContext, next: CallNext) { + const apiKey = context.metadata.apiKey as string | undefined; + + if (!apiKey || !this.validKeys.has(apiKey)) { + throw new AuthorizationError('Invalid API key'); + } + + return next(context); + } +} + +// Usage +const auth = new ApiKeyAuthMiddleware(new Set(['key1', 'key2'])); +``` + +## Example: Rate Limiting Middleware + +```typescript +import { Middleware, RetryableToolError, type MiddlewareContext, type CallNext } from 'arcade-mcp'; + +class RateLimitMiddleware extends Middleware { + private requests = new Map(); + + constructor(private maxRequests = 100, private windowMs = 60_000) { + super(); + } + + async onCallTool(context: MiddlewareContext, next: CallNext) { + const clientId = context.session?.id ?? 'anonymous'; + const now = Date.now(); + const recent = (this.requests.get(clientId) ?? []) + .filter((t) => t > now - this.windowMs); + + if (recent.length >= this.maxRequests) { + throw new RetryableToolError('Rate limit exceeded. Try again later.', { + retryAfterMs: this.windowMs, + }); + } + + this.requests.set(clientId, [...recent, now]); + return next(context); + } +} +``` + +## HTTP-Level Hooks + +For HTTP-level customization, access the underlying Elysia instance via `app.elysia`: + +```typescript +import { MCPApp } from 'arcade-mcp'; + +const app = new MCPApp({ name: 'my-server' }); + +// Log requests +app.elysia.onRequest(({ request }) => { + console.error(`${request.method} ${new URL(request.url).pathname}`); +}); + +// Add response headers +app.elysia.onAfterHandle(({ set }) => { + set.headers['X-Powered-By'] = 'Arcade MCP'; +}); + +app.run({ transport: 'http', port: 8000 }); +``` + + + `app.elysia` gives you the underlying Elysia instance with full access to all lifecycle hooks. + See the [Elysia lifecycle docs](https://elysiajs.com/essential/life-cycle) for available hooks. + + +For most use cases, MCP middleware (the `Middleware` class) is sufficient. HTTP-level auth can use Elysia's `derive` pattern; see the [Elysia docs](https://elysiajs.com/patterns/extends-context). CORS is configured via the `cors` option. diff --git a/app/en/references/mcp/typescript/overview/page.mdx b/app/en/references/mcp/typescript/overview/page.mdx new file mode 100644 index 000000000..725239a1a --- /dev/null +++ b/app/en/references/mcp/typescript/overview/page.mdx @@ -0,0 +1,491 @@ +--- +title: "Arcade MCP (MCP Server SDK) - TypeScript Overview" +description: "Build MCP servers with TypeScript, Bun, Elysia, and Zod 4" +--- + +import { Callout } from "nextra/components"; + +# Arcade MCP (MCP Server SDK) - TypeScript Overview + +`arcade-mcp`, the secure framework for building MCP servers, provides a clean, minimal API to build MCP servers programmatically. It handles tool collection, server configuration, and transport setup with a developer-friendly interface. + +## Installation + +```bash +bun add arcade-mcp +``` + + + **tsconfig:** Run `bun init` to generate one, or ensure yours has these essentials: + + ```json + { + "compilerOptions": { + "strict": true, + "moduleResolution": "bundler", + "target": "ESNext", + "module": "Preserve" + } + } + ``` + + See [Bun TypeScript docs](https://bun.sh/docs/typescript) for the complete recommended config. + + +## Imports + +```typescript +// Main exports +import { MCPApp, tool } from 'arcade-mcp'; +import { NotFoundError, RetryableToolError, FatalToolError } from 'arcade-mcp'; + +// Auth providers +import { Google, GitHub, Slack } from 'arcade-mcp/auth'; + +// Error adapters +import { SlackErrorAdapter, GoogleErrorAdapter } from 'arcade-mcp/adapters'; +``` + +## Quick Start + +```typescript +// server.ts +import { MCPApp } from 'arcade-mcp'; +import { z } from 'zod'; + +const app = new MCPApp({ name: 'my-server', version: '1.0.0' }); + +app.tool('greet', { + description: 'Greet a person by name', + input: z.object({ + name: z.string().describe('The name of the person to greet'), + }), + handler: ({ input }) => `Hello, ${input.name}!`, +}); + +app.run({ transport: 'http', port: 8000, reload: true }); +``` + +```bash +bun run server.ts +``` + +Test it works: + +```bash +curl -X POST http://localhost:8000/mcp \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' +``` + +When Claude (or another AI) calls your `greet` tool with `{ name: "Alex" }`, your handler runs and returns the greeting. + + + **Transport auto-detection:** stdio for Claude Desktop, HTTP when you specify host/port. + The `reload: true` option enables hot reload during development. + + +## API Reference + +### `MCPApp` + +**`arcade-mcp.MCPApp`** + +A type-safe, developer-friendly interface for building MCP servers. Handles tool registration, configuration, and transport. + +### Constructor + +```typescript +new MCPApp(options?: MCPAppOptions) +``` + +```typescript +interface MCPAppOptions { + /** Server name shown to AI clients */ + name?: string; + + /** Server version */ + version?: string; + + /** Human-readable title */ + title?: string; + + /** Usage instructions for AI clients */ + instructions?: string; + + /** Logging level */ + logLevel?: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR'; + + /** Transport type: 'stdio' for Claude Desktop, 'http' for web */ + transport?: 'stdio' | 'http'; + + /** HTTP host (auto-selects HTTP transport if set) */ + host?: string; + + /** HTTP port (auto-selects HTTP transport if set) */ + port?: number; + + /** Hot reload on file changes (development only) */ + reload?: boolean; +} +``` + +**Defaults:** + +| Option | Default | Notes | +|--------|---------|-------| +| `name` | `'ArcadeMCP'` | | +| `version` | `'1.0.0'` | | +| `logLevel` | `'INFO'` | | +| `transport` | `'stdio'` | Auto-switches to `'http'` if host/port set | +| `host` | `'127.0.0.1'` | | +| `port` | `8000` | | + +### `app.tool()` + +Register a tool that AI clients can call. + +```typescript +app.tool(name: string, options: ToolOptions): void +``` + +```typescript +interface ToolOptions< + TInput, + TSecrets extends string = string, + TAuth extends AuthProvider | undefined = undefined +> { + /** Tool description for AI clients */ + description?: string; + + /** Zod schema for input validation */ + input: z.ZodType; + + /** Tool handler function — authorization is non-optional when requiresAuth is set */ + handler: ( + context: ToolContext + ) => unknown | Promise; + + /** OAuth provider for user authentication */ + requiresAuth?: TAuth; + + /** Secret keys required by this tool (use `as const` for type safety) */ + requiresSecrets?: readonly TSecrets[]; + + /** Metadata keys required from the client */ + requiresMetadata?: string[]; + + /** Error adapters for translating upstream errors (e.g., Slack, Google APIs) */ + adapters?: ErrorAdapter[]; +} +``` + +**Handler context (destructure what you need):** + +```typescript +handler: ({ input, authorization, getSecret, getMetadata }) => { + // input — Validated input matching your Zod schema + // authorization — OAuth token/provider (TypeScript narrows to non-optional when requiresAuth is set) + // getSecret — Retrieve secrets (type-safe with `as const`) + // getMetadata — Retrieve metadata from the client +} +``` + +**Return values are auto-wrapped:** + +| Return type | Becomes | +|-------------|---------| +| `string` | `{ content: [{ type: 'text', text: '...' }] }` | +| `object` | `{ content: [{ type: 'text', text: JSON.stringify(...) }] }` | +| `{ content: [...] }` | Passed through unchanged | + +### `app.run()` + +Start the server. + +```typescript +app.run(options?: RunOptions): Promise +``` + +```typescript +interface RunOptions { + transport?: 'stdio' | 'http'; + host?: string; + port?: number; + reload?: boolean; +} +``` + +### `app.addToolsFromModule()` + +Add all tools from a module at once. + + + **`app.tool()` vs `tool()`:** + - `app.tool('name', { ... })` — Register a tool directly + - `tool({ ... })` — Create a tool for `addToolsFromModule()` or runtime `app.tools.add()` + + +Use the standalone `tool()` function to define exportable tools, then `addToolsFromModule()` discovers and registers them: + +```typescript +// tools/math.ts +import { tool } from 'arcade-mcp'; +import { z } from 'zod'; + +export const add = tool({ + description: 'Add two numbers', + input: z.object({ a: z.number(), b: z.number() }), + handler: ({ input }) => input.a + input.b, +}); + +export const multiply = tool({ + description: 'Multiply two numbers', + input: z.object({ a: z.number(), b: z.number() }), + handler: ({ input }) => input.a * input.b, +}); +``` + +```typescript +// server.ts +import * as mathTools from './tools/math'; + +app.addToolsFromModule(mathTools); +``` + +Tool names are inferred from export names (`add`, `multiply`). Override with explicit `name` if needed: + +```typescript +export const calculator = tool({ + name: 'basic-calculator', // explicit name overrides 'calculator' + description: 'Basic arithmetic', + input: z.object({ + operation: z.enum(['add', 'subtract', 'multiply', 'divide']), + a: z.number(), + b: z.number(), + }), + handler: ({ input }) => { + const { operation, a, b } = input; + switch (operation) { + case 'add': return a + b; + case 'subtract': return a - b; + case 'multiply': return a * b; + case 'divide': return a / b; + } + }, +}); +``` + +### Runtime APIs + +After the server starts, you can modify tools, prompts, and resources at runtime: + +```typescript +import { tool } from 'arcade-mcp'; +import { z } from 'zod'; + +// Create and add a tool at runtime +const dynamicTool = tool({ + name: 'dynamic-tool', // name required for runtime registration + description: 'Added at runtime', + input: z.object({ value: z.string() }), + handler: ({ input }) => `Got: ${input.value}`, +}); + +await app.tools.add(dynamicTool); + +// Remove a tool +await app.tools.remove('dynamic-tool'); + +// List all tools +const tools = await app.tools.list(); +``` + +```typescript +// Add a prompt (reusable message templates for AI clients) +await app.prompts.add(prompt, handler); + +// Add a resource (files, data, or content the AI can read) +await app.resources.add(resource); +``` + + + **MCP primitives:** Tools are functions AI can call. Prompts are reusable message templates (like "summarize this"). Resources are data the AI can read (files, database records, API responses). + + +See the [Server reference](/references/mcp/typescript/server) for full prompts and resources API. + +## Examples + +### Simple Tool + +```typescript +import { MCPApp } from 'arcade-mcp'; +import { z } from 'zod'; + +const app = new MCPApp({ name: 'example-server' }); + +app.tool('echo', { + description: 'Echo the text back', + input: z.object({ + text: z.string().describe('The text to echo'), + }), + handler: ({ input }) => `Echo: ${input.text}`, +}); + +app.run({ transport: 'http', host: '0.0.0.0', port: 8000 }); +``` + +### With OAuth and Secrets + +Use `requiresAuth` when your tool needs to act on behalf of a user (e.g., access their Google account). Use `requiresSecrets` for API keys your server needs. + +```typescript +import { MCPApp } from 'arcade-mcp'; +import { Google } from 'arcade-mcp/auth'; +import { z } from 'zod'; + +const app = new MCPApp({ name: 'my-server' }); + +app.tool('getProfile', { + description: 'Get user profile from Google', + input: z.object({ + userId: z.string(), + }), + requiresAuth: Google({ scopes: ['profile'] }), + requiresSecrets: ['API_KEY'] as const, // as const enables type-safe getSecret() + handler: async ({ input, authorization, getSecret }) => { + const token = authorization.token; // User's OAuth token + const apiKey = getSecret('API_KEY'); // ✅ Type-safe, autocomplete works + // getSecret('OTHER'); // ❌ TypeScript error: not in requiresSecrets + return { userId: input.userId }; + }, +}); + +app.run(); +``` + + + **Auth & Secrets:** + - **OAuth:** Arcade coordinates user consent. Tokens are passed to your handler automatically. + - **Secrets:** Loaded from environment variables. Never log or return them. + + +### With Required Metadata + +Request metadata from the client: + +```typescript +app.tool('contextAware', { + description: 'A tool that uses client context', + input: z.object({ query: z.string() }), + requiresMetadata: ['sessionId', 'userAgent'], + handler: ({ input, getMetadata }) => { + const sessionId = getMetadata('sessionId'); // string | undefined + return `Processing ${input.query} for session ${sessionId}`; + }, +}); +``` + +### Schema Metadata for AI Clients + +Use `.describe()` for simple descriptions. Use `.meta()` for richer metadata: + +```typescript +app.tool('search', { + description: 'Search the knowledge base', + input: z.object({ + query: z.string().describe('Search query'), + limit: z.number() + .int() + .min(1) + .max(100) + .default(10) + .meta({ + title: 'Result limit', + examples: [10, 25, 50], + }), + }), + handler: ({ input }) => searchKnowledgeBase(input.query, input.limit), +}); +``` + +Both `.describe()` and `.meta()` are preserved in the JSON Schema that AI clients receive. + +### Async Tool with Error Handling + +```typescript +import { MCPApp, NotFoundError } from 'arcade-mcp'; +import { z } from 'zod'; + +const app = new MCPApp({ name: 'api-server' }); + +app.tool('getUser', { + description: 'Fetch a user by ID', + input: z.object({ + id: z.string().uuid().describe('User ID'), + }), + handler: async ({ input }) => { + const user = await db.users.find(input.id); + + if (!user) { + throw new NotFoundError(`User ${input.id} not found`); + } + + return user; + }, +}); +``` + +### Full Example with All Features + +```typescript +import { MCPApp } from 'arcade-mcp'; +import { Google } from 'arcade-mcp/auth'; +import { z } from 'zod'; + +const app = new MCPApp({ + name: 'full-example', + version: '1.0.0', + instructions: 'Use these tools to manage documents.', + logLevel: 'DEBUG', +}); + +// Simple tool +app.tool('ping', { + description: 'Health check', + input: z.object({}), + handler: () => 'pong', +}); + +// Complex tool with auth and secrets +app.tool('createDocument', { + description: 'Create a new document in Google Drive', + input: z.object({ + title: z.string().min(1).describe('Document title'), + content: z.string().describe('Document content'), + folder: z.string().optional().describe('Parent folder ID'), + }), + requiresAuth: Google({ scopes: ['drive.file'] }), + requiresSecrets: ['DRIVE_API_KEY'] as const, + handler: async ({ input, authorization, getSecret }) => { + const response = await createDriveDocument({ + token: authorization.token, + apiKey: getSecret('DRIVE_API_KEY'), + ...input, + }); + return { documentId: response.id, url: response.webViewLink }; + }, +}); + +// Start server +if (import.meta.main) { + app.run({ transport: 'http', host: '0.0.0.0', port: 8000 }); +} +``` + +```bash +bun run server.ts +``` + diff --git a/app/en/references/mcp/typescript/server/page.mdx b/app/en/references/mcp/typescript/server/page.mdx new file mode 100644 index 000000000..59d4e077a --- /dev/null +++ b/app/en/references/mcp/typescript/server/page.mdx @@ -0,0 +1,273 @@ +--- +title: "Server - Arcade MCP TypeScript Reference" +description: "Reference documentation for the low-level Arcade MCP Server class" +--- + +import { Callout } from "nextra/components"; + +# Server + + + Most users should use `MCPApp` from the [Overview](/references/mcp/typescript/overview). + Use `MCPServer` only when you need low-level control over the server lifecycle, + custom transports, or advanced middleware composition. + + +## `MCPServer` + +**`arcade-mcp.MCPServer`** + +Low-level MCP server with middleware and context support. + +This server provides: + +- Middleware chain for request/response processing +- Context injection for tools +- Component managers for tools, resources, and prompts +- Direct control over server lifecycle +- Bidirectional communication with MCP clients + +### Constructor + +```typescript +new MCPServer(options: MCPServerOptions) +``` + +```typescript +interface MCPServerOptions { + /** Collection of tools to serve */ + catalog: ToolCatalog; + + /** Server name */ + name?: string; + + /** Server version */ + version?: string; + + /** Human-readable title */ + title?: string; + + /** Usage instructions for AI clients */ + instructions?: string; + + /** Configuration object */ + settings?: MCPSettings; + + /** Request/response middleware */ + middleware?: Middleware[]; + + /** Lifecycle manager for startup/shutdown */ + lifespan?: LifespanManager; + + /** Disable auth (development only, never in production) */ + authDisabled?: boolean; + + /** Arcade API key */ + arcadeApiKey?: string; + + /** Arcade API URL */ + arcadeApiUrl?: string; +} +``` + +**Defaults:** + +| Option | Default | Notes | +|--------|---------|-------| +| `name` | `'ArcadeMCP'` | | +| `version` | `'1.0.0'` | | +| `arcadeApiKey` | `Bun.env.ARCADE_API_KEY` | | +| `arcadeApiUrl` | `'https://api.arcade.dev'` | | +| `authDisabled` | `false` | ⚠️ Never enable in production | + +### Methods + +#### `start()` + +```typescript +async start(): Promise +``` + +Initialize the server. Call before running a transport. + +#### `stop()` + +```typescript +async stop(): Promise +``` + +Gracefully shut down the server. Waits for in-flight requests to complete. + +#### `handleMessage()` + +```typescript +async handleMessage( + message: MCPMessage, + session?: ServerSession +): Promise +``` + +Handle a single MCP message. Used internally by transports. + +#### `runConnection()` + +```typescript +async runConnection( + readStream: ReadableStream, + writeStream: WritableStream, + initOptions?: Record +): Promise +``` + +Run a single MCP connection. Used for custom transport implementations. + +### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `tools` | `ToolManager` | Runtime tool operations (add, remove, list) | +| `resources` | `ResourceManager` | Runtime resource operations | +| `prompts` | `PromptManager` | Runtime prompt operations | + +### Examples + +#### stdio Transport + +```typescript +import { MCPServer, ToolCatalog, StdioTransport } from 'arcade-mcp'; + +const catalog = new ToolCatalog(); +// catalog.add(...) to register tools + +const server = new MCPServer({ + catalog, + name: 'example', + version: '1.0.0', +}); + +await server.start(); +try { + const transport = new StdioTransport(); + await transport.run(server); +} finally { + await server.stop(); +} +``` + +#### HTTP Transport + +```typescript +import { MCPServer, ToolCatalog, HTTPTransport } from 'arcade-mcp'; + +const catalog = new ToolCatalog(); + +const server = new MCPServer({ + catalog, + name: 'example', + version: '1.0.0', +}); + +await server.start(); +try { + const transport = new HTTPTransport({ host: '0.0.0.0', port: 8000 }); + await transport.run(server); +} finally { + await server.stop(); +} +``` + +#### With Middleware + +```typescript +import { + MCPServer, + ToolCatalog, + LoggingMiddleware, + ErrorHandlingMiddleware, +} from 'arcade-mcp'; + +const server = new MCPServer({ + catalog: new ToolCatalog(), + middleware: [ + new ErrorHandlingMiddleware({ maskErrorDetails: true }), + new LoggingMiddleware({ logLevel: 'DEBUG' }), + ], +}); +``` + + + Middleware runs in order. `ErrorHandlingMiddleware` first means it catches errors + from all subsequent middleware. + + +#### With Lifespan Manager + +```typescript +import { MCPServer, ToolCatalog, createLifespan } from 'arcade-mcp'; + +const lifespan = createLifespan({ + async onStart(server) { + await db.connect(); + }, + async onStop(server) { + await db.disconnect(); + }, +}); + +const server = new MCPServer({ + catalog: new ToolCatalog(), + lifespan, +}); +``` + +#### Runtime Tool Management + +Use `tool()` to create tool objects for runtime registration: + +```typescript +import { MCPServer, ToolCatalog, tool } from 'arcade-mcp'; +import { z } from 'zod'; + +const server = new MCPServer({ catalog: new ToolCatalog() }); + +await server.start(); + +// Create a tool object +const dynamicTool = tool({ + name: 'dynamic-tool', // required for runtime registration + description: 'Added at runtime', + input: z.object({ value: z.string() }), + handler: ({ input }) => `Got: ${input.value}`, +}); + +// Add it to the running server +await server.tools.add(dynamicTool); + +// List all tools +const tools = await server.tools.list(); +console.error(`Server has ${tools.length} tools`); + +// Remove a tool +await server.tools.remove('dynamic-tool'); +``` + +`tool()` takes the same options as `app.tool()` and returns a `MaterializedTool` object suitable for the runtime APIs. + +#### Custom Transport + +For WebSocket or other custom transports, implement a class that calls `server.runConnection()`: + +```typescript +import { MCPServer, ToolCatalog } from 'arcade-mcp'; + +// Pseudocode - implement stream adapters for your transport +class CustomTransport { + async run(server: MCPServer) { + // Accept connections, create readable/writable streams + const { readable, writable } = await acceptConnection(); + await server.runConnection(readable, writable); + } +} +``` + +See the SDK source for `StdioTransport` and `HTTPTransport` as reference implementations. diff --git a/app/en/references/mcp/typescript/settings/page.mdx b/app/en/references/mcp/typescript/settings/page.mdx new file mode 100644 index 000000000..cea1fe956 --- /dev/null +++ b/app/en/references/mcp/typescript/settings/page.mdx @@ -0,0 +1,335 @@ +--- +title: "Settings - Arcade MCP TypeScript Reference" +description: "Configuration options for MCP servers" +--- + +import { Callout } from "nextra/components"; + +# Settings + +Configure your MCP server via constructor options or environment variables. + + + For most users, pass options directly to `MCPApp`. Use `MCPSettings` only when you need + to share configuration across multiple servers or load from a config file. + + +## Quick Start + +```typescript +import { MCPApp } from 'arcade-mcp'; + +const app = new MCPApp({ + name: 'my-server', + version: '1.0.0', + logLevel: 'DEBUG', +}); +``` + +## Environment Variables + +The SDK reads these environment variables as defaults. Constructor options take precedence. + +| Variable | Maps to | Example | +|----------|---------|---------| +| `MCP_SERVER_NAME` | `name` | `"My Server"` | +| `MCP_SERVER_VERSION` | `version` | `"1.0.0"` | +| `MCP_LOG_LEVEL` | `logLevel` | `DEBUG`, `INFO`, `WARNING`, `ERROR` | +| `MCP_DEBUG` | `debug` | `true`, `false` | +| `MCP_HTTP_HOST` | `host` | `0.0.0.0` | +| `MCP_HTTP_PORT` | `port` | `8080` | +| `ARCADE_API_KEY` | `arcadeApiKey` | `arc_...` | +| `ARCADE_API_URL` | `arcadeApiUrl` | `https://api.arcade.dev` | + +Access environment variables with `Bun.env`: + +```typescript +const debug = Bun.env.MCP_DEBUG === 'true'; +const port = Number(Bun.env.MCP_HTTP_PORT) || 8000; +``` + +## MCPSettings + +For advanced configuration, use the `MCPSettings` class: + +```typescript +import { MCPSettings } from 'arcade-mcp'; + +// Load from environment variables +const settings = MCPSettings.fromEnv(); +``` + +Or construct with explicit options: + +```typescript +import { MCPSettings } from 'arcade-mcp'; + +const settings = new MCPSettings({ + debug: true, + server: { + name: 'My MCP Server', + version: '1.0.0', + title: 'My Tools', + instructions: 'Use these tools to manage documents.', + }, + middleware: { + enableLogging: true, + logLevel: 'DEBUG', + maskErrorDetails: false, + }, + transport: { + type: 'http', + httpHost: '0.0.0.0', + httpPort: 8000, + }, + arcade: { + apiKey: Bun.env.ARCADE_API_KEY, + apiUrl: 'https://api.arcade.dev', + }, + notifications: { + enabled: true, + }, + toolEnvironment: { + // Additional secrets available to tools + DATABASE_URL: Bun.env.DATABASE_URL, + REDIS_URL: Bun.env.REDIS_URL, + }, +}); +``` + +### Using with MCPServer + +```typescript +import { MCPServer, MCPSettings, ToolCatalog } from 'arcade-mcp'; + +const settings = MCPSettings.fromEnv(); + +const server = new MCPServer({ + catalog: new ToolCatalog(), + settings, +}); +``` + +### Getting Tool Secrets + +```typescript +const settings = MCPSettings.fromEnv(); + +// Get all secrets available to tools +const secrets = settings.toolSecrets(); +// => { DATABASE_URL: '...', REDIS_URL: '...', ... } +``` + +## Settings Reference + +### MCPSettings + +```typescript +interface MCPSettings { + /** Enable debug mode (verbose logging, stack traces) */ + debug?: boolean; + + /** Server configuration */ + server?: ServerSettings; + + /** Middleware configuration */ + middleware?: MiddlewareSettings; + + /** Transport configuration */ + transport?: TransportSettings; + + /** Arcade API configuration */ + arcade?: ArcadeSettings; + + /** Notification settings */ + notifications?: NotificationSettings; + + /** Tool environment / secrets */ + toolEnvironment?: ToolEnvironmentSettings; +} +``` + +### ServerSettings + +```typescript +interface ServerSettings { + /** Server name shown to AI clients */ + name: string; + + /** Server version */ + version: string; + + /** Human-readable title */ + title?: string; + + /** Usage instructions for AI clients */ + instructions?: string; +} +``` + +### MiddlewareSettings + +```typescript +interface MiddlewareSettings { + /** Enable request logging */ + enableLogging: boolean; + + /** Log level */ + logLevel: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR'; + + /** Hide stack traces in error responses (always true in production) */ + maskErrorDetails: boolean; +} +``` + +### TransportSettings + +```typescript +interface TransportSettings { + /** Transport type */ + type: 'stdio' | 'http'; + + /** HTTP host (only for http transport) */ + httpHost: string; + + /** HTTP port (only for http transport) */ + httpPort: number; +} +``` + +### ArcadeSettings + +```typescript +interface ArcadeSettings { + /** Arcade API key for auth and tool hosting */ + apiKey?: string; + + /** Arcade API URL (defaults to production) */ + apiUrl?: string; +} +``` + +### NotificationSettings + +```typescript +interface NotificationSettings { + /** Enable notifications to clients */ + enabled: boolean; + + /** Notification timeout in milliseconds */ + timeoutMs?: number; +} +``` + +### ToolEnvironmentSettings + +Environment variables available as secrets to tools: + +```typescript +interface ToolEnvironmentSettings { + /** Key-value pairs of secrets */ + [key: string]: string | undefined; +} +``` + + + Any environment variable not prefixed with `MCP_` or `ARCADE_` is automatically + available as a tool secret via `getSecret()`. + + +## Configuration Precedence + +Settings are resolved in this order (first wins): + +1. Constructor options +2. `MCPSettings` object +3. Environment variables +4. Default values + +## Configuration Patterns + +### Development vs Production + +```typescript +import { MCPApp } from 'arcade-mcp'; + +const isDev = Bun.env.NODE_ENV !== 'production'; + +const app = new MCPApp({ + name: 'my-server', + logLevel: isDev ? 'DEBUG' : 'INFO', + // In development, show full error details + // ErrorHandlingMiddleware reads this internally +}); + +app.run({ + host: isDev ? '127.0.0.1' : '0.0.0.0', + port: Number(Bun.env.PORT) || 8000, + reload: isDev, +}); +``` + +### Loading from Config File + +```typescript +import { MCPSettings } from 'arcade-mcp'; + +// Load from JSON file +const configFile = Bun.file('./config.json'); +const config = await configFile.json(); + +const settings = new MCPSettings(config); +``` + +### Sharing Configuration + +```typescript +import { MCPSettings, MCPServer, ToolCatalog } from 'arcade-mcp'; + +// Shared settings across multiple servers +const baseSettings = MCPSettings.fromEnv(); + +const serverA = new MCPServer({ + catalog: catalogA, + settings: baseSettings, + name: 'server-a', +}); + +const serverB = new MCPServer({ + catalog: catalogB, + settings: baseSettings, + name: 'server-b', +}); +``` + +### Validating Configuration at Startup + +```typescript +import { MCPApp, FatalToolError } from 'arcade-mcp'; + +const requiredEnvVars = ['ARCADE_API_KEY', 'DATABASE_URL']; + +for (const envVar of requiredEnvVars) { + if (!Bun.env[envVar]) { + throw new FatalToolError(`Missing required environment variable: ${envVar}`); + } +} + +const app = new MCPApp({ name: 'my-server' }); +``` + +## Type-Safe Configuration + +Use TypeScript's `satisfies` for type-checked config objects: + +```typescript +import type { MCPAppOptions } from 'arcade-mcp'; + +const config = { + name: 'my-server', + version: '1.0.0', + logLevel: 'DEBUG', +} satisfies MCPAppOptions; + +const app = new MCPApp(config); +``` diff --git a/app/en/references/mcp/typescript/transports/page.mdx b/app/en/references/mcp/typescript/transports/page.mdx new file mode 100644 index 000000000..9c59a2a68 --- /dev/null +++ b/app/en/references/mcp/typescript/transports/page.mdx @@ -0,0 +1,324 @@ +--- +title: "Transport Modes - Arcade MCP TypeScript Reference" +description: "stdio and HTTP transport modes for MCP servers" +--- + +import { Callout } from "nextra/components"; + +# Transport Modes + +Transports define how your MCP server communicates with AI clients. Choose based on your deployment: + +| Transport | Use case | +|-----------|----------| +| **stdio** | Claude Desktop, VS Code, local tools | +| **HTTP** | Web deployments, cloud hosting, multiple clients | + + + stdio is the default. Use `transport: 'http'` for web deployments. + + +## stdio Transport + +Communicates via standard input/output. Claude Desktop spawns your server as a subprocess. + +### When to Use + +- Claude Desktop integration +- VS Code extensions +- Command-line tools +- Local development + +### Usage + +```typescript +import { MCPApp } from 'arcade-mcp'; +import { z } from 'zod'; + +const app = new MCPApp({ name: 'my-server' }); + +app.tool('ping', { + input: z.object({}), + handler: () => 'pong', +}); + +app.run({ transport: 'stdio' }); +``` + +```bash +bun run server.ts +``` + +### Claude Desktop Configuration + +**Recommended: Use the Arcade CLI** + +```bash +arcade configure claude --host local +``` + +This configures Claude Desktop to run your server as a stdio subprocess. + +**Manual configuration:** + +Edit Claude Desktop's config file: + +| Platform | Path | +|----------|------| +| **macOS** | `~/Library/Application Support/Claude/claude_desktop_config.json` | +| **Windows** | `%APPDATA%\Claude\claude_desktop_config.json` | +| **Linux** | `~/.config/Claude/claude_desktop_config.json` | + +```json +{ + "mcpServers": { + "my-tools": { + "command": "bun", + "args": ["run", "/path/to/server.ts"], + "cwd": "/path/to/your/tools" + } + } +} +``` + + + With stdio, all `stdout` is protocol data. Use `console.error()` for logging, never `console.log()`. + + +## HTTP Transport + +REST API with Server-Sent Events (SSE) for streaming. Built on Elysia. + +### When to Use + +- Web applications +- Cloud deployments (Railway, Fly.io, etc.) +- Multiple concurrent clients +- Behind load balancers + +### Usage + +```typescript +import { MCPApp } from 'arcade-mcp'; +import { z } from 'zod'; + +const app = new MCPApp({ name: 'my-server' }); + +app.tool('ping', { + input: z.object({}), + handler: () => 'pong', +}); + +app.run({ transport: 'http', host: '0.0.0.0', port: 8080 }); +``` + +### Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/worker/health` | GET | Health check (returns 200 OK) | +| `/mcp` | GET | SSE stream for server-initiated messages | +| `/mcp` | POST | Send JSON-RPC message | +| `/mcp` | DELETE | Terminate session (when using session IDs) | + + + **Response modes:** POST requests may receive either `application/json` (single response) or + `text/event-stream` (SSE stream). The SDK handles both automatically. Clients should include + `Accept: application/json, text/event-stream` in requests. + + +### API Documentation (Optional) + +Add Elysia's OpenAPI plugin for interactive API docs: + +```bash +bun add @elysiajs/openapi +``` + +```typescript +import { openapi } from '@elysiajs/openapi'; + +// Add before app.run() +app.elysia.use(openapi({ path: '/docs' })); +``` + +Scalar UI will be available at `/docs`. See the [Elysia OpenAPI docs](https://elysiajs.com/plugins/openapi). + +### Development Mode + +Enable hot reload for faster development: + +```typescript +app.run({ + transport: 'http', + host: '127.0.0.1', + port: 8000, + reload: true, +}); +``` + +When `reload: true`, the server restarts automatically when you save files. + +### TLS / HTTPS + +For production, use a reverse proxy (nginx, Caddy) for TLS termination. This is the recommended approach: + +```nginx +# nginx example +server { + listen 443 ssl; + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://127.0.0.1:8000; + } +} +``` + +For advanced TLS configuration, see [Server](./server). + +### Docker + +```dockerfile +FROM oven/bun:1 +WORKDIR /app +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile +COPY . . +EXPOSE 8000 +CMD ["bun", "run", "server.ts"] +``` + +```bash +docker build -t my-mcp-server . +docker run -p 8000:8000 -e ARCADE_API_KEY=arc_... my-mcp-server +``` + +## Environment Variables + +```bash +MCP_SERVER_NAME="My MCP Server" +MCP_SERVER_VERSION="1.0.0" +MCP_LOG_LEVEL=DEBUG +MCP_HTTP_HOST=0.0.0.0 +MCP_HTTP_PORT=8080 +``` + +Access with `Bun.env`: + +```typescript +const port = Number(Bun.env.MCP_HTTP_PORT) || 8000; +``` + +## Security + +### stdio + +- Runs in the security context of the parent process +- No network exposure +- Safe for local use + +### HTTP + + + HTTP transport exposes network endpoints. For production: + + +| Requirement | Solution | +|-------------|----------| +| **Use HTTPS** | Configure TLS or use a reverse proxy | +| **Enable authentication** | Use Arcade auth or custom middleware | +| **Rate limiting** | Add rate limiting middleware | +| **Firewall** | Never bind to `0.0.0.0` without firewall rules | + +### CORS + +For browser clients, add the Elysia CORS plugin: + +```bash +bun add @elysiajs/cors +``` + +```typescript +import { MCPApp } from 'arcade-mcp'; +import { cors } from '@elysiajs/cors'; + +const app = new MCPApp({ name: 'my-server' }); + +app.elysia.use(cors({ + origin: ['https://myapp.com'], + credentials: true, +})); +``` + +See the [Elysia CORS docs](https://elysiajs.com/plugins/cors) for all options. + +## Advanced Transport Features + +### Custom Middleware (HTTP) + +Add custom middleware to HTTP transports via the underlying Elysia instance: + +```typescript +import { MCPApp } from 'arcade-mcp'; + +const app = new MCPApp({ name: 'my-server' }); + +// Add custom response headers +app.elysia.onAfterHandle(({ set }) => { + set.headers['X-Custom-Header'] = 'value'; +}); + +// Log all requests +app.elysia.onRequest(({ request }) => { + console.log(`${request.method} ${request.url}`); +}); +``` + +### Transport Events + +Listen to transport lifecycle events: + +```typescript +app.elysia.onStart(({ server }) => { + console.log(`Server running at ${server?.url}`); +}); + +app.elysia.onStop(() => { + console.log('Server shutting down...'); +}); +``` + +## Transport Selection + +Select transport based on environment or CLI args: + +```typescript +import { MCPApp } from 'arcade-mcp'; +import { z } from 'zod'; + +const app = new MCPApp({ name: 'my-server' }); + +app.tool('ping', { + input: z.object({}), + handler: () => 'pong', +}); + +// Check command line args +const transport = Bun.argv[2] === 'stdio' ? 'stdio' : 'http'; + +app.run({ + transport, + host: '0.0.0.0', + port: 8000, +}); +``` + +```bash +# Run with HTTP +bun run server.ts + +# Run with stdio (for Claude Desktop) +bun run server.ts stdio +``` diff --git a/app/en/references/mcp/typescript/types/page.mdx b/app/en/references/mcp/typescript/types/page.mdx new file mode 100644 index 000000000..1d0d21248 --- /dev/null +++ b/app/en/references/mcp/typescript/types/page.mdx @@ -0,0 +1,484 @@ +--- +title: "Types - Arcade MCP TypeScript Reference" +description: "Core types and interfaces for the MCP protocol" +--- + +import { Callout } from "nextra/components"; + +# Types + +Core TypeScript types used in the SDK. + + + Most users only need `ToolContext`. The other types are for advanced use cases + like custom transports or protocol-level work. + + +## Common Types + +### `ToolContext` + +Context passed to your tool handlers. Destructure what you need: + +```typescript +interface AuthInfo { + token: string; + provider: string; +} + +interface ToolContext< + TInput, + TSecrets extends string = string, + TAuth extends boolean = false +> { + /** Validated input matching your Zod schema */ + input: TInput; + + /** OAuth token and provider — non-optional when requiresAuth is set */ + authorization: TAuth extends true ? AuthInfo : AuthInfo | undefined; + + /** + * Get a secret value. Only keys declared in requiresSecrets are allowed. + * Validated at startup — guaranteed to exist at runtime. + */ + getSecret(key: TSecrets): string; + + /** Metadata from the client (if requiresMetadata was set) */ + metadata: Record; +} +``` + +**Example — Type-safe authorization:** + +```typescript +import { z } from 'zod'; +import { Google } from 'arcade-mcp/auth'; + +// Without requiresAuth: authorization is optional +app.tool('publicTool', { + input: z.object({ query: z.string() }), + handler: ({ authorization }) => { + authorization?.token; // Must use optional chaining + }, +}); + +// With requiresAuth: authorization is guaranteed +app.tool('privateTool', { + input: z.object({ query: z.string() }), + requiresAuth: Google({ scopes: ['profile'] }), + handler: ({ authorization }) => { + authorization.token; // ✅ No optional chaining needed + }, +}); +``` + +**Example — Type-safe secrets:** + +```typescript +app.tool('search', { + input: z.object({ query: z.string() }), + requiresSecrets: ['API_KEY'] as const, + handler: ({ getSecret }) => { + getSecret('API_KEY'); // ✅ Autocomplete works + // getSecret('OTHER'); // ❌ TypeScript error + }, +}); +``` + +Secrets are validated at startup. Missing secrets fail fast with a clear error. + +### Type Inference with Zod + +The SDK fully leverages Zod's type inference. Your handler receives typed input automatically: + +```typescript +import { z } from 'zod'; + +const searchInput = z.object({ + query: z.string(), + limit: z.number().int().min(1).max(100).default(10), + filters: z.object({ + category: z.enum(['all', 'docs', 'code']).optional(), + after: z.coerce.date().optional(), // Coerces ISO strings to Date + }).optional(), +}); + +app.tool('search', { + input: searchInput, + handler: ({ input }) => { + // TypeScript knows: + // - input.query is string + // - input.limit is number (default applied) + // - input.filters?.category is 'all' | 'docs' | 'code' | undefined + // - input.filters?.after is Date | undefined + return search(input); + }, +}); +``` + +## Tool Response Types + +Handlers can return any value. The SDK auto-wraps: + +| Return type | Becomes | +|-------------|---------| +| `string` | `{ content: [{ type: 'text', text }] }` | +| `object` | `{ content: [{ type: 'text', text: JSON.stringify(obj) }] }` | +| `{ content: [...] }` | Passed through unchanged | + +### `ContentItem` + +For full control over responses, return content items directly: + +```typescript +type ContentItem = TextContent | ImageContent | ResourceContent; + +interface TextContent { + type: 'text'; + text: string; +} + +interface ImageContent { + type: 'image'; + data: string; // Base64 encoded + mimeType: string; // e.g., 'image/png' +} + +interface ResourceContent { + type: 'resource'; + uri: string; + mimeType?: string; + text?: string; + blob?: string; // Base64 encoded +} +``` + +**Example:** + +```typescript +import { z } from 'zod'; + +app.tool('screenshot', { + input: z.object({}), + handler: async () => { + const screenshot = await captureScreen(); + return { + content: [{ + type: 'image', + data: screenshot.toBase64(), + mimeType: 'image/png', + }], + }; + }, +}); +``` + +### `CallToolResult` + +The complete tool result structure: + +```typescript +interface CallToolResult { + /** Content items to return to the client */ + content: ContentItem[]; + + /** Optional structured data (for programmatic access) */ + structuredContent?: Record; + + /** Whether this result represents an error */ + isError?: boolean; +} +``` + +## Schema Types + +The SDK uses Zod 4 for input validation. + +### Converting Schemas to JSON Schema + +Use `z.toJSONSchema()` to convert Zod schemas for AI clients: + +```typescript +import { z } from 'zod'; + +const schema = z.object({ + firstName: z.string().describe('Your first name'), + lastName: z.string().meta({ title: 'last_name' }), + age: z.number().meta({ examples: [12, 99] }), +}); + +z.toJSONSchema(schema); +// => { +// type: 'object', +// properties: { +// firstName: { type: 'string', description: 'Your first name' }, +// lastName: { type: 'string', title: 'last_name' }, +// age: { type: 'number', examples: [12, 99] } +// }, +// required: ['firstName', 'lastName', 'age'] +// } +``` + +### Adding Metadata + +Use `.describe()` for simple descriptions or `.meta()` for richer metadata: + +```typescript +// Simple description (Zod 3 compatible) +z.string().describe('User email address'); + +// Rich metadata (Zod 4) +z.string().meta({ + id: 'email_address', + title: 'Email', + description: 'User email address', + examples: ['user@example.com'], +}); + +// .describe() is shorthand for .meta({ description: ... }) +z.string().describe('An email'); +// equivalent to: +z.string().meta({ description: 'An email' }); +``` + +Both are preserved in JSON Schema output. + +### Extracting TypeScript Types + +Use `z.infer` to extract TypeScript types from schemas: + +```typescript +import { z } from 'zod'; + +const userSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + email: z.string().email(), + role: z.enum(['admin', 'user', 'guest']), +}); + +// Extract the type +type User = z.infer; + +// Use in your code +function processUser(user: User) { + // user.id is string + // user.role is 'admin' | 'user' | 'guest' +} +``` + +## Protocol Types + +These are low-level types for custom transports or debugging. + +### `MCPMessage` + +Base type for all MCP protocol messages: + +```typescript +type MCPMessage = JSONRPCRequest | JSONRPCResponse | JSONRPCNotification; + +interface JSONRPCRequest { + jsonrpc: '2.0'; + id: string | number; + method: string; + params?: Record; +} + +interface JSONRPCResponse { + jsonrpc: '2.0'; + id: string | number; + result?: unknown; + error?: { + code: number; + message: string; + data?: unknown; + }; +} + +interface JSONRPCNotification { + jsonrpc: '2.0'; + method: string; + params?: Record; +} +``` + +### `ServerSession` + +```typescript +interface ServerSession { + /** Unique session identifier */ + id: string; + + /** Client information (if provided during initialization) */ + clientInfo?: { + name: string; + version: string; + }; + + /** Client capabilities (tools, resources, prompts support) */ + capabilities?: Record; +} +``` + +### `SessionMessage` + +```typescript +interface SessionMessage { + message: MCPMessage; + session: ServerSession; +} +``` + +## Auth Types + +### `AuthProvider` + +```typescript +interface AuthProvider { + /** Provider identifier (e.g., 'google', 'github') */ + provider: string; + + /** OAuth scopes required */ + scopes: readonly string[]; +} +``` + +### Creating Auth Providers + +```typescript +import { Google, GitHub, Slack } from 'arcade-mcp/auth'; + +// Google with specific scopes +Google({ scopes: ['profile', 'email'] }) + +// GitHub with repo access +GitHub({ scopes: ['repo', 'user'] }) + +// Slack +Slack({ scopes: ['chat:write', 'users:read'] }) +``` + +## Error Adapter Types + +### `ErrorAdapter` + +Translates vendor-specific exceptions into Arcade errors: + +```typescript +interface ErrorAdapter { + /** Identifier for logging/metrics */ + slug: string; + + /** Translate an exception into an Arcade error, or null if not handled */ + fromException(error: unknown): MCPError | null; +} +``` + +### Built-in Adapters + +```typescript +import { + SlackErrorAdapter, + GoogleErrorAdapter, + MicrosoftGraphErrorAdapter, + HTTPErrorAdapter, + GraphQLErrorAdapter, +} from 'arcade-mcp/adapters'; + +app.tool('sendMessage', { + input: z.object({ channel: z.string(), text: z.string() }), + adapters: [new SlackErrorAdapter()], + handler: async ({ input }) => { + // SlackApiError → UpstreamError automatically + await slack.chat.postMessage(input); + return 'Sent!'; + }, +}); +``` + +| Adapter | Handles | +|---------|---------| +| `SlackErrorAdapter` | Slack SDK errors | +| `GoogleErrorAdapter` | Google API Client errors | +| `MicrosoftGraphErrorAdapter` | Microsoft Graph SDK errors | +| `HTTPErrorAdapter` | fetch/HTTP library errors | +| `GraphQLErrorAdapter` | GraphQL client errors | + +Adapters are tried in order. First match wins. `HTTPErrorAdapter` is always added as fallback. + +## Utility Types + +### `MaterializedTool` + +A tool object ready for registration. Created via `tool()`: + +```typescript +import { tool } from 'arcade-mcp'; +import { z } from 'zod'; + +const myTool = tool({ + name: 'my-tool', // required for runtime registration + description: 'Does something', + input: z.object({ value: z.string() }), + handler: ({ input }) => `Got: ${input.value}`, +}); + +// Use with runtime APIs +await app.tools.add(myTool); +await server.tools.add(myTool); +``` + +### `ToolOptions` + +Complete tool configuration type: + +```typescript +interface ToolOptions< + TInput, + TSecrets extends string = string, + TAuth extends AuthProvider | undefined = undefined +> { + description?: string; + input: z.ZodType; + handler: ( + context: ToolContext + ) => unknown | Promise; + requiresAuth?: TAuth; + requiresSecrets?: readonly TSecrets[]; + requiresMetadata?: string[]; + adapters?: ErrorAdapter[]; +} +``` + +The `TAuth` type parameter enables conditional typing: when `requiresAuth` is set, `authorization` becomes non-optional in the handler context. + +### `MCPAppOptions` + +```typescript +interface MCPAppOptions { + name?: string; + version?: string; + title?: string; + instructions?: string; + logLevel?: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR'; + transport?: 'stdio' | 'http'; + host?: string; + port?: number; + reload?: boolean; + cors?: CorsOptions; +} +``` + +### `CorsOptions` + +```typescript +interface CorsOptions { + origin?: string | string[] | boolean; + methods?: string[]; + allowedHeaders?: string[]; + exposedHeaders?: string[]; + credentials?: boolean; + maxAge?: number; +} +``` From 42f22e92267cff918165372b9f3dab1b87b924a3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Dec 2025 07:51:04 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=A4=96=20Regenerate=20LLMs.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/llms.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/public/llms.txt b/public/llms.txt index dd735a0b4..78b7cd922 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -1,4 +1,4 @@ - + # Arcade @@ -28,13 +28,20 @@ Arcade delivers three core capabilities: Deploy agents even your security team w - [Arcade API Reference](https://docs.arcade.dev/en/references/api.md): The Arcade API Reference documentation provides users with essential information on how to interact with the Arcade API, including the base URL for requests and links to the OpenAPI specification. It emphasizes the requirement of having an account in good standing and adherence to the Terms of - [Arcade MCP (MCP Server SDK) - Python Overview](https://docs.arcade.dev/en/references/mcp/python/overview.md): This documentation page provides an overview of the Arcade MCP (MCP Server SDK) for Python, detailing its purpose as a secure framework for programmatically building MCP servers with a minimal API. Users will learn about the key components, such as the `M +- [Arcade MCP (MCP Server SDK) - TypeScript Overview](https://docs.arcade.dev/en/references/mcp/typescript/overview.md): The Arcade MCP (MCP Server SDK) - TypeScript Overview documentation provides users with a comprehensive guide to building secure MCP servers using TypeScript, Bun, Elysia, and Zod 4. It covers installation, API references, and practical - [Errors](https://docs.arcade.dev/en/references/mcp/python/errors.md): This documentation page provides an overview of domain-specific error types associated with the MCP server and its components, detailing the MCP exception hierarchy for improved error handling and debugging. Users can learn about various exceptions, such as `MCPError`, `ServerError`, +- [Errors](https://docs.arcade.dev/en/references/mcp/typescript/errors.md): This documentation page provides a comprehensive guide on error types specific to MCP servers within the Arcade MCP TypeScript SDK, helping users effectively manage and communicate errors in their tools. It details common error categories, retry-aware errors, and best practices for creating and handling - [Middleware](https://docs.arcade.dev/en/references/mcp/python/middleware.md): This documentation page provides an overview of the Middleware component in the Arcade MCP Server SDK for Python, detailing how users can intercept and modify requests and responses during processing. It outlines the base classes and methods for creating custom middleware, as well as built-in middleware +- [Middleware](https://docs.arcade.dev/en/references/mcp/typescript/middleware.md): This documentation page provides a comprehensive guide on implementing and utilizing middleware within the Arcade MCP TypeScript SDK, allowing users to intercept and modify MCP requests and responses. It outlines built-in middleware options for logging and error handling, as well as instructions for creating custom - [Server](https://docs.arcade.dev/en/references/mcp/python/server.md): This documentation page provides a reference for the `MCPServer` class in the Arcade MCP Python library, detailing its purpose as a low-level server for hosting Arcade tools over the MCP protocol. Users will learn about the server's features, including middleware support +- [Server](https://docs.arcade.dev/en/references/mcp/typescript/server.md): This documentation page provides a reference for the `MCPServer` class in the Arcade MCP TypeScript library, detailing its low-level functionalities for managing server lifecycle, middleware, and client communication. It guides users on when to use `MCPServer` - [Settings](https://docs.arcade.dev/en/references/mcp/python/settings.md): This documentation page provides an overview of global configuration and environment-driven settings for the Arcade MCP Server, detailing how to manage and utilize various settings containers like MCPSettings and its sub-settings. Users will learn how to create settings from environment variables, convert them to +- [Settings](https://docs.arcade.dev/en/references/mcp/typescript/settings.md): This documentation page provides guidance on configuring MCP servers using constructor options and environment variables, detailing the available settings and their precedence. Users can learn how to set up their servers quickly, manage advanced configurations with the `MCPSettings` class, and utilize environment - [Telemetry](https://docs.arcade.dev/en/references/mcp/telemetry.md): This documentation page explains the telemetry data collected by the `arcade-mcp` framework, detailing its purpose, what data is tracked, and how users can opt-out of data sharing. It emphasizes that participation is optional and outlines the types of usage information - [Transport Modes](https://docs.arcade.dev/en/references/mcp/python/transports.md): This documentation page provides an overview of the different transport modes (stdio and HTTP) available for MCP servers, detailing their characteristics, usage scenarios, and configuration options. Users will learn how to choose the appropriate transport based on their application needs, whether for desktop +- [Transport Modes](https://docs.arcade.dev/en/references/mcp/typescript/transports.md): This documentation page provides a comprehensive guide on the different transport modes available for MCP servers, specifically focusing on stdio and HTTP options. Users will learn how to choose the appropriate transport based on their deployment needs, configure their servers for local or web environments, - [Types](https://docs.arcade.dev/en/references/mcp/python/types.md): This documentation page provides an overview of core Pydantic models and enums used in the MCP protocol, specifically detailing the `CallToolResult` and `SessionMessage` types. It helps users understand how to construct JSON-RPC requests and responses, as +- [Types](https://docs.arcade.dev/en/references/mcp/typescript/types.md): This documentation page provides a comprehensive reference for the core TypeScript types and interfaces used in the MCP protocol within the Arcade SDK. It helps users understand and implement essential types, such as `ToolContext`, for tool handlers, as well as advanced types for ## Arcade Cli From 80db33a5f6d7d60750f452239e2731c23e676e1f Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 22 Dec 2025 15:43:08 -0800 Subject: [PATCH 3/5] docs: Improve TypeScript SDK docs clarity and consistency - Overview: Clarify tool() vs app.tool() usage, add curl comment - Transports: Fix Windows path, clarify SSE flow, Docker healthcheck - Server: Clarify WHATWG streams, add catalog.add() callout - Middleware: Add execution order note, clarify hooks, fix auth example - Types: Add AuthInfo.userInfo comment, explain as const, dedupe ContentItem --- app/en/references/mcp/typescript/_meta.tsx | 3 + .../references/mcp/typescript/errors/page.mdx | 272 ++++--- .../mcp/typescript/examples/page.mdx | 663 ++++++++++++++++++ .../mcp/typescript/middleware/page.mdx | 374 ++++++---- .../mcp/typescript/overview/page.mdx | 540 +++++--------- .../references/mcp/typescript/server/page.mdx | 88 ++- .../mcp/typescript/settings/page.mdx | 199 ++++-- .../mcp/typescript/transports/page.mdx | 248 +++---- .../references/mcp/typescript/types/page.mdx | 459 ++++++++++-- 9 files changed, 1890 insertions(+), 956 deletions(-) create mode 100644 app/en/references/mcp/typescript/examples/page.mdx diff --git a/app/en/references/mcp/typescript/_meta.tsx b/app/en/references/mcp/typescript/_meta.tsx index 9f25b0ab0..33997ef59 100644 --- a/app/en/references/mcp/typescript/_meta.tsx +++ b/app/en/references/mcp/typescript/_meta.tsx @@ -12,6 +12,9 @@ const meta: MetaRecord = { overview: { title: "Overview", }, + examples: { + title: "Examples", + }, transports: { title: "Transports", }, diff --git a/app/en/references/mcp/typescript/errors/page.mdx b/app/en/references/mcp/typescript/errors/page.mdx index 6d9de4fd7..14e7dfe37 100644 --- a/app/en/references/mcp/typescript/errors/page.mdx +++ b/app/en/references/mcp/typescript/errors/page.mdx @@ -9,19 +9,19 @@ import { Callout } from "nextra/components"; The SDK provides specific error types for different failure scenarios. Use these in your tools to return meaningful errors to AI clients. -## Common Errors +## Context Errors -These are the errors you'll use most often in your tools: +Errors caused by user input or missing resources. Extend `MCPContextError`. ### `NotFoundError` Requested resource doesn't exist. ```typescript -import { MCPApp, NotFoundError } from 'arcade-mcp'; +import { ArcadeMCP, NotFoundError } from 'arcade-mcp-server'; import { z } from 'zod'; -const app = new MCPApp({ name: 'user-service' }); +const app = new ArcadeMCP({ name: 'user-service' }); app.tool('getUser', { input: z.object({ id: z.string() }), @@ -40,111 +40,48 @@ app.tool('getUser', { User lacks permission for the requested action. ```typescript -import { MCPApp, AuthorizationError } from 'arcade-mcp'; -import { Google } from 'arcade-mcp/auth'; -import { z } from 'zod'; - -const app = new MCPApp({ name: 'project-service' }); +import { AuthorizationError } from 'arcade-mcp-server'; -app.tool('deleteProject', { - input: z.object({ projectId: z.string() }), - requiresAuth: Google({ scopes: ['profile'] }), - handler: async ({ input, authorization }) => { - const project = await db.findProject(input.projectId); - const userEmail = await fetchUserEmail(authorization.token); - - if (project.ownerEmail !== userEmail) { - throw new AuthorizationError('Only the owner can delete this project'); - } - - await db.deleteProject(input.projectId); - return { deleted: true }; - }, -}); +// In your tool handler: +if (project.ownerEmail !== userEmail) { + throw new AuthorizationError('Only the owner can delete this project'); +} ``` -### `ToolExecutionError` - -Base class for tool execution errors. Use one of the specific subclasses: - -- `RetryableToolError` — can be retried -- `FatalToolError` — unrecoverable, don't retry -- `ContextRequiredToolError` — needs user input before retry -- `UpstreamError` — external API failure - -See [Retry-Aware Errors](#retry-aware-errors) below. - ### `ResourceError` -Error in resource management. +Error in resource management (MCP resources, not tool execution). ```typescript -import { MCPApp, ResourceError } from 'arcade-mcp'; -import { z } from 'zod'; +import { ResourceError } from 'arcade-mcp-server'; -const app = new MCPApp({ name: 'file-service' }); - -app.tool('readFile', { - input: z.object({ path: z.string() }), - handler: async ({ input }) => { - try { - return await Bun.file(input.path).text(); - } catch { - throw new ResourceError(`Cannot read file: ${input.path}`); - } - }, -}); +throw new ResourceError('Cannot read resource: config://settings'); ``` ### `PromptError` -Error in prompt management. +Error in prompt management (MCP prompts, not tool execution). ```typescript -import { PromptError } from 'arcade-mcp'; +import { PromptError } from 'arcade-mcp-server'; -// Thrown when a prompt is not found or invalid throw new PromptError('Prompt template not found: greeting'); ``` -### `UpstreamError` - -External API or service failure. Typically thrown by error adapters. - -```typescript -import { UpstreamError } from 'arcade-mcp'; - -throw new UpstreamError('Slack API error: channel_not_found', { - statusCode: 404, // required -}); -``` - -### `UpstreamRateLimitError` - -Rate limit from an external API. Includes retry information. - -```typescript -import { UpstreamRateLimitError } from 'arcade-mcp'; +## Retry-Aware Errors -throw new UpstreamRateLimitError('Rate limited by Slack', { - retryAfterMs: 60_000, // required -}); -``` +These errors include metadata that helps AI orchestrators decide whether and how to retry. - You rarely throw `UpstreamError` manually. Use [error adapters](/references/mcp/typescript/types#error-adapter-types) to automatically translate SDK errors. + Don't use `ToolExecutionError` directly — always use a specific subclass. -## Retry-Aware Errors - -These errors include metadata that helps AI orchestrators decide whether and how to retry. - ### `RetryableToolError` The operation failed but can be retried, optionally with guidance for the AI. ```typescript -import { RetryableToolError } from 'arcade-mcp'; +import { RetryableToolError } from 'arcade-mcp-server'; // Simple retry throw new RetryableToolError('Service temporarily unavailable'); @@ -165,7 +102,7 @@ throw new RetryableToolError('Search returned no results', { Unrecoverable error — the AI should not retry this operation. ```typescript -import { FatalToolError } from 'arcade-mcp'; +import { FatalToolError } from 'arcade-mcp-server'; throw new FatalToolError('Account has been permanently deleted'); @@ -180,7 +117,7 @@ throw new FatalToolError('Configuration error', { The operation needs additional context from the user before it can proceed. ```typescript -import { ContextRequiredToolError } from 'arcade-mcp'; +import { ContextRequiredToolError } from 'arcade-mcp-server'; throw new ContextRequiredToolError('Multiple users found matching "John"', { additionalPromptContent: 'Please specify: John Smith (john@work.com) or John Doe (john@home.com)', // required @@ -192,6 +129,34 @@ throw new ContextRequiredToolError('Multiple users found matching "John"', { Use `ContextRequiredToolError` when the AI needs to ask the user for clarification. +### `UpstreamError` + +External API or service failure. `canRetry` is true for 5xx and 429, false otherwise. + +```typescript +import { UpstreamError } from 'arcade-mcp-server'; + +throw new UpstreamError('Slack API error: channel_not_found', { + statusCode: 404, // required +}); +``` + +### `UpstreamRateLimitError` + +Rate limit from an external API. Extends `UpstreamError` with `statusCode: 429` auto-set. + +```typescript +import { UpstreamRateLimitError } from 'arcade-mcp-server'; + +throw new UpstreamRateLimitError('Rate limited by Slack', { + retryAfterMs: 60_000, // required — milliseconds until retry +}); +``` + + + You rarely throw these manually. Use [error adapters](/references/mcp/typescript/types#error-adapter-types) to automatically translate SDK errors. + + ## Constructor Signatures All tool errors use an options object pattern. Required fields are marked. @@ -244,6 +209,30 @@ new UpstreamRateLimitError(message: string, options: { | `extra` | Structured metadata for telemetry/adapters | | `cause` | ES2022 error chaining for stack traces | +All tool errors also expose these read-only properties: + +| Property | Type | Description | +|----------|------|-------------| +| `canRetry` | `boolean` | Whether this error is retryable | +| `kind` | `ErrorKind` | Classification for telemetry | +| `statusCode` | `number \| undefined` | HTTP status code equivalent | + +## SDK-Internal Errors + + + These errors are thrown by the SDK, not by user code. They appear in error messages and logs + to help you debug. You don't need to import or throw them directly. + + +| Error | When Thrown | +|-------|-------------| +| `ToolDefinitionError` | Tool definition is invalid | +| `ToolInputSchemaError` | Input schema is malformed | +| `ToolOutputSchemaError` | Output schema is malformed | +| `ToolInputError` | Input doesn't match schema at runtime | +| `ToolOutputError` | Output can't be serialized | +| `ToolkitLoadError` | Toolkit fails to import (missing deps, syntax error) | + ## Error Hierarchy All errors extend from `MCPError`: @@ -260,7 +249,7 @@ MCPError (base) ├── TransportError └── ServerError ├── RequestError - │ └── ServerRequestError (server-to-client requests) + │ └── ServerRequestError ├── ResponseError ├── SessionError └── LifespanError @@ -268,14 +257,18 @@ MCPError (base) ToolkitError (base for tool errors) └── ToolError └── ToolRuntimeError - └── ToolExecutionError - ├── RetryableToolError (can retry) - ├── FatalToolError (do not retry) - ├── ContextRequiredToolError (needs user input) - └── UpstreamError (external API failure) - └── UpstreamRateLimitError + ├── RetryableToolError (canRetry: true) + ├── FatalToolError (canRetry: false) + ├── ContextRequiredToolError (canRetry: false) + └── UpstreamError (canRetry: true for 5xx/429) + └── UpstreamRateLimitError (canRetry: true) ``` + + SDK-internal errors (`ToolDefinitionError`, `ToolInputError`, etc.) are omitted. + See [SDK-Internal Errors](#sdk-internal-errors) for the full list. + + ### When to Use Each Category | Error Type | Use when... | HTTP Analogy | @@ -283,49 +276,41 @@ ToolkitError (base for tool errors) | `MCPContextError` | User/input caused the error | 4xx errors | | `MCPRuntimeError` | Server/infrastructure caused the error | 5xx errors | -## Creating Custom Errors +## ErrorKind -Extend `ToolExecutionError` for domain-specific tool errors: +Each error has a `kind` property for telemetry and logging: ```typescript -import { ToolExecutionError } from 'arcade-mcp'; +import { ErrorKind } from 'arcade-mcp-server'; -interface QuotaExceededOptions { - limitType: string; - developerMessage?: string; - extra?: Record; -} +// Example: log errors by kind +console.log(error.kind); // e.g., ErrorKind.TOOL_RUNTIME_RETRY +``` -class QuotaExceededError extends ToolExecutionError { - readonly limitType: string; +`UpstreamError` auto-sets `kind` from `statusCode` (401/403 → `AUTH_ERROR`, 404 → `NOT_FOUND`, 429 → `RATE_LIMIT`, 5xx → `SERVER_ERROR`). - constructor(message: string, options: QuotaExceededOptions) { - super(message, options); - this.name = 'QuotaExceededError'; - this.limitType = options.limitType; - } -} - -// Usage -throw new QuotaExceededError('Monthly API quota exceeded', { - limitType: 'api_calls', - extra: { currentUsage: 10000, limit: 10000 }, -}); -``` +## Creating Custom Errors -For MCP-level errors (resources, prompts), extend `MCPContextError`: +Extend a specific error type — not `ToolExecutionError` directly: ```typescript -import { MCPContextError } from 'arcade-mcp'; +import { FatalToolError } from 'arcade-mcp-server'; + +class QuotaExceededError extends FatalToolError { + readonly limitType: string; -class InvalidResourceFormatError extends MCPContextError { - constructor(uri: string) { - super(`Invalid resource format: ${uri}`); - this.name = 'InvalidResourceFormatError'; + constructor(message: string, limitType: string) { + super(message, { extra: { limitType } }); + this.name = 'QuotaExceededError'; + this.limitType = limitType; } } + +throw new QuotaExceededError('Monthly API quota exceeded', 'api_calls'); ``` +For retryable errors, extend `RetryableToolError`. For MCP-level errors, extend `MCPContextError`. + ## Best Practices @@ -374,48 +359,33 @@ throw new RetryableToolError(`Invalid email: "${input.email}"`, { When calling external services, wrap their errors with cause chaining for debugging: ```typescript -import { RetryableToolError } from 'arcade-mcp'; -import { z } from 'zod'; - -app.tool('fetchWeather', { - input: z.object({ city: z.string() }), - handler: async ({ input }) => { - try { - return await weatherApi.get(input.city); - } catch (error) { - // Wrap with context and preserve the original error for debugging - throw new RetryableToolError( - `Failed to fetch weather for ${input.city}. Please try again.`, - { cause: error } // Preserves stack trace for debugging - ); - } - }, -}); +import { RetryableToolError } from 'arcade-mcp-server'; + +// In your tool handler: +try { + return await weatherApi.get(input.city); +} catch (error) { + throw new RetryableToolError( + `Failed to fetch weather for ${input.city}. Please try again.`, + { cause: error } // Preserves stack trace for debugging + ); +} ``` ## Error Handling in Middleware +Use middleware to log, enrich, or transform errors before they reach the client: + ```typescript -import { - Middleware, - MCPError, - type MiddlewareContext, - type CallNext, -} from 'arcade-mcp'; - -class ErrorEnrichmentMiddleware extends Middleware { +import { Middleware, MCPError, type MiddlewareContext, type CallNext } from 'arcade-mcp-server'; + +class ErrorLoggingMiddleware extends Middleware { async onCallTool(context: MiddlewareContext, next: CallNext) { try { return await next(context); } catch (error) { if (error instanceof MCPError) { - // Log structured error info - console.error({ - type: error.name, - message: error.message, - tool: context.message.params?.name, - session: context.session?.id, - }); + console.error({ type: error.name, message: error.message }); } throw error; // Re-throw for ErrorHandlingMiddleware } diff --git a/app/en/references/mcp/typescript/examples/page.mdx b/app/en/references/mcp/typescript/examples/page.mdx new file mode 100644 index 000000000..5bbc51d91 --- /dev/null +++ b/app/en/references/mcp/typescript/examples/page.mdx @@ -0,0 +1,663 @@ +--- +title: "Examples - Arcade MCP TypeScript Reference" +description: "Complete example servers for common use cases" +--- + +import { Callout } from "nextra/components"; + +# Examples + +Complete, runnable examples for common MCP server patterns. + +## Simple Server + +A minimal server with one tool. Start here. + +```typescript +// examples/simple/server.ts +import { ArcadeMCP } from 'arcade-mcp-server'; +import { z } from 'zod'; + +const app = new ArcadeMCP({ name: 'simple-server', version: '1.0.0' }); + +app.tool('echo', { + description: 'Echo the input back', + input: z.object({ + message: z.string().describe('Message to echo'), + }), + handler: ({ input }) => input.message, +}); + +// Run with: bun run server.ts +// Or for stdio: bun run server.ts stdio +const transport = process.argv[2] === 'stdio' ? 'stdio' : 'http'; +app.run({ transport, port: 8000 }); +``` + +Test it: + +```bash +curl -X POST http://localhost:8000/mcp \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{"message":"hello"}}}' +``` + +## OAuth Server + +A server with Google OAuth. Arcade handles token management. + +```typescript +// examples/oauth/server.ts +import { ArcadeMCP } from 'arcade-mcp-server'; +import { Google } from 'arcade-mcp-server/auth'; +import { z } from 'zod'; + +const app = new ArcadeMCP({ name: 'oauth-server', version: '1.0.0' }); + +app.tool('listDriveFiles', { + description: 'List files in Google Drive', + input: z.object({ + maxResults: z.number().default(10).describe('Maximum files to return'), + }), + requiresAuth: Google({ + scopes: ['https://www.googleapis.com/auth/drive.readonly'], + }), + handler: async ({ input, authorization }) => { + const response = await fetch( + `https://www.googleapis.com/drive/v3/files?pageSize=${input.maxResults}`, + { + headers: { Authorization: `Bearer ${authorization.token}` }, + } + ); + + if (!response.ok) { + throw new Error(`Drive API error: ${response.statusText}`); + } + + const data = await response.json(); + return data.files.map((f: { name: string; id: string }) => ({ + name: f.name, + id: f.id, + })); + }, +}); + +app.tool('getProfile', { + description: 'Get the authenticated user profile', + input: z.object({}), + requiresAuth: Google({ + scopes: ['https://www.googleapis.com/auth/userinfo.profile'], + }), + handler: async ({ authorization }) => { + const response = await fetch( + 'https://www.googleapis.com/oauth2/v2/userinfo', + { + headers: { Authorization: `Bearer ${authorization.token}` }, + } + ); + return response.json(); + }, +}); + +app.run({ transport: 'http', port: 8000 }); +``` + + + OAuth requires an Arcade account. Set `ARCADE_API_KEY` in your environment. + [Create an Arcade account](https://api.arcade.dev/signup) to get started. + + +## Logging Server + +Demonstrates MCP protocol logging (sent to the AI client, not just console). + +```typescript +// examples/logging/server.ts +import { ArcadeMCP } from 'arcade-mcp-server'; +import { z } from 'zod'; + +const app = new ArcadeMCP({ name: 'logging-server', version: '1.0.0' }); + +app.tool('processData', { + description: 'Process data with verbose logging', + input: z.object({ + data: z.string().describe('Data to process'), + }), + handler: async ({ input, log }) => { + // These logs are sent to the MCP client (Claude, etc.) + await log.debug(`Starting to process: ${input.data}`); + await log.info('Processing step 1...'); + + // Simulate work + await new Promise((resolve) => setTimeout(resolve, 500)); + + await log.info('Processing step 2...'); + await log.warning('This step took longer than expected'); + + await new Promise((resolve) => setTimeout(resolve, 500)); + + await log.info('Processing complete'); + + return `Processed: ${input.data.toUpperCase()}`; + }, +}); + +app.run({ transport: 'http', port: 8000 }); +``` + + + With stdio transport, use `console.error()` for local debugging. MCP protocol + logs via the `log` handler parameter go to the AI client. + + +## Progress Reporting + +Long-running operations with progress updates. + +```typescript +// examples/progress/server.ts +import { ArcadeMCP } from 'arcade-mcp-server'; +import { z } from 'zod'; + +const app = new ArcadeMCP({ name: 'progress-server', version: '1.0.0' }); + +app.tool('analyzeFiles', { + description: 'Analyze multiple files with progress reporting', + input: z.object({ + fileCount: z.number().min(1).max(100).describe('Number of files to analyze'), + }), + handler: async ({ input, progress }) => { + const results: string[] = []; + + for (let i = 0; i < input.fileCount; i++) { + // Report progress to the client + await progress.report(i, input.fileCount, `Analyzing file ${i + 1} of ${input.fileCount}`); + + // Simulate file analysis + await new Promise((resolve) => setTimeout(resolve, 200)); + results.push(`file_${i + 1}: OK`); + } + + await progress.report(input.fileCount, input.fileCount, 'Analysis complete'); + + return { analyzed: input.fileCount, results }; + }, +}); + +app.run({ transport: 'http', port: 8000 }); +``` + +## Production Server + +A production-ready server with middleware, rate limiting, and organized tools. + +```typescript +// examples/production/server.ts +import { ArcadeMCP } from 'arcade-mcp-server'; + +import { add, multiply } from './tools/calculator'; +import { getWeather } from './tools/weather'; +import { RateLimitMiddleware } from './middleware/rate-limit'; +import { AuditMiddleware } from './middleware/audit'; + +const app = new ArcadeMCP({ + name: 'production-server', + version: '1.0.0', + instructions: 'A production MCP server with calculator and weather tools.', +}); + +// HTTP request logging +app.onRequest(({ request }) => { + console.error(`[HTTP] ${request.method} ${new URL(request.url).pathname}`); +}); + +// Register tools — name is always first arg +app.tool('calculator_add', add); +app.tool('calculator_multiply', multiply); +app.tool('weather', getWeather); + +const port = Number(process.env.PORT) || 8000; +const host = process.env.HOST || '0.0.0.0'; + +// Run with middleware (passed to underlying MCPServer) +app.run({ + transport: 'http', + host, + port, + middleware: [ + new RateLimitMiddleware({ maxRequests: 100, windowMs: 60_000 }), + new AuditMiddleware({ logFile: './audit.log' }), + ], +}); +``` + +```typescript +// examples/production/tools/calculator.ts +import { tool } from 'arcade-mcp-server'; +import { z } from 'zod'; + +export const add = tool({ + description: 'Add two numbers', + input: z.object({ + a: z.number().describe('First number'), + b: z.number().describe('Second number'), + }), + handler: ({ input }) => input.a + input.b, +}); + +export const multiply = tool({ + description: 'Multiply two numbers', + input: z.object({ + a: z.number().describe('First number'), + b: z.number().describe('Second number'), + }), + handler: ({ input }) => input.a * input.b, +}); +``` + +```typescript +// examples/production/tools/weather.ts +import { tool } from 'arcade-mcp-server'; +import { z } from 'zod'; + +export const getWeather = tool({ + description: 'Get current weather for a city', + input: z.object({ + city: z.string().describe('City name'), + }), + requiresSecrets: ['WEATHER_API_KEY'] as const, + handler: async ({ input, getSecret }) => { + const apiKey = getSecret('WEATHER_API_KEY'); + + const response = await fetch( + `https://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${encodeURIComponent(input.city)}` + ); + + if (!response.ok) { + throw new Error(`Weather API error: ${response.statusText}`); + } + + const data = await response.json(); + return { + city: data.location.name, + temp_c: data.current.temp_c, + condition: data.current.condition.text, + }; + }, +}); +``` + +```typescript +// examples/production/middleware/rate-limit.ts +import { + Middleware, + RetryableToolError, + type MiddlewareContext, + type CallNext, +} from 'arcade-mcp-server'; + +interface RateLimitOptions { + maxRequests: number; + windowMs: number; +} + +export class RateLimitMiddleware extends Middleware { + private requests = new Map(); + private maxRequests: number; + private windowMs: number; + + constructor(options: RateLimitOptions) { + super(); + this.maxRequests = options.maxRequests; + this.windowMs = options.windowMs; + } + + async onCallTool(context: MiddlewareContext, next: CallNext) { + const clientId = context.sessionId ?? 'anonymous'; + const now = Date.now(); + + const recent = (this.requests.get(clientId) ?? []).filter( + (t) => t > now - this.windowMs + ); + + if (recent.length >= this.maxRequests) { + throw new RetryableToolError('Rate limit exceeded. Try again later.', { + retryAfterMs: this.windowMs, + }); + } + + this.requests.set(clientId, [...recent, now]); + return next(context); + } +} +``` + +```typescript +// examples/production/middleware/audit.ts +import { + Middleware, + type MiddlewareContext, + type CallNext, +} from 'arcade-mcp-server'; +import { appendFile } from 'node:fs/promises'; + +interface AuditOptions { + logFile: string; +} + +export class AuditMiddleware extends Middleware { + private logFile: string; + + constructor(options: AuditOptions) { + super(); + this.logFile = options.logFile; + } + + async onCallTool(context: MiddlewareContext, next: CallNext) { + const start = performance.now(); + const result = await next(context); + const elapsed = performance.now() - start; + + const entry = { + timestamp: new Date().toISOString(), + sessionId: context.sessionId, + method: context.method, + elapsedMs: elapsed.toFixed(2), + }; + + // Fire and forget - don't block the response + appendFile(this.logFile, JSON.stringify(entry) + '\n').catch(console.error); + + return result; + } +} +``` + +### Dockerfile + +```dockerfile +# examples/production/Dockerfile +FROM oven/bun:1 + +WORKDIR /app + +# Install dependencies +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile + +# Copy source +COPY . . + +# Runtime configuration +ENV HOST=0.0.0.0 +ENV PORT=8000 +EXPOSE 8000 + +CMD ["bun", "run", "server.ts"] +``` + +```bash +# Build and run +docker build -t my-mcp-server . +docker run -p 8000:8000 \ + -e ARCADE_API_KEY=arc_... \ + -e WEATHER_API_KEY=... \ + -e ALLOWED_ORIGINS=https://myapp.com \ + my-mcp-server +``` + +## Tool Chaining & Resources + +Advanced example demonstrating tool-to-tool calls, resource access, and user elicitation. + +```typescript +// examples/advanced/server.ts +import { ArcadeMCP } from 'arcade-mcp-server'; +import { z } from 'zod'; + +const app = new ArcadeMCP({ + name: 'advanced-server', + version: '1.0.0', +}); + +// Expose a resource (e.g., config file) +app.resource('config://settings', { + description: 'Application settings', + mimeType: 'application/json', + handler: async () => JSON.stringify({ + maxRetries: 3, + timeout: 5000, + }), +}); + +// A helper tool that can be called by other tools +app.tool('validate', { + description: 'Validate data against a pattern', + input: z.object({ + data: z.string(), + pattern: z.string(), + }), + handler: ({ input }) => { + const regex = new RegExp(input.pattern); + return { valid: regex.test(input.data), data: input.data }; + }, +}); + +// Main tool that uses resources, calls other tools, and elicits user input +app.tool('processWithApproval', { + description: 'Process data with validation and user approval', + input: z.object({ + data: z.string().describe('Data to process'), + }), + handler: async ({ input, log, resources, tools, ui }) => { + // 1. Read configuration from resource (get() returns single item) + await log.info('Loading configuration...'); + const config = await resources.get('config://settings'); + const settings = JSON.parse(config.text ?? '{}'); + + // 2. Call another tool for validation + await log.info('Validating input...'); + const result = await tools.call('validate', { + data: input.data, + pattern: '^[a-zA-Z]+$', + }); + + // tools.call returns CallToolResult with content array + if (result.isError) { + return { error: 'Validation failed', data: input.data }; + } + + // Parse structured content from the result + const validation = result.structuredContent as { valid: boolean } | undefined; + if (!validation?.valid) { + return { error: 'Validation failed', data: input.data }; + } + + // 3. Ask user for confirmation via elicitation + await log.info('Requesting user approval...'); + const approval = await ui.elicit('Approve processing?', z.object({ + approved: z.boolean().describe('Approve this operation'), + notes: z.string().optional().describe('Optional notes'), + })); + + if (approval.action !== 'accept' || !approval.content?.approved) { + return { cancelled: true, reason: 'User declined' }; + } + + // 4. Process with configured settings + await log.info(`Processing with timeout: ${settings.timeout}ms`); + return { + processed: input.data.toUpperCase(), + settings, + notes: approval.content?.notes, + }; + }, +}); + +app.run({ transport: 'http', port: 8000 }); +``` + + + `ui.elicit` requires client support. Claude Desktop and many MCP clients support it. + Always handle the `decline` and `cancel` cases. + + +## Multi-Provider OAuth + +A server supporting multiple OAuth providers. + +```typescript +// examples/multi-oauth/server.ts +import { ArcadeMCP } from 'arcade-mcp-server'; +import { Google, GitHub, Slack } from 'arcade-mcp-server/auth'; +import { z } from 'zod'; + +const app = new ArcadeMCP({ name: 'multi-oauth-server', version: '1.0.0' }); + +app.tool('getGoogleProfile', { + description: 'Get Google user profile', + input: z.object({}), + requiresAuth: Google({ + scopes: ['https://www.googleapis.com/auth/userinfo.profile'], + }), + handler: async ({ authorization }) => { + const res = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { + headers: { Authorization: `Bearer ${authorization.token}` }, + }); + return res.json(); + }, +}); + +app.tool('listGitHubRepos', { + description: 'List GitHub repositories for the authenticated user', + input: z.object({ + visibility: z.enum(['all', 'public', 'private']).default('all'), + }), + requiresAuth: GitHub({ scopes: ['repo'] }), + handler: async ({ input, authorization }) => { + const res = await fetch( + `https://api.github.com/user/repos?visibility=${input.visibility}`, + { + headers: { + Authorization: `Bearer ${authorization.token}`, + Accept: 'application/vnd.github+json', + }, + } + ); + const repos = await res.json(); + return repos.map((r: { name: string; html_url: string }) => ({ + name: r.name, + url: r.html_url, + })); + }, +}); + +app.tool('sendSlackMessage', { + description: 'Send a message to a Slack channel', + input: z.object({ + channel: z.string().describe('Channel ID (e.g., C01234567)'), + text: z.string().describe('Message text'), + }), + requiresAuth: Slack({ scopes: ['chat:write'] }), + handler: async ({ input, authorization }) => { + const res = await fetch('https://slack.com/api/chat.postMessage', { + method: 'POST', + headers: { + Authorization: `Bearer ${authorization.token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + channel: input.channel, + text: input.text, + }), + }); + + const data = await res.json(); + if (!data.ok) { + throw new Error(`Slack API error: ${data.error}`); + } + + return { sent: true, ts: data.ts }; + }, +}); + +app.run({ transport: 'http', port: 8000 }); +``` + +## Testing Tools + +Unit testing tools without running a full server. + +```typescript +// examples/testing/calculator.test.ts +import { describe, expect, test } from 'bun:test'; +import { add, multiply } from './tools/calculator'; + +describe('calculator tools', () => { + test('add returns correct sum', async () => { + // Call handler directly with minimal mock context + const result = await add.handler({ + input: { a: 2, b: 3 }, + }); + + expect(result).toBe(5); + }); + + test('multiply returns correct product', async () => { + const result = await multiply.handler({ + input: { a: 4, b: 5 }, + }); + + expect(result).toBe(20); + }); +}); +``` + +For tools that need secrets or auth, provide mocks: + +```typescript +// examples/testing/weather.test.ts +import { describe, expect, test, mock } from 'bun:test'; +import { getWeather } from './tools/weather'; + +describe('weather tool', () => { + test('fetches weather with API key', async () => { + // Mock fetch + globalThis.fetch = mock(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ + location: { name: 'Seattle' }, + current: { temp_c: 12, condition: { text: 'Cloudy' } }, + }), + }) + ) as typeof fetch; + + const result = await getWeather.handler({ + input: { city: 'Seattle' }, + getSecret: (key: string) => key === 'WEATHER_API_KEY' ? 'test-key' : '', + }); + + expect(result).toEqual({ + city: 'Seattle', + temp_c: 12, + condition: 'Cloudy', + }); + }); +}); +``` + +Run tests: + +```bash +bun test +``` + +## Next Steps + +- [**Overview**](/references/mcp/typescript/overview) — Core concepts and API +- [**Transports**](/references/mcp/typescript/transports) — stdio and HTTP configuration +- [**Middleware**](/references/mcp/typescript/middleware) — Request/response interception +- [**Types**](/references/mcp/typescript/types) — TypeScript interfaces and schemas + diff --git a/app/en/references/mcp/typescript/middleware/page.mdx b/app/en/references/mcp/typescript/middleware/page.mdx index 7e042fd9d..04a4bf77b 100644 --- a/app/en/references/mcp/typescript/middleware/page.mdx +++ b/app/en/references/mcp/typescript/middleware/page.mdx @@ -8,42 +8,34 @@ import { Callout } from "nextra/components"; # Middleware - Most users don't need custom middleware. The SDK includes logging and error handling by default. - Use middleware when you need request/response interception, custom authentication, or metrics. + Most users don't need custom middleware. The SDK includes logging and error + handling by default. Use middleware when you need request validation, custom + authentication, rate limiting, or metrics. Middleware intercepts MCP messages before they reach your tools and after responses are generated. -## Built-in Middleware - -### `LoggingMiddleware` - -Logs all MCP messages with timing. Enabled by default. - -```typescript -import { LoggingMiddleware } from 'arcade-mcp'; +## MCP Middleware vs HTTP Hooks -new LoggingMiddleware({ logLevel: 'INFO' }) -``` - -### `ErrorHandlingMiddleware` +The SDK has two extension points. Pick the right one: -Catches errors and returns safe error responses. Enabled by default. +| Layer | What it intercepts | Use for | +|-------|-------------------|---------| +| **MCP Middleware** | MCP JSON-RPC messages (`tools/call`, `resources/read`, etc.) | Auth on tool calls, rate limiting, tool call logging, metrics | +| **HTTP Hooks** (`app.onRequest`) | Raw HTTP requests (HTTP transport only) | Request logging, custom headers | -```typescript -import { ErrorHandlingMiddleware } from 'arcade-mcp'; - -new ErrorHandlingMiddleware({ maskErrorDetails: true }) -``` +**Rule of thumb:** If you're inspecting tool names or arguments, use MCP middleware. If you're inspecting HTTP headers or request paths, use `app.onRequest()`. -Set `maskErrorDetails: false` in development to see full stack traces. - -## Custom Middleware +## Quick Start Extend the `Middleware` class and override handler methods: ```typescript -import { Middleware, type MiddlewareContext, type CallNext } from 'arcade-mcp'; +import { + Middleware, + type MiddlewareContext, + type CallNext, +} from 'arcade-mcp-server'; class TimingMiddleware extends Middleware { async onMessage(context: MiddlewareContext, next: CallNext) { @@ -56,28 +48,72 @@ class TimingMiddleware extends Middleware { } ``` +Add it to your server: + +```typescript +import { MCPServer, ToolCatalog } from 'arcade-mcp-server'; + +const server = new MCPServer({ + catalog: new ToolCatalog(), + middleware: [new TimingMiddleware()], +}); +``` + - With stdio transport, use `console.error()` for logging. All `stdout` is protocol data. + With stdio transport, use `console.error()` for logging. All `stdout` is + reserved for protocol data. -### Available Hooks - -| Hook | When it runs | -|------|--------------| -| `onMessage` | Every message (use for logging, timing) | -| `onRequest` | All request messages | -| `onNotification` | All notification messages | -| `onCallTool` | Tool invocations | -| `onListTools` | Tool listing requests | -| `onListResources` | Resource listing requests | -| `onReadResource` | Resource read requests | +## How Middleware Executes + +Middleware wraps your handlers like an onion. Each middleware can run code before and after calling `next()`: + +```text +Request enters + ↓ +┌─────────────────────────────────────────────┐ +│ ErrorHandling (catches all errors) │ +│ ┌─────────────────────────────────────┐ │ +│ │ Logging (logs all requests) │ │ +│ │ ┌─────────────────────────────┐ │ │ +│ │ │ Your Middleware │ │ │ +│ │ │ ↓ │ │ │ +│ │ │ Handler │ │ │ +│ │ │ ↓ │ │ │ +│ │ └─────────────────────────────┘ │ │ +│ └─────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ + ↓ +Response exits +``` + +The outermost middleware (`ErrorHandling`) runs first on request and last on response. This means: + +- If your middleware throws an error, `ErrorHandling` catches it +- If auth fails, the error is logged and formatted safely + +**Middleware array order = outside-in.** First in array wraps everything. + +## Available Hooks + +Override these methods to intercept specific message types: + +| Hook | When it runs | +| ------------------------- | ---------------------------------- | +| `onMessage` | Every message (requests + notifications) | +| `onRequest` | Request messages only (expects response) | +| `onNotification` | Notification messages only (no response) | +| `onCallTool` | Tool invocations | +| `onListTools` | Tool listing requests | +| `onListResources` | Resource listing requests | +| `onReadResource` | Resource read requests | | `onListResourceTemplates` | Resource template listing requests | -| `onListPrompts` | Prompt listing requests | -| `onGetPrompt` | Prompt retrieval requests | +| `onListPrompts` | Prompt listing requests | +| `onGetPrompt` | Prompt retrieval requests | -### Hook Signatures +Within each middleware, hooks run in order: `onMessage` → `onRequest`/`onNotification` → method-specific hook. -All hooks follow the same pattern: +All hooks follow this pattern: ```typescript async onHookName( @@ -87,126 +123,119 @@ async onHookName( ``` - Call `next(context)` to continue the chain -- Modify `context.message` before calling `next` to alter the request - Modify the result after calling `next` to alter the response - Throw an error to abort processing - Return early (without calling `next`) to short-circuit -## Composing Middleware +## Built-in Middleware -Combine multiple middleware into a single handler: +The SDK includes two middleware enabled by default: -```typescript -import { - composeMiddleware, - LoggingMiddleware, - ErrorHandlingMiddleware, -} from 'arcade-mcp'; - -const composed = composeMiddleware( - new ErrorHandlingMiddleware({ maskErrorDetails: false }), - new LoggingMiddleware({ logLevel: 'DEBUG' }), - new TimingMiddleware() -); -``` +**`LoggingMiddleware`** — Logs all MCP messages with timing. -Pass middleware directly to `MCPServer`: +**`ErrorHandlingMiddleware`** — Catches errors and returns safe error responses. -```typescript -import { MCPServer, ToolCatalog } from 'arcade-mcp'; +You don't need to add these manually. Use `middlewareOptions` to configure them: +```typescript const server = new MCPServer({ catalog: new ToolCatalog(), - middleware: [ - new ErrorHandlingMiddleware({ maskErrorDetails: false }), - new LoggingMiddleware({ logLevel: 'DEBUG' }), - ], + middlewareOptions: { + logLevel: 'DEBUG', // More verbose logging + maskErrorDetails: false, // Show full errors in development + }, }); ``` -Use `composeMiddleware` when you need to combine middleware into reusable units: +| Option | Type | Default | Description | +| --------------------- | ------------------------------------------- | -------- | --------------------------- | +| `enableLogging` | `boolean` | `true` | Enable logging middleware | +| `logLevel` | `'DEBUG' \| 'INFO' \| 'WARNING' \| 'ERROR'` | `'INFO'` | Log level | +| `enableErrorHandling` | `boolean` | `true` | Enable error handling | +| `maskErrorDetails` | `boolean` | `true` | Hide error details (secure) | + + + In production, keep `maskErrorDetails: true` (the default) to avoid leaking + internal details. Set to `false` only in development. + + +**Note:** `middlewareOptions` configures built-in middleware. The `middleware` array is for your custom middleware only—built-in middleware is never added there. + +### Environment Variables + +You can also configure via environment: + +| Variable | Default | Description | +| -------------------------------------- | ------- | ------------------------- | +| `MCP_MIDDLEWARE_ENABLE_LOGGING` | `true` | Enable logging middleware | +| `MCP_MIDDLEWARE_LOG_LEVEL` | `INFO` | Log level | +| `MCP_MIDDLEWARE_ENABLE_ERROR_HANDLING` | `true` | Enable error handling | +| `MCP_MIDDLEWARE_MASK_ERROR_DETAILS` | `true` | Mask error details | ```typescript -const authAndLogging = composeMiddleware( - new AuthMiddleware(), - new LoggingMiddleware() -); +import { MCPServer, middlewareOptionsFromEnv, ToolCatalog } from 'arcade-mcp-server'; const server = new MCPServer({ catalog: new ToolCatalog(), - middleware: [authAndLogging, new MetricsMiddleware()], + middlewareOptions: middlewareOptionsFromEnv(), }); ``` - - Middleware runs in order. The first wraps the second, which wraps the third. - `ErrorHandlingMiddleware` first means it catches errors from all subsequent middleware. - +See [Settings](/references/mcp/typescript/settings) for the complete configuration reference. -## MiddlewareContext +### Using a Custom Error Handler -Context passed to all middleware handlers: +To replace the built-in error handler: ```typescript -interface MiddlewareContext { - /** The MCP message being processed */ - message: T; - - /** Mutable metadata to pass between middleware */ - metadata: Record; - - /** Client session info (if available) */ - session?: ServerSession; -} +const server = new MCPServer({ + catalog: new ToolCatalog(), + middlewareOptions: { enableErrorHandling: false }, + middleware: [new MyCustomErrorHandler()], +}); ``` -### Sharing Data Between Middleware +## Composing Middleware -Use `metadata` to pass data between middleware: +Use `composeMiddleware` to group related middleware into reusable units: ```typescript -class AuthMiddleware extends Middleware { - async onMessage(context: MiddlewareContext, next: CallNext) { - const userId = await validateToken(context.message); - context.metadata.userId = userId; // Available to subsequent middleware - return next(context); - } -} +import { composeMiddleware } from 'arcade-mcp-server'; -class AuditMiddleware extends Middleware { - async onCallTool(context: MiddlewareContext, next: CallNext) { - const userId = context.metadata.userId; // From AuthMiddleware - await logToolCall(userId, context.message); - return next(context); - } -} -``` +// Group auth-related middleware +const authStack = composeMiddleware( + new AuthMiddleware(), + new AuditMiddleware() +); -### Creating Modified Context +const server = new MCPServer({ + catalog: new ToolCatalog(), + middleware: [authStack, new MetricsMiddleware()], +}); +``` -Use object spread to create a modified context: +This is equivalent to passing them in order: ```typescript -class TransformMiddleware extends Middleware { - async onMessage(context: MiddlewareContext, next: CallNext) { - const modifiedContext = { - ...context, - metadata: { ...context.metadata, transformed: true }, - }; - return next(modifiedContext); - } -} +const server = new MCPServer({ + // ... + middleware: [new AuthMiddleware(), new AuditMiddleware(), new MetricsMiddleware()], +}); ``` +Use composition when you want to reuse the same middleware group across multiple servers or share it as a package. + ## Example: Auth Middleware +Validate an API key passed via `context.metadata`: + ```typescript import { Middleware, AuthorizationError, type MiddlewareContext, type CallNext, -} from 'arcade-mcp'; +} from 'arcade-mcp-server'; class ApiKeyAuthMiddleware extends Middleware { constructor(private validKeys: Set) { @@ -223,28 +252,38 @@ class ApiKeyAuthMiddleware extends Middleware { return next(context); } } - -// Usage -const auth = new ApiKeyAuthMiddleware(new Set(['key1', 'key2'])); ``` -## Example: Rate Limiting Middleware + + How `apiKey` gets into `metadata` depends on your transport and session setup. For Arcade-managed auth, use `requiresAuth` on your tools instead — see [Overview](/references/mcp/typescript/overview#adding-oauth). + + +## Example: Rate Limiting ```typescript -import { Middleware, RetryableToolError, type MiddlewareContext, type CallNext } from 'arcade-mcp'; +import { + Middleware, + RetryableToolError, + type MiddlewareContext, + type CallNext, +} from 'arcade-mcp-server'; class RateLimitMiddleware extends Middleware { private requests = new Map(); - constructor(private maxRequests = 100, private windowMs = 60_000) { + constructor( + private maxRequests = 100, + private windowMs = 60_000 + ) { super(); } async onCallTool(context: MiddlewareContext, next: CallNext) { - const clientId = context.session?.id ?? 'anonymous'; + const clientId = context.sessionId ?? 'anonymous'; const now = Date.now(); - const recent = (this.requests.get(clientId) ?? []) - .filter((t) => t > now - this.windowMs); + const recent = (this.requests.get(clientId) ?? []).filter( + (t) => t > now - this.windowMs + ); if (recent.length >= this.maxRequests) { throw new RetryableToolError('Rate limit exceeded. Try again later.', { @@ -258,31 +297,86 @@ class RateLimitMiddleware extends Middleware { } ``` -## HTTP-Level Hooks +## Lifecycle Hooks -For HTTP-level customization, access the underlying Elysia instance via `app.elysia`: +For server startup, shutdown, and HTTP request hooks, use `app.onStart()`, `app.onStop()`, and `app.onRequest()`. + +See [Transports](/references/mcp/typescript/transports#lifecycle-hooks) for the full reference. For low-level control with `MCPServer`, use the `lifespan` option — see [Server](/references/mcp/typescript/server#with-lifespan-manager). + +--- + +## Advanced: MiddlewareContext + +Context passed to all middleware handlers: ```typescript -import { MCPApp } from 'arcade-mcp'; +interface MiddlewareContext { + readonly message: T; // The MCP message being processed + readonly source: 'client' | 'server'; + readonly type: 'request' | 'notification'; + readonly method: string | null; // e.g., "tools/call", "resources/read" + readonly timestamp: Date; + readonly requestId: string | null; + readonly sessionId: string | null; + readonly mcpContext: Context | null; // Advanced: MCP runtime access + metadata: Record; // Mutable: pass data between middleware +} +``` -const app = new MCPApp({ name: 'my-server' }); +### Sharing Data Between Middleware -// Log requests -app.elysia.onRequest(({ request }) => { - console.error(`${request.method} ${new URL(request.url).pathname}`); -}); +Use `metadata` to pass data down the chain: -// Add response headers -app.elysia.onAfterHandle(({ set }) => { - set.headers['X-Powered-By'] = 'Arcade MCP'; -}); +```typescript +class AuthMiddleware extends Middleware { + async onMessage(context: MiddlewareContext, next: CallNext) { + const userId = await validateToken(context.message); + context.metadata.userId = userId; + return next(context); + } +} + +class AuditMiddleware extends Middleware { + async onCallTool(context: MiddlewareContext, next: CallNext) { + const userId = context.metadata.userId; // From AuthMiddleware + await logToolCall(userId, context.message); + return next(context); + } +} +``` -app.run({ transport: 'http', port: 8000 }); +### Creating Modified Context + +Use object spread to create a modified context: + +```typescript +class TransformMiddleware extends Middleware { + async onMessage(context: MiddlewareContext, next: CallNext) { + const modifiedContext = { + ...context, + metadata: { ...context.metadata, transformed: true }, + }; + return next(modifiedContext); + } +} ``` +### MCP Runtime Access (Advanced) + +The `mcpContext` field provides access to MCP protocol capabilities: + +| Property | Purpose | +| --------------- | ------------------------------------------- | +| `log` | Send logs via MCP protocol | +| `progress` | Report progress for long-running operations | +| `resources` | Read MCP resources | +| `tools` | Call other tools programmatically | +| `prompts` | Access MCP prompts | +| `sampling` | Create messages using the client's model | +| `ui` | Elicit input from the user | +| `notifications` | Send notifications to the client | + - `app.elysia` gives you the underlying Elysia instance with full access to all lifecycle hooks. - See the [Elysia lifecycle docs](https://elysiajs.com/essential/life-cycle) for available hooks. + Most middleware won't need `mcpContext`. Use `console.error()` for logging + and `metadata` for passing data between middleware. - -For most use cases, MCP middleware (the `Middleware` class) is sufficient. HTTP-level auth can use Elysia's `derive` pattern; see the [Elysia docs](https://elysiajs.com/patterns/extends-context). CORS is configured via the `cors` option. diff --git a/app/en/references/mcp/typescript/overview/page.mdx b/app/en/references/mcp/typescript/overview/page.mdx index 725239a1a..f79b03538 100644 --- a/app/en/references/mcp/typescript/overview/page.mdx +++ b/app/en/references/mcp/typescript/overview/page.mdx @@ -5,64 +5,82 @@ description: "Build MCP servers with TypeScript, Bun, Elysia, and Zod 4" import { Callout } from "nextra/components"; -# Arcade MCP (MCP Server SDK) - TypeScript Overview +# Arcade MCP TypeScript SDK -`arcade-mcp`, the secure framework for building MCP servers, provides a clean, minimal API to build MCP servers programmatically. It handles tool collection, server configuration, and transport setup with a developer-friendly interface. +## What is MCP? + +The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is an open standard that lets AI assistants call external tools and access data. When you ask Claude to "search my docs" or "send a Slack message," MCP makes it happen. + +An **MCP server** exposes **tools** (functions the AI can call), **resources** (data the AI can read), and **prompts** (reusable templates). The AI client connects to your server and discovers what's available. + +## What is Arcade MCP? + +`arcade-mcp-server` is a TypeScript SDK for building MCP servers. It works standalone — no account required — for tools that use API keys or no auth at all. + +When your tools need to act on behalf of users (sending emails, accessing files, posting to Slack), connect to [Arcade](https://arcade.dev). Arcade manages OAuth consent flows, token storage, and refresh — so you don't build auth UIs or store credentials. + +- **Clean API** — Register tools with Zod schemas, get type-safe handlers +- **Transport flexibility** — stdio for Claude Desktop, HTTP for web deployments +- **Secrets from env vars** — Type-safe access, validated at startup +- **OAuth via Arcade** — Add `requiresAuth` to let tools act as the user (Google, GitHub, Slack, etc.) + + + Works without Arcade. Need user OAuth? [Sign up →](https://api.arcade.dev/signup) + ## Installation +### Bun (recommended) + ```bash -bun add arcade-mcp +bun add arcade-mcp-server ``` - - **tsconfig:** Run `bun init` to generate one, or ensure yours has these essentials: - - ```json - { - "compilerOptions": { - "strict": true, - "moduleResolution": "bundler", - "target": "ESNext", - "module": "Preserve" - } - } - ``` - - See [Bun TypeScript docs](https://bun.sh/docs/typescript) for the complete recommended config. - +Requires Bun 1.3+ with `strict: true` and `moduleResolution: bundler` in tsconfig. Bun runs TypeScript directly — just `bun run server.ts`. See [Bun TypeScript docs](https://bun.sh/docs/typescript). -## Imports +### Node.js -```typescript -// Main exports -import { MCPApp, tool } from 'arcade-mcp'; -import { NotFoundError, RetryableToolError, FatalToolError } from 'arcade-mcp'; +The SDK uses Elysia for HTTP transport. Elysia supports Node.js via the [`@elysiajs/node`](https://elysiajs.com/integrations/node) adapter. + +```bash +npm install arcade-mcp-server @elysiajs/node +``` -// Auth providers -import { Google, GitHub, Slack } from 'arcade-mcp/auth'; +```typescript +import { ArcadeMCP } from 'arcade-mcp-server'; +import { node } from '@elysiajs/node'; -// Error adapters -import { SlackErrorAdapter, GoogleErrorAdapter } from 'arcade-mcp/adapters'; +const app = new ArcadeMCP({ + name: 'my-server', + version: '1.0.0', + adapter: node(), // Enable Node.js runtime +}); ``` +Requires Node.js 18+ and `"type": "module"` in package.json. Use [tsx](https://github.com/privatenumber/tsx) to run TypeScript directly: `npx tsx server.ts`. + + + Bun offers faster startup and native TypeScript. Node.js works via Elysia's adapter with the same API. Choose based on your deployment environment. + + ## Quick Start ```typescript // server.ts -import { MCPApp } from 'arcade-mcp'; +import { ArcadeMCP } from 'arcade-mcp-server'; import { z } from 'zod'; -const app = new MCPApp({ name: 'my-server', version: '1.0.0' }); +const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' }); app.tool('greet', { description: 'Greet a person by name', input: z.object({ - name: z.string().describe('The name of the person to greet'), + name: z.string().describe('The name to greet'), }), handler: ({ input }) => `Hello, ${input.name}!`, }); +// reload: true restarts on file changes (dev only) app.run({ transport: 'http', port: 8000, reload: true }); ``` @@ -70,168 +88,97 @@ app.run({ transport: 'http', port: 8000, reload: true }); bun run server.ts ``` -Test it works: +Test it: ```bash +# List all tools your server exposes (JSON-RPC format) curl -X POST http://localhost:8000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' ``` -When Claude (or another AI) calls your `greet` tool with `{ name: "Alex" }`, your handler runs and returns the greeting. - - - **Transport auto-detection:** stdio for Claude Desktop, HTTP when you specify host/port. - The `reload: true` option enables hot reload during development. - - -## API Reference - -### `MCPApp` - -**`arcade-mcp.MCPApp`** - -A type-safe, developer-friendly interface for building MCP servers. Handles tool registration, configuration, and transport. +## Adding OAuth -### Constructor +When your tool needs to act on behalf of a user, use `requiresAuth`. Arcade handles the OAuth consent flow, token refresh, and secure storage. ```typescript -new MCPApp(options?: MCPAppOptions) -``` - -```typescript -interface MCPAppOptions { - /** Server name shown to AI clients */ - name?: string; - - /** Server version */ - version?: string; - - /** Human-readable title */ - title?: string; - - /** Usage instructions for AI clients */ - instructions?: string; - - /** Logging level */ - logLevel?: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR'; - - /** Transport type: 'stdio' for Claude Desktop, 'http' for web */ - transport?: 'stdio' | 'http'; +import { ArcadeMCP } from 'arcade-mcp-server'; +import { Google } from 'arcade-mcp-server/auth'; +import { z } from 'zod'; - /** HTTP host (auto-selects HTTP transport if set) */ - host?: string; +const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' }); - /** HTTP port (auto-selects HTTP transport if set) */ - port?: number; +app.tool('getProfile', { + description: 'Get the current user profile from Google', + input: z.object({}), + requiresAuth: Google({ scopes: ['https://www.googleapis.com/auth/userinfo.profile'] }), + handler: async ({ authorization }) => { + const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { + headers: { Authorization: `Bearer ${authorization.token}` }, + }); + return response.json(); + }, +}); - /** Hot reload on file changes (development only) */ - reload?: boolean; -} +app.run({ transport: 'http', port: 8000 }); ``` -**Defaults:** +When you add `requiresAuth`, the handler receives an `authorization` object with the user's OAuth token. The AI never sees the token — it stays server-side. -| Option | Default | Notes | -|--------|---------|-------| -| `name` | `'ArcadeMCP'` | | -| `version` | `'1.0.0'` | | -| `logLevel` | `'INFO'` | | -| `transport` | `'stdio'` | Auto-switches to `'http'` if host/port set | -| `host` | `'127.0.0.1'` | | -| `port` | `8000` | | +## Adding Secrets -### `app.tool()` - -Register a tool that AI clients can call. +For API keys your server needs (not user OAuth tokens), use `requiresSecrets`: ```typescript -app.tool(name: string, options: ToolOptions): void -``` - -```typescript -interface ToolOptions< - TInput, - TSecrets extends string = string, - TAuth extends AuthProvider | undefined = undefined -> { - /** Tool description for AI clients */ - description?: string; - - /** Zod schema for input validation */ - input: z.ZodType; - - /** Tool handler function — authorization is non-optional when requiresAuth is set */ - handler: ( - context: ToolContext - ) => unknown | Promise; - - /** OAuth provider for user authentication */ - requiresAuth?: TAuth; - - /** Secret keys required by this tool (use `as const` for type safety) */ - requiresSecrets?: readonly TSecrets[]; - - /** Metadata keys required from the client */ - requiresMetadata?: string[]; - - /** Error adapters for translating upstream errors (e.g., Slack, Google APIs) */ - adapters?: ErrorAdapter[]; -} -``` - -**Handler context (destructure what you need):** - -```typescript -handler: ({ input, authorization, getSecret, getMetadata }) => { - // input — Validated input matching your Zod schema - // authorization — OAuth token/provider (TypeScript narrows to non-optional when requiresAuth is set) - // getSecret — Retrieve secrets (type-safe with `as const`) - // getMetadata — Retrieve metadata from the client -} +app.tool('analyze', { + description: 'Analyze text with AI', + input: z.object({ text: z.string() }), + requiresSecrets: ['OPENAI_API_KEY'] as const, + handler: async ({ input, getSecret }) => { + const apiKey = getSecret('OPENAI_API_KEY'); // Type-safe, validated at startup + + const response = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: input.text }], + }), + }); + return response.json(); + }, +}); ``` -**Return values are auto-wrapped:** +Secrets are read from environment variables and validated at startup. -| Return type | Becomes | -|-------------|---------| -| `string` | `{ content: [{ type: 'text', text: '...' }] }` | -| `object` | `{ content: [{ type: 'text', text: JSON.stringify(...) }] }` | -| `{ content: [...] }` | Passed through unchanged | +## Organizing Tools -### `app.run()` +Tools are always registered with `app.tool('name', options)`. The name is the first argument. -Start the server. +### Inline (most projects) -```typescript -app.run(options?: RunOptions): Promise -``` +Define tools directly — `app.tool()` provides full type inference: ```typescript -interface RunOptions { - transport?: 'stdio' | 'http'; - host?: string; - port?: number; - reload?: boolean; -} +app.tool('greet', { + description: 'Greet someone', + input: z.object({ name: z.string() }), + handler: ({ input }) => `Hello, ${input.name}!`, +}); ``` -### `app.addToolsFromModule()` - -Add all tools from a module at once. - - - **`app.tool()` vs `tool()`:** - - `app.tool('name', { ... })` — Register a tool directly - - `tool({ ... })` — Create a tool for `addToolsFromModule()` or runtime `app.tools.add()` - +### Multi-file (larger projects) -Use the standalone `tool()` function to define exportable tools, then `addToolsFromModule()` discovers and registers them: +For tools defined in separate files, use `tool()` to preserve type inference, then register with `app.tool()`: ```typescript // tools/math.ts -import { tool } from 'arcade-mcp'; +import { tool } from 'arcade-mcp-server'; import { z } from 'zod'; export const add = tool({ @@ -239,253 +186,98 @@ export const add = tool({ input: z.object({ a: z.number(), b: z.number() }), handler: ({ input }) => input.a + input.b, }); - -export const multiply = tool({ - description: 'Multiply two numbers', - input: z.object({ a: z.number(), b: z.number() }), - handler: ({ input }) => input.a * input.b, -}); ``` ```typescript // server.ts -import * as mathTools from './tools/math'; +import { ArcadeMCP } from 'arcade-mcp-server'; +import { add } from './tools/math'; -app.addToolsFromModule(mathTools); -``` - -Tool names are inferred from export names (`add`, `multiply`). Override with explicit `name` if needed: - -```typescript -export const calculator = tool({ - name: 'basic-calculator', // explicit name overrides 'calculator' - description: 'Basic arithmetic', - input: z.object({ - operation: z.enum(['add', 'subtract', 'multiply', 'divide']), - a: z.number(), - b: z.number(), - }), - handler: ({ input }) => { - const { operation, a, b } = input; - switch (operation) { - case 'add': return a + b; - case 'subtract': return a - b; - case 'multiply': return a * b; - case 'divide': return a / b; - } - }, -}); -``` - -### Runtime APIs - -After the server starts, you can modify tools, prompts, and resources at runtime: - -```typescript -import { tool } from 'arcade-mcp'; -import { z } from 'zod'; - -// Create and add a tool at runtime -const dynamicTool = tool({ - name: 'dynamic-tool', // name required for runtime registration - description: 'Added at runtime', - input: z.object({ value: z.string() }), - handler: ({ input }) => `Got: ${input.value}`, -}); - -await app.tools.add(dynamicTool); - -// Remove a tool -await app.tools.remove('dynamic-tool'); - -// List all tools -const tools = await app.tools.list(); -``` - -```typescript -// Add a prompt (reusable message templates for AI clients) -await app.prompts.add(prompt, handler); - -// Add a resource (files, data, or content the AI can read) -await app.resources.add(resource); -``` - - - **MCP primitives:** Tools are functions AI can call. Prompts are reusable message templates (like "summarize this"). Resources are data the AI can read (files, database records, API responses). - - -See the [Server reference](/references/mcp/typescript/server) for full prompts and resources API. - -## Examples - -### Simple Tool - -```typescript -import { MCPApp } from 'arcade-mcp'; -import { z } from 'zod'; - -const app = new MCPApp({ name: 'example-server' }); - -app.tool('echo', { - description: 'Echo the text back', - input: z.object({ - text: z.string().describe('The text to echo'), - }), - handler: ({ input }) => `Echo: ${input.text}`, -}); - -app.run({ transport: 'http', host: '0.0.0.0', port: 8000 }); -``` - -### With OAuth and Secrets - -Use `requiresAuth` when your tool needs to act on behalf of a user (e.g., access their Google account). Use `requiresSecrets` for API keys your server needs. - -```typescript -import { MCPApp } from 'arcade-mcp'; -import { Google } from 'arcade-mcp/auth'; -import { z } from 'zod'; - -const app = new MCPApp({ name: 'my-server' }); - -app.tool('getProfile', { - description: 'Get user profile from Google', - input: z.object({ - userId: z.string(), - }), - requiresAuth: Google({ scopes: ['profile'] }), - requiresSecrets: ['API_KEY'] as const, // as const enables type-safe getSecret() - handler: async ({ input, authorization, getSecret }) => { - const token = authorization.token; // User's OAuth token - const apiKey = getSecret('API_KEY'); // ✅ Type-safe, autocomplete works - // getSecret('OTHER'); // ❌ TypeScript error: not in requiresSecrets - return { userId: input.userId }; - }, -}); - -app.run(); +const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' }); +app.tool('add', add); // Name provided at registration +app.run({ transport: 'http', port: 8000 }); ``` - **Auth & Secrets:** - - **OAuth:** Arcade coordinates user consent. Tokens are passed to your handler automatically. - - **Secrets:** Loaded from environment variables. Never log or return them. + **Why `tool()`?** Exporting a plain object loses the schema→handler type connection. The `tool()` wrapper preserves it. -### With Required Metadata +## Runtime Capabilities -Request metadata from the client: +Tool handlers receive a context object. Destructure what you need: ```typescript -app.tool('contextAware', { - description: 'A tool that uses client context', - input: z.object({ query: z.string() }), - requiresMetadata: ['sessionId', 'userAgent'], - handler: ({ input, getMetadata }) => { - const sessionId = getMetadata('sessionId'); // string | undefined - return `Processing ${input.query} for session ${sessionId}`; +app.tool('processData', { + description: 'Process data from a resource', + input: z.object({ uri: z.string() }), + handler: async ({ input, log, progress, resources }) => { + await log.info('Starting...'); + const data = await resources.get(input.uri); + await progress.report(1, 2); + // ... process data ... + await progress.report(2, 2, 'Done'); + return data; }, }); ``` -### Schema Metadata for AI Clients +**Common context fields:** -Use `.describe()` for simple descriptions. Use `.meta()` for richer metadata: +| Field | Description | +|-------|-------------| +| `input` | Validated input matching your Zod schema | +| `authorization` | OAuth token (when `requiresAuth` is set) | +| `getSecret` | Get secret values (when `requiresSecrets` is set) | +| `log` | Send logs to the client (`log.info()`, `log.error()`, etc.) | +| `progress` | Report progress for long-running operations | +| `resources` | Read resources exposed by the server | +| `tools` | Call other tools by name | -```typescript -app.tool('search', { - description: 'Search the knowledge base', - input: z.object({ - query: z.string().describe('Search query'), - limit: z.number() - .int() - .min(1) - .max(100) - .default(10) - .meta({ - title: 'Result limit', - examples: [10, 25, 50], - }), - }), - handler: ({ input }) => searchKnowledgeBase(input.query, input.limit), -}); -``` +See [Types](/references/mcp/typescript/types) for the complete `ToolContext` reference including `prompts`, `sampling`, `ui`, and `notifications`. -Both `.describe()` and `.meta()` are preserved in the JSON Schema that AI clients receive. +## Lifecycle Hooks -### Async Tool with Error Handling +Hook into server startup, shutdown, and HTTP requests: ```typescript -import { MCPApp, NotFoundError } from 'arcade-mcp'; -import { z } from 'zod'; +const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' }); -const app = new MCPApp({ name: 'api-server' }); - -app.tool('getUser', { - description: 'Fetch a user by ID', - input: z.object({ - id: z.string().uuid().describe('User ID'), - }), - handler: async ({ input }) => { - const user = await db.users.find(input.id); +app.onStart(async () => { + await db.connect(); +}); - if (!user) { - throw new NotFoundError(`User ${input.id} not found`); - } +app.onStop(async () => { + await db.disconnect(); +}); - return user; - }, +app.onRequest(({ request }) => { + console.error(`${request.method} ${request.url}`); }); + +app.run({ transport: 'http', port: 8000 }); ``` -### Full Example with All Features +See [Transports](/references/mcp/typescript/transports#lifecycle-hooks) for the full reference. -```typescript -import { MCPApp } from 'arcade-mcp'; -import { Google } from 'arcade-mcp/auth'; -import { z } from 'zod'; +## ArcadeMCP vs MCPServer -const app = new MCPApp({ - name: 'full-example', - version: '1.0.0', - instructions: 'Use these tools to manage documents.', - logLevel: 'DEBUG', -}); +| Class | Description | +|-------|-------------| +| **`ArcadeMCP`** | High-level. Manages transport, lifecycle, provides `app.tool()`. | +| **`MCPServer`** | Low-level. No transport — you wire it yourself. | -// Simple tool -app.tool('ping', { - description: 'Health check', - input: z.object({}), - handler: () => 'pong', -}); +Start with `ArcadeMCP`. Use `MCPServer` when embedding MCP in an existing app, building custom transports, or testing without network. See [Server](/references/mcp/typescript/server) for details. -// Complex tool with auth and secrets -app.tool('createDocument', { - description: 'Create a new document in Google Drive', - input: z.object({ - title: z.string().min(1).describe('Document title'), - content: z.string().describe('Document content'), - folder: z.string().optional().describe('Parent folder ID'), - }), - requiresAuth: Google({ scopes: ['drive.file'] }), - requiresSecrets: ['DRIVE_API_KEY'] as const, - handler: async ({ input, authorization, getSecret }) => { - const response = await createDriveDocument({ - token: authorization.token, - apiKey: getSecret('DRIVE_API_KEY'), - ...input, - }); - return { documentId: response.id, url: response.webViewLink }; - }, -}); - -// Start server -if (import.meta.main) { - app.run({ transport: 'http', host: '0.0.0.0', port: 8000 }); -} -``` + + Python SDK has the same pattern: [`MCPApp`](/references/mcp/python/overview) (high-level) wraps [`MCPServer`](/references/mcp/python/server) (low-level). + -```bash -bun run server.ts -``` +## Next Steps +- [**Examples**](/references/mcp/typescript/examples) — Complete, runnable example servers +- [**Transports**](/references/mcp/typescript/transports) — stdio for Claude Desktop, HTTP for web +- [**Server**](/references/mcp/typescript/server) — Low-level APIs, prompts, resources +- [**Middleware**](/references/mcp/typescript/middleware) — Request/response interception +- [**Errors**](/references/mcp/typescript/errors) — Error types and handling +- [**Settings**](/references/mcp/typescript/settings) — Configuration and environment variables +- [**Types**](/references/mcp/typescript/types) — TypeScript interfaces and schemas diff --git a/app/en/references/mcp/typescript/server/page.mdx b/app/en/references/mcp/typescript/server/page.mdx index 59d4e077a..91192333f 100644 --- a/app/en/references/mcp/typescript/server/page.mdx +++ b/app/en/references/mcp/typescript/server/page.mdx @@ -8,14 +8,19 @@ import { Callout } from "nextra/components"; # Server - Most users should use `MCPApp` from the [Overview](/references/mcp/typescript/overview). - Use `MCPServer` only when you need low-level control over the server lifecycle, - custom transports, or advanced middleware composition. + **Most users should use [`ArcadeMCP`](/references/mcp/typescript/overview)** — it handles transport and provides a simpler API. This page covers `MCPServer`, the low-level core for advanced use cases. +## When to Use `MCPServer` + +- **Embedding** — Add MCP endpoints to an existing HTTP server +- **Custom transports** — WebSocket, IPC, or other non-standard transports +- **Multi-tenant** — Isolated server instances with different tools/auth +- **Testing** — Instantiate without starting a transport + ## `MCPServer` -**`arcade-mcp.MCPServer`** +**`arcade-mcp-server.MCPServer`** Low-level MCP server with middleware and context support. @@ -76,7 +81,7 @@ interface MCPServerOptions { |--------|---------|-------| | `name` | `'ArcadeMCP'` | | | `version` | `'1.0.0'` | | -| `arcadeApiKey` | `Bun.env.ARCADE_API_KEY` | | +| `arcadeApiKey` | `process.env.ARCADE_API_KEY` | | | `arcadeApiUrl` | `'https://api.arcade.dev'` | | | `authDisabled` | `false` | ⚠️ Never enable in production | @@ -119,7 +124,7 @@ async runConnection( ): Promise ``` -Run a single MCP connection. Used for custom transport implementations. +Run a single MCP connection using [WHATWG Streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) (not Node.js streams). ### Properties @@ -129,15 +134,20 @@ Run a single MCP connection. Used for custom transport implementations. | `resources` | `ResourceManager` | Runtime resource operations | | `prompts` | `PromptManager` | Runtime prompt operations | -### Examples +## Examples -#### stdio Transport +### stdio Transport ```typescript -import { MCPServer, ToolCatalog, StdioTransport } from 'arcade-mcp'; +import { MCPServer, ToolCatalog, StdioTransport } from 'arcade-mcp-server'; +import { z } from 'zod'; const catalog = new ToolCatalog(); -// catalog.add(...) to register tools +catalog.add('ping', { + description: 'Health check', + input: z.object({}), + handler: () => 'pong', +}); const server = new MCPServer({ catalog, @@ -154,12 +164,22 @@ try { } ``` -#### HTTP Transport + + `catalog.add()` is the low-level API for `MCPServer`. It's equivalent to `app.tool()` on `ArcadeMCP` — both use name as first argument, options second. + + +### HTTP Transport ```typescript -import { MCPServer, ToolCatalog, HTTPTransport } from 'arcade-mcp'; +import { MCPServer, ToolCatalog, HTTPTransport } from 'arcade-mcp-server'; +import { z } from 'zod'; const catalog = new ToolCatalog(); +catalog.add('ping', { + description: 'Health check', + input: z.object({}), + handler: () => 'pong', +}); const server = new MCPServer({ catalog, @@ -176,7 +196,7 @@ try { } ``` -#### With Middleware +### With Middleware ```typescript import { @@ -184,7 +204,7 @@ import { ToolCatalog, LoggingMiddleware, ErrorHandlingMiddleware, -} from 'arcade-mcp'; +} from 'arcade-mcp-server'; const server = new MCPServer({ catalog: new ToolCatalog(), @@ -200,10 +220,10 @@ const server = new MCPServer({ from all subsequent middleware. -#### With Lifespan Manager +### With Lifespan Manager ```typescript -import { MCPServer, ToolCatalog, createLifespan } from 'arcade-mcp'; +import { MCPServer, ToolCatalog, createLifespan } from 'arcade-mcp-server'; const lifespan = createLifespan({ async onStart(server) { @@ -220,54 +240,54 @@ const server = new MCPServer({ }); ``` -#### Runtime Tool Management +### Runtime Tool Management -Use `tool()` to create tool objects for runtime registration: +Add and remove tools from a running server: ```typescript -import { MCPServer, ToolCatalog, tool } from 'arcade-mcp'; +import { MCPServer, ToolCatalog, tool } from 'arcade-mcp-server'; import { z } from 'zod'; const server = new MCPServer({ catalog: new ToolCatalog() }); await server.start(); -// Create a tool object +// Create a tool object (tool() provides type inference) const dynamicTool = tool({ - name: 'dynamic-tool', // required for runtime registration description: 'Added at runtime', input: z.object({ value: z.string() }), handler: ({ input }) => `Got: ${input.value}`, }); -// Add it to the running server -await server.tools.add(dynamicTool); +// Add it to the running server — name is first arg +await server.tools.add('dynamic-tool', dynamicTool); // List all tools const tools = await server.tools.list(); console.error(`Server has ${tools.length} tools`); -// Remove a tool +// Remove a tool by name await server.tools.remove('dynamic-tool'); ``` -`tool()` takes the same options as `app.tool()` and returns a `MaterializedTool` object suitable for the runtime APIs. - -#### Custom Transport +### Custom Transport -For WebSocket or other custom transports, implement a class that calls `server.runConnection()`: +For WebSocket or other custom transports, implement a class that calls `server.runConnection()` with readable/writable Web Streams: ```typescript -import { MCPServer, ToolCatalog } from 'arcade-mcp'; +import { MCPServer } from 'arcade-mcp-server'; -// Pseudocode - implement stream adapters for your transport class CustomTransport { async run(server: MCPServer) { - // Accept connections, create readable/writable streams - const { readable, writable } = await acceptConnection(); - await server.runConnection(readable, writable); + // Your transport creates read/write streams for each connection + // then passes them to the server: + // + // await server.runConnection(readableStream, writableStream); + // + // The server handles MCP message parsing and routing. + // Your transport handles I/O (WebSocket frames, TCP packets, etc.) } } ``` -See the SDK source for `StdioTransport` and `HTTPTransport` as reference implementations. +See the SDK source for `StdioTransport` and `HTTPTransport` as complete reference implementations. diff --git a/app/en/references/mcp/typescript/settings/page.mdx b/app/en/references/mcp/typescript/settings/page.mdx index cea1fe956..d51574630 100644 --- a/app/en/references/mcp/typescript/settings/page.mdx +++ b/app/en/references/mcp/typescript/settings/page.mdx @@ -10,16 +10,16 @@ import { Callout } from "nextra/components"; Configure your MCP server via constructor options or environment variables. - For most users, pass options directly to `MCPApp`. Use `MCPSettings` only when you need + For most users, pass options directly to `ArcadeMCP`. Use `MCPSettings` only when you need to share configuration across multiple servers or load from a config file. ## Quick Start ```typescript -import { MCPApp } from 'arcade-mcp'; +import { ArcadeMCP } from 'arcade-mcp-server'; -const app = new MCPApp({ +const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0', logLevel: 'DEBUG', @@ -30,22 +30,69 @@ const app = new MCPApp({ The SDK reads these environment variables as defaults. Constructor options take precedence. +### Server Configuration + | Variable | Maps to | Example | |----------|---------|---------| | `MCP_SERVER_NAME` | `name` | `"My Server"` | | `MCP_SERVER_VERSION` | `version` | `"1.0.0"` | -| `MCP_LOG_LEVEL` | `logLevel` | `DEBUG`, `INFO`, `WARNING`, `ERROR` | +| `MCP_SERVER_TITLE` | `title` | `"My Tools"` | +| `MCP_SERVER_INSTRUCTIONS` | `instructions` | `"Use these tools to..."` | | `MCP_DEBUG` | `debug` | `true`, `false` | -| `MCP_HTTP_HOST` | `host` | `0.0.0.0` | -| `MCP_HTTP_PORT` | `port` | `8080` | -| `ARCADE_API_KEY` | `arcadeApiKey` | `arc_...` | -| `ARCADE_API_URL` | `arcadeApiUrl` | `https://api.arcade.dev` | + +### Runtime Configuration + +These override `app.run()` options: + +| Variable | Maps to | Example | +|----------|---------|---------| +| `ARCADE_SERVER_TRANSPORT` | `transport` | `stdio`, `http` | +| `ARCADE_SERVER_HOST` | `host` | `0.0.0.0` | +| `ARCADE_SERVER_PORT` | `port` | `8080` | +| `ARCADE_SERVER_RELOAD` | `reload` | `0`, `1` | + +### Middleware Configuration + +| Variable | Maps to | Example | +|----------|---------|---------| +| `MCP_MIDDLEWARE_LOG_LEVEL` | `logLevel` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` | +| `MCP_MIDDLEWARE_ENABLE_LOGGING` | `enableLogging` | `true`, `false` | +| `MCP_MIDDLEWARE_ENABLE_ERROR_HANDLING` | `enableErrorHandling` | `true`, `false` | +| `MCP_MIDDLEWARE_MASK_ERROR_DETAILS` | `maskErrorDetails` | `true`, `false` | + +### Transport Configuration + +| Variable | Maps to | Example | +|----------|---------|---------| +| `MCP_TRANSPORT_SESSION_TIMEOUT_SECONDS` | `sessionTimeoutSeconds` | `300` | +| `MCP_TRANSPORT_CLEANUP_INTERVAL_SECONDS` | `cleanupIntervalSeconds` | `10` | +| `MCP_TRANSPORT_MAX_SESSIONS` | `maxSessions` | `1000` | +| `MCP_TRANSPORT_MAX_QUEUE_SIZE` | `maxQueueSize` | `1000` | + +### Notification Configuration + +| Variable | Maps to | Example | +|----------|---------|---------| +| `MCP_NOTIFICATION_RATE_LIMIT_PER_MINUTE` | `rateLimitPerMinute` | `60` | +| `MCP_NOTIFICATION_DEFAULT_DEBOUNCE_MS` | `defaultDebounceMs` | `100` | +| `MCP_NOTIFICATION_MAX_QUEUED_NOTIFICATIONS` | `maxQueuedNotifications` | `1000` | + +### Arcade Integration + +| Variable | Maps to | Example | +|----------|---------|---------| +| `ARCADE_API_KEY` | `apiKey` | `arc_...` | +| `ARCADE_API_URL` | `apiUrl` | `https://api.arcade.dev` | +| `ARCADE_AUTH_DISABLED` | `authDisabled` | `true`, `false` | +| `ARCADE_WORKER_SECRET` | `serverSecret` | `"secret_..."` | +| `ARCADE_ENVIRONMENT` | `environment` | `dev`, `prod` | +| `ARCADE_USER_ID` | `userId` | `"user_123"` | Access environment variables with `Bun.env`: ```typescript const debug = Bun.env.MCP_DEBUG === 'true'; -const port = Number(Bun.env.MCP_HTTP_PORT) || 8000; +const port = Number(Bun.env.ARCADE_SERVER_PORT) || 8000; ``` ## MCPSettings @@ -53,7 +100,7 @@ const port = Number(Bun.env.MCP_HTTP_PORT) || 8000; For advanced configuration, use the `MCPSettings` class: ```typescript -import { MCPSettings } from 'arcade-mcp'; +import { MCPSettings } from 'arcade-mcp-server'; // Load from environment variables const settings = MCPSettings.fromEnv(); @@ -62,7 +109,7 @@ const settings = MCPSettings.fromEnv(); Or construct with explicit options: ```typescript -import { MCPSettings } from 'arcade-mcp'; +import { MCPSettings } from 'arcade-mcp-server'; const settings = new MCPSettings({ debug: true, @@ -74,20 +121,26 @@ const settings = new MCPSettings({ }, middleware: { enableLogging: true, + enableErrorHandling: true, logLevel: 'DEBUG', maskErrorDetails: false, }, transport: { - type: 'http', - httpHost: '0.0.0.0', - httpPort: 8000, + sessionTimeoutSeconds: 300, + cleanupIntervalSeconds: 10, + maxSessions: 1000, + maxQueueSize: 1000, }, arcade: { apiKey: Bun.env.ARCADE_API_KEY, apiUrl: 'https://api.arcade.dev', + authDisabled: false, + environment: 'dev', }, - notifications: { - enabled: true, + notification: { + rateLimitPerMinute: 60, + defaultDebounceMs: 100, + maxQueuedNotifications: 1000, }, toolEnvironment: { // Additional secrets available to tools @@ -100,7 +153,7 @@ const settings = new MCPSettings({ ### Using with MCPServer ```typescript -import { MCPServer, MCPSettings, ToolCatalog } from 'arcade-mcp'; +import { MCPServer, MCPSettings, ToolCatalog } from 'arcade-mcp-server'; const settings = MCPSettings.fromEnv(); @@ -135,14 +188,14 @@ interface MCPSettings { /** Middleware configuration */ middleware?: MiddlewareSettings; - /** Transport configuration */ + /** Transport configuration (session/connection management) */ transport?: TransportSettings; /** Arcade API configuration */ arcade?: ArcadeSettings; /** Notification settings */ - notifications?: NotificationSettings; + notification?: NotificationSettings; /** Tool environment / secrets */ toolEnvironment?: ToolEnvironmentSettings; @@ -171,32 +224,45 @@ interface ServerSettings { ```typescript interface MiddlewareSettings { - /** Enable request logging */ - enableLogging: boolean; + /** Enable request logging (default: true) */ + enableLogging?: boolean; - /** Log level */ - logLevel: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR'; + /** Enable error handling middleware (default: true) */ + enableErrorHandling?: boolean; - /** Hide stack traces in error responses (always true in production) */ - maskErrorDetails: boolean; + /** Log level (default: 'INFO') */ + logLevel?: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL'; + + /** Hide stack traces in error responses (default: false) */ + maskErrorDetails?: boolean; } ``` ### TransportSettings +Session and connection management for HTTP transport: + ```typescript interface TransportSettings { - /** Transport type */ - type: 'stdio' | 'http'; + /** Session timeout in seconds (default: 300) */ + sessionTimeoutSeconds?: number; + + /** Cleanup interval for expired sessions in seconds (default: 10) */ + cleanupIntervalSeconds?: number; - /** HTTP host (only for http transport) */ - httpHost: string; + /** Maximum concurrent sessions (default: 1000) */ + maxSessions?: number; - /** HTTP port (only for http transport) */ - httpPort: number; + /** Maximum queue size per session (default: 1000) */ + maxQueueSize?: number; } ``` + + Transport *type* (`stdio` or `http`), host, and port are passed to `app.run()`, not stored in settings. + TransportSettings controls session behavior, not connection parameters. + + ### ArcadeSettings ```typescript @@ -204,20 +270,37 @@ interface ArcadeSettings { /** Arcade API key for auth and tool hosting */ apiKey?: string; - /** Arcade API URL (defaults to production) */ + /** Arcade API URL (default: 'https://api.arcade.dev') */ apiUrl?: string; + + /** Disable authentication (default: false) */ + authDisabled?: boolean; + + /** Server secret for worker endpoints (required to enable worker routes) */ + serverSecret?: string; + + /** Environment: 'dev' or 'prod' (default: 'dev') */ + environment?: 'dev' | 'prod'; + + /** User ID for Arcade environment */ + userId?: string; } ``` ### NotificationSettings +Rate limiting and queuing for client notifications: + ```typescript interface NotificationSettings { - /** Enable notifications to clients */ - enabled: boolean; + /** Maximum notifications per minute per client (default: 60) */ + rateLimitPerMinute?: number; - /** Notification timeout in milliseconds */ - timeoutMs?: number; + /** Default debounce time in milliseconds (default: 100) */ + defaultDebounceMs?: number; + + /** Maximum queued notifications per client (default: 1000) */ + maxQueuedNotifications?: number; } ``` @@ -233,7 +316,7 @@ interface ToolEnvironmentSettings { ``` - Any environment variable not prefixed with `MCP_` or `ARCADE_` is automatically + Any environment variable not prefixed with `MCP_` or starting with `_` is automatically available as a tool secret via `getSecret()`. @@ -241,7 +324,7 @@ interface ToolEnvironmentSettings { Settings are resolved in this order (first wins): -1. Constructor options +1. Constructor options / `app.run()` options 2. `MCPSettings` object 3. Environment variables 4. Default values @@ -251,11 +334,11 @@ Settings are resolved in this order (first wins): ### Development vs Production ```typescript -import { MCPApp } from 'arcade-mcp'; +import { ArcadeMCP } from 'arcade-mcp-server'; const isDev = Bun.env.NODE_ENV !== 'production'; -const app = new MCPApp({ +const app = new ArcadeMCP({ name: 'my-server', logLevel: isDev ? 'DEBUG' : 'INFO', // In development, show full error details @@ -263,6 +346,7 @@ const app = new MCPApp({ }); app.run({ + transport: 'http', host: isDev ? '127.0.0.1' : '0.0.0.0', port: Number(Bun.env.PORT) || 8000, reload: isDev, @@ -272,7 +356,7 @@ app.run({ ### Loading from Config File ```typescript -import { MCPSettings } from 'arcade-mcp'; +import { MCPSettings } from 'arcade-mcp-server'; // Load from JSON file const configFile = Bun.file('./config.json'); @@ -281,41 +365,20 @@ const config = await configFile.json(); const settings = new MCPSettings(config); ``` -### Sharing Configuration - -```typescript -import { MCPSettings, MCPServer, ToolCatalog } from 'arcade-mcp'; - -// Shared settings across multiple servers -const baseSettings = MCPSettings.fromEnv(); - -const serverA = new MCPServer({ - catalog: catalogA, - settings: baseSettings, - name: 'server-a', -}); - -const serverB = new MCPServer({ - catalog: catalogB, - settings: baseSettings, - name: 'server-b', -}); -``` - ### Validating Configuration at Startup ```typescript -import { MCPApp, FatalToolError } from 'arcade-mcp'; +import { ArcadeMCP } from 'arcade-mcp-server'; const requiredEnvVars = ['ARCADE_API_KEY', 'DATABASE_URL']; for (const envVar of requiredEnvVars) { if (!Bun.env[envVar]) { - throw new FatalToolError(`Missing required environment variable: ${envVar}`); + throw new Error(`Missing required environment variable: ${envVar}`); } } -const app = new MCPApp({ name: 'my-server' }); +const app = new ArcadeMCP({ name: 'my-server' }); ``` ## Type-Safe Configuration @@ -323,13 +386,13 @@ const app = new MCPApp({ name: 'my-server' }); Use TypeScript's `satisfies` for type-checked config objects: ```typescript -import type { MCPAppOptions } from 'arcade-mcp'; +import type { ArcadeMCPOptions } from 'arcade-mcp-server'; const config = { name: 'my-server', version: '1.0.0', logLevel: 'DEBUG', -} satisfies MCPAppOptions; +} satisfies ArcadeMCPOptions; -const app = new MCPApp(config); +const app = new ArcadeMCP(config); ``` diff --git a/app/en/references/mcp/typescript/transports/page.mdx b/app/en/references/mcp/typescript/transports/page.mdx index 9c59a2a68..e4fd00669 100644 --- a/app/en/references/mcp/typescript/transports/page.mdx +++ b/app/en/references/mcp/typescript/transports/page.mdx @@ -7,37 +7,31 @@ import { Callout } from "nextra/components"; # Transport Modes -Transports define how your MCP server communicates with AI clients. Choose based on your deployment: +Transports define how your MCP server talks to AI clients. Pick based on how you're deploying: -| Transport | Use case | -|-----------|----------| -| **stdio** | Claude Desktop, VS Code, local tools | -| **HTTP** | Web deployments, cloud hosting, multiple clients | +| Transport | How it works | Use for | +|-----------|--------------|---------| +| **stdio** | Client spawns your server as a subprocess, communicates via stdin/stdout | Claude Desktop, VS Code, Cursor, CLI tools | +| **HTTP** | Your server runs standalone, clients connect via HTTP + SSE | Cloud deployments, multiple clients, load balancers | - stdio is the default. Use `transport: 'http'` for web deployments. + **Rule of thumb:** Use stdio for desktop apps (they spawn your server). Use HTTP when your server runs independently. ## stdio Transport -Communicates via standard input/output. Claude Desktop spawns your server as a subprocess. - -### When to Use - -- Claude Desktop integration -- VS Code extensions -- Command-line tools -- Local development +Communicates via standard input/output. The AI client spawns your server as a subprocess and pipes JSON-RPC messages through stdin/stdout. ### Usage ```typescript -import { MCPApp } from 'arcade-mcp'; +import { ArcadeMCP } from 'arcade-mcp-server'; import { z } from 'zod'; -const app = new MCPApp({ name: 'my-server' }); +const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' }); app.tool('ping', { + description: 'Health check', input: z.object({}), handler: () => 'pong', }); @@ -53,8 +47,11 @@ bun run server.ts **Recommended: Use the Arcade CLI** +Install the Arcade CLI, then configure Claude Desktop: + ```bash -arcade configure claude --host local +pip install arcade-cli # requires Python +arcade configure claude ``` This configures Claude Desktop to run your server as a stdio subprocess. @@ -66,7 +63,7 @@ Edit Claude Desktop's config file: | Platform | Path | |----------|------| | **macOS** | `~/Library/Application Support/Claude/claude_desktop_config.json` | -| **Windows** | `%APPDATA%\Claude\claude_desktop_config.json` | +| **Windows** | `C:\Users\\AppData\Roaming\Claude\claude_desktop_config.json` | | **Linux** | `~/.config/Claude/claude_desktop_config.json` | ```json @@ -87,24 +84,18 @@ Edit Claude Desktop's config file: ## HTTP Transport -REST API with Server-Sent Events (SSE) for streaming. Built on Elysia. - -### When to Use - -- Web applications -- Cloud deployments (Railway, Fly.io, etc.) -- Multiple concurrent clients -- Behind load balancers +REST API with Server-Sent Events (SSE) for streaming. Built on [Elysia](https://elysiajs.com/). ### Usage ```typescript -import { MCPApp } from 'arcade-mcp'; +import { ArcadeMCP } from 'arcade-mcp-server'; import { z } from 'zod'; -const app = new MCPApp({ name: 'my-server' }); +const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' }); app.tool('ping', { + description: 'Health check', input: z.object({}), handler: () => 'pong', }); @@ -112,37 +103,52 @@ app.tool('ping', { app.run({ transport: 'http', host: '0.0.0.0', port: 8080 }); ``` -### Endpoints +### How HTTP Transport Works -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/worker/health` | GET | Health check (returns 200 OK) | -| `/mcp` | GET | SSE stream for server-initiated messages | -| `/mcp` | POST | Send JSON-RPC message | -| `/mcp` | DELETE | Terminate session (when using session IDs) | +The client-server flow: - - **Response modes:** POST requests may receive either `application/json` (single response) or - `text/event-stream` (SSE stream). The SDK handles both automatically. Clients should include - `Accept: application/json, text/event-stream` in requests. - +1. **Request/Response:** Client POSTs JSON-RPC to `/mcp`, gets a JSON response. This works independently — no SSE connection required. +2. **Real-time updates (optional):** For progress updates or logs during long-running tools, clients open an SSE stream via `GET /mcp` *before* making POST requests. The SDK routes `progress.report()` and `log.info()` calls to this stream. +3. **Session cleanup:** Client can DELETE `/mcp` to end a session (for multi-client scenarios with session tracking) -### API Documentation (Optional) +You just write handlers and return values. The SDK handles protocol wiring. -Add Elysia's OpenAPI plugin for interactive API docs: +### SSE and Progress Updates -```bash -bun add @elysiajs/openapi -``` +When you call `progress.report()` or `log.info()` in your tool handler, the SDK automatically streams those updates to the client via SSE: ```typescript -import { openapi } from '@elysiajs/openapi'; +app.tool('processData', { + description: 'Process a list of items with progress updates', + input: z.object({ items: z.array(z.string()) }), + handler: async ({ input, progress, log }) => { + await log.info('Starting processing...'); + + for (let i = 0; i < input.items.length; i++) { + await processItem(input.items[i]); + await progress.report(i + 1, input.items.length); // Sent via SSE + } -// Add before app.run() -app.elysia.use(openapi({ path: '/docs' })); + return { processed: input.items.length }; + }, +}); ``` -Scalar UI will be available at `/docs`. See the [Elysia OpenAPI docs](https://elysiajs.com/plugins/openapi). +The client receives these updates in real-time over the SSE stream. You don't need to manage the streaming — just call the context methods and the SDK routes them to the right connection. + +### Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/worker/health` | GET | Health check (returns 200 OK) | +| `/mcp` | GET | Opens an SSE stream for server-initiated messages (progress updates, logs). Clients keep this open to receive real-time updates during tool execution. | +| `/mcp` | POST | Client sends JSON-RPC requests (tool calls, list requests). Returns JSON response. | +| `/mcp` | DELETE | Ends a client session. Use when your client tracks session IDs across requests (e.g., cleanup when a browser tab closes). Most stdio clients don't use this. | + + + MCP clients should include `Accept: application/json, text/event-stream` on POST requests. + The SDK returns JSON for immediate responses and streams via SSE when needed. + ### Development Mode @@ -157,7 +163,7 @@ app.run({ }); ``` -When `reload: true`, the server restarts automatically when you save files. +When `reload: true`, the SDK watches for file changes in your current working directory and restarts the server automatically when you save TypeScript, JavaScript, or `.env` files. ### TLS / HTTPS @@ -176,7 +182,7 @@ server { } ``` -For advanced TLS configuration, see [Server](./server). +For native TLS in Bun, see the [Bun.serve TLS docs](https://bun.sh/docs/api/http#tls). ### Docker @@ -187,6 +193,8 @@ COPY package.json bun.lock ./ RUN bun install --frozen-lockfile COPY . . EXPOSE 8000 +HEALTHCHECK --interval=30s --timeout=3s \ + CMD bun -e "fetch('http://localhost:8000/worker/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" CMD ["bun", "run", "server.ts"] ``` @@ -198,121 +206,127 @@ docker run -p 8000:8000 -e ARCADE_API_KEY=arc_... my-mcp-server ## Environment Variables ```bash +# Server identity MCP_SERVER_NAME="My MCP Server" MCP_SERVER_VERSION="1.0.0" -MCP_LOG_LEVEL=DEBUG -MCP_HTTP_HOST=0.0.0.0 -MCP_HTTP_PORT=8080 + +# Logging +MCP_MIDDLEWARE_LOG_LEVEL=DEBUG + +# Runtime overrides (override app.run() options) +ARCADE_SERVER_TRANSPORT=http +ARCADE_SERVER_HOST=0.0.0.0 +ARCADE_SERVER_PORT=8080 +ARCADE_SERVER_RELOAD=false ``` -Access with `Bun.env`: +Access with `Bun.env` or `process.env`: ```typescript -const port = Number(Bun.env.MCP_HTTP_PORT) || 8000; +const port = Number(process.env.ARCADE_SERVER_PORT) || 8000; ``` -## Security +## Production Checklist -### stdio +Before deploying an HTTP server to production, verify each item: -- Runs in the security context of the parent process -- No network exposure -- Safe for local use +### Security -### HTTP +- [ ] **HTTPS enabled** — Use a reverse proxy (nginx, Caddy) or native TLS +- [ ] **Authentication** — Require API keys or tokens via Arcade or custom middleware +- [ ] **Rate limiting** — Add rate limiting middleware to prevent abuse +- [ ] **Bind address** — Use `127.0.0.1` for local-only, or firewall rules for public - - HTTP transport exposes network endpoints. For production: - +### Reliability -| Requirement | Solution | -|-------------|----------| -| **Use HTTPS** | Configure TLS or use a reverse proxy | -| **Enable authentication** | Use Arcade auth or custom middleware | -| **Rate limiting** | Add rate limiting middleware | -| **Firewall** | Never bind to `0.0.0.0` without firewall rules | +- [ ] **Error handling** — Use specific error types (`RetryableToolError`, `FatalToolError`) — see [Errors](/references/mcp/typescript/errors) +- [ ] **Lifecycle hooks** — Add `onStart`/`onStop` for database connections, cleanup +- [ ] **Health endpoint** — Verify `/worker/health` returns 200 OK -### CORS +### Observability -For browser clients, add the Elysia CORS plugin: +- [ ] **Logging** — Set `MCP_MIDDLEWARE_LOG_LEVEL=INFO` (or `DEBUG` for troubleshooting) +- [ ] **Metrics** — Add middleware to track request counts, latencies +- [ ] **Error tracking** — Integrate with Sentry, Datadog, or similar -```bash -bun add @elysiajs/cors -``` +### Deployment -```typescript -import { MCPApp } from 'arcade-mcp'; -import { cors } from '@elysiajs/cors'; +- [ ] **Environment variables** — Store secrets in env vars, not code +- [ ] **Container health checks** — Docker `HEALTHCHECK` or Kubernetes probes +- [ ] **Graceful shutdown** — Server waits for in-flight requests on SIGTERM -const app = new MCPApp({ name: 'my-server' }); + + For a complete production example, see [Examples](/references/mcp/typescript/examples). + -app.elysia.use(cors({ - origin: ['https://myapp.com'], - credentials: true, -})); -``` +## Security -See the [Elysia CORS docs](https://elysiajs.com/plugins/cors) for all options. +### stdio -## Advanced Transport Features +- Runs in the same process security context as the parent (Claude Desktop, etc.) +- No network ports opened +- No additional auth needed — the parent process already trusts you -### Custom Middleware (HTTP) +### HTTP -Add custom middleware to HTTP transports via the underlying Elysia instance: + + HTTP transport opens network endpoints. Lock it down before deploying to production. + -```typescript -import { MCPApp } from 'arcade-mcp'; +| Risk | Mitigation | +|------|------------| +| **Unencrypted traffic** | Use HTTPS (reverse proxy or TLS config) | +| **Unauthorized access** | Require auth tokens via Arcade or custom middleware | +| **Abuse/DoS** | Add rate limiting middleware | +| **Open to the internet** | Bind to `127.0.0.1` for local-only, or use firewall rules | -const app = new MCPApp({ name: 'my-server' }); +## Lifecycle Hooks -// Add custom response headers -app.elysia.onAfterHandle(({ set }) => { - set.headers['X-Custom-Header'] = 'value'; -}); +Hook into server startup, shutdown, and HTTP requests: -// Log all requests -app.elysia.onRequest(({ request }) => { - console.log(`${request.method} ${request.url}`); +```typescript +app.onStart(async () => { + await db.connect(); + console.error('Server ready'); }); -``` - -### Transport Events -Listen to transport lifecycle events: - -```typescript -app.elysia.onStart(({ server }) => { - console.log(`Server running at ${server?.url}`); +app.onStop(async () => { + await db.disconnect(); + console.error('Server stopped'); }); -app.elysia.onStop(() => { - console.log('Server shutting down...'); +// HTTP request logging (HTTP transport only) +app.onRequest(({ request }) => { + console.error(`${request.method} ${request.url}`); }); ``` +| Hook | When it runs | Transport | +|------|--------------|-----------| +| `onStart` | Before server accepts connections | Both | +| `onStop` | During graceful shutdown | Both | +| `onRequest` | Every HTTP request | HTTP only | + ## Transport Selection Select transport based on environment or CLI args: ```typescript -import { MCPApp } from 'arcade-mcp'; +import { ArcadeMCP } from 'arcade-mcp-server'; import { z } from 'zod'; -const app = new MCPApp({ name: 'my-server' }); +const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' }); app.tool('ping', { + description: 'Health check', input: z.object({}), handler: () => 'pong', }); // Check command line args -const transport = Bun.argv[2] === 'stdio' ? 'stdio' : 'http'; +const transport = process.argv[2] === 'stdio' ? 'stdio' : 'http'; -app.run({ - transport, - host: '0.0.0.0', - port: 8000, -}); +app.run({ transport, port: 8000 }); ``` ```bash diff --git a/app/en/references/mcp/typescript/types/page.mdx b/app/en/references/mcp/typescript/types/page.mdx index 1d0d21248..0c365c058 100644 --- a/app/en/references/mcp/typescript/types/page.mdx +++ b/app/en/references/mcp/typescript/types/page.mdx @@ -14,16 +14,22 @@ Core TypeScript types used in the SDK. like custom transports or protocol-level work. + + Types marked with 📘 are not yet documented in the Python SDK reference (though + most exist in the Python implementation). This helps track documentation parity. + + ## Common Types -### `ToolContext` +### `ToolContext` 📘 Context passed to your tool handlers. Destructure what you need: ```typescript interface AuthInfo { token: string; - provider: string; + /** Provider-specific user data (e.g., sub, email, name). Shape depends on provider and scopes. */ + userInfo: Record; } interface ToolContext< @@ -34,7 +40,7 @@ interface ToolContext< /** Validated input matching your Zod schema */ input: TInput; - /** OAuth token and provider — non-optional when requiresAuth is set */ + /** OAuth token and user info — non-optional when requiresAuth is set */ authorization: TAuth extends true ? AuthInfo : AuthInfo | undefined; /** @@ -43,8 +49,225 @@ interface ToolContext< */ getSecret(key: TSecrets): string; - /** Metadata from the client (if requiresMetadata was set) */ + /** Metadata from Arcade auth (e.g., client_id, coordinator_url) */ metadata: Record; + + /** User identifier for this request (from auth, config, or session) */ + userId: string | null; + + /** Unique identifier for this MCP session (null for stdio without session management) */ + sessionId: string | null; + + /** Unique identifier for this specific request */ + requestId: string | null; + + // ─── Runtime Capabilities ───────────────────────────────────────── + + /** MCP protocol logging — sent to the AI client, not console */ + log: { + debug(message: string): Promise; + info(message: string): Promise; + warning(message: string): Promise; + error(message: string): Promise; + }; + + /** Progress reporting for long-running operations */ + progress: { + report(progress: number, total?: number, message?: string): Promise; + }; + + /** Read resources exposed by the server */ + resources: { + /** Read a resource, returns array (may have multiple parts) */ + read(uri: string): Promise; + /** Convenience: read and return first content item, throws if not found */ + get(uri: string): Promise; + /** List available resources */ + list(): Promise; + /** List URI templates for dynamic resources */ + listTemplates(): Promise; + /** List client roots (directories the client has shared) */ + listRoots(): Promise; + }; + + /** Call other tools from within a tool handler */ + tools: { + /** Execute a tool and get raw result */ + call(name: string, args?: Record): Promise; + /** List available tools */ + list(): Promise; + }; + + /** Access prompts exposed by the server */ + prompts: { + get(name: string, args?: Record): Promise; + list(): Promise; + }; + + /** Request LLM completions from the client (if supported) */ + sampling: { + createMessage(request: CreateMessageRequest): Promise; + }; + + /** Create rich UI elements (elicitation, forms) */ + ui: { + /** + * Request structured input from the user. + * Schema must be an object with primitive properties only + * (string, number, integer, boolean). String formats: email, uri, date, date-time. + */ + elicit>( + message: string, + schema: T, + options?: { timeout?: number } + ): Promise>>; + }; + + /** Notify client when server capabilities change */ + notifications: { + tools: { listChanged(): Promise }; + resources: { listChanged(): Promise }; + prompts: { listChanged(): Promise }; + }; +} + +// ─── Supporting Types ─────────────────────────────────────────────── + +/** Content returned when reading a resource */ +interface ResourceContents { + uri: string; + mimeType?: string; + text?: string; + blob?: string; // Base64-encoded binary +} + +/** Resource metadata */ +interface Resource { + uri: string; + name: string; + description?: string; + mimeType?: string; +} + +/** URI template for dynamic resources */ +interface ResourceTemplate { + uriTemplate: string; + name: string; + description?: string; + mimeType?: string; +} + +/** Client root directory */ +interface Root { + uri: string; + name?: string; +} + +/** Tool metadata */ +interface Tool { + name: string; + description?: string; + inputSchema: JsonSchema; +} + +/** Prompt metadata */ +interface Prompt { + name: string; + description?: string; + arguments?: PromptArgument[]; +} + +/** Result from getting a prompt */ +interface GetPromptResult { + description?: string; + messages: PromptMessage[]; +} + +/** Request for LLM sampling */ +interface CreateMessageRequest { + messages: SamplingMessage[]; + systemPrompt?: string; + maxTokens: number; + temperature?: number; + stopSequences?: string[]; +} + +/** Message for/from sampling */ +interface SamplingMessage { + role: 'user' | 'assistant'; + content: TextContent | ImageContent | AudioContent; +} + +/** Result from sampling.createMessage */ +interface CreateMessageResult { + role: 'assistant'; + content: TextContent | ImageContent | AudioContent; + model: string; + stopReason?: 'endTurn' | 'stopSequence' | 'maxTokens'; +} + +/** Result from tools.call */ +interface CallToolResult { + content: ContentItem[]; + structuredContent?: Record; + isError?: boolean; +} + +/** Result from elicitation UI */ +interface ElicitResult { + action: 'accept' | 'decline' | 'cancel'; + content?: T; +} + +/** JSON Schema (subset used by MCP) */ +type JsonSchema = Record; + +/** Prompt argument definition */ +interface PromptArgument { + name: string; + description?: string; + required?: boolean; +} + +/** Message in a prompt */ +interface PromptMessage { + role: 'user' | 'assistant'; + content: TextContent | ImageContent | AudioContent | EmbeddedResource | ResourceLink; +} + +/** Text content block */ +interface TextContent { + type: 'text'; + text: string; +} + +/** Image content block */ +interface ImageContent { + type: 'image'; + data: string; // Base64 encoded + mimeType: string; +} + +/** Audio content block */ +interface AudioContent { + type: 'audio'; + data: string; // Base64 encoded + mimeType: string; +} + +/** Embedded resource content */ +interface EmbeddedResource { + type: 'resource'; + resource: ResourceContents; +} + +/** Link to a resource by URI */ +interface ResourceLink { + type: 'resource_link'; + uri: string; + name: string; + description?: string; + mimeType?: string; } ``` @@ -52,7 +275,7 @@ interface ToolContext< ```typescript import { z } from 'zod'; -import { Google } from 'arcade-mcp/auth'; +import { Google } from 'arcade-mcp-server/auth'; // Without requiresAuth: authorization is optional app.tool('publicTool', { @@ -77,7 +300,7 @@ app.tool('privateTool', { ```typescript app.tool('search', { input: z.object({ query: z.string() }), - requiresSecrets: ['API_KEY'] as const, + requiresSecrets: ['API_KEY'] as const, // as const is required! handler: ({ getSecret }) => { getSecret('API_KEY'); // ✅ Autocomplete works // getSecret('OTHER'); // ❌ TypeScript error @@ -85,9 +308,80 @@ app.tool('search', { }); ``` + + **Why `as const`?** Without it, TypeScript infers `string[]` instead of the literal tuple `['API_KEY']`. The `as const` preserves the exact strings so `getSecret()` can type-check against them. + + Secrets are validated at startup. Missing secrets fail fast with a clear error. -### Type Inference with Zod +### Runtime Capabilities 📘 + +Tools have access to 8 runtime capabilities via the context object. Destructure what you need: + +```typescript +app.tool('processData', { + input: z.object({ uri: z.string() }), + handler: async ({ + input, + log, // MCP protocol logging + progress, // Progress reporting + resources, // Read server resources + tools, // Call other tools + prompts, // Access prompts + sampling, // Request LLM completions + ui, // Elicitation / forms + notifications // Notify list changes + }) => { + // Log progress to the client + await log.info('Starting...'); + await progress.report(0, 3); + + // Read a resource (get() returns single item, read() returns array) + const content = await resources.get(input.uri); + await progress.report(1, 3); + + // Call another tool (returns CallToolResult) + const result = await tools.call('analyze', { data: content.text }); + if (result.isError) throw new Error('Analysis failed'); + await progress.report(2, 3); + + // Ask user for confirmation via elicitation + const confirm = await ui.elicit('Proceed?', z.object({ + confirmed: z.boolean(), + })); + + if (confirm.action !== 'accept' || !confirm.content?.confirmed) { + return 'Cancelled by user'; + } + + await progress.report(3, 3, 'Done'); + return result.structuredContent ?? result.content; + }, +}); +``` + +| Capability | Description | +|------------|-------------| +| `log` | Send debug/info/warning/error messages to the client | +| `progress` | Report progress for long-running operations | +| `resources` | Read resources exposed by the server | +| `tools` | Call other tools from within a tool handler | +| `prompts` | Access prompt templates defined on the server | +| `sampling` | Request LLM completions from the client | +| `ui` | Create elicitation forms for user input | +| `notifications` | Notify client when tools/resources/prompts change | + + +Not all clients support all capabilities. `sampling` and `ui.elicit` depend on client support. + + + +**Elicitation Schema Restrictions**: MCP limits elicitation schemas to object types with primitive +properties only (`string`, `number`, `integer`, `boolean`). String properties support formats: +`email`, `uri`, `date`, `date-time`. Nested objects and arrays are not allowed. + + +### Type Inference with Zod 📘 The SDK fully leverages Zod's type inference. Your handler receives typed input automatically: @@ -116,7 +410,7 @@ app.tool('search', { }); ``` -## Tool Response Types +## Tool Response Types 📘 Handlers can return any value. The SDK auto-wraps: @@ -126,33 +420,21 @@ Handlers can return any value. The SDK auto-wraps: | `object` | `{ content: [{ type: 'text', text: JSON.stringify(obj) }] }` | | `{ content: [...] }` | Passed through unchanged | -### `ContentItem` +### `ContentItem` 📘 For full control over responses, return content items directly: ```typescript -type ContentItem = TextContent | ImageContent | ResourceContent; - -interface TextContent { - type: 'text'; - text: string; -} - -interface ImageContent { - type: 'image'; - data: string; // Base64 encoded - mimeType: string; // e.g., 'image/png' -} - -interface ResourceContent { - type: 'resource'; - uri: string; - mimeType?: string; - text?: string; - blob?: string; // Base64 encoded -} +type ContentItem = + | TextContent + | ImageContent + | AudioContent + | EmbeddedResource + | ResourceLink; ``` +Individual content types (`TextContent`, `ImageContent`, etc.) are defined in the `ToolContext` section above. + **Example:** ```typescript @@ -190,11 +472,38 @@ interface CallToolResult { } ``` -## Schema Types + + **Important:** When `isError: true`, the AI client knows the tool failed and can decide whether to retry. Always set this flag when returning error information — don't just return an error message as regular content. + + +**Returning errors correctly:** + +```typescript +app.tool('fetchUser', { + input: z.object({ id: z.string() }), + handler: async ({ input }) => { + const user = await db.findUser(input.id); + + if (!user) { + // ✅ Correct: Set isError so AI knows this failed + return { + content: [{ type: 'text', text: `User ${input.id} not found` }], + isError: true, + }; + } + + return user; + }, +}); +``` + +For most error cases, throw specific error types instead — see [Errors](/references/mcp/typescript/errors). + +## Schema Types 📘 The SDK uses Zod 4 for input validation. -### Converting Schemas to JSON Schema +### Converting Schemas to JSON Schema 📘 Use `z.toJSONSchema()` to convert Zod schemas for AI clients: @@ -219,7 +528,7 @@ z.toJSONSchema(schema); // } ``` -### Adding Metadata +### Adding Metadata 📘 Use `.describe()` for simple descriptions or `.meta()` for richer metadata: @@ -243,7 +552,7 @@ z.string().meta({ description: 'An email' }); Both are preserved in JSON Schema output. -### Extracting TypeScript Types +### Extracting TypeScript Types 📘 Use `z.infer` to extract TypeScript types from schemas: @@ -267,11 +576,11 @@ function processUser(user: User) { } ``` -## Protocol Types +## Protocol Types 📘 These are low-level types for custom transports or debugging. -### `MCPMessage` +### `MCPMessage` 📘 Base type for all MCP protocol messages: @@ -303,7 +612,7 @@ interface JSONRPCNotification { } ``` -### `ServerSession` +### `ServerSession` 📘 ```typescript interface ServerSession { @@ -323,6 +632,8 @@ interface ServerSession { ### `SessionMessage` +*Also documented in Python SDK* + ```typescript interface SessionMessage { message: MCPMessage; @@ -330,9 +641,9 @@ interface SessionMessage { } ``` -## Auth Types +## Auth Types 📘 -### `AuthProvider` +### `AuthProvider` 📘 ```typescript interface AuthProvider { @@ -344,10 +655,10 @@ interface AuthProvider { } ``` -### Creating Auth Providers +### Creating Auth Providers 📘 ```typescript -import { Google, GitHub, Slack } from 'arcade-mcp/auth'; +import { Google, GitHub, Slack } from 'arcade-mcp-server/auth'; // Google with specific scopes Google({ scopes: ['profile', 'email'] }) @@ -359,9 +670,9 @@ GitHub({ scopes: ['repo', 'user'] }) Slack({ scopes: ['chat:write', 'users:read'] }) ``` -## Error Adapter Types +## Error Adapter Types 📘 -### `ErrorAdapter` +### `ErrorAdapter` 📘 Translates vendor-specific exceptions into Arcade errors: @@ -370,12 +681,30 @@ interface ErrorAdapter { /** Identifier for logging/metrics */ slug: string; - /** Translate an exception into an Arcade error, or null if not handled */ - fromException(error: unknown): MCPError | null; + /** Translate an exception into an Arcade tool error, or null if not handled */ + fromException(error: unknown): ToolError | null; +} + +/** Base class for tool execution errors */ +class ToolError extends Error { + canRetry: boolean; + statusCode?: number; + developerMessage?: string; +} + +/** Error from an upstream service (API, database, etc.) */ +class UpstreamError extends ToolError {} + +/** Error that should not be retried */ +class FatalToolError extends ToolError {} + +/** Error that can be retried */ +class RetryableToolError extends ToolError { + retryAfterMs?: number; } ``` -### Built-in Adapters +### Built-in Adapters 📘 ```typescript import { @@ -384,7 +713,7 @@ import { MicrosoftGraphErrorAdapter, HTTPErrorAdapter, GraphQLErrorAdapter, -} from 'arcade-mcp/adapters'; +} from 'arcade-mcp-server/adapters'; app.tool('sendMessage', { input: z.object({ channel: z.string(), text: z.string() }), @@ -407,29 +736,28 @@ app.tool('sendMessage', { Adapters are tried in order. First match wins. `HTTPErrorAdapter` is always added as fallback. -## Utility Types +## Utility Types 📘 -### `MaterializedTool` +### `MaterializedTool` 📘 -A tool object ready for registration. Created via `tool()`: +A tool object with type inference. Created via `tool()`: ```typescript -import { tool } from 'arcade-mcp'; +import { tool } from 'arcade-mcp-server'; import { z } from 'zod'; const myTool = tool({ - name: 'my-tool', // required for runtime registration description: 'Does something', input: z.object({ value: z.string() }), - handler: ({ input }) => `Got: ${input.value}`, + handler: ({ input }) => `Got: ${input.value}`, // input is typed }); -// Use with runtime APIs -await app.tools.add(myTool); -await server.tools.add(myTool); +// Register with app.tool() or runtime APIs — name is always first arg +app.tool('my-tool', myTool); +await server.tools.add('my-tool', myTool); ``` -### `ToolOptions` +### `ToolOptions` 📘 Complete tool configuration type: @@ -453,10 +781,10 @@ interface ToolOptions< The `TAuth` type parameter enables conditional typing: when `requiresAuth` is set, `authorization` becomes non-optional in the handler context. -### `MCPAppOptions` +### `ArcadeMCPOptions` 📘 ```typescript -interface MCPAppOptions { +interface ArcadeMCPOptions { name?: string; version?: string; title?: string; @@ -466,19 +794,6 @@ interface MCPAppOptions { host?: string; port?: number; reload?: boolean; - cors?: CorsOptions; -} -``` - -### `CorsOptions` - -```typescript -interface CorsOptions { - origin?: string | string[] | boolean; - methods?: string[]; - allowedHeaders?: string[]; - exposedHeaders?: string[]; - credentials?: boolean; - maxAge?: number; + adapter?: object; // For Node.js: node() from @elysiajs/node } ``` From b8046ee27c9e8c19e5a392fb4de058ba71561d9d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 22 Dec 2025 15:54:12 -0800 Subject: [PATCH 4/5] docs: Fix Python SDK server examples to use public start/stop methods --- app/en/references/mcp/python/server/page.mdx | 24 +++++++++++++++---- .../references/mcp/python/transports/page.mdx | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/en/references/mcp/python/server/page.mdx b/app/en/references/mcp/python/server/page.mdx index 813876708..060841ffa 100644 --- a/app/en/references/mcp/python/server/page.mdx +++ b/app/en/references/mcp/python/server/page.mdx @@ -72,6 +72,22 @@ Initialize MCP server. | `arcade_api_key` | `str \| None` | Arcade API key (overrides settings) | `None` | | `arcade_api_url` | `str \| None` | Arcade API URL (overrides settings) | `None` | +#### `start` + +```python +async start() +``` + +Initialize the server. Call before running a transport. + +#### `stop` + +```python +async stop() +``` + +Gracefully shut down the server. + #### `handle_message` ```python @@ -122,13 +138,13 @@ from arcade_mcp_server.transports.stdio import StdioTransport async def main(): catalog = ToolCatalog() server = MCPServer(catalog=catalog, name="example", version="1.0.0") - await server._start() + await server.start() try: # Run stdio transport loop transport = StdioTransport() await transport.run(server) finally: - await server._stop() + await server.stop() if __name__ == "__main__": asyncio.run(main()) @@ -145,12 +161,12 @@ from arcade_mcp_server.transports.http_streamable import HTTPStreamableTransport async def run_http(): catalog = ToolCatalog() server = MCPServer(catalog=catalog) - await server._start() + await server.start() try: transport = HTTPStreamableTransport(host="0.0.0.0", port=8000) await transport.run(server) finally: - await server._stop() + await server.stop() asyncio.run(run_http()) ``` diff --git a/app/en/references/mcp/python/transports/page.mdx b/app/en/references/mcp/python/transports/page.mdx index 07a70e46a..9308877c3 100644 --- a/app/en/references/mcp/python/transports/page.mdx +++ b/app/en/references/mcp/python/transports/page.mdx @@ -50,7 +50,7 @@ app.run(transport="stdio") For Claude Desktop, use the `arcade configure` command: ```bash -arcade configure claude --host local +arcade configure claude ``` Or manually edit `~/Library/Application Support/Claude/claude_desktop_config.json`: From 5657da352b8e532933a9fd9f825e3bc8123efe85 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 22 Dec 2025 16:47:53 -0800 Subject: [PATCH 5/5] docs(typescript): clarity improvements for SDK reference - overview: clarify Arcade as hosted auth service - overview: de-emphasize tool() wrapper, link to Types page - transports: note SSE is handled automatically by SDK - middleware: clarify when to use middleware vs app.onRequest() - types: add Quick Reference table for ToolContext - settings: explain MCP_* vs ARCADE_* env var naming convention --- .../mcp/typescript/middleware/page.mdx | 5 ++- .../mcp/typescript/overview/page.mdx | 38 ++----------------- .../mcp/typescript/settings/page.mdx | 24 +++++++++++- .../mcp/typescript/transports/page.mdx | 2 +- .../references/mcp/typescript/types/page.mdx | 15 ++++++++ 5 files changed, 46 insertions(+), 38 deletions(-) diff --git a/app/en/references/mcp/typescript/middleware/page.mdx b/app/en/references/mcp/typescript/middleware/page.mdx index 04a4bf77b..c0176d0b7 100644 --- a/app/en/references/mcp/typescript/middleware/page.mdx +++ b/app/en/references/mcp/typescript/middleware/page.mdx @@ -9,8 +9,9 @@ import { Callout } from "nextra/components"; Most users don't need custom middleware. The SDK includes logging and error - handling by default. Use middleware when you need request validation, custom - authentication, rate limiting, or metrics. + handling by default. Use middleware when you need to inspect MCP message content + (tool names, arguments) — for HTTP-level concerns like request logging or headers, + use `app.onRequest()` instead. Middleware intercepts MCP messages before they reach your tools and after responses are generated. diff --git a/app/en/references/mcp/typescript/overview/page.mdx b/app/en/references/mcp/typescript/overview/page.mdx index f79b03538..57b4a99b2 100644 --- a/app/en/references/mcp/typescript/overview/page.mdx +++ b/app/en/references/mcp/typescript/overview/page.mdx @@ -17,7 +17,7 @@ An **MCP server** exposes **tools** (functions the AI can call), **resources** ( `arcade-mcp-server` is a TypeScript SDK for building MCP servers. It works standalone — no account required — for tools that use API keys or no auth at all. -When your tools need to act on behalf of users (sending emails, accessing files, posting to Slack), connect to [Arcade](https://arcade.dev). Arcade manages OAuth consent flows, token storage, and refresh — so you don't build auth UIs or store credentials. +When your tools need to act on behalf of users (sending emails, accessing files, posting to Slack), connect to [Arcade](https://arcade.dev). Arcade is a hosted auth service that manages OAuth consent flows, token storage, and refresh — so you don't build auth UIs or store credentials. - **Clean API** — Register tools with Zod schemas, get type-safe handlers - **Transport flexibility** — stdio for Claude Desktop, HTTP for web deployments @@ -80,7 +80,7 @@ app.tool('greet', { handler: ({ input }) => `Hello, ${input.name}!`, }); -// reload: true restarts on file changes (dev only) +// reload: true watches for file changes and restarts (dev only) app.run({ transport: 'http', port: 8000, reload: true }); ``` @@ -160,9 +160,7 @@ Secrets are read from environment variables and validated at startup. Tools are always registered with `app.tool('name', options)`. The name is the first argument. -### Inline (most projects) - -Define tools directly — `app.tool()` provides full type inference: +For most projects, define tools inline — `app.tool()` provides full type inference: ```typescript app.tool('greet', { @@ -172,35 +170,7 @@ app.tool('greet', { }); ``` -### Multi-file (larger projects) - -For tools defined in separate files, use `tool()` to preserve type inference, then register with `app.tool()`: - -```typescript -// tools/math.ts -import { tool } from 'arcade-mcp-server'; -import { z } from 'zod'; - -export const add = tool({ - description: 'Add two numbers', - input: z.object({ a: z.number(), b: z.number() }), - handler: ({ input }) => input.a + input.b, -}); -``` - -```typescript -// server.ts -import { ArcadeMCP } from 'arcade-mcp-server'; -import { add } from './tools/math'; - -const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' }); -app.tool('add', add); // Name provided at registration -app.run({ transport: 'http', port: 8000 }); -``` - - - **Why `tool()`?** Exporting a plain object loses the schema→handler type connection. The `tool()` wrapper preserves it. - +For larger projects with tools in separate files, see [the `tool()` wrapper](/references/mcp/typescript/types#materializedtool) in Types. ## Runtime Capabilities diff --git a/app/en/references/mcp/typescript/settings/page.mdx b/app/en/references/mcp/typescript/settings/page.mdx index d51574630..dd32cc634 100644 --- a/app/en/references/mcp/typescript/settings/page.mdx +++ b/app/en/references/mcp/typescript/settings/page.mdx @@ -30,6 +30,10 @@ const app = new ArcadeMCP({ The SDK reads these environment variables as defaults. Constructor options take precedence. + + **Naming convention:** `MCP_*` vars configure the SDK (server identity, middleware, sessions). `ARCADE_*` vars configure Arcade platform integration and runtime overrides typically set by deploy scripts. + + ### Server Configuration | Variable | Maps to | Example | @@ -173,6 +177,10 @@ const secrets = settings.toolSecrets(); // => { DATABASE_URL: '...', REDIS_URL: '...', ... } ``` + + `toolSecrets()` returns the **pool** of available secrets. When a tool declares `requiresSecrets: ['API_KEY']`, the SDK pulls that key from this pool and injects it into the handler. Missing secrets are validated at startup. + + ## Settings Reference ### MCPSettings @@ -355,10 +363,24 @@ app.run({ ### Loading from Config File +**config.json:** + +```json +{ + "debug": true, + "server": { + "name": "my-server", + "version": "1.0.0" + }, + "middleware": { + "logLevel": "DEBUG" + } +} +``` + ```typescript import { MCPSettings } from 'arcade-mcp-server'; -// Load from JSON file const configFile = Bun.file('./config.json'); const config = await configFile.json(); diff --git a/app/en/references/mcp/typescript/transports/page.mdx b/app/en/references/mcp/typescript/transports/page.mdx index e4fd00669..52cac517a 100644 --- a/app/en/references/mcp/typescript/transports/page.mdx +++ b/app/en/references/mcp/typescript/transports/page.mdx @@ -111,7 +111,7 @@ The client-server flow: 2. **Real-time updates (optional):** For progress updates or logs during long-running tools, clients open an SSE stream via `GET /mcp` *before* making POST requests. The SDK routes `progress.report()` and `log.info()` calls to this stream. 3. **Session cleanup:** Client can DELETE `/mcp` to end a session (for multi-client scenarios with session tracking) -You just write handlers and return values. The SDK handles protocol wiring. +You just write handlers and return values. The SDK handles protocol wiring — you don't need to manage SSE connections manually. ### SSE and Progress Updates diff --git a/app/en/references/mcp/typescript/types/page.mdx b/app/en/references/mcp/typescript/types/page.mdx index 0c365c058..ecda72c7c 100644 --- a/app/en/references/mcp/typescript/types/page.mdx +++ b/app/en/references/mcp/typescript/types/page.mdx @@ -19,6 +19,21 @@ Core TypeScript types used in the SDK. most exist in the Python implementation). This helps track documentation parity. +## Quick Reference + +| You want to... | Use | +|----------------|-----| +| Access validated input | `input` | +| Get OAuth token | `authorization.token` | +| Read a secret | `getSecret('KEY')` | +| Log to AI client | `log.info()`, `log.error()` | +| Report progress | `progress.report(current, total)` | +| Read a resource | `resources.get(uri)` | +| Call another tool | `tools.call(name, args)` | +| Ask user for input | `ui.elicit(message, schema)` | + +All are properties of `ToolContext`, passed to your handler. + ## Common Types ### `ToolContext` 📘