Skip to Content
API & SDKsArcade MCPTypeScriptOverview

Arcade MCP (MCP Server SDK) - TypeScript Overview

arcade-mcp, the secure framework for building servers, provides a clean, minimal API to build programmatically. It handles collection, server configuration, and transport setup with a developer-friendly interface.

Installation

Terminal
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  for the complete recommended config.

Imports

TypeScript
// Main exports import { MCPApp, MCPServer, materializeTool } 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 });
Terminal
bun run server.ts

Test it works:

Terminal
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 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

An Elysia-powered interface for building servers. Handles registration, configuration, and server lifecycle.

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; /** CORS configuration for HTTP transport */ cors?: CorsOptions; }

Defaults:

OptionDefaultNotes
name'ArcadeMCP'
version'1.0.0'
logLevel'INFO'
transport'stdio'Auto-switches to 'http' if host/port set
host'127.0.0.1'
port8000

app.tool()

Register a 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<TInput>; /** Tool handler function — authorization is non-optional when requiresAuth is set */ handler: ( context: ToolContext<TInput, TSecrets, TAuth extends AuthProvider ? true : false> ) => unknown | Promise<unknown>; /** 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 (destructure what you need):

TypeScript
handler: ({ input, authorization, getSecret, metadata }) => { // 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`) // metadata — Additional metadata from the client }

Return values are auto-wrapped:

Return typeBecomes
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<void>
TypeScript
interface RunOptions { transport?: 'stdio' | 'http'; host?: string; port?: number; reload?: boolean; }

Runtime APIs

After the server starts, you can modify , prompts, and resources at runtime.

Use materializeTool() to create objects for runtime registration:

TypeScript
import { materializeTool } from 'arcade-mcp'; import { z } from 'zod'; const tool = materializeTool({ name: 'dynamic-tool', description: 'Added at runtime', input: z.object({ value: z.string() }), handler: ({ input }) => `Got: ${input.value}`, }); await app.tools.add(tool); // Remove a tool await app.tools.remove('tool-name'); // List all tools const tools = await app.tools.list();
TypeScript
// Add a prompt await app.prompts.add(prompt, handler); // Add a resource await app.resources.add(resource);

Lifecycle Hooks

TypeScript
app.onStart(async () => { // Initialize database connections, etc. }); app.onStop(async () => { // Clean up resources });

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 ). Use requiresSecrets for 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();

Never log or return secrets. The SDK ensures secrets stay server-side.

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, metadata }) => { const sessionId = metadata.sessionId as string; 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 when the SDK converts schemas to JSON Schema for AI clients via z.toJSONSchema().

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', }); // Lifecycle hooks app.onStart(async () => { await db.connect(); }); app.onStop(async () => { await db.disconnect(); }); // 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 }); }
Terminal
bun run server.ts
Last updated on

Arcade MCP (MCP Server SDK) - TypeScript Overview | Arcade Docs