Transport Modes
Transports define how your server talks to AI clients. Pick based on how you’re deploying:
| Transport | How it works | Use for |
|---|---|---|
| stdio | Client spawns your server as a subprocess, communicates via stdin/stdout | Claude Desktop, VS Code, Cursor, CLI tools |
| HTTP | Your server runs standalone, clients connect via HTTP + SSE | Cloud deployments, multiple clients, load balancers |
Rule of thumb: Use stdio for desktop apps (they spawn your server). Use HTTP when your server runs independently.
stdio Transport
Communicates via standard input/output. The AI client spawns your server as a subprocess and pipes JSON-RPC messages through stdin/stdout.
Usage
import { ArcadeMCP } from 'arcade-mcp-server';
import { z } from 'zod';
const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' });
app.tool('ping', {
description: 'Health check',
input: z.object({}),
handler: () => 'pong',
});
app.run({ transport: 'stdio' });bun run server.tsClaude Desktop Configuration
Recommended: Use the Arcade CLI
Install the Arcade CLI, then configure Claude Desktop:
pip install arcade-cli # requires Python
arcade configure claudeThis configures Claude Desktop to run your server as a stdio subprocess.
Manual configuration:
Edit Claude Desktop’s config file:
| Platform | Path |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | C:\Users\<you>\AppData\Roaming\Claude\claude_desktop_config.json |
| Linux | ~/.config/Claude/claude_desktop_config.json |
{
"mcpServers": {
"my-tools": {
"command": "bun",
"args": ["run", "/path/to/server.ts"],
"cwd": "/path/to/your/tools"
}
}
}With stdio, all stdout is protocol data. Use console.error() for logging, never console.log().
HTTP Transport
REST API with Server-Sent Events (SSE) for streaming. Built on Elysia .
Usage
import { ArcadeMCP } from 'arcade-mcp-server';
import { z } from 'zod';
const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' });
app.tool('ping', {
description: 'Health check',
input: z.object({}),
handler: () => 'pong',
});
app.run({ transport: 'http', host: '0.0.0.0', port: 8080 });How HTTP Transport Works
The client-server flow:
- Request/Response: Client POSTs JSON-RPC to
/mcp, gets a JSON response. This works independently — no SSE connection required. - Real-time updates (optional): For progress updates or logs during long-running , clients open an SSE stream via
GET /mcpbefore making POST requests. The SDK routesprogress.report()andlog.info()calls to this stream. - Session cleanup: Client can DELETE
/mcpto end a session (for multi-client scenarios with session tracking)
You just write handlers and return values. The SDK handles protocol wiring.
SSE and Progress Updates
When you call progress.report() or log.info() in your handler, the SDK automatically streams those updates to the client via SSE:
app.tool('processData', {
description: 'Process a list of items with progress updates',
input: z.object({ items: z.array(z.string()) }),
handler: async ({ input, progress, log }) => {
await log.info('Starting processing...');
for (let i = 0; i < input.items.length; i++) {
await processItem(input.items[i]);
await progress.report(i + 1, input.items.length); // Sent via SSE
}
return { processed: input.items.length };
},
});The client receives these updates in real-time over the SSE stream. You don’t need to manage the streaming — just call the methods and the SDK routes them to the right connection.
Endpoints
| Endpoint | Method | Description |
|---|---|---|
/worker/health | GET | Health check (returns 200 OK) |
/mcp | GET | Opens an SSE stream for server-initiated messages (progress updates, logs). Clients keep this open to receive real-time updates during tool execution. |
/mcp | POST | Client sends JSON-RPC requests (tool calls, list requests). Returns JSON response. |
/mcp | DELETE | Ends a client session. Use when your client tracks session IDs across requests (e.g., cleanup when a browser tab closes). Most stdio clients don’t use this. |
clients should include Accept: application/json, text/event-stream on POST requests.
The SDK returns JSON for immediate responses and streams via SSE when needed.
Development Mode
Enable hot reload for faster development:
app.run({
transport: 'http',
host: '127.0.0.1',
port: 8000,
reload: true,
});When reload: true, the SDK watches for file changes in your current working directory and restarts the server automatically when you save TypeScript, JavaScript, or .env files.
TLS / HTTPS
For production, use a reverse proxy (nginx, Caddy) for TLS termination. This is the recommended approach:
# nginx example
server {
listen 443 ssl;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:8000;
}
}For native TLS in Bun, see the Bun.serve TLS docs .
Docker
FROM oven/bun:1
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
CMD bun -e "fetch('http://localhost:8000/worker/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["bun", "run", "server.ts"]docker build -t my-mcp-server .
docker run -p 8000:8000 -e ARCADE_API_KEY=arc_... my-mcp-serverEnvironment Variables
# Server identity
MCP_SERVER_NAME="My MCP Server"
MCP_SERVER_VERSION="1.0.0"
# Logging
MCP_MIDDLEWARE_LOG_LEVEL=DEBUG
# Runtime overrides (override app.run() options)
ARCADE_SERVER_TRANSPORT=http
ARCADE_SERVER_HOST=0.0.0.0
ARCADE_SERVER_PORT=8080
ARCADE_SERVER_RELOAD=falseAccess with Bun.env or process.env:
const port = Number(process.env.ARCADE_SERVER_PORT) || 8000;Production Checklist
Before deploying an HTTP server to production, verify each item:
Security
- HTTPS enabled — Use a reverse proxy (nginx, Caddy) or native TLS
- Authentication — Require or tokens via Arcade or custom middleware
- Rate limiting — Add rate limiting middleware to prevent abuse
- Bind address — Use
127.0.0.1for local-only, or firewall rules for public
Reliability
- Error handling — Use specific error types (
RetryableToolError,FatalToolError) — see Errors - Lifecycle hooks — Add
onStart/onStopfor database connections, cleanup - Health endpoint — Verify
/worker/healthreturns 200 OK
Observability
- Logging — Set
MCP_MIDDLEWARE_LOG_LEVEL=INFO(orDEBUGfor troubleshooting) - Metrics — Add middleware to track request counts, latencies
- Error tracking — Integrate with Sentry, Datadog, or similar
Deployment
- Environment variables — Store secrets in env vars, not code
- Container health checks — Docker
HEALTHCHECKor Kubernetes probes - Graceful shutdown — Server waits for in-flight requests on SIGTERM
For a complete production example, see Examples.
Security
stdio
- Runs in the same process security as the parent (Claude Desktop, etc.)
- No network ports opened
- No additional auth needed — the parent process already trusts you
HTTP
HTTP transport opens network endpoints. Lock it down before deploying to production.
| Risk | Mitigation |
|---|---|
| Unencrypted traffic | Use HTTPS (reverse proxy or TLS config) |
| Unauthorized access | Require auth tokens via Arcade or custom middleware |
| Abuse/DoS | Add rate limiting middleware |
| Open to the internet | Bind to 127.0.0.1 for local-only, or use firewall rules |
Lifecycle Hooks
Hook into server startup, shutdown, and HTTP requests:
app.onStart(async () => {
await db.connect();
console.error('Server ready');
});
app.onStop(async () => {
await db.disconnect();
console.error('Server stopped');
});
// HTTP request logging (HTTP transport only)
app.onRequest(({ request }) => {
console.error(`${request.method} ${request.url}`);
});| Hook | When it runs | Transport |
|---|---|---|
onStart | Before server accepts connections | Both |
onStop | During graceful shutdown | Both |
onRequest | Every HTTP request | HTTP only |
Transport Selection
Select transport based on environment or CLI args:
import { ArcadeMCP } from 'arcade-mcp-server';
import { z } from 'zod';
const app = new ArcadeMCP({ name: 'my-server', version: '1.0.0' });
app.tool('ping', {
description: 'Health check',
input: z.object({}),
handler: () => 'pong',
});
// Check command line args
const transport = process.argv[2] === 'stdio' ? 'stdio' : 'http';
app.run({ transport, port: 8000 });# Run with HTTP
bun run server.ts
# Run with stdio (for Claude Desktop)
bun run server.ts stdio