MCP Hub
Back to servers

@cyanheads/mcp-ts-core

Requires Setup

TypeScript framework for building Model Context Protocol (MCP) servers. Declarative tool/resource/prompt definitions, pluggable auth, multi-backend storage, OpenTelemetry observability, and support for both local (stdio/HTTP) and edge (Cloudflare Workers)

npmGitHub313/wk
Stars
119
Forks
20
Updated
Mar 15, 2026
Validated
Mar 16, 2026

Quick Install

npx -y @cyanheads/mcp-ts-core

@cyanheads/mcp-ts-core

TypeScript framework for building Model Context Protocol (MCP) servers. Declarative tool/resource/prompt definitions, pluggable auth, multi-backend storage, OpenTelemetry observability, and support for both local (stdio/HTTP) and edge (Cloudflare Workers) runtimes.

Version MCP Spec MCP SDK License

TypeScript Bun


What is this?

@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.

Features

  • Declarative definitionstool(), resource(), prompt() builders with Zod schemas. Framework handles registration, validation, and response formatting.
  • Unified Context — handlers receive a single 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).
  • Inline authauth: ['scope'] on definitions. No wrapper functions. Framework checks scopes before calling your handler.
  • Task toolstask: true flag for long-running operations. Framework manages the full lifecycle (create, poll, progress, complete/fail/cancel).
  • Structured error handling — Handlers throw freely; the framework catches, classifies, and formats. Error factories (notFound(), validationError(), serviceUnavailable(), etc.) for precise control when the code matters. Auto-classification from plain Error messages when it doesn't.
  • Multi-backend storagein-memory, filesystem, Supabase, Cloudflare D1/KV/R2. Swap providers via env var without changing tool logic. Cursor pagination, batch ops, TTL, tenant isolation.
  • Pluggable authnone, jwt, or oauth modes. JWT with local secret or OAuth with JWKS verification.
  • Observability — Pino structured logging with optional OpenTelemetry tracing and metrics. Request IDs, trace correlation, tool execution metrics — all automatic.
  • Local + edge — Same code runs on stdio, HTTP (Hono), and Cloudflare Workers. createApp() for Node, createWorkerHandler() for Workers.
  • Tiered dependencies — Core deps always installed. Parsers, sanitization, scheduling, OTEL SDK, Supabase, OpenAI — optional peers. Install what you use.
  • Agent-first DX — Ships CLAUDE.md with full exports catalog, patterns, and contracts. AI coding agents can build on the framework with zero ramp-up.

Quick start

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

That 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.

What you get

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'),
  }),
  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.

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                               # Points to core's CLAUDE.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
OTEL_ENABLEDEnable OpenTelemetryfalse
OPENROUTER_API_KEYOpenRouter LLM API key

See CLAUDE.md for the full configuration reference.

API overview

Entry points

FunctionPurpose
createApp(options)Node.js server — handles full lifecycle
createWorkerHandler(options)Cloudflare Workers — returns { fetch, scheduled }

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)

Context

Handlers receive a unified Context object:

PropertyTypeDescription
ctx.logContextLoggerRequest-scoped logger (auto-correlates requestId, traceId, tenantId)
ctx.stateContextStateTenant-scoped key-value storage
ctx.elicitFunction?Ask the user for input (when client supports it)
ctx.sampleFunction?Request LLM completion from the client
ctx.signalAbortSignalCancellation signal
ctx.progressContextProgress?Task progress reporting (when task: true)
ctx.requestIdstringUnique request ID
ctx.tenantIdstring?Tenant ID (from JWT or 'default' for stdio)

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 { createMockContext } from '@cyanheads/mcp-ts-core/testing';

See CLAUDE.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_code_review_samplingctx.sample for LLM completion
template_image_testImage content blocks
template_async_countdowntask: true with ctx.progress
template_data_explorerMCP Apps with linked UI resource

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, { sample } for LLM mocking, { elicit } for elicitation mocking, { progress: true } for task tools.

Documentation

  • CLAUDE.md — Complete API reference: exports catalog, patterns, Context interface, error codes, auth, config, testing. Ships in the npm package.
  • CHANGELOG.md — Version history

Development

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)

Contributing

Issues and pull requests welcome. Run checks before submitting:

bun run devcheck
bun run test

License

Apache 2.0 — see LICENSE.


Reviews

No reviews yet

Sign in to write a review