Sandbox
@cyanheads/mcp-ts-core

TypeScript framework for MCP servers

This package provides the core pieces for building an MCP server: definition builders, a runtime entry point, auth, storage, logging, and testing utilities. It also ships agent-facing docs, skills, and project templates so your builder can follow the same patterns while creating and maintaining the server.

151 stars28 forksTypeScriptUpdated 8d ago
Who it's for

Builders who want to connect tools, data, or workflows to an MCP-capable agent.

What it delivers

You can build and ship an MCP server without hand-assembling the runtime, contracts, and test harness.

What it does

Typed MCP definitions

Defines tools, resources, prompts, and app-level wrappers with TypeScript types and Zod schemas.

Agent-facing project scaffold

Ships `CLAUDE.md`, `AGENTS.md`, skills, and templates so an agent can work inside the project with the right context.

Auth and storage

Supports JWT or OAuth access checks and tenant-scoped state with pluggable backends.

Runtime for Bun, Node, and Workers

Runs as stdio or HTTP on Bun and Node.js, and also supports Cloudflare Workers.

Testing and linting tools

Includes mock contexts, contract tests, fuzz tests, and MCP definition linting.

How to get it

  1. 1Servers can run on Bun, Node.js 24 or later, or Cloudflare Workers.
    bunx @cyanheads/mcp-ts-core init my-mcp-server
    cd my-mcp-server
    bun install

README

@cyanheads/mcp-ts-core

Agent-native TypeScript framework for MCP servers.

Give your agent the infrastructure, patterns, and skills to build and ship your server.


Build AI tools for anything you can describe.

Connect an API, a dataset, or a workflow to an AI agent through the Model Context Protocol (MCP). Your project holds the domain code; @cyanheads/mcp-ts-core provides the auth, storage, logging, and deployment underneath it.

Agent-native means your agent knows what to do. Every scaffold includes framework documentation and Agent Skills: reusable workflows for designing tools, writing tests, reviewing security, and publishing releases. You decide what the server should do; your agent has the patterns and checks to help implement it.

The framework stays a dependency. Infrastructure fixes arrive through package upgrades — run the maintenance skill and your agent updates core, pulls the latest skills, and integrates them into your project.

Quick start

Servers can run on Bun, Node.js 24 or later, or Cloudflare Workers.

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

Open the project in Claude Code, Codex, or your preferred agent and give it a concrete starting point:

Build an MCP server for my team's inventory API. We need to find products, check stock across warehouses, investigate stock movements, and record adjustments and transfers. Let's get started.

The scaffold includes a source tree, build and test configuration, CLAUDE.md/AGENTS.md, Agent Skills, and plugin metadata for Claude Code and Codex.

Already have a TypeScript project? Install the framework directly with bun add @cyanheads/mcp-ts-core and register your definitions with createApp().

A tool is a schema and a function

Here's a complete server that searches a small catalog. To try it in the scaffolded project, replace src/index.ts with:

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

const catalog = ['Notebook', 'Mechanical pencil', 'Desk lamp'];

const search = tool('catalog_search', {
  description: 'Search catalog item names. An empty query lists all items.',
  annotations: { readOnlyHint: true },
  input: z.object({
    query: z.string().describe('Text to find in an item name'),
  }),
  output: z.object({
    items: z.array(z.string()).describe('Matching item names'),
  }),
  async handler({ query }) {
    return {
      items: catalog.filter((name) =>
        name.toLowerCase().includes(query.toLowerCase()),
      ),
    };
  },
});

await createApp({ name: 'catalog-mcp-server', title: 'catalog-mcp-server', tools: [search] });

Build and run it over HTTP:

bun run rebuild
bun run start:http

Connect your MCP client to http://127.0.0.1:3010/mcp (Streamable HTTP), or configure stdio with bun /absolute/path/to/dist/index.js.

What comes with it

You need to…The framework provides
Give an assistant useful capabilitiesTyped builders for tools, resources, prompts, and interactive MCP Apps
Help an agent use those capabilities correctlyServer instructions, result enrichment, and declared errors with recovery guidance
Control access and keep stateJWT/OAuth, per-definition scopes, and tenant-scoped storage with swappable backends
Run locally or host a servicestdio and HTTP on Bun/Node.js; a separate entry point for Cloudflare Workers
Understand failures and catch mistakesStructured logs, optional OpenTelemetry, definition linting, contract tests, and fuzz testing

Optional integrations such as DuckDB, Supabase, and the OpenTelemetry SDK are peer dependencies, installed when you need them.

Give agents useful results

Use enrichment and ctx.enrich() for result context such as totals, applied filters, and empty-result notices. Declare failures and recovery guidance in errors, then throw with the typed ctx.fail(). Both contracts are visible to clients before a call.

Here, runSearch(query, limit) returns { items, total, parsed } (matches, total before the limit, and parsed query), or null if the index is unavailable:

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().int().min(1).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', undefined, ctx.recoveryFor('index_unavailable'));
    }
    ctx.enrich({ effectiveQuery: res.parsed, totalCount: res.total });
    if (res.items.length === 0) {
      ctx.enrich({ notice: `No matches for "${input.query}". Try broader terms.` });
    }
    return { items: res.items }; // enrichment never rides in the domain return
  },
});

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

Enrichment and error contracts are advertised through tools/list and checked by the definition linter. ctx.recoveryFor() includes the declared recovery hint in the error response.

Same data across client surfaces

MCP hosts differ in what they expose to the agent: some use content[], some use structuredContent, and some use both. The framework keeps tool-result data in sync across both surfaces, so the agent receives the same information whichever one its host exposes. structuredContent carries structured JSON; content[] carries the same data as text.

format() controls the text representation, and the format-parity linter enforces that every output field is represented. Without a custom formatter, the framework uses JSON text. Declared enrichment is mirrored into both surfaces automatically. For example, this formatter presents the item names as a markdown list:

format: (result) => [{
  type: 'text',
  text: result.items.length > 0
    ? result.items.map((name) => `- ${name}`).join('\n')
    : 'No matching items.',
}],

Resources

Resources expose data at a URI. This definition delegates the lookup to your own getItem() service:

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) {
    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.

Runtime and integration details

  • Auth and storage: Declare auth: ['scope'] on a definition to check access before dispatch. Choose JWT or OAuth authentication. Tenant-scoped ctx.state supports in-memory, filesystem, Supabase, and Cloudflare D1/KV/R2 storage; select the backend through configuration.
  • Client interaction: Return ctx.requestInput(...) to request confirmation, model sampling, or the client's roots. The handler runs again with responses available on ctx.inputs.
  • Protocol compatibility: HTTP supports the 2026-07-28 revision's per-request _meta envelope and session-based 2025-era clients. The SDK's compatibility layer handles input requests for older clients.
  • Server presentation: instructions provides guidance during initialization without repeating it in every tool description. Identity fields such as title, websiteUrl, description, and icons populate client server information, the /.well-known/mcp.json server card, and the HTTP landing page.
  • Definition checks: lint:mcp checks names, schemas, scopes, annotations, format parity, and JSON Schema portability at build time. These checks do not run at server startup.
  • DataCanvas: An optional DuckDB workspace for SQL queries across API results and CSV/Parquet/JSON exports. Agents can share a workspace through an opaque canvas token. Enable it with CANVAS_PROVIDER_TYPE=duckdb and install @duckdb/node-api; it requires Bun or Node.js. See brapi-mcp-server for a walkthrough of loading API results into a dataframe and querying them with SQL.

See the framework reference for configuration and handler patterns, and the observability guide for Pino logging and OpenTelemetry traces and metrics.

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

Framework infrastructure lives in node_modules; your source tree contains the server's definitions, configuration, and domain services.

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 (optional peer dependency @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)Bun or 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 shared Context, with typed helpers for declared enrichment and error contracts:

PropertyTypeDescription
ctx.logContextLoggerRequest-scoped logger (auto-correlates requestId, traceId, tenantId); also mirrored to the client as notifications/message
ctx.stateContextStateTenant-scoped key-value storage
ctx.requestInput(spec) => neverSuspend and ask the caller for more input; the handler is re-entered with the answers
ctx.inputsContextInputsReader over a retried request's responses — .accepted(), .view(), .state(), .dropped
ctx.enrichEnrich / TypedEnrich<E>Add declared result context to structured output and text content
ctx.contentContentCollectAttach image/audio blocks to content[]content.image(data, mimeType), content.audio(...), or a raw block
ctx.fail(reason, msg?, data?) => McpErrorCreates an error for throw ctx.fail(...); available with a declared errors contract
ctx.recoveryFor(reason) => objectResolves a declared recovery hint to { recovery: { hint } } — spread into ctx.fail's data argument
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.requestIdstringUnique request ID
ctx.tenantIdstring?Tenant ID (JWT tid claim, or 'default' for stdio and HTTP+MCP_AUTH_MODE=none)
ctx.authAuthContext?Token claims and scopes when the request is authenticated
ctx.sessionIdstring?HTTP session ID in stateful/auto session mode — a scoping key, not an authorization principal
ctx.uriURL?The parsed resource URI; set in resource handlers only

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 { mcpTest, toolContractSuite } from '@cyanheads/mcp-ts-core/testing/vitest';
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 core patterns:

ToolPattern
template_echo_messageBasic tool with format, auth
template_cat_factExternal API call, error factories
template_madlibs_elicitationctx.requestInput / ctx.inputs for multi-round-trip input
template_image_testImage content blocks
template_data_explorerMCP Apps with a linked HTML UI resource

Testing

import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
import { mcpTest, toolContractSuite } from '@cyanheads/mcp-ts-core/testing/vitest';
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, and { inputResponses, requestState } to drive a multi-round-trip handler into its second round.

/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().

For fuzz testing, /testing/fuzz uses fast-check to generate valid inputs from Zod schemas and adversarial payloads that probe for crashes, data leaks, and prototype pollution:

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. Directory-based changelogs that work well for Agents. Entries include agent-specific notes per version as needed.

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, audit, outdated, secrets/TODO scan
bun run lint:mcp       # validate MCP definitions against spec
bun run test:all       # rebuild + coverage + Node.js + Workers + integration

License

Apache 2.0 — see LICENSE.


Files in the repo

Repository payload42 top-level entries
  • .github
  • .husky
  • .vscode
  • changelog
  • docs
  • examples
  • handoffs
  • scripts
  • skills
  • src
  • templates
  • tests
  • .dockerignore
  • .env.example
  • .gitattributes
  • .gitignore
  • .markdownlint.jsonc
  • .mcpbignore
  • AGENTS.md
  • biome.json
  • bun.lock
  • bunfig.toml
  • CHANGELOG.md
  • CITATION.cff
  • CLAUDE.md
  • devcheck.config.json
  • Dockerfile
  • LICENSE
  • package.json
  • README.md
  • repomix.config.json
  • server.json
  • tsconfig.base.json
  • tsconfig.build.json
  • tsconfig.json
  • tsconfig.scripts.json
  • tsconfig.worker.json
  • tsdoc.json
  • typedoc.json
  • vitest.config.base.mjs
  • vitest.config.ts
  • wrangler.jsonc

Discussion (0)

Ask about usage, or say what you built with it

Sign in to join the discussion.

No comments yet. Be the first to say what this is good for.

More frameworks & sdks

HKUDS/nanobotFrameworks & SDKs

Ultra-lightweight, open-source, self-hosted personal AI agent framework in Python with WebUI, tools, memory, MCP, multi-agent workflows, automation, and chat apps

48k
microsoft/
SkillOpt
microsoft/SkillOptFrameworks & SDKs

SkillOpt is a text-space optimizer that trains reusable natural-language skills for frozen LLM agents through trajectory-driven edits, validation-gated updates, and deployable best_skill.md artifacts.

17k
omnigent-ai/omnigentFrameworks & SDKs

Omnigent is an open-source AI agent framework and meta-harness: orchestrate Claude Code, Codex, Cursor, Pi, and custom agents — swap harnesses without rewriting, enforce policies and sandboxing, and collaborate in real time from any device.

9.8k
kyegomez/
OpenMythos
kyegomez/OpenMythosFrameworks & SDKs

A theoretical reconstruction of the Claude Mythos architecture, built from first principles using the available research literature.

15k
D4Vinci/ScraplingFrameworks & SDKs

🕷️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!

80k