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
bun add arcade-mcptsconfig: Run bun init to generate one, or ensure yours has these essentials:
{
"compilerOptions": {
"strict": true,
"moduleResolution": "bundler",
"target": "ESNext",
"module": "Preserve"
}
}See Bun TypeScript docs for the complete recommended config.
Imports
// Main exports
import { MCPApp, MCPServer, 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
// 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 });bun run server.tsTest it works:
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 transport.
Constructor
new MCPApp(options?: MCPAppOptions)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 that AI clients can call.
app.tool(name: string, options: ToolOptions): voidinterface 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):
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 type | Becomes |
|---|---|
string | { content: [{ type: 'text', text: '...' }] } |
object | { content: [{ type: 'text', text: JSON.stringify(...) }] } |
{ content: [...] } | Passed through unchanged |
app.run()
Start the server.
app.run(options?: RunOptions): Promise<void>interface RunOptions {
transport?: 'stdio' | 'http';
host?: string;
port?: number;
reload?: boolean;
}app.addToolsFromModule()
Add all from a module at once. Use the standalone tool() function to define exportable tools, then addToolsFromModule() discovers and registers them:
// 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,
});// 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:
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:
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();// 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);See the Server reference for full prompts and resources API.
Examples
Simple Tool
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.
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:
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:
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
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
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 });
}bun run server.ts