---
summary: "Workers boot under `nodejs_compat` (#124), adds `instructions` on entry points (#91), cross-vendor portability lint family (#132), definition linting moves to build-time only. Plus RFC 8414 §3 well-known mount, SSRF hardening, tenant-id boundary check."
breaking: true
security: true
---

# 0.9.0 — 2026-05-11

Cloudflare Workers now boot and serve real MCP JSON-RPC under `nodejs_compat` ([#124](https://github.com/cyanheads/mcp-ts-core/issues/124)) — previously a few module-load paths assumed Node and silently broke. Also new: the MCP spec's `instructions` field on `createApp`/`createWorkerHandler` ([#91](https://github.com/cyanheads/mcp-ts-core/issues/91)) and a cross-vendor JSON Schema portability lint family ([#132](https://github.com/cyanheads/mcp-ts-core/issues/132)) whose `error`-level default-on rule may fail `bun run lint:mcp` and `bun run devcheck` on schemas using `z.url()`/`z.cuid()`/etc. (see Migration below). The startup linter gate has been removed — definition linting is now build-time only, so new lint rules are additive and never break deployed servers.

## Added

- **`CreateAppOptions.instructions?: string`** ([#91](https://github.com/cyanheads/mcp-ts-core/issues/91)) — server-level orientation text included on every `initialize` response. Spec-compliant clients SHOULD forward it to the model as session-level system context. Use for deployment-specific guidance (configured shortcuts, regional notes, scope hints) instead of leaking text into every tool description.
- **`WorkerHandlerOptions.instructions?: string | ((env) => string)`** ([#91](https://github.com/cyanheads/mcp-ts-core/issues/91)) — the resolver form runs inside `initializeApp(env)` after `injectEnvVars`, so `env`-derived text doesn't fight Workers' module-load lifecycle. Type extends `Omit<CreateAppOptions, 'instructions'>`. Empty string is treated as unset.
- **Portability lint family** ([#132](https://github.com/cyanheads/mcp-ts-core/issues/132), folding in [#122](https://github.com/cyanheads/mcp-ts-core/issues/122)) — five new rules walk the emitted JSON Schema and flag patterns that hard-fail or silently drop at OpenAI/Gemini:
  - `schema-format-portability` (**error**, default-on) — `format` value outside the allowlist. Default = OpenAI's nine (`date-time`, `time`, `date`, `duration`, `email`, `hostname`, `ipv4`, `ipv6`, `uuid`). Field report: [cyanheads/git-mcp-server#47](https://github.com/cyanheads/git-mcp-server/issues/47) (`gpt-5-codex` hard-rejects `format: "uri"`).
  - `schema-anyof-needs-type` (warning, default-on) — `anyOf`/`oneOf` branch missing top-level `type` (Gemini rejects with `400: reference to undefined schema`).
  - `schema-no-discriminator-keyword` (warning, default-on) — OpenAPI `discriminator` leaked into the emitted schema.
  - `schema-no-defs` (warning, opt-in) — `$defs`/`$ref` emission. Opt-in because SEP-1576 moves the community toward more `$defs`.
  - `schema-dialect-tag` (warning, opt-in) — missing top-level `$schema`. Forward-compat for hand-built schemas.
- **`LintInput.formatAllowlist`** and **`LintInput.portability: 'strict'`** — programmatic overrides on `validateDefinitions`. Env-var equivalent: `MCP_LINT_PORTABILITY=strict` promotes opt-in rules to warnings.
- **`/.well-known/oauth-protected-resource/{path}` path-suffixed mount** ([#95](https://github.com/cyanheads/mcp-ts-core/issues/95)) — RFC 8414 §3 metadata is now reachable at the path-suffixed variant in addition to the root well-known path, so clients that resolve metadata relative to the MCP endpoint can discover it without extra config.
- **Worker-runtime test suite** ([#124](https://github.com/cyanheads/mcp-ts-core/issues/124)) — `tests/worker/create-worker-handler.worker.test.ts` exercises HTTP binding injection, scheduled events, the origin guard, and end-to-end MCP `initialize → notifications/initialized → tools/list → tools/call` under `@cloudflare/vitest-pool-workers`. Asserts `runtimeCaps.isWorkerLike === true` inside the isolate.
- **Source-level type contract suite** — locks public exports for `tool()` / `resource()` / `prompt()` / `appTool()` / `appResource()` and `CreateAppOptions`.
- **Export-map structural suite** — validates `package.json` `exports` shape against the runtime imports README and skills demonstrate.

## Changed

- **Worker runtime detection** uses `navigator.userAgent === 'Cloudflare-Workers'` instead of `!process.versions.node` ([#124](https://github.com/cyanheads/mcp-ts-core/issues/124)). Under `nodejs_compat`, `process.versions.node` is a truthy string, so the previous `!isNode && hasWorkerGlobalScope()` check always evaluated `false`. `isWorkerLike` is now independent of `isNode`: both can be true.
- **HTTP transport bootstrap split** ([#124](https://github.com/cyanheads/mcp-ts-core/issues/124)) — `httpTransport.ts` now exports only the runtime-agnostic `createHttpApp`; `@hono/node-server` `serve()` and the `node:http` port-probe moved to a new `httpServer.ts`. Worker bundles tree-shake the Node-server bootstrap.
- **`logger.isServerless` check** is deferred until first log call ([#124](https://github.com/cyanheads/mcp-ts-core/issues/124)) — previously captured at module load before `createWorkerHandler` could set `IS_SERVERLESS`.
- **Node-only call sites gated on `!isWorkerLike`** ([#124](https://github.com/cyanheads/mcp-ts-core/issues/124)) — `config.logsPath`, `node-cron` dynamic import, and `canUseNodeSDK()` (NodeSDK needs `perf_hooks` + `worker_threads`, partial/absent in the polyfill). Workers fall back to lightweight telemetry mode; framework offloads Worker-native OTLP export to the app layer.
- **Definition linting is build-time only.** `createApp()` no longer invokes `validateDefinitions` at startup. Lint runs via `bun run lint:mcp` (standalone) and `bun run devcheck` (gate) — the same surface, just not on every boot. Faster cold starts (especially on Workers), and new lint rules are additive going forward instead of potential breaking changes for deployed servers.
- **Changelog summary cap raised 250 → 350 chars** ([#129](https://github.com/cyanheads/mcp-ts-core/issues/129)) — `SUMMARY_MAX_LENGTH` in `scripts/build-changelog.ts`, propagated through `changelog/template.md`, the consumer templates, and `CLAUDE.md`/`AGENTS.md`. 250 was forcing operator-relevant context out of recent summaries (Authentik/Keycloak/Zitadel specificity trimmed from v0.8.20, full Workers + instructions + portability scope cut from v0.9.0's first draft).
- **`polish-docs-meta` README template links `OTEL_ENABLED` to framework telemetry docs** ([#130](https://github.com/cyanheads/mcp-ts-core/issues/130)) — `Enable OpenTelemetry.` → `Enable [OpenTelemetry instrumentation](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry) (spans, metrics, completion logs).` so downstream READMEs scaffolded from the skill point readers at exporter setup, sampling, and attribute catalog instead of leaving the flag as a dead end.
- **Agent-observed correctness principle codified across the design surface** ([#131](https://github.com/cyanheads/mcp-ts-core/issues/131)) — *server reports what only the server can know; agent decides what only the agent can know*. Lands in `design-mcp-server` (Output design top-level bullet), `add-tool` (new `Mutator response design` subsection + unit-bearing numeric-name checklist item), `tool-defs-analysis` (two new audit categories — mutator observability and unit-bearing numeric names; the audit walks 12 categories now, up from 10), `field-test` (mutator-trigger situational row), and `api-errors` (new `When not to throw` section).
- **Agent protocol prose condensed** — `CLAUDE.md`/`AGENTS.md` and `templates/CLAUDE.md`/`templates/AGENTS.md`: trims across Error Handling (Recommended path, `ctx.recoveryFor`, inline contracts, error-path parity), Consumers, Auth, server config, changelog, and First Session sections. ~150 words saved in the root protocol, ~120 in the templates; content preserved.
- **Skill bumps:** `api-linter` 1.2 → 1.3, `polish-docs-meta` 1.7 → 1.8, `design-mcp-server` 2.10 → 2.11, `add-tool` 2.8 → 2.9, `tool-defs-analysis` 1.1 → 1.2, `field-test` 2.3 → 2.4, `api-errors` 1.5 → 1.6.

## Fixed

- **`fetchWithTimeout` SSRF DNS validation in Workers** ([#70](https://github.com/cyanheads/mcp-ts-core/issues/70), [#120](https://github.com/cyanheads/mcp-ts-core/issues/120), [#126](https://github.com/cyanheads/mcp-ts-core/issues/126)) — was skipped under `!isWorkerLike` based on a stale assumption that Workers had no DNS API. Workers now run the same pre-flight A/AAAA validation as Node and Bun. Stale JSDoc corrected.
- **SSRF IPv6 coverage** ([#70](https://github.com/cyanheads/mcp-ts-core/issues/70)) — broadened the blocklist to cover IPv4-mapped IPv6 (`::ffff:127.0.0.1`), unique-local `fc00::/7`, link-local `fe80::/10`, and the full multicast range. Errors are now status-aware (4xx/5xx mapped to typed `McpError` codes via `httpErrorFromResponse`) and response bodies are capped at 1 MB.
- **`FileSystemProvider` re-validates `tenantId` at the storage boundary** ([#72](https://github.com/cyanheads/mcp-ts-core/issues/72)) — defense-in-depth check rejecting non-conformant tenant ids reaching the provider, even when `ctx.state` already validated upstream.
- **Cursor HMAC key eager init restored under Node** — lazy getter for Worker bundles was over-correction; module-load `randomBytes(32)` works fine in the Workers isolate. Worker bundle stays clean either way.
- **`mcp.rate_limit.rejections_total` no longer carries the unbounded `key` label** ([#114](https://github.com/cyanheads/mcp-ts-core/issues/114)) — previous label could blow up Prometheus cardinality on tenant-scoped keys. Bounded labels only.
- **Logger rate-limit cache eviction** ([#115](https://github.com/cyanheads/mcp-ts-core/issues/115)) — expired entries are pruned every flush instead of growing unbounded until process exit.
- **`echo` example resource** — `requestUri` switched from `z.url()` to `z.string()` since `z.url()` emits `format: "uri"` which the new `schema-format-portability` rule errors on.
- **Coverage gap-fill** — landing manifest (`isApp` marker, resource/prompt materialization, env-example normalization, connect-snippet overrides), format-parity (record/tuple/literal/permissive matching), and the HTML landing page (resource/prompt sections).

## Security

- **SSRF defense in Workers is now enforced end-to-end** — DNS validation + IP blocklist on every `fetchWithTimeout` call, regardless of runtime. See `## Fixed` for the IPv6/status/body details.
- **Tenant-id boundary validation in `FileSystemProvider`** ([#72](https://github.com/cyanheads/mcp-ts-core/issues/72)) — defense-in-depth against a future caller bypassing `ctx.state`'s upper-layer check.
- **Rate-limit metric cardinality bound** ([#114](https://github.com/cyanheads/mcp-ts-core/issues/114)) — closes a metric-exhaustion vector on tenant-scoped rate-limit keys.

## Migration

The new `schema-format-portability` rule (error, default-on) rejects schemas whose emitted JSON Schema contains `format` values outside the OpenAI nine. Existing servers using `z.url()` (emits `uri`), `z.cuid()`/`z.cuid2()`/`z.ulid()`/`z.nanoid()`, `z.base64()`, or `z.jwt()` will fail `bun run lint:mcp` and `bun run devcheck` with the new lint diagnostic. Deployed servers continue to run — lint no longer gates `createApp()` startup — but CI will go red until you fix or override.

Two upgrade paths:

- **Recommended.** Drop the format method and move the constraint into describe text where the model can read it. Runtime behavior is unchanged (the format constraint was advisory in the JSON Schema anyway).

  ```ts
  // Before
  homepage: z.url().describe('Project homepage')
  // After
  homepage: z.string().describe('Project homepage (absolute URL)')
  ```

- **Override.** If you target only vendors that accept the format and want to keep the Zod method, call `validateDefinitions` directly from a custom lint script with a widened allowlist:

  ```ts
  validateDefinitions({ formatAllowlist: ['email', 'uuid', 'date-time', 'uri'], ... });
  ```

  The bundled `bun run lint:mcp` doesn't expose flags — fork the script or write your own when an override is needed.

See `skills/api-linter/SKILL.md#schema-format-portability` for the full Zod → emitted-format table.
