PluginBench
MCP Server
Active
Apache-2.0

io.github.cyanheads/mcp-ts-template MCP Server

io.github.cyanheads/mcp-ts-template

TypeScript framework for building MCP servers with declarative tools, auth, storage, and observability.

What is the io.github.cyanheads/mcp-ts-template MCP server?

@cyanheads/mcp-ts-core is a TypeScript framework for building MCP servers with declarative tool, resource, and prompt definitions. It handles transports, authentication, multi-backend storage, OpenTelemetry observability, and lifecycle management, letting you focus on domain logic rather than infrastructure. It supports Node.js, Bun, and Cloudflare Workers.

A production-ready TypeScript framework that simplifies MCP server development by providing declarative builders for tools, resources, and prompts with built-in support for authentication, error contracts, structured enrichment, multi-backend storage (in-memory, filesystem, Supabase, Cloudflare), and observability. Use it to wrap APIs and expose workflows to AI agents without writing transport or auth boilerplate.

How to install io.github.cyanheads/mcp-ts-template

Copy-paste configuration for popular MCP clients.

transport: stdio
Config generated by PluginBench — verify against the source before use.
Environment / auth
  • MCP_LOG_LEVEL

    Sets the minimum log level for output (e.g., 'debug', 'info', 'warn').

  • MCP_HTTP_HOST

    The hostname for the HTTP server.

  • MCP_HTTP_PORT

    The port to run the HTTP server on.

  • MCP_HTTP_ENDPOINT_PATH

    The endpoint path for the MCP server.

  • MCP_AUTH_MODE

    Authentication mode to use: 'none', 'jwt', or 'oauth'.

~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "mcp-ts-template": {
      "command": "bun",
      "args": [
        "-y",
        "mcp-ts-template",
        "run",
        "start:stdio"
      ],
      "env": {
        "MCP_LOG_LEVEL": "<YOUR_MCP_LOG_LEVEL>",
        "MCP_HTTP_HOST": "<YOUR_MCP_HTTP_HOST>",
        "MCP_HTTP_PORT": "<YOUR_MCP_HTTP_PORT>",
        "MCP_HTTP_ENDPOINT_PATH": "<YOUR_MCP_HTTP_ENDPOINT_PATH>",
        "MCP_AUTH_MODE": "<YOUR_MCP_AUTH_MODE>"
      }
    }
  }
}

Tools & capabilities

Tools this server exposes to the agent.

  • tool() — Builder to define a tool with input/output schemas, error contracts, enrichment, and async handler
  • resource() — Builder to define a resource with URI template parameters and async handler
  • prompt() — Builder to define a prompt with arguments and generation logic
  • appTool() — Builder to define an MCP Apps tool with auto-populated interactive HTML UI metadata
  • appResource() — Builder to define an MCP Apps HTML resource with correct MIME type and UI mirroring
  • createApp() — Entry point for Node.js/Bun servers; handles full lifecycle, config parsing, transport startup, and graceful shutdown
  • createWorkerHandler() — Entry point for Cloudflare Workers; returns an ExportedHandler with same definitions

Use cases

  • Build a tool that wraps an external API with typed error contracts and structured enrichment for richer agent reasoning
  • Create a multi-tenant MCP server with per-tenant storage and JWT/OAuth authentication
  • Expose long-running operations as task tools with progress reporting and cancellation support
  • Define resources that agents can subscribe to for real-time updates via notification APIs
  • Deploy the same tool definitions to Node.js, Bun, and Cloudflare Workers without code changes

io.github.cyanheads/mcp-ts-template MCP server FAQ

What is @cyanheads/mcp-ts-core?

It's a TypeScript framework for building MCP servers. You install it as a dependency (don't fork it), define tools/resources/prompts declaratively, and the framework handles transports, auth, storage, logging, and telemetry. Your agent collaborates with you to design domain logic.

Is it free?

Yes. The framework is open-source under Apache 2.0 license. Optional peer dependencies (Supabase, DuckDB, OpenTelemetry SDK) are installed only if you use them.

How do I install it in Cursor or Claude?

Install via npm: `npm install @cyanheads/mcp-ts-core`. Use the init command `bunx @cyanheads/mcp-ts-core init my-mcp-server` to scaffold a project with CLAUDE.md/AGENTS.md docs and Agent Skills that guide your coding agent through the full development cycle.

What authentication modes does it support?

Three modes: `none` (no auth), `jwt` (JWT signing with a secret key), and `oauth` (local secret or JWKS). Set via `MCP_AUTH_MODE` env var. Auth scopes are declared on tool/resource definitions and checked before dispatch.

What storage backends are available?

In-memory, filesystem, Supabase, and Cloudflare D1/KV/R2. Swap via `STORAGE_PROVIDER_TYPE` env var without changing handler code. Handlers use a unified `ctx.state` interface.

Does it support observability?

Yes. Built-in Pino logging with request correlation and tool metrics. Optional OpenTelemetry traces and metrics. All tool calls are logged with duration and payload sizes.

README (reference)

Source of truth, from the repository.

<div align="center"> <h1>@cyanheads/mcp-ts-core</h1> <p><b>Agent-native TypeScript framework for building MCP servers. Build tools, not infrastructure. Declarative definitions with auth, multi-backend storage, OpenTelemetry, and first-class support for Bun/Node/Cloudflare Workers.</b></p> </div> <div align="center">

Version License MCP Spec

MCP SDK TypeScript Bun

Framework

</div>

What is this?

@cyanheads/mcp-ts-core is the infrastructure layer for TypeScript MCP servers. Install it as a dependency — don't fork it. Your agent collaborates with you to design and build the tools, resources, and prompts for your server.

The framework handles the plumbing: transports, auth, config, logging, telemetry, & more.

import { createApp, tool, z } from '@cyanheads/mcp-ts-core';
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';

const search = tool('search', {
  description: 'Search the catalog and return ranked matches.',
  annotations: { readOnlyHint: true },
  input: z.object({
    query: z.string().describe('Search terms'),
    limit: z.number().default(10).describe('Max results'),
  }),
  output: z.object({
    items: z.array(z.string()).describe('Matching item names, best first'),
  }),
  enrichment: {
    effectiveQuery: z.string().describe('Query as the server parsed it'),
    totalCount: z.number().describe('Total matches before the limit'),
    notice: z.string().optional().describe('Guidance when nothing matched'),
  },
  errors: [
    {
      reason: 'index_unavailable',
      code: JsonRpcErrorCode.ServiceUnavailable,
      when: 'The upstream search index is unreachable.',
      retryable: true,
      recovery: 'Retry in a few seconds — the index may be briefly unavailable.',
    },
  ],
  handler: async (input, ctx) => {
    const res = await runSearch(input.query, input.limit);
    if (!res) throw ctx.fail('index_unavailable'); // genuine failure → typed error contract
    ctx.enrich({ effectiveQuery: res.parsed, totalCount: res.total });
    if (res.items.length === 0) {
      ctx.enrich({ notice: `No matches for "${input.query}". Try broader terms.` }); // empty result → notice, not a throw
    }
    return { items: res.items }; // enrichment never rides in the domain return
  },
});

await createApp({ tools: [search] });

That's a complete MCP server, and it shows both of the framework's core contracts.

enrichment carries the context an agent reasons with (the parsed query, the true total, an empty-result notice); the framework merges it into structuredContent and mirrors it into content[], so structuredContent-only clients (Claude Code) and content[]-only clients (Claude Desktop) both see it, no format() needed. The typed errors[] contract handles genuine failures (an empty result is a notice, not a throw), and the linter cross-checks both against the handler body. Both publish in tools/list, so clients preview a tool's success and failure shapes.

The rest is automatic: every tool call is logged with duration, payload sizes, and request correlation, and createApp() handles config parsing, logger init, transport startup, signal handlers, and graceful shutdown.

Quick start

bunx @cyanheads/mcp-ts-core init my-mcp-server
cd my-mcp-server
bun install

You get a scaffolded project with CLAUDE.md/AGENTS.md, Agent Skills, plugin metadata (Codex + Claude Code), and a src/ tree ready for your tools. Infrastructure (transports, auth, storage, telemetry, lifecycle, linting) lives in node_modules. What's left is domain: which APIs to wrap, which workflows to expose.

Start your coding agent (e.g. Claude Code, Codex) and describe what you want. The agent knows what to do from there. The included Agent Skills cover the full cycle: setup, design-mcp-server, scaffolding, testing, security-pass, release-and-publish, maintenance, & more.

What you get

The headline tool returns structured output. Clients that read structuredContent (Claude Code) get it directly. To also render markdown for clients that read content[] (Claude Desktop), add a format(). The format-parity linter checks it renders every output field, so the two surfaces never drift:

import { tool, z } from '@cyanheads/mcp-ts-core';

export const itemSearch = tool('item_search', {
  description: 'Search for items by query.',
  input: z.object({
    query: z.string().describe('Search query'),
    limit: z.number().default(10).describe('Max results'),
  }),
  output: z.object({
    items: z.array(z.string()).describe('Search results'),
  }),
  async handler(input) {
    const results = await doSearch(input.query, input.limit);
    return { items: results };
  },
  format: (result) => [
    { type: 'text', text: result.items.map((name) => `- ${name}`).join('\n') },
  ],
});

And resources:

import { resource, z } from '@cyanheads/mcp-ts-core';

export const itemData = resource('items://{itemId}', {
  description: 'Retrieve item data by ID.',
  params: z.object({
    itemId: z.string().describe('Item ID'),
  }),
  async handler(params, ctx) {
    return await getItem(params.itemId);
  },
});

Everything registers through createApp() in your entry point:

await createApp({
  name: 'my-mcp-server',
  version: '0.1.0',
  tools: allToolDefinitions,
  resources: allResourceDefinitions,
  prompts: allPromptDefinitions,
  instructions: 'Brief composition hints for the model.', // optional, sent on every `initialize`
});

It also works on Cloudflare Workers with createWorkerHandler() — same definitions, different entry point.

Features

  • Declarative definitions — tool(), resource(), prompt() builders with Zod schemas; appTool()/appResource() add interactive HTML UIs.
  • Server-level orientation — instructions on createApp/createWorkerHandler rides every initialize for the model. Cross-tool composition hints, regional notes, scope guidance — without leaking text into every tool description.
  • Server identity — optional title, websiteUrl, description, icons (SEP-973) on createApp/createWorkerHandler flow to initialize serverInfo, the /.well-known/mcp.json server card, and the landing page.
  • Unified Context — one ctx for logging, tenant-scoped storage, elicitation, cancellation, and task progress.
  • Auth — auth: ['scope'] on definitions, checked before dispatch (no wrapper code). Modes: none, jwt, or oauth (local secret or JWKS).
  • Task tools — task: true for long-running ops; framework manages create/poll/progress/complete/cancel.
  • Definition linter — validates names, schemas, auth scopes, annotations, format-parity, and cross-vendor JSON Schema portability at build time. Run via lint:mcp or devcheck — not invoked at server startup.
  • Typed error contracts — declare errors: [{ reason, code, when, recovery, retryable? }] and handlers get a typed ctx.fail(reason, …). Contracts publish in tools/list so clients preview failure modes; the linter cross-checks the handler. Factories (notFound(), httpErrorFromResponse(), …) cover ad-hoc throws; plain Error auto-classifies.
  • Multi-backend storage — in-memory, filesystem, Supabase, Cloudflare D1/KV/R2. Swap via env var; handlers don't change.
  • DataCanvas (optional) — Tier 3 SQL/analytical workspace backed by DuckDB. Register tabular data from upstream APIs, run SQL across registered tables, export CSV/Parquet/JSON. Token-sharing model (opaque canvas_id) for multi-agent collaboration; sliding TTL + per-tenant scoping. Opt-in via CANVAS_PROVIDER_TYPE=duckdb; fails closed on Workers.
  • Observability — Pino logging + optional OpenTelemetry traces/metrics. Request correlation and tool metrics automatic.
  • Tiered dependencies — parsers, OTEL SDK, Supabase, OpenAI as optional peers. Install what you use.
  • Agent-first DX — ships CLAUDE.md / AGENTS.md and Agent Skills that give your coding agent full framework knowledge — it can scaffold tools, write tests, run security audits, and ship releases without you writing the boilerplate.

Server structure

my-mcp-server/
  src/
    index.ts                              # createApp() entry point
    worker.ts                             # createWorkerHandler() (optional)
    config/
      server-config.ts                    # Server-specific env vars
    services/
      [domain]/                           # Domain services (init/accessor pattern)
    mcp-server/
      tools/definitions/                  # Tool definitions (.tool.ts)
      resources/definitions/              # Resource definitions (.resource.ts)
      prompts/definitions/                # Prompt definitions (.prompt.ts)
  package.json
  tsconfig.json                           # extends @cyanheads/mcp-ts-core/tsconfig.base.json
  CLAUDE.md / AGENTS.md                   # Point to core's CLAUDE.md / AGENTS.md for framework docs

No src/utils/, no src/storage/, no src/types-global/, no src/mcp-server/transports/ — infrastructure lives in node_modules.

Configuration

All core config is Zod-validated from environment variables. Server-specific config uses a separate Zod schema with lazy parsing.

VariableDescriptionDefault
MCP_TRANSPORT_TYPEstdio or httpstdio
MCP_HTTP_PORTHTTP server port3010
MCP_HTTP_HOSTHTTP server hostname127.0.0.1
MCP_AUTH_MODEnone, jwt, or oauthnone
MCP_AUTH_SECRET_KEYJWT signing secret (required for jwt mode)—
STORAGE_PROVIDER_TYPEin-memory, filesystem, supabase, cloudflare-d1/kv/r2in-memory
CANVAS_PROVIDER_TYPEnone or duckdb (Tier 3, optional peer dep @duckdb/node-api)none
OTEL_ENABLEDEnable OpenTelemetryfalse
OPENROUTER_API_KEYOpenRouter LLM API key—

See CLAUDE.md/AGENTS.md for the full configuration reference.

API overview

Entry points

FunctionPurpose
createApp(options)Node.js server — handles full lifecycle
createWorkerHandler(options)Cloudflare Workers — returns an ExportedHandler

Builders

BuilderUsage
tool(name, options)Define a tool with handler(input, ctx)
resource(uriTemplate, options)Define a resource with handler(params, ctx)
prompt(name, options)Define a prompt with generate(args)
appTool(name, options)Define an MCP Apps tool with auto-populated _meta.ui
appResource(uriTemplate, options)Define an MCP Apps HTML resource with the correct MIME type and _meta.ui mirroring for read content

Context

Handlers receive a unified Context object:

PropertyTypeDescription
ctx.logContextLoggerRequest-scoped logger (auto-correlates requestId, traceId, tenantId)
ctx.stateContextStateTenant-scoped key-value storage
ctx.elicitElicitFn?Ask the user for input — form schema, or .url() for an external link (when client supports it)
ctx.fail(reason, msg?, data?) => McpErrorTyped error throw — reason checked against errors[] contract at compile time
ctx.signalAbortSignalCancellation signal
ctx.notifyResourceUpdatedFunction?Notify subscribed clients a resource changed
ctx.notifyResourceListChangedFunction?Notify clients the resource list changed
ctx.notifyPromptListChangedFunction?Notify clients the prompt list changed
ctx.notifyToolListChangedFunction?Notify clients the tool list changed
ctx.progressContextProgress?Task progress reporting (when task: true)
ctx.requestIdstringUnique request ID
ctx.tenantIdstring?Tenant ID (JWT tid claim, or 'default' for stdio and HTTP+MCP_AUTH_MODE=none)

Subpath exports

import { createApp, tool, resource, prompt } from '@cyanheads/mcp-ts-core';
import { createWorkerHandler } from '@cyanheads/mcp-ts-core/worker';
import { McpError, JsonRpcErrorCode, notFound, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
import { checkScopes } from '@cyanheads/mcp-ts-core/auth';
import { markdown, fetchWithTimeout } from '@cyanheads/mcp-ts-core/utils';
import { OpenRouterProvider, GraphService } from '@cyanheads/mcp-ts-core/services';
import type { DataCanvas, CanvasInstance } from '@cyanheads/mcp-ts-core/canvas';
import { validateDefinitions } from '@cyanheads/mcp-ts-core/linter';
import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
import { fuzzTool, fuzzResource, fuzzPrompt } from '@cyanheads/mcp-ts-core/testing/fuzz';

See CLAUDE.md/AGENTS.md for the complete exports reference.

Examples

The examples/ directory contains a reference server consuming core through public exports, demonstrating all patterns:

ToolPattern
template_echo_messageBasic tool with format, auth
template_cat_factExternal API call, error factories
template_madlibs_elicitationctx.elicit for interactive input
template_image_testImage content blocks
template_async_countdowntask: true with ctx.progress
template_data_explorerMCP Apps with linked UI resource via appTool()/appResource() builders

Testing

import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
import { myTool } from '@/mcp-server/tools/definitions/my-tool.tool.js';

const ctx = createMockContext();
const input = myTool.input.parse({ query: 'test' });
const result = await myTool.handler(input, ctx);

createMockContext() provides a recording log, a working state, and a signal. State runs on a real StorageService over an in-memory provider — the same key validation and TTL expiry a deployed server applies — scoped to tenant 'default' unless { tenantId } says otherwise. Pass { errors: myTool.errors } for a typed ctx.fail matching the definition's contract, { elicit } for elicitation mocking, { progress: true } for task tools.

/testing also exports createMockSession() for session-bound contexts, createFetchMock() for upstream HTTP boundaries, and runToolContract() to drive a definition through schema, handler, formatting, and error-envelope checks. /testing/vitest adds the mcpTest fixtures (ctx, session, fetchMock, storage) and toolContractSuite().

Fuzz testing

Schema-aware fuzz testing via fast-check. Generates valid inputs from Zod schemas and adversarial payloads (prototype pollution, injection strings, type confusion) to verify handler invariants.

import { fuzzTool } from '@cyanheads/mcp-ts-core/testing/fuzz';

const report = await fuzzTool(myTool, { numRuns: 100 });
expect(report.crashes).toHaveLength(0);
expect(report.leaks).toHaveLength(0);
expect(report.prototypePollution).toBe(false);

Also exports fuzzResource, fuzzPrompt, zodToArbitrary, and ADVERSARIAL_STRINGS for custom property-based tests.

Documentation

  • CLAUDE.md/AGENTS.md — Framework reference: exports catalog, patterns, Context interface, error codes, auth, config, testing. Ships in the npm package and is auto-accessible in your project after init.
  • docs/telemetry/ — OpenTelemetry: full catalog of spans, metrics, and attributes the framework emits (observability.md), plus an example Grafana dashboard and vendor-agnostic query recipes for Datadog, New Relic, Honeycomb (dashboards.md).
  • CHANGELOG.md — Version history. Each entry includes a summary, migration notes, and links to commits/issues.

Development

bun run rebuild        # clean + build (scripts/clean.ts + scripts/build.ts)
bun run devcheck       # full gate: lint/format, typecheck, MCP defs, framework antipatterns, docs/skills/changelog sync, tests, audit, outdated, secrets/TODO scan
bun run lint:mcp       # validate MCP definitions against spec
bun run test:all       # vitest: unit + Workers pool + integration

License

Apache 2.0 — see LICENSE.


<div align="center"> <p> <a href="https://github.com/sponsors/cyanheads">Sponsor this project</a> • <a href="https://www.buymeacoffee.com/cyanheads">Buy me a coffee</a> </p> </div>

Related MCP servers

Offline US medical code lookup and crosswalk — ICD-10-CM/PCS, HCPCS Level II, RxNorm. Keyless.

1
TypeScript
Apache-2.0
View repository →

MET Museum collection via MCP — 500K+ artworks, metadata, provenance, open-access images.

3
TypeScript
Apache-2.0
View repository →

Search MusicBrainz artists, releases, works, labels; resolve ISRC/ISWC/barcode; fetch cover art.

1
TypeScript
Apache-2.0
View repository →

Plan US National Park Service trips — parks, alerts, campgrounds, things to do, events.

1
TypeScript
Apache-2.0
View repository →

Decode VINs, search recalls, complaints, crash ratings, and investigations.

1
TypeScript
Apache-2.0
View repository →

Search and audit NIST NVD CVEs by keyword, severity, CWE, CISA KEV status, and CPE.

1
TypeScript
Apache-2.0
View repository →