Skip to Content
API & SDKsArcade MCPTypeScriptOverview

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 requiresAuth to let act as the (Google, GitHub, Slack, etc.)

Works without Arcade. Need OAuth? Sign up → 

Installation

Terminal
bun add arcade-mcp-server

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 .

Node.js

The SDK uses Elysia for HTTP transport. Elysia supports Node.js via the @elysiajs/node adapter.

Terminal
npm install arcade-mcp-server @elysiajs/node
TypeScript
import { 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

TypeScript
// 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 });
Terminal
bun run server.ts

Test it:

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

TypeScript
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:

TypeScript
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:

TypeScript
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():

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.

Runtime Capabilities

Tool handlers receive a object. Destructure what you need:

TypeScript
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:

FieldDescription
inputValidated input matching your Zod schema
authorizationOAuth token (when requiresAuth is set)
getSecretGet secret values (when requiresSecrets is set)
logSend logs to the client (log.info(), log.error(), etc.)
progressReport progress for long-running operations
resourcesRead resources exposed by the server
toolsCall 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:

TypeScript
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

ClassDescription
ArcadeMCPHigh-level. Manages transport, lifecycle, provides app.tool().
MCPServerLow-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.

Python SDK has the same pattern: MCPApp (high-level) wraps MCPServer (low-level).

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
Last updated on

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