by cyanheads
Agent-native TypeScript framework for MCP servers.
# Add to your Claude Code skills
git clone https://github.com/cyanheads/mcp-ts-coreLast scanned: 5/30/2026
{
"issues": [],
"status": "PASSED",
"scannedAt": "2026-05-30T16:15:39.612Z",
"npmAuditRan": true,
"pipAuditRan": true
}See how mcp-ts-core compares with popular alternatives.
mcp-ts-core is an open-source ai agents skill for AI coding assistants such as Claude Code, Codex CLI, and ChatGPT, built by cyanheads. Agent-native TypeScript framework for MCP servers. It has 151 GitHub stars.
Yes. mcp-ts-core passed SkillsLLM's automated security scan — a dependency vulnerability audit plus prompt-injection heuristics — with no high-severity issues. You can read the full report in the Security Report section on this page.
Clone the repository with "git clone https://github.com/cyanheads/mcp-ts-core" and add it to your Claude Code skills directory (see the Installation section above).
mcp-ts-core is primarily written in TypeScript. It is open-source under cyanheads on GitHub, so you can review or fork the full source.
Yes. SkillsLLM lists many other AI Agents skills you can browse and compare side by side. Open the AI Agents category from the badge at the top of this page, or use the Related Skills and comparison links further down to weigh mcp-ts-core against similar tools.
No comments yet. Be the first to share your thoughts!
⚠️ Third-Party Software Notice
This skill is third-party open-source software developed and hosted independently on GitHub. SkillsLLM is an informational directory and does not control or maintain the underlying repository.
Any security checks, ratings, or warnings displayed by SkillsLLM are automated and limited in scope. They do not constitute a security certification or guarantee that the software is safe, error-free, or free from malicious code, vulnerabilities, compromised dependencies, or prompt-injection risks.
Review the source code, permissions, dependencies, and configuration before installing or running any third-party skill. Use is at your own risk. To the maximum extent permitted by applicable law, SkillsLLM is not liable for losses arising from third-party software.
Quick start · Capabilities · API reference · Examples
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.
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().
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.
| 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:
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.
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 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.
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.