Arcade MCP TypeScript SDK
What is MCP?
The Model Context Protocol () is an open standard that lets AI assistants call external and access data. When you ask Claude to “search my docs” or “send a Slack message,” MCP makes it happen.
An server exposes (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 servers. It works standalone — no required — for tools that use or no auth at all.
When your need to act on behalf of (sending emails, accessing files, posting to Slack), connect to Arcade . Arcade manages OAuth consent flows, token storage, and refresh — so you don’t build auth UIs or store credentials.
- Clean API — Register 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
requiresAuthto let act as the (Google, GitHub, Slack, etc.)
Works without Arcade. Need OAuth? Sign up →
Installation
Bun (recommended)
bun add arcade-mcp-serverRequires Bun 1.3+ with strict: true and moduleResolution: bundler in tsconfig. Bun runs TypeScript directly — just bun run server.ts. See Bun TypeScript docs .
Node.js
The SDK uses Elysia for HTTP transport. Elysia supports Node.js via the @elysiajs/node adapter.
npm install arcade-mcp-server @elysiajs/nodeimport { ArcadeMCP } from 'arcade-mcp-server';
import { node } from '@elysiajs/node';
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 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
// server.ts
import { ArcadeMCP } from 'arcade-mcp-server';
import { z } from 'zod';
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 to greet'),
}),
handler: ({ input }) => `Hello, ${input.name}!`,
});
// reload: true restarts on file changes (dev only)
app.run({ transport: 'http', port: 8000, reload: true });bun run server.tsTest it:
# 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":{}}'Adding OAuth
When your needs to act on behalf of a , use requiresAuth. Arcade handles the OAuth consent flow, token refresh, and secure storage.
import { ArcadeMCP } from 'arcade-mcp-server';
import { Google } from 'arcade-mcp-server/auth';
import { z } from 'zod';
const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' });
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();
},
});
app.run({ transport: 'http', port: 8000 });When you add requiresAuth, the handler receives an authorization object with the ’s OAuth token. The AI never sees the token — it stays server-side.
Adding Secrets
For your server needs (not OAuth tokens), use requiresSecrets:
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();
},
});Secrets are read from environment variables and validated at startup.
Organizing Tools
are always registered with app.tool('name', options). The name is the first argument.
Inline (most projects)
Define directly — app.tool() provides full type inference:
app.tool('greet', {
description: 'Greet someone',
input: z.object({ name: z.string() }),
handler: ({ input }) => `Hello, ${input.name}!`,
});Multi-file (larger projects)
For defined in separate files, use tool() to preserve type inference, then register with app.tool():
// 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,
});// 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.
Runtime Capabilities
Tool handlers receive a object. Destructure what you need:
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;
},
});Common fields:
| 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 |
See Types for the complete ToolContext reference including prompts, sampling, ui, and notifications.
Lifecycle Hooks
Hook into server startup, shutdown, and HTTP requests:
const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' });
app.onStart(async () => {
await db.connect();
});
app.onStop(async () => {
await db.disconnect();
});
app.onRequest(({ request }) => {
console.error(`${request.method} ${request.url}`);
});
app.run({ transport: 'http', port: 8000 });See Transports for the full reference.
ArcadeMCP vs MCPServer
| Class | Description |
|---|---|
ArcadeMCP | High-level. Manages transport, lifecycle, provides app.tool(). |
MCPServer | Low-level. No transport — you wire it yourself. |
Start with ArcadeMCP. Use MCPServer when embedding in an existing app, building custom transports, or testing without network. See Server for details.
Next Steps
- Examples — Complete, runnable example servers
- Transports — stdio for Claude Desktop, HTTP for web
- Server — Low-level APIs, prompts, resources
- Middleware — Request/response interception
- Errors — Error types and handling
- Settings — Configuration and environment variables
- Types — TypeScript interfaces and schemas