TypeScript template for building MCP servers with declarative tooling, observability, and auth.
@cyanheads/mcp-ts-core is the infrastructure layer for TypeScript MCP servers. Install it as a dependency — don't fork it. You write tools, resources, and prompts; the framework handles transports, auth, storage, config, logging, telemetry, and lifecycle.
import { createApp, tool, z } from '@cyanheads/mcp-ts-core';
const greet = tool('greet', {
description: 'Greet someone by name and return a personalized message.',
annotations: { readOnlyHint: true },
input: z.object({ name: z.string().describe('Name of the person to greet') }),
output: z.object({ message: z.string().describe('The greeting message') }),
handler: async (input) => ({ message: `Hello, ${input.name}!` }),
});
await createApp({ tools: [greet] });That's a complete MCP server. Every tool call is automatically logged with duration, payload sizes, memory usage, and request correlation — no instrumentation code needed. createApp() handles config parsing, logger init, transport startup, signal handlers, and graceful shutdown.
tool(), resource(), prompt() builders with Zod schemas. Framework handles registration, validation, and response formatting.ctx object with ctx.log (request-scoped logging), ctx.state (tenant-scoped storage), ctx.elicit (user prompting), ctx.sample (LLM completion), and ctx.signal (cancellation).auth: ['scope'] on definitions. No wrapper functions. Framework checks scopes before calling your handler.task: true flag for long-running operations. Framework manages the full lifecycle (create, poll, progress, complete/fail/cancel).validateDefinitions() checks tools, resources, and prompts against MCP spec at startup. Name format, schema structure, .describe() presence, JSON Schema serializability, auth scope validity, annotation coherence, and URI template–params alignment. Also available as a standalone CLI (lint:mcp) and devcheck step.notFound(), validationError(), serviceUnavailable(), etc.) for precise control when the code matters. Auto-classification from plain Error messages when it doesn't.in-memory, filesystem, Supabase, Cloudflare D1/KV/R2. Swap providers via env var without changing tool logic. Cursor pagination, batch ops, TTL, tenant isolation.none, jwt, or oauth modes. JWT with local secret or OAuth with JWKS verification.createApp() for Node, createWorkerHandler() for Workers.CLAUDE.md with full exports catalog, patterns, and contracts. AI coding agents can build on the framework with zero ramp-up.bunx @cyanheads/mcp-ts-core init my-mcp-server
cd my-mcp-server
bun installThat gives you a working project with CLAUDE.md, skills, config files, and a scaffolded src/ directory. Open it in your editor, start your coding agent, and tell it what tools to build. The agent learns the framework from the included docs and skills — tool definitions, resources, services, testing patterns, all of it.
Here's what tool definitions look like:
import { tool, z } from '@cyanheads/mcp-ts-core';
export const search = tool('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 };
},
});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,
});It also works on Cloudflare Workers with createWorkerHandler() — same definitions, different entry point.
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 # Points to core's CLAUDE.md for framework docsNo src/utils/, no src/storage/, no src/types-global/, no src/mcp-server/transports/ — infrastructure lives in node_modules.
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 |
OTEL_ENABLED | Enable OpenTelemetry | false |
OPENROUTER_API_KEY | OpenRouter LLM API key | — |
See CLAUDE.md for the full configuration reference.
| Function | Purpose |
|---|---|
createApp(options) | Node.js server — handles full lifecycle |
createWorkerHandler(options) | Cloudflare Workers — returns { fetch, scheduled } |
| 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) |
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 | Function? | Ask the user for input (when client supports it) |
ctx.sample | Function? | Request LLM completion from the client |
ctx.signal | AbortSignal | Cancellation signal |
ctx.notifyResourceUpdated | Function? | Notify subscribed clients a resource changed |
ctx.notifyResourceListChanged | Function? | Notify clients the resource list changed |
ctx.progress | ContextProgress? | Task progress reporting (when task: true) |
ctx.requestId | string | Unique request ID |
ctx.tenantId | string? | Tenant ID (from JWT or 'default' for stdio) |
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 { validateDefinitions } from '@cyanheads/mcp-ts-core/linter';
import { createMockContext } from '@cyanheads/mcp-ts-core/testing';See CLAUDE.md for the complete exports reference.
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_code_review_sampling | ctx.sample for LLM completion |
template_image_test | Image content blocks |
template_async_countdown | task: true with ctx.progress |
template_data_explorer | MCP Apps with linked UI resource |
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, { sample } for LLM mocking, { elicit } for elicitation mocking, { progress: true } for task tools.
bun run build # tsc && tsc-alias
bun run devcheck # lint, format, typecheck, security
bun run test # vitest
bun run dev:stdio # dev mode (stdio)
bun run dev:http # dev mode (HTTP)Issues and pull requests welcome. Run checks before submitting:
bun run devcheck
bun run testApache 2.0 — see LICENSE.