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, 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 });
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

A type-safe, developer-friendly interface for building servers. Handles 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:

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, 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 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; }

app.addToolsFromModule()

Add all from a module at once.

app.tool() vs tool():

  • app.tool('name', { ... }) — Register a directly
  • tool({ ... }) — Create a for addToolsFromModule() or runtime app.tools.add()

Use the standalone tool() function to define exportable , 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);

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 , 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);

primitives: 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 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 ). 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();

How auth works: When a requires auth, Arcade coordinates consent via OAuth. The token is passed to your handler automatically.

Secrets are loaded from environment variables at startup. 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 }); }
Terminal
bun run server.ts
Last updated on

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