io.github.cyanheads/mcp-ts-core MCP Server
io.github.cyanheads/mcp-ts-core
Agent-native TypeScript framework for building MCP servers with declarative tool, resource, and prompt definitions.
What is the io.github.cyanheads/mcp-ts-core MCP server?
The @cyanheads/mcp-ts-core MCP server is a TypeScript framework for building Model Context Protocol servers with declarative definitions, built-in auth, multi-backend storage, and OpenTelemetry support. It handles transports, config, logging, and lifecycle management, letting you focus on defining tools, resources, and prompts rather than infrastructure. It runs on Node.js, Bun, and Cloudflare Workers.
@cyanheads/mcp-ts-core is an infrastructure layer for TypeScript MCP servers. Instead of forking or building from scratch, you install it as a dependency and use its declarative builders (tool(), resource(), prompt()) to define your server's capabilities. The framework automatically handles transports, authentication, structured logging, error contracts, multi-backend storage, and observability—leaving you to focus on domain logic and agent collaboration.
How to install io.github.cyanheads/mcp-ts-core
Copy-paste configuration for popular MCP clients.
MCP_LOG_LEVELSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
MCP_HTTP_HOSTThe hostname for the HTTP server.
MCP_HTTP_PORTThe port to run the HTTP server on.
MCP_HTTP_ENDPOINT_PATHThe endpoint path for the MCP server.
MCP_AUTH_MODEAuthentication mode to use: 'none', 'jwt', or 'oauth'.
Tools & capabilities
Tools this server exposes to the agent.
tool()— Builder for defining tools with Zod schemas, typed error contracts, enrichment metadata, and async handlersresource()— Builder for defining resources with URI templates, Zod parameter schemas, and handlersprompt()— Builder for defining prompts with argument schemas and generation handlersappTool()— Builder for MCP Apps tools with auto-populated UI metadataappResource()— Builder for MCP Apps HTML resources with correct MIME type and UI mirroringcreateApp()— Entry point for Node.js servers; handles full lifecycle, config parsing, transport startup, and graceful shutdowncreateWorkerHandler()— Entry point for Cloudflare Workers; returns an ExportedHandler with the same definitions
Use cases
- Build a complete MCP server with tools, resources, and prompts using declarative definitions without writing transport or auth boilerplate
- Define typed error contracts for tools so clients preview failure modes and handlers get compile-time validation
- Create long-running task tools with progress reporting and cancellation support via ctx.progress and ctx.signal
- Deploy the same tool definitions across Node.js, Bun, and Cloudflare Workers by swapping entry points
- Integrate multi-backend storage (in-memory, filesystem, Supabase, Cloudflare D1/KV/R2) by changing an environment variable without modifying handler code
io.github.cyanheads/mcp-ts-core MCP server FAQ
It's a TypeScript framework for building MCP servers. You install it as a dependency and use declarative builders (tool, resource, prompt) to define your server's capabilities. The framework handles all infrastructure: transports, auth, logging, error contracts, storage, and observability.
Yes. The framework is open-source under the Apache 2.0 license.
Install via npm: `npm install @cyanheads/mcp-ts-core`. Then use `bunx @cyanheads/mcp-ts-core init my-mcp-server` to scaffold a new project with example tools, resources, and Agent Skills.
Auth is optional and configurable. Set `MCP_AUTH_MODE` to `none` (default), `jwt`, or `oauth`. The framework checks auth scopes before dispatching tool calls—no wrapper code needed.
In-memory, filesystem, Supabase, and Cloudflare D1/KV/R2. Swap backends via the `STORAGE_PROVIDER_TYPE` environment variable without changing handler code.
Yes. Use `createWorkerHandler()` instead of `createApp()`. The same tool, resource, and prompt definitions work on both Node.js and Workers.
README (reference)
Source of truth, from the repository.
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 —
instructionsoncreateApp/createWorkerHandlerrides everyinitializefor 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) oncreateApp/createWorkerHandlerflow toinitializeserverInfo, the/.well-known/mcp.jsonserver card, and the landing page. - Unified Context — one
ctxfor logging, tenant-scoped storage, elicitation, cancellation, and task progress. - Auth —
auth: ['scope']on definitions, checked before dispatch (no wrapper code). Modes:none,jwt, oroauth(local secret or JWKS). - Task tools —
task: truefor 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:mcpordevcheck— not invoked at server startup. - Typed error contracts — declare
errors: [{ reason, code, when, recovery, retryable? }]and handlers get a typedctx.fail(reason, …). Contracts publish intools/listso clients preview failure modes; the linter cross-checks the handler. Factories (notFound(),httpErrorFromResponse(), …) cover ad-hoc throws; plainErrorauto-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 viaCANVAS_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.mdand 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.
| Variable | Description | Default |
|---|---|---|
MCP_TRANSPORT_TYPE | stdio or http | stdio |
MCP_HTTP_PORT | HTTP server port | 3010 |
MCP_HTTP_HOST | HTTP server hostname | 127.0.0.1 |
MCP_AUTH_MODE | none, jwt, or oauth | none |
MCP_AUTH_SECRET_KEY | JWT signing secret (required for jwt mode) | — |
STORAGE_PROVIDER_TYPE | in-memory, filesystem, supabase, cloudflare-d1/kv/r2 | in-memory |
CANVAS_PROVIDER_TYPE | none or duckdb (Tier 3, optional peer dep @duckdb/node-api) | none |
OTEL_ENABLED | Enable OpenTelemetry | false |
OPENROUTER_API_KEY | OpenRouter LLM API key | — |
See CLAUDE.md/AGENTS.md for the full configuration reference.
API overview
Entry points
| Function | Purpose |
|---|---|
createApp(options) | Node.js server — handles full lifecycle |
createWorkerHandler(options) | Cloudflare Workers — returns an ExportedHandler |
Builders
| Builder | Usage |
|---|---|
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:
| Property | Type | Description |
|---|---|---|
ctx.log | ContextLogger | Request-scoped logger (auto-correlates requestId, traceId, tenantId) |
ctx.state | ContextState | Tenant-scoped key-value storage |
ctx.elicit | ElicitFn? | Ask the user for input — form schema, or .url() for an external link (when client supports it) |
ctx.fail | (reason, msg?, data?) => McpError | Typed error throw — reason checked against errors[] contract at compile time |
ctx.signal | AbortSignal | Cancellation signal |
ctx.notifyResourceUpdated | Function? | Notify subscribed clients a resource changed |
ctx.notifyResourceListChanged | Function? | Notify clients the resource list changed |
ctx.notifyPromptListChanged | Function? | Notify clients the prompt list changed |
ctx.notifyToolListChanged | Function? | Notify clients the tool list changed |
ctx.progress | ContextProgress? | Task progress reporting (when task: true) |
ctx.requestId | string | Unique request ID |
ctx.tenantId | string? | 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:
| Tool | Pattern |
|---|---|
template_echo_message | Basic tool with format, auth |
template_cat_fact | External API call, error factories |
template_madlibs_elicitation | ctx.elicit for interactive input |
template_image_test | Image content blocks |
template_async_countdown | task: true with ctx.progress |
template_data_explorer | MCP 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
TypeScript framework for building MCP servers with declarative tools, auth, storage, and observability.
Offline US medical code lookup and crosswalk — ICD-10-CM/PCS, HCPCS Level II, RxNorm. Keyless.
MET Museum collection via MCP — 500K+ artworks, metadata, provenance, open-access images.
Search MusicBrainz artists, releases, works, labels; resolve ISRC/ISWC/barcode; fetch cover art.
Plan US National Park Service trips — parks, alerts, campgrounds, things to do, events.
Decode VINs, search recalls, complaints, crash ratings, and investigations.
