io.github.cyanheads/mcp-ts-template
编码与调试by cyanheads
面向生产环境的 TypeScript 模板,用于构建可扩展的 MCP servers,并内置 observability 能力。
想把 MCP Server 真正做到可上线,这套 TypeScript 生产模板能帮你快速搭好可扩展架构,连可观测性都提前配齐。
什么是 io.github.cyanheads/mcp-ts-template?
面向生产环境的 TypeScript 模板,用于构建可扩展的 MCP servers,并内置 observability 能力。
README
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. Define your domain logic with the builders and let the framework take care of the rest.
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, showing both flagship contracts. enrichment carries the context an agent reasons with — the parsed query, the true total, an empty-result notice — which the framework merges into structuredContent and mirrors 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). The linter cross-checks both against the handler body, and both publish in tools/list so clients preview a tool's success and failure shapes. Every tool call is automatically logged with duration, payload sizes, and request correlation — no instrumentation code needed; 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 (i.e. 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({ tenantId: 'test-tenant' });
const input = myTool.input.parse({ query: 'test' });
const result = await myTool.handler(input, ctx);
createMockContext() provides stubbed log, state, and signal. Pass { tenantId } for state operations, { elicit } for elicitation mocking, { progress: true } for task tools.
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>
常见问题
io.github.cyanheads/mcp-ts-template 是什么?
面向生产环境的 TypeScript 模板,用于构建可扩展的 MCP servers,并内置 observability 能力。
相关 Skills
前端设计
by anthropics
面向组件、页面、海报和 Web 应用开发,按鲜明视觉方向生成可直接落地的前端代码与高质感 UI,适合做 landing page、Dashboard 或美化现有界面,避开千篇一律的 AI 审美。
✎ 想把页面做得既能上线又有设计感,就用前端设计:组件到整站都能产出,难得的是能避开千篇一律的 AI 味。
网页应用测试
by anthropics
用 Playwright 为本地 Web 应用编写自动化测试,支持启动开发服务器、校验前端交互、排查 UI 异常、抓取截图与浏览器日志,适合调试动态页面和回归验证。
✎ 借助 Playwright 一站式验证本地 Web 应用前端功能,调 UI 时还能同步查看日志和截图,定位问题更快。
网页构建器
by anthropics
面向复杂 claude.ai HTML artifact 开发,快速初始化 React + Tailwind CSS + shadcn/ui 项目并打包为单文件 HTML,适合需要状态管理、路由或多组件交互的页面。
✎ 在 claude.ai 里做复杂网页 Artifact 很省心,多组件、状态和路由都能顺手搭起来,React、Tailwind 与 shadcn/ui 组合效率高、成品也更精致。
相关 MCP Server
GitHub
编辑精选by GitHub
GitHub 是 MCP 官方参考服务器,让 Claude 直接读写你的代码仓库和 Issues。
✎ 这个参考服务器解决了开发者想让 AI 安全访问 GitHub 数据的问题,适合需要自动化代码审查或 Issue 管理的团队。但注意它只是参考实现,生产环境得自己加固安全。
Context7 文档查询
编辑精选by Context7
Context7 是实时拉取最新文档和代码示例的智能助手,让你告别过时资料。
✎ 它能解决开发者查找文档时信息滞后的问题,特别适合快速上手新库或跟进更新。不过,依赖外部源可能导致偶尔的数据延迟,建议结合官方文档使用。
by tldraw
tldraw 是让 AI 助手直接在无限画布上绘图和协作的 MCP 服务器。
✎ 这解决了 AI 只能输出文本、无法视觉化协作的痛点——想象让 Claude 帮你画流程图或白板讨论。最适合需要快速原型设计或头脑风暴的开发者。不过,目前它只是个基础连接器,你得自己搭建画布应用才能发挥全部潜力。