The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the MCP Ts Core listing page.
Agent-native TypeScript framework for MCP servers.
Give your agent the infrastructure, patterns, and skills to build and ship your server.
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.
Servers can run on Bun, Node.js 24 or later, or Cloudflare Workers.
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().
Here's a complete server that searches a small catalog. To try it in the scaffolded project, replace src/index.ts with:
Build and run it over 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.
| You need to… | The framework provides |
|---|---|
| Give an assistant useful capabilities | Typed builders for tools, resources, prompts, and interactive MCP Apps |
| Help an agent use those capabilities correctly | Server instructions, result enrichment, and declared errors with recovery guidance |
| Control access and keep state | JWT/OAuth, per-definition scopes, and tenant-scoped storage with swappable backends |
| Run locally or host a service | stdio and HTTP on Bun/Node.js; a separate entry point for Cloudflare Workers |
| Understand failures and catch mistakes | Structured 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.
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:
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.
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:
Resources expose data at a URI. This definition delegates the lookup to your own getItem() service:
Everything registers through createApp() in your entry point:
It also works on Cloudflare Workers with createWorkerHandler() — same definitions, different entry point.
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.ctx.requestInput(...) to request confirmation, model sampling, or the client's roots. The handler runs again with responses available on ctx.inputs._meta envelope and session-based 2025-era clients. The SDK's compatibility layer handles input requests for older clients.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.lint:mcp checks names, schemas, scopes, annotations, format parity, and JSON Schema portability at build time. These checks do not run at server startup.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.
Framework infrastructure lives in node_modules; your source tree contains the server's definitions, configuration, and domain services.
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 (optional peer dependency @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.
| Function | Purpose |
|---|---|
createApp(options) | Bun or Node.js server — handles full lifecycle |
createWorkerHandler(options) | Cloudflare Workers — returns an ExportedHandler |
| 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 |
Handlers receive a shared Context, with typed helpers for declared enrichment and error contracts:
| Property | Type | Description |
|---|---|---|
ctx.log | ContextLogger | Request-scoped logger (auto-correlates requestId, traceId, tenantId); also mirrored to the client as notifications/message |
ctx.state | ContextState | Tenant-scoped key-value storage |
ctx.requestInput | (spec) => never | Suspend and ask the caller for more input; the handler is re-entered with the answers |
ctx.inputs | ContextInputs | Reader over a retried request's responses — .accepted(), .view(), .state(), .dropped |
ctx.enrich | Enrich / TypedEnrich<E> | Add declared result context to structured output and text content |
ctx.content | ContentCollect | Attach image/audio blocks to content[] — content.image(data, mimeType), content.audio(...), or a raw block |
ctx.fail | (reason, msg?, data?) => McpError | Creates an error for throw ctx.fail(...); available with a declared errors contract |
ctx.recoveryFor | (reason) => object | Resolves a declared recovery hint to { recovery: { hint } } — spread into ctx.fail's data argument |
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.requestId | string | Unique request ID |
ctx.tenantId | string? | Tenant ID (JWT tid claim, or 'default' for stdio and HTTP+MCP_AUTH_MODE=none) |
ctx.auth | AuthContext? | Token claims and scopes when the request is authenticated |
ctx.sessionId | string? | HTTP session ID in stateful/auto session mode — a scoping key, not an authorization principal |
ctx.uri | URL? | The parsed resource URI; set in resource handlers only |
See CLAUDE.md/AGENTS.md for the complete exports reference.
The examples/ directory contains a reference server consuming core through public exports, demonstrating core patterns:
| Tool | Pattern |
|---|---|
template_echo_message | Basic tool with format, auth |
template_cat_fact | External API call, error factories |
template_madlibs_elicitation | ctx.requestInput / ctx.inputs for multi-round-trip input |
template_image_test | Image content blocks |
template_data_explorer | MCP Apps with a linked HTML UI resource |
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:
Also exports fuzzResource, fuzzPrompt, zodToArbitrary, and ADVERSARIAL_STRINGS for custom property-based tests.
init.Apache 2.0 — see LICENSE.