AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified MIT Self-run

Arcade Mcp Ts

mcp-arcadeai-arcade-mcp-ts · by ArcadeAI

TypeScript framework for building MCP servers with built-in OAuth (21 providers), secret injection, middleware, multi-user JWT auth, and worker routes. Wraps @modelcontextprotocol/sdk.

No reviews yet
0 installs
5 views
0.0% view→install

Install

$ agentstack add mcp-arcadeai-arcade-mcp-ts

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-arcadeai-arcade-mcp-ts)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
4mo ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Arcade Mcp Ts? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

@arcadeai/arcade-mcp

TypeScript MCP framework with secret injection, OAuth auth providers, multi-user support, worker routes, and middleware. Wraps the official @modelcontextprotocol/sdk — never forks or patches it.

Quick Start

bun add @arcadeai/arcade-mcp
import { MCPApp } from "@arcadeai/arcade-mcp";
import { z } from "zod";

const app = new MCPApp({
  name: "MyServer",
  version: "1.0.0",
  instructions: "A helpful tool server",
});

app.tool(
  "greet",
  {
    description: "Greet someone by name",
    parameters: z.object({
      name: z.string().describe("Name to greet"),
    }),
  },
  async (args) => `Hello, ${args.name}!`,
);

app.run(); // stdio by default

Run it:

bun run server.ts

Or over HTTP:

app.run({ transport: "http", port: 8000 });

CLI Auto-Discovery

Run an MCP server without writing a server file. The CLI auto-discovers tool modules in the current directory:

npx @arcadeai/arcade-mcp          # auto-discover tools, run stdio
npx @arcadeai/arcade-mcp --http   # auto-discover tools, run HTTP

Tool modules are discovered from:

  • *.tools.ts / *.tools.js files (e.g., math.tools.ts)
  • Any file inside a tools/ directory (e.g., tools/greet.ts)

Each file should export tool definitions:

// tools/greet.ts
import { z } from "zod";

export const greetTools = {
  greet: {
    options: {
      description: "Greet someone",
      parameters: z.object({ name: z.string() }),
    },
    handler: async (args) => `Hello, ${args.name}!`,
  },
};

CLI options:

| Flag | Default | Description | |---|---|---| | --http | — | Use HTTP transport (default: stdio) | | --host | 127.0.0.1 | HTTP host | | --port | 8000 | HTTP port | | --name | directory name | App name | | --dir | cwd | Directory to scan | | --dev | — | Auto-reload on file changes (HTTP only) |

> Node.js + TypeScript: Use npx tsx arcade-mcp or Bun to import .ts tool files directly.

Dev Mode (Auto-Reload)

Watch source files and automatically restart the server on changes:

npx @arcadeai/arcade-mcp --http --dev

Or programmatically:

app.run({ transport: "http", dev: true });

When a .ts, .js, .mts, or .mjs file changes, the server stops, re-imports tool modules with fresh copies, and restarts. Files in node_modules/, dist/, and hidden directories are ignored.

> Note: Dev mode only works with HTTP transport. Stdio sessions cannot be restarted.

You can also enable dev mode via the ARCADE_SERVER_RELOAD=1 environment variable.

Features

  • Builder APIapp.tool(name, options, handler) with method chaining
  • Secret injection — env vars auto-captured and injected into tool context
  • OAuth auth providers — 21 providers: GitHub, Google, Slack, Microsoft, Linear, Notion, etc.
  • Context object — namespaced facades for logging, progress, sampling, resources, tools, UI
  • Middleware — composable onion-model middleware with method-specific hooks
  • Multi-user HTTP auth — JWT Bearer token validation via JWKS
  • Worker routes/worker/tools, /worker/tools/invoke, /worker/health
  • Error hierarchy — structured errors with retry support, upstream error mapping
  • Promptsapp.prompt(name, options, handler) with argument validation and runtime management
  • Resourcesapp.resource(uri, options, handler) with MIME types and runtime management
  • Dev mode — auto-reload on file changes with --dev flag (HTTP only)
  • Resumable streams — optional event store for HTTP stream resumability via Last-Event-ID
  • Evals — evaluate LLM tool-calling accuracy with critics, rubrics, and Hungarian-optimal matching
  • Dual transport — stdio and HTTP (Elysia + StreamableHTTP)
  • Runtime compatible — Bun and Node.js (no Bun.* APIs in library code)

Tool Options

Basic Tool

app.tool(
  "echo",
  {
    description: "Echo a message",
    parameters: z.object({
      message: z.string(),
    }),
  },
  async (args) => args.message,
);

Tool with OAuth

import { auth } from "@arcadeai/arcade-mcp";

app.tool(
  "star_repo",
  {
    description: "Star a GitHub repository",
    parameters: z.object({
      owner: z.string(),
      repo: z.string(),
    }),
    auth: auth.GitHub({ scopes: ["repo"] }),
  },
  async (args, context) => {
    const token = context.getAuthToken();
    // ... use token to call GitHub API
    return { starred: true };
  },
);

Tool with Secrets

app.tool(
  "get_repo",
  {
    description: "Get repo info",
    parameters: z.object({ repo: z.string() }),
    secrets: ["GITHUB_TOKEN"],
  },
  async (args, context) => {
    const token = context.getSecret("GITHUB_TOKEN");
    // ... use token
  },
);

Any env var not prefixed with MCP_ or _ is available as a tool secret.

Tool with Behavior Hints

Annotate tools with behavioral hints that map to MCP ToolAnnotations:

app.tool(
  "delete_file",
  {
    description: "Delete a file from the workspace",
    parameters: z.object({ path: z.string() }),
    behavior: {
      readOnly: false,
      destructive: true,
      idempotent: true,
      openWorld: false,
    },
  },
  async (args) => {
    // ...
  },
);

These hints are exposed as readOnlyHint, destructiveHint, idempotentHint, and openWorldHint in the MCP tool listing.

Deprecated Tools

Mark tools as deprecated — the message is prepended to the description:

app.tool(
  "old_search",
  {
    description: "Search for items",
    parameters: z.object({ query: z.string() }),
    deprecationMessage: "Use search_v2 instead",
  },
  async (args) => {
    // ...
  },
);
// Description seen by clients: "[DEPRECATED: Use search_v2 instead] Search for items"

Tool Title

Provide a human-readable display name:

app.tool(
  "gh_star",
  {
    description: "Star a GitHub repository",
    parameters: z.object({ repo: z.string() }),
    title: "Star Repository",
  },
  async (args) => {
    // ...
  },
);

Toolkit Versioning

The app's name, version, and title are automatically attached to every tool as toolkit metadata. You can also override toolkit info per-tool:

app.tool(
  "myTool",
  {
    description: "A tool with custom toolkit info",
    parameters: z.object({}),
    toolkit: { name: "my-toolkit", version: "1.2.0" },
  },
  async () => {},
);

Versions are normalized to semver — "1" becomes "1.0.0", "v1.2" becomes "1.2.0".

Prompts

Register prompts with app.prompt(name, options, handler?):

app.prompt(
  "greeting",
  {
    description: "Generate a greeting",
    arguments: [{ name: "name", description: "Name to greet", required: true }],
  },
  (args) => ({
    messages: [
      {
        role: "user",
        content: { type: "text", text: `Please greet ${args.name} warmly.` },
      },
    ],
  }),
);

Options: description? and arguments? (array of { name, description?, required? }). If no handler is provided, a default handler returns the description as a user message.

Runtime management (after app.run()):

app.prompts.add("new-prompt", { description: "Added at runtime" }, handler);
app.prompts.remove("new-prompt");
app.prompts.list(); // returns registered prompt names

Resources

Register resources with app.resource(uri, options, handler?):

app.resource(
  "config://app",
  { description: "Application configuration", mimeType: "application/json" },
  (uri) => ({
    contents: [
      {
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({ name: "EchoServer", version: "1.0.0" }),
      },
    ],
  }),
);

Options: description? and mimeType?. If no handler is provided, a default handler returns empty text content.

Runtime management (after app.run()):

app.resources.add("data://users", { mimeType: "application/json" }, handler);
app.resources.remove("data://users");
app.resources.list(); // returns registered resource URIs

Auth Providers

Factory functions for 21 OAuth2 providers:

import { auth } from "@arcadeai/arcade-mcp";

auth.GitHub({ scopes: ["repo"] })
auth.Google({ scopes: ["https://www.googleapis.com/auth/calendar"] })
auth.Slack({ scopes: ["chat:write"], id: "my-slack" })
auth.Microsoft()
auth.Linear()
auth.Notion()
// ... Asana, Atlassian, Attio, ClickUp, Discord, Dropbox,
//     Figma, Hubspot, LinkedIn, PagerDuty, Reddit, Spotify,
//     Twitch, X, Zoom

Arcade Cloud Auth (Local Development)

Tools with auth requirements automatically resolve OAuth tokens through Arcade Cloud. There are two ways to set up credentials:

Option 1: Arcade CLI (recommended)

Install the Arcade CLI and sign in. This stores credentials in ~/.arcade/credentials.yaml which the framework reads automatically:

pip install arcade-ai
arcade login

That's it — no environment variables needed. Run your server and tools will authenticate through your Arcade account:

bun run examples/github-tools/server.ts

Option 2: Environment variables

Set ARCADE_API_KEY and ARCADE_USER_ID directly:

export ARCADE_API_KEY="your-arcade-api-key"
export ARCADE_USER_ID="your-user-id"

Environment variables take priority over the credentials file.

How it works

When a tool with auth is called, the framework calls Arcade Cloud's authorization API:

  1. First call — returns an authorization URL. Visit the URL in your browser to complete the OAuth flow.
  2. Retry the tool — the token is now available and injected into context.getAuthToken().

This is automatic — no code changes needed. The same tools work both locally (via Arcade Cloud auth) and deployed (via worker routes where Arcade Cloud injects tokens directly).

Set ARCADE_AUTH_DISABLED=true to skip auth resolution (useful for testing with mock tokens).

Context

Tool handlers receive (args, context). The context provides namespaced facades:

app.tool("example", opts, async (args, context) => {
  // Secrets & auth
  context.getSecret("API_KEY");
  context.getAuthToken();
  context.getAuthTokenOrEmpty();

  // Logging
  context.log.info("Processing request");
  context.log.debug("Details", { extra: "data" });
  context.log.warning("Watch out");
  context.log.error("Something failed");

  // Progress
  await context.progress.report(50, 100, "Halfway done");

  // Notifications (deduplicated, flushed at end of request)
  await context.notifications.tools.listChanged();
  await context.notifications.resources.listChanged();
  await context.notifications.prompts.listChanged();

  // Metadata
  context.signal;      // AbortSignal
  context.sessionId;   // string | undefined
  context.requestId;   // string
  context.userId;      // string | undefined
});

Middleware

Composable middleware with an onion model. Override any hook:

import { Middleware, composeMiddleware } from "@arcadeai/arcade-mcp";

class RateLimitMiddleware extends Middleware {
  async onCallTool(context, next) {
    // before
    const result = await next(context);
    // after
    return result;
  }
}

const app = new MCPApp({
  name: "MyServer",
  version: "1.0.0",
  middleware: composeMiddleware(
    new RateLimitMiddleware(),
  ),
});

Available hooks: onMessage, onRequest, onCallTool, onListTools, onReadResource, onListResources, onListResourceTemplates, onGetPrompt, onListPrompts.

Built-in middleware (enabled by default):

  • ErrorHandlingMiddleware — catches errors, returns structured MCP error responses
  • LoggingMiddleware — logs request/response timing (auto-detects TTY for pretty output; override with MCP_LOG_FORMAT=json|pretty)

Multi-User HTTP Auth

Validate JWT Bearer tokens against JWKS endpoints:

import { MCPApp, JWTResourceServerValidator } from "@arcadeai/arcade-mcp";

const app = new MCPApp({
  name: "MyServer",
  version: "1.0.0",
  auth: new JWTResourceServerValidator({
    canonicalUrl: "https://mcp.example.com/mcp",
    authorizationServers: [{
      authorizationServerUrl: "https://auth.example.com",
      issuer: "https://auth.example.com",
      jwksUri: "https://auth.example.com/.well-known/jwks.json",
      algorithm: "RS256",
      expectedAudiences: ["my-client-id"],
    }],
  }),
});

app.run({ transport: "http", port: 8000 });

Supports RFC 9728 OAuth Protected Resource Metadata discovery. When canonicalUrl has a non-root path (e.g. https://example.com/mcp), both /.well-known/oauth-protected-resource and /.well-known/oauth-protected-resource/mcp are registered for backward compatibility. Responses include CORS headers.

Resumable Streams

Enable HTTP stream resumability so disconnected clients can resume from where they left off using the Last-Event-ID header:

import { MCPApp, InMemoryEventStore } from "@arcadeai/arcade-mcp";

const app = new MCPApp({ name: "MyServer", version: "1.0.0" });

app.run({
  transport: "http",
  eventStore: new InMemoryEventStore(),
});

The InMemoryEventStore is suitable for single-process deployments. For distributed systems, implement the EventStore interface with a persistent backend:

import type { EventStore, EventId, StreamId } from "@arcadeai/arcade-mcp";
import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";

class RedisEventStore implements EventStore {
  async storeEvent(streamId: StreamId, message: JSONRPCMessage): Promise {
    // Store in Redis...
  }
  async replayEventsAfter(
    lastEventId: EventId,
    { send }: { send: (eventId: EventId, message: JSONRPCMessage) => Promise },
  ): Promise {
    // Replay from Redis...
  }
}

Session Management

The HTTP transport uses an HTTPSessionManager that supports stateful (default) and stateless modes, TTL-based session eviction, and max session caps:

app.run({
  transport: "http",
  stateless: false,       // true = fresh transport per request, no session reuse
  sessionTtlMs: 300_000,  // evict idle sessions after 5 minutes
  maxSessions: 100,       // reject new sessions with 503 when at capacity
});

In stateful mode (default), sessions are reused via the mcp-session-id header. Invalid session IDs receive a 400 response.

In stateless mode, every request gets a fresh transport and server — no sessions are tracked.

You can also use HTTPSessionManager directly for more control:

import { HTTPSessionManager } from "@arcadeai/arcade-mcp";

const manager = new HTTPSessionManager({
  server: arcadeMcpServer,
  sessionTtlMs: 60_000,
  maxSessions: 50,
});

// In your HTTP handler:
const response = await manager.handleRequest(request, { authInfo });

// Graceful shutdown:
await manager.close();

Each stateful HTTP session is backed by a ServerSession that adds:

  • Initialization state trackingNOT_INITIALIZED → INITIALIZING → INITIALIZED
  • Server-initiated requestscreateMessage(), elicitInput(), listRoots() with timeout and error handling
  • Session-scoped data — key/value storage per session via getData() / setData()
  • Notification broadcastingNotificationManager sends tool/resource/prompt list-changed notifications to all or selected sessions

The Context facades context.sampling.createMessage() and context.ui.elicit() automatically delegate to the ServerSession when available.

Worker Routes

When ARCADE_WORKER_SECRET is set, expose tool execution endpoints for Arcade Cloud integration:

import { createWorkerRoutes } from "@arcadeai/arcade-mcp";

const workerApp = createWorkerRoutes({
  catalog: app.catalog,
  secret:

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ArcadeAI](https://github.com/ArcadeAI)
- **Source:** [ArcadeAI/arcade-mcp-ts](https://github.com/ArcadeAI/arcade-mcp-ts)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.