# mcp-ts-template [Health: Active]

**Category:** 📊 Monitoring  
**Repository:** https://github.com/cyanheads/mcp-ts-template  
**GitHub Stars:** 150  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/mcp-ts-template

## Description
A production-grade TypeScript template for scalable MCP servers with built-in observability.

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `bunx` (confidence: high):

```json
"mcpServers": {
  "mcp-ts-template": {
    "command": "bunx",
    "args": ["@cyanheads/mcp-ts-core"]
  }
}
```

## Documentation & README

<div align="center">
  <h1>@cyanheads/mcp-ts-core</h1>
  <p><b>Agent-native TypeScript framework for MCP servers.</b></p>
  <p>Give your agent the infrastructure, patterns, and skills to build and ship your server.</p>
</div>

<div align="center">

[![Version](https://img.shields.io/badge/Version-0.12.8-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2026--07--28-8A2BE2.svg?style=flat-square)](https://modelcontextprotocol.io/specification/2026-07-28)

[![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.0%2B-blueviolet.svg?style=flat-square)](https://bun.sh/)

[Quick start](#quick-start) · [Capabilities](#what-comes-with-it) · [API reference](#api-overview) · [Examples](#examples)

</div>

---

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

```bash
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:

```ts
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:

```bash
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 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.

## 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:

```ts
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:

```ts
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:

```ts
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:

```ts
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](https://github.com/cyanheads/brapi-mcp-server#working-with-dataframes) for a walkthrough of loading API results into a dataframe and querying them with SQL.

See the [framework reference](https://github.com/cyanheads/mcp-ts-template/blob/HEAD/CLAUDE.md) for configuration and handler patterns, and the [observability guide](https://github.com/cyanheads/mcp-ts-template/blob/HEAD/docs/telemetry/observability.md) for Pino logging and OpenTelemetry traces and metrics.

## Server structure

```text
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.

| 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](https://github.com/cyanheads/mcp-ts-template/blob/HEAD/CLAUDE.md) for the full configuration reference.

## API overview

### Entry points

| Function | Purpose |
|:---------|:--------|
| `createApp(options)` | Bun or 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 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 |

### Subpath exports

```ts
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](https://github.com/cyanheads/mcp-ts-template/blob/HEAD/CLAUDE.md) for the complete exports reference.

## Examples

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 |

## Testing

```ts
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:

```ts
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](https://github.com/cyanheads/mcp-ts-template/blob/HEAD/CLAUDE.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/](https://github.com/cyanheads/mcp-ts-template/blob/HEAD/docs/telemetry/)** — OpenTelemetry: full catalog of spans, metrics, and attributes the framework emits ([observability.md](https://github.com/cyanheads/mcp-ts-template/blob/HEAD/docs/telemetry/observability.md)), plus an example Grafana dashboard and vendor-agnostic query recipes for Datadog, New Relic, Honeycomb ([dashboards.md](https://github.com/cyanheads/mcp-ts-template/blob/HEAD/docs/telemetry/dashboards.md)).
- **[CHANGELOG.md](https://github.com/cyanheads/mcp-ts-template/blob/HEAD/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

```bash
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](https://github.com/cyanheads/mcp-ts-template/blob/HEAD/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>

