@cyanheads/mcp-ts-core 0.12.5 → 0.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +26 -14
- package/CLAUDE.md +26 -14
- package/README.md +124 -76
- package/biome.json +1 -1
- package/changelog/0.12.x/0.12.6.md +44 -0
- package/changelog/0.12.x/0.12.7.md +39 -0
- package/dist/core/app.d.ts +2 -2
- package/dist/core/app.js +2 -2
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +2 -2
- package/dist/core/worker.d.ts +2 -2
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +1 -1
- package/dist/core/worker.js.map +1 -1
- package/dist/mcp-server/apps/appBuilders.d.ts +2 -2
- package/dist/mcp-server/apps/appBuilders.js +2 -2
- package/dist/mcp-server/cacheHints.d.ts +1 -1
- package/dist/mcp-server/prompts/prompt-registration.d.ts +1 -1
- package/dist/mcp-server/prompts/prompt-registration.js +1 -1
- package/dist/mcp-server/resources/resource-registration.d.ts +3 -3
- package/dist/mcp-server/resources/resource-registration.js +1 -1
- package/dist/mcp-server/resources/resourceSubscriptions.d.ts +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +2 -2
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +4 -4
- package/dist/mcp-server/server.d.ts +4 -4
- package/dist/mcp-server/server.js +1 -1
- package/dist/mcp-server/tools/tool-registration.d.ts +3 -3
- package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
- package/dist/mcp-server/tools/tool-registration.js +9 -3
- package/dist/mcp-server/tools/tool-registration.js.map +1 -1
- package/dist/mcp-server/tools/utils/deferredInputSchema.d.ts +39 -0
- package/dist/mcp-server/tools/utils/deferredInputSchema.d.ts.map +1 -0
- package/dist/mcp-server/tools/utils/deferredInputSchema.js +33 -0
- package/dist/mcp-server/tools/utils/deferredInputSchema.js.map +1 -0
- package/dist/mcp-server/tools/utils/disabled-tool.d.ts +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +11 -3
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +29 -7
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/auth/authFactory.d.ts +1 -1
- package/dist/mcp-server/transports/auth/authFactory.js +2 -2
- package/dist/mcp-server/transports/auth/authMiddleware.d.ts +1 -1
- package/dist/mcp-server/transports/auth/authMiddleware.js +1 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.js +1 -1
- package/dist/mcp-server/transports/auth/lib/claimParser.d.ts +1 -1
- package/dist/mcp-server/transports/auth/strategies/authStrategy.d.ts +1 -1
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts +2 -2
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +1 -1
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts +2 -2
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.d.ts +1 -1
- package/dist/mcp-server/transports/http/httpServer.d.ts +1 -1
- package/dist/mcp-server/transports/http/httpServer.js +1 -1
- package/dist/mcp-server/transports/http/httpTransport.d.ts +3 -3
- package/dist/mcp-server/transports/http/httpTransport.js +12 -12
- package/dist/mcp-server/transports/http/landing-page/handler.d.ts +1 -1
- package/dist/mcp-server/transports/http/sessionStore.js +1 -1
- package/dist/mcp-server/transports/manager.d.ts +2 -2
- package/dist/mcp-server/transports/manager.js +3 -3
- package/dist/services/llm/providers/openrouter.provider.d.ts +1 -1
- package/dist/services/speech/providers/whisper.provider.d.ts.map +1 -1
- package/dist/services/speech/providers/whisper.provider.js +4 -2
- package/dist/services/speech/providers/whisper.provider.js.map +1 -1
- package/dist/storage/core/StorageService.d.ts +1 -1
- package/dist/storage/core/StorageService.js +1 -1
- package/dist/storage/core/storageFactory.d.ts +2 -2
- package/dist/storage/core/storageFactory.js +6 -6
- package/dist/storage/core/storageValidation.d.ts.map +1 -1
- package/dist/storage/core/storageValidation.js +5 -3
- package/dist/storage/core/storageValidation.js.map +1 -1
- package/dist/storage/providers/cloudflare/d1Provider.d.ts +1 -1
- package/dist/storage/providers/cloudflare/d1Provider.js +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.d.ts +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.js +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.d.ts +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.js +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.js +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts +3 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.js +11 -7
- package/dist/storage/providers/inMemory/inMemoryProvider.js.map +1 -1
- package/dist/storage/providers/supabase/supabaseProvider.d.ts +2 -2
- package/dist/storage/providers/supabase/supabaseProvider.js +1 -1
- package/dist/testing/fuzz.d.ts.map +1 -1
- package/dist/testing/fuzz.js +92 -53
- package/dist/testing/fuzz.js.map +1 -1
- package/dist/utils/formatting/diffFormatter.d.ts +2 -2
- package/dist/utils/formatting/diffFormatter.js +4 -4
- package/dist/utils/formatting/tableFormatter.d.ts +2 -2
- package/dist/utils/formatting/tableFormatter.js +3 -3
- package/dist/utils/formatting/treeFormatter.d.ts +3 -3
- package/dist/utils/formatting/treeFormatter.js +4 -4
- package/dist/utils/internal/error-handler/errorHandler.js +6 -6
- package/dist/utils/internal/error-handler/helpers.js +1 -1
- package/dist/utils/internal/error-handler/types.d.ts +1 -1
- package/dist/utils/internal/health.js +2 -2
- package/dist/utils/internal/lazyImport.js +1 -1
- package/dist/utils/internal/logger.d.ts +3 -3
- package/dist/utils/internal/logger.js +4 -4
- package/dist/utils/internal/performance.d.ts +1 -1
- package/dist/utils/internal/performance.js +6 -6
- package/dist/utils/internal/requestContext.d.ts +1 -1
- package/dist/utils/internal/requestContext.js +2 -2
- package/dist/utils/metrics/tokenCounter.d.ts +1 -1
- package/dist/utils/metrics/tokenCounter.js +2 -2
- package/dist/utils/network/fetchWithTimeout.d.ts +1 -1
- package/dist/utils/network/fetchWithTimeout.js +6 -6
- package/dist/utils/network/httpError.js +1 -1
- package/dist/utils/network/retry.d.ts +1 -1
- package/dist/utils/network/retry.js +1 -1
- package/dist/utils/pagination/pagination.d.ts +2 -2
- package/dist/utils/pagination/pagination.js +4 -4
- package/dist/utils/parsing/csvParser.d.ts +4 -4
- package/dist/utils/parsing/csvParser.js +6 -6
- package/dist/utils/parsing/dateParser.d.ts +3 -3
- package/dist/utils/parsing/dateParser.js +6 -6
- package/dist/utils/parsing/frontmatterParser.d.ts +2 -2
- package/dist/utils/parsing/frontmatterParser.js +3 -3
- package/dist/utils/parsing/htmlExtractor.d.ts +3 -3
- package/dist/utils/parsing/htmlExtractor.js +5 -5
- package/dist/utils/parsing/jsonParser.d.ts +4 -4
- package/dist/utils/parsing/jsonParser.js +6 -6
- package/dist/utils/parsing/pdfParser.d.ts +2 -2
- package/dist/utils/parsing/pdfParser.js +4 -4
- package/dist/utils/parsing/xmlParser.d.ts +4 -4
- package/dist/utils/parsing/xmlParser.js +5 -5
- package/dist/utils/parsing/yamlParser.d.ts +4 -4
- package/dist/utils/parsing/yamlParser.js +6 -6
- package/dist/utils/scheduling/scheduler.d.ts +3 -3
- package/dist/utils/scheduling/scheduler.js +5 -5
- package/dist/utils/security/rateLimiter.d.ts +2 -2
- package/dist/utils/security/rateLimiter.js +3 -3
- package/dist/utils/security/rateLimiter.js.map +1 -1
- package/dist/utils/security/sanitization.d.ts +3 -3
- package/dist/utils/security/sanitization.js +7 -7
- package/dist/utils/telemetry/instrumentation.d.ts.map +1 -1
- package/dist/utils/telemetry/instrumentation.js +7 -8
- package/dist/utils/telemetry/instrumentation.js.map +1 -1
- package/dist/utils/telemetry/trace.d.ts +1 -1
- package/dist/utils/telemetry/trace.js +1 -1
- package/package.json +33 -26
- package/scripts/check-framework-antipatterns.ts +4 -1
- package/scripts/devcheck.ts +309 -37
- package/scripts/lint-packaging.ts +28 -6
- package/skills/add-tool/SKILL.md +33 -1
- package/skills/api-errors/SKILL.md +2 -1
- package/skills/api-workers/SKILL.md +3 -3
- package/skills/design-mcp-server/SKILL.md +6 -1
- package/skills/field-test/SKILL.md +70 -30
- package/skills/git-wrapup/SKILL.md +4 -3
- package/templates/AGENTS.md +2 -2
- package/templates/CLAUDE.md +2 -2
- package/templates/package.json +6 -6
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Package:** `@cyanheads/mcp-ts-core`
|
|
4
|
-
**Version:** 0.12.
|
|
4
|
+
**Version:** 0.12.7
|
|
5
5
|
**Engines:** Bun ≥1.3.0, Node ≥24.0.0
|
|
6
6
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
7
7
|
**Zod:** ^4.5.4
|
|
@@ -59,7 +59,7 @@ Both paths share the same public API. Init copies starter `package.json`, config
|
|
|
59
59
|
| `/utils` | formatting, encoding, network, pagination, overflow (`outlineOnOverflow`, `OUTLINE_VARIANT`, `selectSections`, `formatOutline`), logging, runtime, telemetry, token counting, parsers†, sanitization†, scheduling† | All utilities (†optional peer deps) |
|
|
60
60
|
| `/services` | `OpenRouterProvider`, `SpeechService`, `createSpeechProvider`, `ElevenLabsProvider`, `WhisperProvider`, `GraphService`, provider interfaces and types | LLM, Speech (TTS/STT), Graph services |
|
|
61
61
|
| `/linter` | `validateDefinitions`, `LintReport`, `LintDiagnostic`, `LintInput`, `LintSeverity` | Definition validation |
|
|
62
|
-
| `/testing` | `createMockContext`, `createMockSession`, `createFetchMock`, `runToolContract`, `createMockLogger`, `getEnrichment`, `getContentBlocks`, `createInMemoryStorage` | Test kit for handlers and upstream HTTP boundaries |
|
|
62
|
+
| `/testing` | `createMockContext`, `createMockSession`, `createFetchMock`, `runToolContract`, `createMockLogger`, `getEnrichment`, `getContentBlocks`, `createInMemoryStorage`, `expectInputRequired` | Test kit for handlers and upstream HTTP boundaries |
|
|
63
63
|
| `/testing/fuzz` | `fuzzTool`, `fuzzResource`, `fuzzPrompt`, `zodToArbitrary`, `adversarialArbitrary`, `ADVERSARIAL_STRINGS` | Fuzz testing |
|
|
64
64
|
| `/testing/vitest` | `mcpTest`, `toolContractSuite`, `McpTestFixtures` (+ re-exported `/testing` helpers) | Vitest fixtures and tool conformance suites (optional peer `vitest`) |
|
|
65
65
|
|
|
@@ -76,7 +76,7 @@ import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
|
76
76
|
import { getMyService } from '@/services/my-domain/my-service.js';
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
Build configs exported for consumer extension: `tsconfig.json` extends `@cyanheads/mcp-ts-core/tsconfig.base.json`, `biome.json` extends `@cyanheads/mcp-ts-core/biome`, `vitest.config.ts`
|
|
79
|
+
Build configs exported for consumer extension: `tsconfig.json` extends `@cyanheads/mcp-ts-core/tsconfig.base.json`, `biome.json` extends `@cyanheads/mcp-ts-core/biome`, `vitest.config.ts` folds in `@cyanheads/mcp-ts-core/vitest.config` via `mergeConfig`.
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
@@ -115,7 +115,7 @@ await createApp({
|
|
|
115
115
|
|
|
116
116
|
**Identity fields** — Optional `title`, `websiteUrl`, `description`, `icons` (SEP-973) pass through to the SDK's `initialize` serverInfo and to the server manifest, keeping the `/.well-known/mcp.json` server card and landing page consistent with what `initialize` reports. Explicit `description` wins over `MCP_SERVER_DESCRIPTION`/package.json.
|
|
117
117
|
|
|
118
|
-
**Also available** — `landing` (`LandingConfig`, HTTP transport only: landing-page config, all fields optional), `context: { exposeStatelessSessionId }` (populate `ctx.sessionId` from the SDK's per-request token in stateless HTTP mode; default `false`), and `cacheHints` (2026-07-28 `ttlMs`/`cacheScope` per cacheable operation — see Adding a Resource for the per-resource override).
|
|
118
|
+
**Also available** — `landing` (`LandingConfig`, HTTP transport only: landing-page config, all fields optional), `context: { exposeStatelessSessionId }` (populate `ctx.sessionId` from the SDK's per-request token in stateless HTTP mode; default `false`), `eventBus` (the `ServerEventBus` backing `subscriptions/listen`; defaults to an in-process bus — supply one for a multi-isolate or multi-process runtime, Workers most of all), and `cacheHints` (2026-07-28 `ttlMs`/`cacheScope` per cacheable operation — see Adding a Resource for the per-resource override).
|
|
119
119
|
|
|
120
120
|
### Cloudflare Workers — `createWorkerHandler(options)`
|
|
121
121
|
|
|
@@ -146,6 +146,7 @@ interface CoreServices {
|
|
|
146
146
|
logger: Logger;
|
|
147
147
|
storage: StorageService;
|
|
148
148
|
rateLimiter: RateLimiter;
|
|
149
|
+
notify: ServerNotifier; // out-of-request list-changed / resource-updated publishing
|
|
149
150
|
canvas?: DataCanvas; // present when CANVAS_PROVIDER_TYPE=duckdb; never on Workers
|
|
150
151
|
llmProvider?: ILlmProvider;
|
|
151
152
|
speechService?: SpeechService;
|
|
@@ -299,7 +300,7 @@ interface Context {
|
|
|
299
300
|
readonly uri?: URL; // present for resource handlers
|
|
300
301
|
readonly content: ContentCollect; // media blocks → prepended to content[]; never in structuredContent
|
|
301
302
|
readonly enrich: Enrich; // success-path agent context → structuredContent + content[]; typed on HandlerContext<R, E>
|
|
302
|
-
recoveryFor(reason: string): { recovery: { hint: string } } |
|
|
303
|
+
recoveryFor(reason: string): { recovery: { hint: string } } | Record<string, never>; // opt-in contract resolver
|
|
303
304
|
}
|
|
304
305
|
```
|
|
305
306
|
|
|
@@ -388,7 +389,7 @@ async handler(input, ctx) {
|
|
|
388
389
|
|
|
389
390
|
**`ctx.recoveryFor(reason)`** returns `{}` when no contract exists (spread-safe). Typed against the declared reason union on `HandlerContext<R>`. Works in services: `throw validationError(msg, { reason: 'X', ...ctx.recoveryFor('X') })`. Opt-in — author spreads explicitly.
|
|
390
391
|
|
|
391
|
-
**Contracts are inline, per-tool.** Don't extract shared `errors[]` constants — locality is the point, and dynamic `recovery` hints need tool-specific context. Declare domain-specific failures only; **baseline codes** (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) are auto-allowed by conformance lint. The lint scans handler source only — service-layer throws still reach clients via auto-classification.
|
|
392
|
+
**Contracts are inline, per-tool.** Don't extract shared `errors[]` constants — locality is the point, and dynamic `recovery` hints need tool-specific context. Declare domain-specific failures only; **baseline codes** (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) are auto-allowed by conformance lint. The lint scans handler source only — service-layer throws still reach clients via auto-classification.
|
|
392
393
|
|
|
393
394
|
**Fallback for ad-hoc throws** (no contract entry fits, prototype tools, service-layer code): use error factories.
|
|
394
395
|
|
|
@@ -398,7 +399,7 @@ throw notFound('Item not found', { itemId: '123' });
|
|
|
398
399
|
throw validationError('Missing required field: name', { field: 'name' });
|
|
399
400
|
```
|
|
400
401
|
|
|
401
|
-
Available factories: `invalidParams`, `invalidRequest`, `notFound`, `forbidden`, `unauthorized`, `validationError`, `conflict`, `rateLimited`, `timeout`, `serviceUnavailable`, `configurationError`, `internalError`, `serializationError`, `databaseError`. All accept `(message, data?, options?)` where `options` is `{ cause?: unknown }`.
|
|
402
|
+
Available factories: `invalidParams`, `invalidRequest`, `notFound`, `forbidden`, `unauthorized`, `validationError`, `conflict`, `rateLimited`, `timeout`, `serviceUnavailable`, `configurationError`, `internalError`, `serializationError`, `databaseError`, `requestCancelled`. All accept `(message, data?, options?)` where `options` is `{ cause?: unknown }`.
|
|
402
403
|
|
|
403
404
|
For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { service, data })` from `/utils` — maps the full status table (401/403/408/422/429/5xx) and captures body + `Retry-After`.
|
|
404
405
|
|
|
@@ -467,7 +468,7 @@ describe('myTool', () => {
|
|
|
467
468
|
});
|
|
468
469
|
```
|
|
469
470
|
|
|
470
|
-
**`createMockContext` options:** `createMockContext()` (state included), `{ tenantId: 'test-tenant' }` (explicit tenant; defaults to `'default'`, as stdio resolves it), `{ errors: myTool.errors }` (typed `ctx.fail`), `{
|
|
471
|
+
**`createMockContext` options:** `createMockContext()` (state included), `{ tenantId: 'test-tenant' }` (explicit tenant; defaults to `'default'`, as stdio resolves it), `{ errors: myTool.errors }` (typed `ctx.fail`), `{ inputResponses }` / `{ requestState }` (seed `ctx.inputs` to drive a multi-round-trip handler's second round directly), plus `auth`, `sessionId`, `signal`, `requestId`, `uri`, and the four `notify*` callbacks.
|
|
471
472
|
|
|
472
473
|
**`ctx.state` in tests is the production path.** The mock backs it with a real `StorageService` over an `InMemoryProvider`, so key validation (`[a-zA-Z0-9_.\-/]+` — colons rejected) and TTL expiry behave exactly as they do in a deployment. Passing `errors` narrows the return type to `HandlerContext<ReasonOf<…>>`, which is what a definition declaring a contract types its handler's `ctx` as — so `definition.handler(input, ctx)` typechecks.
|
|
473
474
|
|
|
@@ -544,16 +545,27 @@ Skills live in `skills/<name>/SKILL.md`; the full list is discoverable via the a
|
|
|
544
545
|
| `bun run lint:mcp` | Validate MCP definitions against spec |
|
|
545
546
|
| `bun run format` | Auto-fix Biome lint/format issues (safe fixes only) |
|
|
546
547
|
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior, not just formatting |
|
|
547
|
-
| `bun run
|
|
548
|
-
| `bun run
|
|
549
|
-
| `bun run test
|
|
550
|
-
| `bun run test:
|
|
551
|
-
| `bun run test:
|
|
548
|
+
| `bun run tree` | Regenerate `docs/tree.md` after the directory structure changes |
|
|
549
|
+
| `bun run typecheck:worker` | The workerd type environment (`tsconfig.worker.json`). Cloudflare's ambient globals declare `Buffer` as `any` and cannot share a program with `@types/node`, so the worker lane is checked separately, against the built declarations — build first |
|
|
550
|
+
| `bun run test` | Every root project — unit, leak-gate, compliance, smoke, fuzz, typecheck (Bun runtime) |
|
|
551
|
+
| `bun run test:unit` / `:smoke` / `:fuzz` / `:compliance` / `:typecheck` | One root project via `--project`. `test:typecheck` runs the `.test-d.ts` contracts, whose `@ts-expect-error` cases are the negative assertions |
|
|
552
|
+
| `bun run test:leak-gate` | The retention gate's own sentinel suite. Each case spawns a full Vitest run, so it is excluded from the `unit` project |
|
|
553
|
+
| `bun run test:coverage` | Root projects with coverage thresholds enforced |
|
|
554
|
+
| `bun run test:integration` | Real server subprocesses over stdio and HTTP |
|
|
555
|
+
| `bun run test:worker` | The framework under real `workerd`, then a standalone Worker bundle through the Wrangler toolchain — real Node on both legs |
|
|
556
|
+
| `bun run test:package` | Rebuilds, packs the tarball, and consumes it as an external project would (exports, declarations, both runtimes) |
|
|
557
|
+
| `bun run test:node` | Root projects + integration under real Node via `scripts/with-node.ts`, which bypasses Bun's `node` PATH shim |
|
|
558
|
+
| `bun run test:order` | Root projects on real Node in shuffled file order under a pinned seed — catches inter-file state leakage |
|
|
559
|
+
| `bun run test:leaks` | Real-Node root async-resource retention gate; [scope and evidence](tests/leaks/README.md) |
|
|
560
|
+
| `bun run test:all` | Release gate: `rebuild` → `test:coverage` → `test:node` → `test:worker` → `test:integration`. `test:package` and `test:leaks` are separate lanes |
|
|
561
|
+
| `bun run bench` / `bench:node` / `bench:io` / `bench:io:bun` | Microbenchmarks (`tests/benchmarks/micro`) and opt-in transport I/O measurements (`tests/benchmarks/io`) |
|
|
552
562
|
| `bun run start:stdio` | Production mode (stdio, after build) |
|
|
553
563
|
| `bun run start:http` | Production mode (HTTP, after build) |
|
|
554
564
|
| `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
|
|
555
565
|
| `bun run changelog:check` | Verify `CHANGELOG.md` is in sync with `changelog/` (used by devcheck) |
|
|
556
566
|
|
|
567
|
+
Lane configs live in `tests/config/` (`vitest.integration.ts`, `vitest.worker.ts`, `vitest.worker-bundle.ts`, `vitest.package.ts`, `vitest.benchmark.ts`, `vitest.performance.ts`, `vitest.leaks.ts`, `vitest.leak-sentinels.ts`). Two configs stay at the repo root: `vitest.config.ts`, the root `projects` list a bare `vitest` invocation discovers, and `vitest.config.base.mjs`, the published `./vitest.config` export.
|
|
568
|
+
|
|
557
569
|
After `bun update --latest`, run the `maintenance` skill to investigate changelogs, adopt upstream changes, and sync project skills.
|
|
558
570
|
|
|
559
571
|
---
|
|
@@ -591,7 +603,7 @@ security: false # optional, default fals
|
|
|
591
603
|
|
|
592
604
|
Badge order when both set: `· ⚠️ Breaking · 🛡️ Security`. Summary > 350 chars or malformed boolean fails `changelog:check`.
|
|
593
605
|
|
|
594
|
-
**Section order
|
|
606
|
+
**Section order:** the Keep a Changelog sequence — Added, Changed, Deprecated, Removed, Fixed, Security — then `Dependencies` last. Omit empty sections. Pre-release versions consolidate as sub-headers inside the final version's file — no separate files per pre-release.
|
|
595
607
|
|
|
596
608
|
---
|
|
597
609
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Package:** `@cyanheads/mcp-ts-core`
|
|
4
|
-
**Version:** 0.12.
|
|
4
|
+
**Version:** 0.12.7
|
|
5
5
|
**Engines:** Bun ≥1.3.0, Node ≥24.0.0
|
|
6
6
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
7
7
|
**Zod:** ^4.5.4
|
|
@@ -59,7 +59,7 @@ Both paths share the same public API. Init copies starter `package.json`, config
|
|
|
59
59
|
| `/utils` | formatting, encoding, network, pagination, overflow (`outlineOnOverflow`, `OUTLINE_VARIANT`, `selectSections`, `formatOutline`), logging, runtime, telemetry, token counting, parsers†, sanitization†, scheduling† | All utilities (†optional peer deps) |
|
|
60
60
|
| `/services` | `OpenRouterProvider`, `SpeechService`, `createSpeechProvider`, `ElevenLabsProvider`, `WhisperProvider`, `GraphService`, provider interfaces and types | LLM, Speech (TTS/STT), Graph services |
|
|
61
61
|
| `/linter` | `validateDefinitions`, `LintReport`, `LintDiagnostic`, `LintInput`, `LintSeverity` | Definition validation |
|
|
62
|
-
| `/testing` | `createMockContext`, `createMockSession`, `createFetchMock`, `runToolContract`, `createMockLogger`, `getEnrichment`, `getContentBlocks`, `createInMemoryStorage` | Test kit for handlers and upstream HTTP boundaries |
|
|
62
|
+
| `/testing` | `createMockContext`, `createMockSession`, `createFetchMock`, `runToolContract`, `createMockLogger`, `getEnrichment`, `getContentBlocks`, `createInMemoryStorage`, `expectInputRequired` | Test kit for handlers and upstream HTTP boundaries |
|
|
63
63
|
| `/testing/fuzz` | `fuzzTool`, `fuzzResource`, `fuzzPrompt`, `zodToArbitrary`, `adversarialArbitrary`, `ADVERSARIAL_STRINGS` | Fuzz testing |
|
|
64
64
|
| `/testing/vitest` | `mcpTest`, `toolContractSuite`, `McpTestFixtures` (+ re-exported `/testing` helpers) | Vitest fixtures and tool conformance suites (optional peer `vitest`) |
|
|
65
65
|
|
|
@@ -76,7 +76,7 @@ import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
|
76
76
|
import { getMyService } from '@/services/my-domain/my-service.js';
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
Build configs exported for consumer extension: `tsconfig.json` extends `@cyanheads/mcp-ts-core/tsconfig.base.json`, `biome.json` extends `@cyanheads/mcp-ts-core/biome`, `vitest.config.ts`
|
|
79
|
+
Build configs exported for consumer extension: `tsconfig.json` extends `@cyanheads/mcp-ts-core/tsconfig.base.json`, `biome.json` extends `@cyanheads/mcp-ts-core/biome`, `vitest.config.ts` folds in `@cyanheads/mcp-ts-core/vitest.config` via `mergeConfig`.
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
@@ -115,7 +115,7 @@ await createApp({
|
|
|
115
115
|
|
|
116
116
|
**Identity fields** — Optional `title`, `websiteUrl`, `description`, `icons` (SEP-973) pass through to the SDK's `initialize` serverInfo and to the server manifest, keeping the `/.well-known/mcp.json` server card and landing page consistent with what `initialize` reports. Explicit `description` wins over `MCP_SERVER_DESCRIPTION`/package.json.
|
|
117
117
|
|
|
118
|
-
**Also available** — `landing` (`LandingConfig`, HTTP transport only: landing-page config, all fields optional), `context: { exposeStatelessSessionId }` (populate `ctx.sessionId` from the SDK's per-request token in stateless HTTP mode; default `false`), and `cacheHints` (2026-07-28 `ttlMs`/`cacheScope` per cacheable operation — see Adding a Resource for the per-resource override).
|
|
118
|
+
**Also available** — `landing` (`LandingConfig`, HTTP transport only: landing-page config, all fields optional), `context: { exposeStatelessSessionId }` (populate `ctx.sessionId` from the SDK's per-request token in stateless HTTP mode; default `false`), `eventBus` (the `ServerEventBus` backing `subscriptions/listen`; defaults to an in-process bus — supply one for a multi-isolate or multi-process runtime, Workers most of all), and `cacheHints` (2026-07-28 `ttlMs`/`cacheScope` per cacheable operation — see Adding a Resource for the per-resource override).
|
|
119
119
|
|
|
120
120
|
### Cloudflare Workers — `createWorkerHandler(options)`
|
|
121
121
|
|
|
@@ -146,6 +146,7 @@ interface CoreServices {
|
|
|
146
146
|
logger: Logger;
|
|
147
147
|
storage: StorageService;
|
|
148
148
|
rateLimiter: RateLimiter;
|
|
149
|
+
notify: ServerNotifier; // out-of-request list-changed / resource-updated publishing
|
|
149
150
|
canvas?: DataCanvas; // present when CANVAS_PROVIDER_TYPE=duckdb; never on Workers
|
|
150
151
|
llmProvider?: ILlmProvider;
|
|
151
152
|
speechService?: SpeechService;
|
|
@@ -299,7 +300,7 @@ interface Context {
|
|
|
299
300
|
readonly uri?: URL; // present for resource handlers
|
|
300
301
|
readonly content: ContentCollect; // media blocks → prepended to content[]; never in structuredContent
|
|
301
302
|
readonly enrich: Enrich; // success-path agent context → structuredContent + content[]; typed on HandlerContext<R, E>
|
|
302
|
-
recoveryFor(reason: string): { recovery: { hint: string } } |
|
|
303
|
+
recoveryFor(reason: string): { recovery: { hint: string } } | Record<string, never>; // opt-in contract resolver
|
|
303
304
|
}
|
|
304
305
|
```
|
|
305
306
|
|
|
@@ -388,7 +389,7 @@ async handler(input, ctx) {
|
|
|
388
389
|
|
|
389
390
|
**`ctx.recoveryFor(reason)`** returns `{}` when no contract exists (spread-safe). Typed against the declared reason union on `HandlerContext<R>`. Works in services: `throw validationError(msg, { reason: 'X', ...ctx.recoveryFor('X') })`. Opt-in — author spreads explicitly.
|
|
390
391
|
|
|
391
|
-
**Contracts are inline, per-tool.** Don't extract shared `errors[]` constants — locality is the point, and dynamic `recovery` hints need tool-specific context. Declare domain-specific failures only; **baseline codes** (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) are auto-allowed by conformance lint. The lint scans handler source only — service-layer throws still reach clients via auto-classification.
|
|
392
|
+
**Contracts are inline, per-tool.** Don't extract shared `errors[]` constants — locality is the point, and dynamic `recovery` hints need tool-specific context. Declare domain-specific failures only; **baseline codes** (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) are auto-allowed by conformance lint. The lint scans handler source only — service-layer throws still reach clients via auto-classification.
|
|
392
393
|
|
|
393
394
|
**Fallback for ad-hoc throws** (no contract entry fits, prototype tools, service-layer code): use error factories.
|
|
394
395
|
|
|
@@ -398,7 +399,7 @@ throw notFound('Item not found', { itemId: '123' });
|
|
|
398
399
|
throw validationError('Missing required field: name', { field: 'name' });
|
|
399
400
|
```
|
|
400
401
|
|
|
401
|
-
Available factories: `invalidParams`, `invalidRequest`, `notFound`, `forbidden`, `unauthorized`, `validationError`, `conflict`, `rateLimited`, `timeout`, `serviceUnavailable`, `configurationError`, `internalError`, `serializationError`, `databaseError`. All accept `(message, data?, options?)` where `options` is `{ cause?: unknown }`.
|
|
402
|
+
Available factories: `invalidParams`, `invalidRequest`, `notFound`, `forbidden`, `unauthorized`, `validationError`, `conflict`, `rateLimited`, `timeout`, `serviceUnavailable`, `configurationError`, `internalError`, `serializationError`, `databaseError`, `requestCancelled`. All accept `(message, data?, options?)` where `options` is `{ cause?: unknown }`.
|
|
402
403
|
|
|
403
404
|
For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { service, data })` from `/utils` — maps the full status table (401/403/408/422/429/5xx) and captures body + `Retry-After`.
|
|
404
405
|
|
|
@@ -467,7 +468,7 @@ describe('myTool', () => {
|
|
|
467
468
|
});
|
|
468
469
|
```
|
|
469
470
|
|
|
470
|
-
**`createMockContext` options:** `createMockContext()` (state included), `{ tenantId: 'test-tenant' }` (explicit tenant; defaults to `'default'`, as stdio resolves it), `{ errors: myTool.errors }` (typed `ctx.fail`), `{
|
|
471
|
+
**`createMockContext` options:** `createMockContext()` (state included), `{ tenantId: 'test-tenant' }` (explicit tenant; defaults to `'default'`, as stdio resolves it), `{ errors: myTool.errors }` (typed `ctx.fail`), `{ inputResponses }` / `{ requestState }` (seed `ctx.inputs` to drive a multi-round-trip handler's second round directly), plus `auth`, `sessionId`, `signal`, `requestId`, `uri`, and the four `notify*` callbacks.
|
|
471
472
|
|
|
472
473
|
**`ctx.state` in tests is the production path.** The mock backs it with a real `StorageService` over an `InMemoryProvider`, so key validation (`[a-zA-Z0-9_.\-/]+` — colons rejected) and TTL expiry behave exactly as they do in a deployment. Passing `errors` narrows the return type to `HandlerContext<ReasonOf<…>>`, which is what a definition declaring a contract types its handler's `ctx` as — so `definition.handler(input, ctx)` typechecks.
|
|
473
474
|
|
|
@@ -544,16 +545,27 @@ Skills live in `skills/<name>/SKILL.md`; the full list is discoverable via the a
|
|
|
544
545
|
| `bun run lint:mcp` | Validate MCP definitions against spec |
|
|
545
546
|
| `bun run format` | Auto-fix Biome lint/format issues (safe fixes only) |
|
|
546
547
|
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior, not just formatting |
|
|
547
|
-
| `bun run
|
|
548
|
-
| `bun run
|
|
549
|
-
| `bun run test
|
|
550
|
-
| `bun run test:
|
|
551
|
-
| `bun run test:
|
|
548
|
+
| `bun run tree` | Regenerate `docs/tree.md` after the directory structure changes |
|
|
549
|
+
| `bun run typecheck:worker` | The workerd type environment (`tsconfig.worker.json`). Cloudflare's ambient globals declare `Buffer` as `any` and cannot share a program with `@types/node`, so the worker lane is checked separately, against the built declarations — build first |
|
|
550
|
+
| `bun run test` | Every root project — unit, leak-gate, compliance, smoke, fuzz, typecheck (Bun runtime) |
|
|
551
|
+
| `bun run test:unit` / `:smoke` / `:fuzz` / `:compliance` / `:typecheck` | One root project via `--project`. `test:typecheck` runs the `.test-d.ts` contracts, whose `@ts-expect-error` cases are the negative assertions |
|
|
552
|
+
| `bun run test:leak-gate` | The retention gate's own sentinel suite. Each case spawns a full Vitest run, so it is excluded from the `unit` project |
|
|
553
|
+
| `bun run test:coverage` | Root projects with coverage thresholds enforced |
|
|
554
|
+
| `bun run test:integration` | Real server subprocesses over stdio and HTTP |
|
|
555
|
+
| `bun run test:worker` | The framework under real `workerd`, then a standalone Worker bundle through the Wrangler toolchain — real Node on both legs |
|
|
556
|
+
| `bun run test:package` | Rebuilds, packs the tarball, and consumes it as an external project would (exports, declarations, both runtimes) |
|
|
557
|
+
| `bun run test:node` | Root projects + integration under real Node via `scripts/with-node.ts`, which bypasses Bun's `node` PATH shim |
|
|
558
|
+
| `bun run test:order` | Root projects on real Node in shuffled file order under a pinned seed — catches inter-file state leakage |
|
|
559
|
+
| `bun run test:leaks` | Real-Node root async-resource retention gate; [scope and evidence](tests/leaks/README.md) |
|
|
560
|
+
| `bun run test:all` | Release gate: `rebuild` → `test:coverage` → `test:node` → `test:worker` → `test:integration`. `test:package` and `test:leaks` are separate lanes |
|
|
561
|
+
| `bun run bench` / `bench:node` / `bench:io` / `bench:io:bun` | Microbenchmarks (`tests/benchmarks/micro`) and opt-in transport I/O measurements (`tests/benchmarks/io`) |
|
|
552
562
|
| `bun run start:stdio` | Production mode (stdio, after build) |
|
|
553
563
|
| `bun run start:http` | Production mode (HTTP, after build) |
|
|
554
564
|
| `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
|
|
555
565
|
| `bun run changelog:check` | Verify `CHANGELOG.md` is in sync with `changelog/` (used by devcheck) |
|
|
556
566
|
|
|
567
|
+
Lane configs live in `tests/config/` (`vitest.integration.ts`, `vitest.worker.ts`, `vitest.worker-bundle.ts`, `vitest.package.ts`, `vitest.benchmark.ts`, `vitest.performance.ts`, `vitest.leaks.ts`, `vitest.leak-sentinels.ts`). Two configs stay at the repo root: `vitest.config.ts`, the root `projects` list a bare `vitest` invocation discovers, and `vitest.config.base.mjs`, the published `./vitest.config` export.
|
|
568
|
+
|
|
557
569
|
After `bun update --latest`, run the `maintenance` skill to investigate changelogs, adopt upstream changes, and sync project skills.
|
|
558
570
|
|
|
559
571
|
---
|
|
@@ -591,7 +603,7 @@ security: false # optional, default fals
|
|
|
591
603
|
|
|
592
604
|
Badge order when both set: `· ⚠️ Breaking · 🛡️ Security`. Summary > 350 chars or malformed boolean fails `changelog:check`.
|
|
593
605
|
|
|
594
|
-
**Section order
|
|
606
|
+
**Section order:** the Keep a Changelog sequence — Added, Changed, Deprecated, Removed, Fixed, Security — then `Dependencies` last. Omit empty sections. Pre-release versions consolidate as sub-headers inside the final version's file — no separate files per pre-release.
|
|
595
607
|
|
|
596
608
|
---
|
|
597
609
|
|
package/README.md
CHANGED
|
@@ -1,25 +1,103 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1>@cyanheads/mcp-ts-core</h1>
|
|
3
|
-
<p><b>Agent-native TypeScript framework for
|
|
3
|
+
<p><b>Agent-native TypeScript framework for MCP servers.</b></p>
|
|
4
|
+
<p>Give your agent the infrastructure, patterns, and skills to build and ship your server.</p>
|
|
4
5
|
</div>
|
|
5
6
|
|
|
6
7
|
<div align="center">
|
|
7
8
|
|
|
8
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://modelcontextprotocol.io/specification/2026-07-28)
|
|
9
10
|
|
|
10
11
|
[](https://modelcontextprotocol.io/) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
12
|
|
|
12
|
-
[
|
|
13
|
+
[Quick start](#quick-start) · [Capabilities](#what-comes-with-it) · [API reference](#api-overview) · [Examples](#examples)
|
|
13
14
|
|
|
14
15
|
</div>
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
18
|
-
##
|
|
19
|
+
## Build AI tools for anything you can describe.
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
Connect an API, a dataset, or a workflow to an AI agent through the Model Context Protocol (MCP). `@cyanheads/mcp-ts-core` gives you and your agent the foundation to build that server, with auth, storage, logging, and deployment support & skills already available.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
**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.
|
|
24
|
+
|
|
25
|
+
Your project holds the domain code. The framework stays a dependency, so infrastructure fixes arrive through package upgrades. Run the `maintenance` skill to have your agent update the framework, grab the latest skills, and integrate them into your project.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
Servers can run on Bun, Node.js 24 or later, or Cloudflare Workers.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bunx @cyanheads/mcp-ts-core init my-mcp-server
|
|
33
|
+
cd my-mcp-server
|
|
34
|
+
bun install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Open the project in Claude Code, Codex, or your preferred agent and give it a concrete starting point:
|
|
38
|
+
|
|
39
|
+
> 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.
|
|
40
|
+
|
|
41
|
+
The scaffold includes a source tree, build and test configuration, `CLAUDE.md`/`AGENTS.md`, Agent Skills, and plugin metadata for Claude Code and Codex.
|
|
42
|
+
|
|
43
|
+
Already have a TypeScript project? Install the framework directly with `bun add @cyanheads/mcp-ts-core` and register your definitions with `createApp()`.
|
|
44
|
+
|
|
45
|
+
## A tool is a schema and a function
|
|
46
|
+
|
|
47
|
+
Here's a complete server that searches a small catalog. To try it in the scaffolded project, replace `src/index.ts` with:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { createApp, tool, z } from '@cyanheads/mcp-ts-core';
|
|
51
|
+
|
|
52
|
+
const catalog = ['Notebook', 'Mechanical pencil', 'Desk lamp'];
|
|
53
|
+
|
|
54
|
+
const search = tool('catalog_search', {
|
|
55
|
+
description: 'Search catalog item names. An empty query lists all items.',
|
|
56
|
+
annotations: { readOnlyHint: true },
|
|
57
|
+
input: z.object({
|
|
58
|
+
query: z.string().describe('Text to find in an item name'),
|
|
59
|
+
}),
|
|
60
|
+
output: z.object({
|
|
61
|
+
items: z.array(z.string()).describe('Matching item names'),
|
|
62
|
+
}),
|
|
63
|
+
async handler({ query }) {
|
|
64
|
+
return {
|
|
65
|
+
items: catalog.filter((name) =>
|
|
66
|
+
name.toLowerCase().includes(query.toLowerCase()),
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await createApp({ name: 'catalog-mcp-server', title: 'catalog-mcp-server', tools: [search] });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Build and run it over HTTP:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bun run rebuild
|
|
79
|
+
bun run start:http
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
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`.
|
|
83
|
+
|
|
84
|
+
## What comes with it
|
|
85
|
+
|
|
86
|
+
| You need to… | The framework provides |
|
|
87
|
+
|:------------|:-----------------------|
|
|
88
|
+
| Give an assistant useful capabilities | Typed builders for tools, resources, prompts, and interactive MCP Apps |
|
|
89
|
+
| Help an agent use those capabilities correctly | Server instructions, result enrichment, and declared errors with recovery guidance |
|
|
90
|
+
| Control access and keep state | JWT/OAuth, per-definition scopes, and tenant-scoped storage with swappable backends |
|
|
91
|
+
| Run locally or host a service | stdio and HTTP on Bun/Node.js; a separate entry point for Cloudflare Workers |
|
|
92
|
+
| Understand failures and catch mistakes | Structured logs, optional OpenTelemetry, definition linting, contract tests, and fuzz testing |
|
|
93
|
+
|
|
94
|
+
Optional integrations such as DuckDB, Supabase, and the OpenTelemetry SDK are peer dependencies, installed when you need them.
|
|
95
|
+
|
|
96
|
+
## Give agents useful results
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
Here, `runSearch(query, limit)` returns `{ items, total, parsed }` (matches, total before the limit, and parsed query), or `null` if the index is unavailable:
|
|
23
101
|
|
|
24
102
|
```ts
|
|
25
103
|
import { createApp, tool, z } from '@cyanheads/mcp-ts-core';
|
|
@@ -30,7 +108,7 @@ const search = tool('search', {
|
|
|
30
108
|
annotations: { readOnlyHint: true },
|
|
31
109
|
input: z.object({
|
|
32
110
|
query: z.string().describe('Search terms'),
|
|
33
|
-
limit: z.number().default(10).describe('Max results'),
|
|
111
|
+
limit: z.number().int().min(1).default(10).describe('Max results'),
|
|
34
112
|
}),
|
|
35
113
|
output: z.object({
|
|
36
114
|
items: z.array(z.string()).describe('Matching item names, best first'),
|
|
@@ -51,10 +129,12 @@ const search = tool('search', {
|
|
|
51
129
|
],
|
|
52
130
|
handler: async (input, ctx) => {
|
|
53
131
|
const res = await runSearch(input.query, input.limit);
|
|
54
|
-
if (!res)
|
|
132
|
+
if (!res) {
|
|
133
|
+
throw ctx.fail('index_unavailable', undefined, ctx.recoveryFor('index_unavailable'));
|
|
134
|
+
}
|
|
55
135
|
ctx.enrich({ effectiveQuery: res.parsed, totalCount: res.total });
|
|
56
136
|
if (res.items.length === 0) {
|
|
57
|
-
ctx.enrich({ notice: `No matches for "${input.query}". Try broader terms.` });
|
|
137
|
+
ctx.enrich({ notice: `No matches for "${input.query}". Try broader terms.` });
|
|
58
138
|
}
|
|
59
139
|
return { items: res.items }; // enrichment never rides in the domain return
|
|
60
140
|
},
|
|
@@ -63,51 +143,26 @@ const search = tool('search', {
|
|
|
63
143
|
await createApp({ tools: [search] });
|
|
64
144
|
```
|
|
65
145
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
**`enrichment`** carries the context an agent reasons with (the parsed query, the true total, an empty-result notice); the framework merges it into `structuredContent` *and* mirrors it into `content[]`, so `structuredContent`-only clients (Claude Code) and `content[]`-only clients (Claude Desktop) both see it, no `format()` needed. The typed **`errors[]`** contract handles genuine failures (an empty result is a `notice`, not a throw), and the linter cross-checks both against the handler body. Both publish in `tools/list`, so clients preview a tool's success *and* failure shapes.
|
|
69
|
-
|
|
70
|
-
The rest is automatic: every tool call is logged with duration, payload sizes, and request correlation, and `createApp()` handles config parsing, logger init, transport startup, signal handlers, and graceful shutdown.
|
|
71
|
-
|
|
72
|
-
## Quick start
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
bunx @cyanheads/mcp-ts-core init my-mcp-server
|
|
76
|
-
cd my-mcp-server
|
|
77
|
-
bun install
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
You get a scaffolded project with `CLAUDE.md`/`AGENTS.md`, Agent Skills, plugin metadata (Codex + Claude Code), and a `src/` tree ready for your tools. Infrastructure (transports, auth, storage, telemetry, lifecycle, linting) lives in `node_modules`. What's left is domain: which APIs to wrap, which workflows to expose.
|
|
146
|
+
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.
|
|
81
147
|
|
|
82
|
-
|
|
148
|
+
### Same data across client surfaces
|
|
83
149
|
|
|
84
|
-
|
|
150
|
+
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.
|
|
85
151
|
|
|
86
|
-
|
|
152
|
+
`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:
|
|
87
153
|
|
|
88
154
|
```ts
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
limit: z.number().default(10).describe('Max results'),
|
|
96
|
-
}),
|
|
97
|
-
output: z.object({
|
|
98
|
-
items: z.array(z.string()).describe('Search results'),
|
|
99
|
-
}),
|
|
100
|
-
async handler(input) {
|
|
101
|
-
const results = await doSearch(input.query, input.limit);
|
|
102
|
-
return { items: results };
|
|
103
|
-
},
|
|
104
|
-
format: (result) => [
|
|
105
|
-
{ type: 'text', text: result.items.map((name) => `- ${name}`).join('\n') },
|
|
106
|
-
],
|
|
107
|
-
});
|
|
155
|
+
format: (result) => [{
|
|
156
|
+
type: 'text',
|
|
157
|
+
text: result.items.length > 0
|
|
158
|
+
? result.items.map((name) => `- ${name}`).join('\n')
|
|
159
|
+
: 'No matching items.',
|
|
160
|
+
}],
|
|
108
161
|
```
|
|
109
162
|
|
|
110
|
-
|
|
163
|
+
### Resources
|
|
164
|
+
|
|
165
|
+
Resources expose data at a URI. This definition delegates the lookup to your own `getItem()` service:
|
|
111
166
|
|
|
112
167
|
```ts
|
|
113
168
|
import { resource, z } from '@cyanheads/mcp-ts-core';
|
|
@@ -138,22 +193,16 @@ await createApp({
|
|
|
138
193
|
|
|
139
194
|
It also works on Cloudflare Workers with `createWorkerHandler()` — same definitions, different entry point.
|
|
140
195
|
|
|
141
|
-
##
|
|
142
|
-
|
|
143
|
-
- **
|
|
144
|
-
- **
|
|
145
|
-
- **
|
|
146
|
-
- **
|
|
147
|
-
- **
|
|
148
|
-
- **
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
- **Typed error contracts** — declare `errors: [{ reason, code, when, recovery, retryable? }]` and handlers get a typed `ctx.fail(reason, …)`. Contracts publish in `tools/list` so clients preview failure modes; the linter cross-checks the handler. Factories (`notFound()`, `httpErrorFromResponse()`, …) cover ad-hoc throws; plain `Error` auto-classifies.
|
|
152
|
-
- **Multi-backend storage** — `in-memory`, filesystem, Supabase, Cloudflare D1/KV/R2. Swap via env var; handlers don't change.
|
|
153
|
-
- **DataCanvas (optional)** — Tier 3 SQL/analytical workspace backed by DuckDB. Register tabular data from upstream APIs, run SQL across registered tables, export CSV/Parquet/JSON. Token-sharing model (opaque `canvas_id`) for multi-agent collaboration; sliding TTL + per-tenant scoping. Opt-in via `CANVAS_PROVIDER_TYPE=duckdb`; fails closed on Workers.
|
|
154
|
-
- **Observability** — Pino logging + optional OpenTelemetry traces/metrics. Request correlation and tool metrics automatic.
|
|
155
|
-
- **Tiered dependencies** — parsers, OTEL SDK, Supabase, OpenAI as optional peers. Install what you use.
|
|
156
|
-
- **Agent-first DX** — ships `CLAUDE.md` / `AGENTS.md` and Agent Skills that give your coding agent full framework knowledge — it can scaffold tools, write tests, run security audits, and ship releases without you writing the boilerplate.
|
|
196
|
+
## Runtime and integration details
|
|
197
|
+
|
|
198
|
+
- **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.
|
|
199
|
+
- **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`.
|
|
200
|
+
- **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.
|
|
201
|
+
- **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.
|
|
202
|
+
- **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.
|
|
203
|
+
- **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.
|
|
204
|
+
|
|
205
|
+
See the [framework reference](CLAUDE.md) for configuration and handler patterns, and the [observability guide](docs/telemetry/observability.md) for Pino logging and OpenTelemetry traces and metrics.
|
|
157
206
|
|
|
158
207
|
## Server structure
|
|
159
208
|
|
|
@@ -175,7 +224,7 @@ my-mcp-server/
|
|
|
175
224
|
CLAUDE.md / AGENTS.md # Point to core's CLAUDE.md / AGENTS.md for framework docs
|
|
176
225
|
```
|
|
177
226
|
|
|
178
|
-
|
|
227
|
+
Framework infrastructure lives in `node_modules`; your source tree contains the server's definitions, configuration, and domain services.
|
|
179
228
|
|
|
180
229
|
## Configuration
|
|
181
230
|
|
|
@@ -189,7 +238,7 @@ All core config is Zod-validated from environment variables. Server-specific con
|
|
|
189
238
|
| `MCP_AUTH_MODE` | `none`, `jwt`, or `oauth` | `none` |
|
|
190
239
|
| `MCP_AUTH_SECRET_KEY` | JWT signing secret (required for `jwt` mode) | — |
|
|
191
240
|
| `STORAGE_PROVIDER_TYPE` | `in-memory`, `filesystem`, `supabase`, `cloudflare-d1`/`kv`/`r2` | `in-memory` |
|
|
192
|
-
| `CANVAS_PROVIDER_TYPE` | `none` or `duckdb` (
|
|
241
|
+
| `CANVAS_PROVIDER_TYPE` | `none` or `duckdb` (optional peer dependency `@duckdb/node-api`) | `none` |
|
|
193
242
|
| `OTEL_ENABLED` | Enable OpenTelemetry | `false` |
|
|
194
243
|
| `OPENROUTER_API_KEY` | OpenRouter LLM API key | — |
|
|
195
244
|
|
|
@@ -201,7 +250,7 @@ See [CLAUDE.md/AGENTS.md](CLAUDE.md) for the full configuration reference.
|
|
|
201
250
|
|
|
202
251
|
| Function | Purpose |
|
|
203
252
|
|:---------|:--------|
|
|
204
|
-
| `createApp(options)` | Node.js server — handles full lifecycle |
|
|
253
|
+
| `createApp(options)` | Bun or Node.js server — handles full lifecycle |
|
|
205
254
|
| `createWorkerHandler(options)` | Cloudflare Workers — returns an `ExportedHandler` |
|
|
206
255
|
|
|
207
256
|
### Builders
|
|
@@ -216,7 +265,7 @@ See [CLAUDE.md/AGENTS.md](CLAUDE.md) for the full configuration reference.
|
|
|
216
265
|
|
|
217
266
|
### Context
|
|
218
267
|
|
|
219
|
-
Handlers receive a
|
|
268
|
+
Handlers receive a shared `Context`, with typed helpers for declared enrichment and error contracts:
|
|
220
269
|
|
|
221
270
|
| Property | Type | Description |
|
|
222
271
|
|:---------|:-----|:------------|
|
|
@@ -224,7 +273,8 @@ Handlers receive a unified `Context` object:
|
|
|
224
273
|
| `ctx.state` | `ContextState` | Tenant-scoped key-value storage |
|
|
225
274
|
| `ctx.requestInput` | `(spec) => never` | Suspend and ask the caller for more input; the handler is re-entered with the answers |
|
|
226
275
|
| `ctx.inputs` | `ContextInputs` | Reader over a retried request's responses — `.accepted()`, `.view()`, `.state()`, `.dropped` |
|
|
227
|
-
| `ctx.
|
|
276
|
+
| `ctx.enrich` | `Enrich` / `TypedEnrich<E>` | Add declared result context to structured output and text content |
|
|
277
|
+
| `ctx.fail` | `(reason, msg?, data?) => McpError` | Creates an error for `throw ctx.fail(...)`; available with a declared `errors` contract |
|
|
228
278
|
| `ctx.signal` | `AbortSignal` | Cancellation signal |
|
|
229
279
|
| `ctx.notifyResourceUpdated` | `Function?` | Notify subscribed clients a resource changed |
|
|
230
280
|
| `ctx.notifyResourceListChanged` | `Function?` | Notify clients the resource list changed |
|
|
@@ -252,7 +302,7 @@ See [CLAUDE.md/AGENTS.md](CLAUDE.md) for the complete exports reference.
|
|
|
252
302
|
|
|
253
303
|
## Examples
|
|
254
304
|
|
|
255
|
-
The `examples/` directory contains a reference server consuming core through public exports, demonstrating
|
|
305
|
+
The `examples/` directory contains a reference server consuming core through public exports, demonstrating core patterns:
|
|
256
306
|
|
|
257
307
|
| Tool | Pattern |
|
|
258
308
|
|:-----|:--------|
|
|
@@ -260,7 +310,7 @@ The `examples/` directory contains a reference server consuming core through pub
|
|
|
260
310
|
| `template_cat_fact` | External API call, error factories |
|
|
261
311
|
| `template_madlibs_elicitation` | `ctx.requestInput` / `ctx.inputs` for multi-round-trip input |
|
|
262
312
|
| `template_image_test` | Image content blocks |
|
|
263
|
-
| `template_data_explorer` | MCP Apps with linked UI resource
|
|
313
|
+
| `template_data_explorer` | MCP Apps with a linked HTML UI resource |
|
|
264
314
|
|
|
265
315
|
## Testing
|
|
266
316
|
|
|
@@ -277,9 +327,7 @@ const result = await myTool.handler(input, ctx);
|
|
|
277
327
|
|
|
278
328
|
`/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()`.
|
|
279
329
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
Schema-aware fuzz testing via `fast-check`. Generates valid inputs from Zod schemas and adversarial payloads (prototype pollution, injection strings, type confusion) to verify handler invariants.
|
|
330
|
+
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:
|
|
283
331
|
|
|
284
332
|
```ts
|
|
285
333
|
import { fuzzTool } from '@cyanheads/mcp-ts-core/testing/fuzz';
|
|
@@ -296,15 +344,15 @@ Also exports `fuzzResource`, `fuzzPrompt`, `zodToArbitrary`, and `ADVERSARIAL_ST
|
|
|
296
344
|
|
|
297
345
|
- **[CLAUDE.md/AGENTS.md](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`.
|
|
298
346
|
- **[docs/telemetry/](docs/telemetry/)** — OpenTelemetry: full catalog of spans, metrics, and attributes the framework emits ([observability.md](docs/telemetry/observability.md)), plus an example Grafana dashboard and vendor-agnostic query recipes for Datadog, New Relic, Honeycomb ([dashboards.md](docs/telemetry/dashboards.md)).
|
|
299
|
-
- **[CHANGELOG.md](CHANGELOG.md)** — Version history. Each entry includes a summary, migration notes, and links to commits/issues.
|
|
347
|
+
- **[CHANGELOG.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.
|
|
300
348
|
|
|
301
349
|
## Development
|
|
302
350
|
|
|
303
351
|
```bash
|
|
304
352
|
bun run rebuild # clean + build (scripts/clean.ts + scripts/build.ts)
|
|
305
|
-
bun run devcheck # full gate: lint/format, typecheck, MCP defs, framework antipatterns, docs/skills/changelog sync,
|
|
353
|
+
bun run devcheck # full gate: lint/format, typecheck, MCP defs, framework antipatterns, docs/skills/changelog sync, audit, outdated, secrets/TODO scan
|
|
306
354
|
bun run lint:mcp # validate MCP definitions against spec
|
|
307
|
-
bun run test:all #
|
|
355
|
+
bun run test:all # rebuild + coverage + Node.js + Workers + integration
|
|
308
356
|
```
|
|
309
357
|
|
|
310
358
|
## License
|
package/biome.json
CHANGED