somamcp 1.5.0 → 2.0.0

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/README.md CHANGED
@@ -2,18 +2,22 @@
2
2
 
3
3
  Framework for building **MCP (Model Context Protocol)** servers with built-in telemetry, introspection, agent feedback, and a backend abstraction layer.
4
4
 
5
- somamcp wraps an underlying MCP framework (currently [FastMCP](https://github.com/punkpeye/fastmcp)) behind a `BackendAdapter` interface — giving you a stable API surface, automatic telemetry, gateway-based server composition, identity/health endpoints with sensible security defaults, and HTTP artifacts out of the box.
5
+ somamcp builds on the official [MCP TypeScript SDK 2.x](https://ts.sdk.modelcontextprotocol.io/v2) behind a `BackendAdapter` interface — giving you a stable API surface, automatic telemetry, gateway-based server composition, identity/health endpoints with sensible security defaults, and HTTP artifacts out of the box. One endpoint serves both the `2026-07-28` protocol revision and 2025-era clients.
6
+
7
+ The main entry, `somamcp`, uses web-standard APIs only, so it runs on Cloudflare Workers without `nodejs_compat`, and on Deno, Bun and Node. `somamcp/node` adds what only Node can do: listening on a port, stdio, file telemetry, and reading content from a path. Coming from 1.x? See [Migrating from 1.x](#migrating-from-1x).
6
8
 
7
9
  ## Features
8
10
 
9
- - **Backend abstraction** — framework-agnostic `BackendAdapter` interface; swap the underlying MCP framework without changing consumer code
11
+ - **Both protocol eras** — the `2026-07-28` revision that current Claude clients request first, and the 2025 `initialize` handshake, from one endpoint
12
+ - **Runs anywhere** — the main entry is WinterTC-only (Workers, Deno, Bun, Node); `somamcp/node` adds a port listener and stdio
13
+ - **Backend abstraction** — framework-agnostic `BackendAdapter` interface; swap the underlying MCP implementation without changing consumer code
10
14
  - **Structured telemetry** — pluggable `TelemetryCollector` with console, file (NDJSON), composite, and [functype-log](https://github.com/jordanburke/functype-log) adapters
11
15
  - **Error classification & enrichment** — errors are auto-classified (validation / timeout / gateway / auth / not_found / internal) with actionable suggestions for LLMs
12
16
  - **Per-tool capture config** — configure input/output capture levels, field redaction, and output size limits per tool
13
17
  - **Gateway system** — connect to remote MCP servers and proxy their tools as local tools
14
18
  - **Identity & build info** — `info` MCP tool returns name, version, build commit, runtime, and capability counts; auto-populated from `SOMAMCP_BUILD_*` env vars
15
19
  - **Two-tier health** — public `/health` (minimal, for Docker/k8s probes) + protected `/health/detail` (full operational state)
16
- - **Protected artifacts** — `protected: true` on any artifact reuses FastMCP's `authenticate` callback; dashboard and `/info` protected by default
20
+ - **Protected artifacts** — `protected: true` on any artifact reuses the server's `authenticate` callback; dashboard and `/info` protected by default
17
21
  - **Method-aware protected routes** — `addRoute({ method, path, protected, handler })` for custom `POST`/`PUT`/etc. endpoints behind the same `authenticate` gate
18
22
  - **Agent feedback tool** — `createFeedbackTool` posts agent-reported issues to GitHub or any webhook, with automatic credential/PII redaction
19
23
  - **HTTP dashboard** — auto-generated overview at `/dashboard` (protected)
@@ -29,8 +33,10 @@ npm install somamcp
29
33
 
30
34
  ## Quick Start
31
35
 
36
+ On Node, import from `somamcp/node` and start a transport:
37
+
32
38
  ```typescript
33
- import { createServer } from "somamcp"
39
+ import { createServer } from "somamcp/node"
34
40
  import { z } from "zod"
35
41
 
36
42
  const server = createServer({
@@ -46,6 +52,18 @@ server.addTool({
46
52
  })
47
53
 
48
54
  await server.start({ transportType: "stdio" })
55
+ // or: await server.start({ transportType: "httpStream", httpStream: { port: 3000 } })
56
+ ```
57
+
58
+ On Cloudflare Workers (or any runtime with `fetch`), import from `somamcp` and serve requests:
59
+
60
+ ```typescript
61
+ import { createServer } from "somamcp"
62
+
63
+ const server = createServer({ name: "my-worker", version: "1.0.0" })
64
+ // ...addTool as above
65
+
66
+ export default { fetch: (request: Request) => server.fetch(request) }
49
67
  ```
50
68
 
51
69
  ## Identity & Health
@@ -106,16 +124,16 @@ createServer({
106
124
 
107
125
  ## Protected Artifacts
108
126
 
109
- Any artifact can opt into auth with `protected: true`. The route runs FastMCP's `authenticate` callback via Hono middleware — same auth model as MCP protocol calls. If `protected: true` is set but no `authenticate` is configured, the route returns 401 unconditionally.
127
+ Any artifact can opt into auth with `protected: true`. The route runs the server's `authenticate` callback via Hono middleware — same auth model as MCP protocol calls. Rejecting, throwing, or resolving to `undefined`/`null` returns 401. If `protected: true` is set but no `authenticate` is configured, the route returns 401 unconditionally.
110
128
 
111
129
  ```typescript
112
- import { createServer, getRequestHeader } from "somamcp"
130
+ import { createServer, getRequestHeader } from "somamcp/node"
113
131
 
114
132
  createServer({
115
133
  name: "my-server",
116
134
  version: "1.0.0",
117
- // `authenticate` receives either a Hono Request (routes/artifacts) or an
118
- // http.IncomingMessage (MCP transport). `getRequestHeader` hides the shape difference.
135
+ // `authenticate` receives the web Request, for MCP calls and for protected
136
+ // routes alike. On stdio there is no request, and it receives `undefined`.
119
137
  authenticate: async (req) => {
120
138
  const header = getRequestHeader(req, "authorization")
121
139
  const token = header?.replace(/^Bearer /, "")
@@ -157,10 +175,10 @@ See [`examples/protected-upload-server`](examples/protected-upload-server) for a
157
175
 
158
176
  ## Content-Array Tool Returns
159
177
 
160
- A tool's `execute` may return a plain string **or** a full content-array with multimodal parts — telemetry passes the return value through unchanged. Use the exported `imageContent` / `audioContent` helpers to build parts without reaching into the backend.
178
+ A tool's `execute` may return a plain string **or** a full content-array with multimodal parts — telemetry passes the return value through unchanged. Use the exported `imageContent` / `audioContent` helpers to build parts without reaching into the backend. Both take `{ buffer }` (any `Uint8Array`, including a Node `Buffer`) or `{ url }` everywhere; the `somamcp/node` versions also take `{ path }`. A URL's or path's extension picks the MIME type.
161
179
 
162
180
  ```typescript
163
- import { createServer, imageContent } from "somamcp"
181
+ import { createServer, imageContent } from "somamcp/node"
164
182
 
165
183
  server.addTool({
166
184
  name: "hello_pixel",
@@ -177,7 +195,8 @@ server.addTool({
177
195
  Telemetry is opt-in via the `telemetry` option. Every tool/resource/prompt call is automatically wrapped with timing, error classification, and optional input/output capture.
178
196
 
179
197
  ```typescript
180
- import { createServer, createCompositeTelemetry, createConsoleTelemetry, createJsonFileTelemetry } from "somamcp"
198
+ // createJsonFileTelemetry writes to disk, so it comes from somamcp/node.
199
+ import { createServer, createCompositeTelemetry, createConsoleTelemetry, createJsonFileTelemetry } from "somamcp/node"
181
200
 
182
201
  const telemetry = createCompositeTelemetry([
183
202
  createConsoleTelemetry(),
@@ -257,6 +276,11 @@ const server = createServer({
257
276
  })
258
277
  ```
259
278
 
279
+ Gateways use the MCP SDK 2.x client. They speak the `2026-07-28` revision to servers that offer it, and fall back to the 2025 `initialize` handshake for the rest. A gateway's `info.protocolVersion` reports what was negotiated.
280
+
281
+ - **`versionNegotiation`:** `"auto"` (the default) probes first with `server/discover`. Some older servers answer that probe with a 5xx error, which fails the connection; set `"legacy"` for those to skip the probe.
282
+ - **`fetch`:** Replaces the global `fetch` for that gateway's requests. The client calls it as a plain function, so wrap a Workers service binding rather than passing its method: `fetch: (url, init) => env.UPSTREAM.fetch(url, init)`.
283
+
260
284
  ## Agent Feedback
261
285
 
262
286
  Let agents file issues against your repo when they hit bugs or confusing behavior. The tool auto-redacts common credential patterns and PII, and includes a tool description that explicitly warns the LLM not to include proprietary data.
@@ -322,19 +346,30 @@ const server = createServer({
322
346
  ],
323
347
  })
324
348
 
349
+ // With createServer from "somamcp/node":
325
350
  await server.start({
326
351
  transportType: "httpStream",
327
352
  httpStream: { port: 8080 },
328
353
  })
329
354
  ```
330
355
 
331
- ## Edge Runtimes (Cloudflare Workers, Deno Deploy, Bun)
356
+ ## Entry Points
357
+
358
+ | Import | Runs on | `createServer` defaults to | Adds |
359
+ | ----------------- | ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------ |
360
+ | `somamcp` | Workers, Deno, Bun, Node | `createSdkBackend` | — |
361
+ | `somamcp/node` | Node (and Bun) | `createNodeBackend` | `start()` on a port or stdio, `createJsonFileTelemetry`, `{ path }` in the content helpers |
362
+ | `somamcp/backend` | anywhere | — | `createSdkBackend` and the backend types, for writing or wrapping a backend |
363
+ | `somamcp/edge` | anywhere | `createSdkBackend` | Nothing: an alias of `somamcp` kept through 2.x so 1.x imports resolve. Removed in 3.0. |
364
+
365
+ `somamcp/node` re-exports everything in `somamcp` and shares its modules rather than copying them, so values such as `UserError` are the same whichever entry you import them from. `pnpm validate` fails if Node-only code reaches anything `somamcp` loads, or if the core is emitted twice. It also runs the main entry inside `workerd`, Cloudflare's Workers runtime, without `nodejs_compat`, with both protocol eras, auth, content helpers, errors and resources (`test/workers/`).
332
366
 
333
- Import from `somamcp/edge` instead of `somamcp`. The API is identical — `backend` just defaults to `createEdgeBackend` rather than the Node FastMCP backend.
367
+ On the main entry, a bare `start()` listens on nothing: it marks the server started and connects gateways. `start()` with a transport rejects with a pointer to `somamcp/node`. On `somamcp/node`, a bare `start()` serves stdio.
368
+
369
+ ### On Workers
334
370
 
335
371
  ```typescript
336
- import { createServer } from "somamcp/edge"
337
- import { z } from "zod"
372
+ import { createServer } from "somamcp"
338
373
 
339
374
  const server = createServer({
340
375
  name: "my-worker",
@@ -343,131 +378,69 @@ const server = createServer({
343
378
  build: { commit: "abc123", environment: "production" },
344
379
  })
345
380
 
346
- server.addTool({
347
- name: "greet",
348
- description: "Greet someone by name",
349
- parameters: z.object({ name: z.string() }),
350
- execute: async ({ name }) => `Hello, ${name}!`,
351
- })
352
-
353
- export default {
354
- fetch: (request: Request) => server.fetch(request),
355
- }
381
+ export default { fetch: (request: Request) => server.fetch(request) }
356
382
  ```
357
383
 
358
384
  `server.fetch` serves both the MCP endpoint and every artifact/route you register, so `/health`, `/info`, and `/dashboard` work as they do on Node.
359
385
 
360
- ### Why a separate entry point
361
-
362
- The root `somamcp` barrel exports `createJsonFileTelemetry`, `imageContent`, and `audioContent`, which import `node:fs`. Importing it from a Worker pulls Node built-ins into the bundle. `somamcp/edge` is built as a separate pass with no shared chunks, and `pnpm check:edge` fails the build if any `node:` import reaches the edge output.
363
-
364
- ### Feature parity
365
-
366
- | Feature | Node (`somamcp`) | Edge (`somamcp/edge`) |
367
- | -------------------------------------------------- | ---------------- | --------------------------------- |
368
- | Tools, resources, prompts | ✅ | ✅ |
369
- | Artifacts, `addRoute`, auth middleware | ✅ | ✅ |
370
- | `authenticate` on the MCP endpoint | ✅ | ✅ — enforced by somamcp |
371
- | `/health`, `/health/detail`, `/info`, `/dashboard` | ✅ | ✅ |
372
- | `info` introspection tool | ✅ | ✅ |
373
- | Telemetry (console, composite, custom) | ✅ | ✅ |
374
- | Tool errors (`isError`) | ✅ | ✅ — sent as JSON-RPC errors |
375
- | File telemetry, `imageContent` / `audioContent` | ✅ | ❌ — need `node:fs` |
376
- | stdio transport | ✅ | ❌ — HTTP only |
377
- | Sessions, `connect` / `disconnect` events | ✅ | ❌ — stateless |
378
- | Gateways (proxying remote MCP servers) | ✅ | ⚠️ requires `start()` — see below |
379
- | Multi-result resources (`load()` returning array) | ✅ | ⚠️ first result only, warns |
380
- | `reportProgress` / `streamContent` in tools | ✅ | ⚠️ inert no-ops |
381
- | `removeTool` / `removeResource` / `removePrompt` | ✅ | ⚠️ warns, does nothing |
382
- | `supportsRemoval` | `true` | `false` |
383
- | `addResourceTemplate` | ✅ | ⚠️ warns, does nothing |
384
-
385
- The ⚠️ rows are limits of `EdgeFastMCP`, which exposes no removal API, has no server→client channel, and treats resources as single-valued. They log through `config.logger` rather than failing, so passing a `logger` is recommended on edge.
386
-
387
- **Capability counts.** `getInfo().capabilities` counts every tool actually served — including the built-in `info` tool and any proxied gateway tools. Because the edge backend reports `supportsRemoval: false`, a `removeTool` there leaves the count untouched: the tool is still being served, so it is still counted.
388
-
389
- **Auth.** `EdgeFastMCPOptions` has no `authenticate` field, so somamcp gates the MCP endpoint itself with the same middleware that protects artifacts and routes. Behaviour matches Node: configure `authenticate`, and unauthenticated MCP calls get a 401.
390
-
391
- **Tool errors.** `EdgeFastMCP` builds its response as `result: { content }` and discards `isError`, which would make a failed tool look successful. The edge adapter rethrows on `isError` so the client receives a JSON-RPC error instead.
386
+ **Types.** The MCP SDK's own `.d.mts` files mention `Buffer`, so a Workers project without `@types/node` needs `skipLibCheck: true` (Wrangler's default). somamcp's own types do not.
392
387
 
393
388
  **Gateways** need `await server.start()` to connect and register proxied tools. Workers forbids network I/O at module scope, so call it lazily inside your first request rather than at the top level. Without it, gateway tools are never registered.
394
389
 
395
- **Registering routes late.** Hono builds its router on the first request and then rejects new routes ("matcher is already built"). Register every artifact and `addRoute` before serving traffic — on edge that means at module scope, not inside the fetch handler.
390
+ **Registering routes late.** Hono builds its router on the first request and then rejects new routes ("matcher is already built"). Register every artifact and `addRoute` before serving traffic — on Workers that means at module scope, not inside the fetch handler.
396
391
 
397
- **Route precedence.** somamcp owns the outer Hono app and falls through to `EdgeFastMCP` for anything unmatched. This ordering is deliberate: `EdgeFastMCP` registers its own `/health` at construction, and Hono is first-match-wins, so mounting it first would shadow somamcp's health artifact.
392
+ **Runtime reporting.** `getInfo().runtime` is `"edge"` on Cloudflare Workers, Deno, and Vercel Edge, and `"node"` otherwise. Edge runtimes are detected positively — `navigator.userAgent === "Cloudflare-Workers"`, or a `Deno` / `EdgeRuntime` global — rather than by the absence of `process`, because Workers with `nodejs_compat` supplies a working `process.versions.node`. On edge, `arch` / `nodeVersion` / `platform` report `"unknown"` or the user agent, rather than describing the compatibility shim. Bun deliberately reports `"node"`: it is a Node-family server runtime with full Node APIs.
398
393
 
399
- **Runtime reporting.** `getInfo().runtime` is `"edge"` on Cloudflare Workers, Deno, and Vercel Edge, and `"node"` otherwise. Edge runtimes are detected positively — `navigator.userAgent === "Cloudflare-Workers"`, or a `Deno` / `EdgeRuntime` global — rather than by the absence of `process`, because Workers with `nodejs_compat` supplies a working `process.versions.node`. On edge, `arch` / `nodeVersion` / `platform` report `"unknown"` or the user agent, rather than describing the compatibility shim.
394
+ ## The SDK Backend
400
395
 
401
- Bun deliberately reports `"node"`: it is a Node-family server runtime with full Node APIs, so that is the accurate answer even though `somamcp/edge` runs there.
396
+ `createSdkBackend` (and `createNodeBackend`, the same backend with Node transports) builds on `@modelcontextprotocol/server` 2.x. One endpoint serves both protocol revisions:
402
397
 
403
- ## MCP SDK 2.x Backend (opt-in)
404
-
405
- `createSdkBackend` builds on the official MCP TypeScript SDK 2.x (`@modelcontextprotocol/server`). One endpoint serves both protocol revisions:
406
-
407
- - **`2026-07-28`**, which current Claude clients request first. The default FastMCP 4.x backend answers that request with HTTP 400, and a client that doesn't retry with the older handshake ends up with no tools.
398
+ - **`2026-07-28`**, which current Claude clients request first.
408
399
  - **2025-era clients**, which open with `initialize`.
409
400
 
410
- Pass it as `backend`. Nothing else changes:
411
-
412
- ```typescript
413
- import { createServer } from "somamcp"
414
- import { createSdkBackend } from "somamcp/backend"
401
+ How it behaves:
415
402
 
416
- const server = createServer({ name: "my-server", version: "1.0.0", backend: createSdkBackend })
417
-
418
- await server.start({ transportType: "httpStream", httpStream: { port: 3000 } })
419
- // or: await server.start({ transportType: "stdio" })
420
- ```
421
-
422
- How it differs from the FastMCP backend:
423
-
424
- - **Stateless only.** Every request gets a fresh SDK server built from somamcp's registry. There are no sessions, `connect`/`disconnect` events never fire, and a 2025 client's `GET` or `DELETE` is answered `405`. `httpStream.stateless: false` and `eventStore` are ignored, with a warning through `logger`.
425
- - **`authenticate` receives a web `Request`**, not Node's `http.IncomingMessage`. Read headers with `getRequestHeader` and the same function works on both backends. Rejecting with a `Response` serves that response as-is. Any other rejection, a synchronous throw, or a nullish result (as on FastMCP) is a `401`. `canAccess` is only ever called with a real session: with none, gated tools are hidden when `authenticate` is configured and shown otherwise, as on FastMCP.
426
- - **Errors follow SDK 2.x and the current spec.** Calling an unknown tool, or one hidden by `canAccess`, is a JSON-RPC `InvalidParams` error, as on FastMCP. Invalid arguments are an `isError` tool result, so the model can read the message and retry. FastMCP reports them as an `InvalidParams` protocol error.
403
+ - **Stateless.** Every request gets a fresh SDK server built from somamcp's registry. There are no sessions, `connect`/`disconnect` events never fire, and a 2025 client's `GET` or `DELETE` is answered `405`. `httpStream.stateless: false` is ignored, with a warning through `logger`.
404
+ - **`authenticate` receives the web `Request`**, for MCP calls and for protected artifacts and routes. Read headers with `getRequestHeader`. On the MCP endpoint, rejecting with a `Response` serves that response as-is; protected artifacts and routes answer with their `onUnauthorized`, or 401. Any other rejection, a synchronous throw, or a nullish result is a `401`. On stdio there is no request: `authenticate` is called once with `undefined`, and a failure serves without a session. `canAccess` is only ever called with a real session: with none, gated tools are hidden when `authenticate` is configured and shown otherwise.
405
+ - **Errors follow SDK 2.x and the current spec.** Calling an unknown tool, or one hidden by `canAccess`, is a JSON-RPC `InvalidParams` error. Invalid arguments and thrown errors (including `UserError`) are `isError` tool results, so the model can read the message and retry.
427
406
  - **Schemas must describe themselves as JSON Schema** (`~standard.jsonSchema`): zod 4, ArkType, or Valibot via `@valibot/to-json-schema`. Other schemas still validate, but clients see an empty argument list; `addTool` warns through `logger`.
428
407
  - **`context.client.version`** is filled in for `2026-07-28` clients, which send their client info on every request. It is `undefined` for 2025 clients, which send it only during `initialize`.
429
- - **`httpStream.host` defaults to `localhost`**, as on FastMCP. Pass `host: "0.0.0.0"` to listen on every interface.
430
- - **`streamKeepalive`** maps to the SDK's SSE comment frames (`enabled` and `intervalMs`; the default is 20 s, as on FastMCP). FastMCP sends log notifications instead. With it on, every tool call is answered as an SSE stream, so a long call that produces no output still gets keepalives. `enableJsonResponse` turns them off; it applies to `2026-07-28` exchanges only, so a 2025 client's responses are unchanged by it.
431
408
  - **`context.log` follows the client's log level.** A `2026-07-28` client receives tool logs only when its request asks for a log level (`io.modelcontextprotocol/logLevel`); otherwise the SDK drops them. 2025 clients receive them as before. Progress (`reportProgress`) is unaffected.
432
- - **Health reports `running` from construction**, because the backend can answer `fetch()` before `start()`. FastMCP reports `stopped` until `start()`. Both report `stopped` after `stop()`.
409
+ - **`streamKeepalive`** (`enabled`, `intervalMs`; default 20 s) writes SSE comment frames onto an in-flight call's own stream. With it on, every tool call is answered as an SSE stream, so a long call that produces no output still gets keepalives. `httpStream.enableJsonResponse` turns them off; it applies to `2026-07-28` exchanges only.
410
+ - **`httpStream.host` defaults to `localhost`.** Pass `host: "0.0.0.0"` to listen on every interface, for example in a container.
411
+ - **Health reports `running` from construction**, because the backend can answer `fetch()` before `start()`. `stop()` closes the listener and in-flight streams and reports `stopped` (so `/health` answers 503), but `fetch()` still answers MCP requests.
433
412
  - **Removal is real:** `removeTool`, `removeResource` and `removePrompt` apply from the next request.
434
- - **`backendOptions`** takes `mcpPath` (the endpoint for `fetch()` use; `httpStream.endpoint` overrides it) and `stdioTransport` (a transport to use in place of the process's stdin/stdout). FastMCP's `ping`, `health`, `roots` and `oauth` options don't apply.
413
+ - **The MCP path comes first.** The backend claims the MCP endpoint before artifacts and routes, so an `addRoute` on that path is never reached.
414
+ - **`backendOptions`** (`SdkBackendOptions`) takes `mcpPath`, the endpoint for `fetch()` use (`httpStream.endpoint` overrides it), and, on `somamcp/node`, `stdioTransport`, a transport to use in place of the process's stdin/stdout.
435
415
 
436
416
  ## Backend Abstraction
437
417
 
438
- The underlying MCP framework is accessed exclusively through the `BackendAdapter` interface. `createServer` accepts a `backend` factory — this is how `somamcp/edge` swaps in the edge adapter:
418
+ The MCP implementation is reached only through the `BackendAdapter` interface. `createServer` accepts a `backend` factory; each entry supplies its default. To wrap or replace the backend, implement `BackendFactory`:
439
419
 
440
420
  ```typescript
441
- import { createServer } from "somamcp"
442
- import { createEdgeBackend } from "somamcp/edge"
443
-
444
- // equivalent to importing createServer from "somamcp/edge"
445
- const server = createServer({ name: "my-server", version: "1.0.0", backend: createEdgeBackend })
446
- ```
447
-
448
- To use a different backend entirely, implement `BackendFactory`:
449
-
450
- ```typescript
451
- import { createFastMCPBackend } from "somamcp/backend"
452
- import type { BackendFactory } from "somamcp/backend"
421
+ import { createSdkBackend, type BackendFactory } from "somamcp/backend"
453
422
 
454
423
  const myBackend: BackendFactory = (config, backendOptions) => {
455
- // ...
424
+ const backend = createSdkBackend(config, backendOptions)
425
+ // ...wrap or extend it
426
+ return backend
456
427
  }
457
428
  ```
458
429
 
459
- Framework-specific options (e.g. FastMCP's OAuth, ping) flow through `backendOptions`:
430
+ ## Migrating from 1.x
460
431
 
461
- ```typescript
462
- createServer({
463
- name: "my-server",
464
- version: "1.0.0",
465
- backendOptions: {
466
- ping: { enabled: true, intervalMs: 30000 },
467
- // ...any FastMCP ServerOptions fields
468
- },
469
- })
470
- ```
432
+ somamcp 2.0 drops FastMCP. If you need FastMCP, stay on somamcp `^1.5`, which has both backends.
433
+
434
+ - **Node servers import from `somamcp/node`.** `start()` with `httpStream` or `stdio` now needs it; on `somamcp` it rejects. Change `import { createServer } from "somamcp"` to `"somamcp/node"`. Everything else `somamcp` exports is re-exported there.
435
+ - **A bare `start()` on `somamcp` no longer serves stdio.** It listens on nothing and resolves, so a stdio server that keeps importing from `somamcp` exits quietly. On `somamcp/node`, a bare `start()` still serves stdio.
436
+ - **`createJsonFileTelemetry` and path-based `imageContent` / `audioContent` moved to `somamcp/node`.** `{ buffer }` and `{ url }` inputs still work from `somamcp`.
437
+ - **`somamcp/edge` is an alias of `somamcp`.** `createEdgeBackend` and `EdgeBackendOptions` are gone; the main entry runs on Workers itself. `createFastMCPBackend` is gone too.
438
+ - **`authenticate` receives the web `Request` everywhere**, including protected routes on Node, where 1.x passed `http.IncomingMessage`. Its type is now `(request: Request | undefined) => Promise<T>`. `getRequestHeader` reads both shapes, so code that uses it keeps working.
439
+ - **A nullish `authenticate` result now fails protected artifacts and routes** with a 401, as it already did on the SDK backend's MCP endpoint. 1.x let it through on routes.
440
+ - **`addResourceTemplate` is typed:** it takes one `ResourceTemplate<T>`.
441
+ - **Removed options:** `httpStream.eventStore`, `streamKeepalive.logLevel`, and FastMCP's `backendOptions` (`ping`, `health`, `roots`, `oauth`).
442
+ - **Behavior from the SDK backend**, if you were on FastMCP: stateless serving, `isError` results for invalid arguments, `running` health from construction, and the rest of [The SDK Backend](#the-sdk-backend).
443
+ - **Gateways** use the SDK 2.x client; see [Gateways](#gateways).
471
444
 
472
445
  ## API Surface
473
446
 
@@ -476,7 +449,7 @@ createServer({
476
449
  - `createServer(options)` → `SomaServerInstance`
477
450
  - Types: `SomaServerOptions`, `SomaServerInstance`, `ServerHealth`, `ServerInfo`, `ServerCapabilities`, `CapabilitiesSummary`, `ToolOptions`
478
451
 
479
- **Primitives** (somamcp-owned, no FastMCP leakage)
452
+ **Primitives** (somamcp-owned, no SDK types leak through)
480
453
 
481
454
  - `Tool`, `Resource`, `Prompt`, `Context`, `Content`, `ContentResult`, `SessionAuth`, `UserError`
482
455
 
@@ -487,7 +460,8 @@ createServer({
487
460
 
488
461
  **Telemetry**
489
462
 
490
- - `createConsoleTelemetry`, `createJsonFileTelemetry`, `createCompositeTelemetry`, `createLogLayerTelemetry`, `NoopTelemetry`
463
+ - `createConsoleTelemetry`, `createCompositeTelemetry`, `createLogLayerTelemetry`, `NoopTelemetry`
464
+ - `createJsonFileTelemetry` (`somamcp/node` only)
491
465
  - `classifyError`, `createEnrichedError`
492
466
  - Types: `TelemetryCollector`, `TelemetryEvent`, `CaptureLevel`, `ToolCaptureConfig`, `ErrorCategory`
493
467
 
@@ -508,14 +482,8 @@ createServer({
508
482
 
509
483
  **Transport**
510
484
 
511
- - Types: `TransportConfig`, `HttpStreamConfig` (accepts `cors`, `stateless`, `eventStore`, `sslCert` / `sslKey` / `sslCa`, plus `port` / `host` / `endpoint` / `enableJsonResponse`)
512
- - `streamKeepalive` on `ServerConfig` — `{ enabled, intervalMs }`, written onto an in-flight tool call's own
513
- response stream so a proxy does not close it as idle during a long, silent call. **Pair it with
514
- `httpStream.stateless`**: a transport-level ping needs the standing server-to-client stream that stateless
515
- does not have, so this is the only keepalive left. It sits on `ServerConfig` rather than `HttpStreamConfig`
516
- because the backend builds its server before `start()` sees the transport. Ignored by the edge backend.
517
- Leave `httpStream.enableJsonResponse` **off**: in JSON-response mode the MCP SDK skips the SSE write for
518
- request-related notifications, so the keepalive is accepted, costs an interval timer, and does nothing.
485
+ - Types: `TransportConfig`, `HttpStreamConfig` (`port`, `host`, `endpoint`, `cors`, `enableJsonResponse`, `stateless`, `sslCert` / `sslKey` / `sslCa`); listening needs `somamcp/node`
486
+ - `streamKeepalive` on `ServerConfig` — `{ enabled, intervalMs }`; see [The SDK Backend](#the-sdk-backend)
519
487
 
520
488
  **Introspection**
521
489
 
@@ -530,11 +498,12 @@ createServer({
530
498
 
531
499
  **Content helpers**
532
500
 
533
- - `imageContent`, `audioContent`
501
+ - `imageContent`, `audioContent` (`{ buffer }` or `{ url }`; `somamcp/node` adds `{ path }`)
502
+ - Types: `ContentInput`, `NodeContentInput` (`somamcp/node`)
534
503
 
535
- **Backend** (via `somamcp/backend`)
504
+ **Backend** (`somamcp` and `somamcp/backend`)
536
505
 
537
- - `createFastMCPBackend`, `createSdkBackend`
506
+ - `createSdkBackend`; `createNodeBackend` (`somamcp/node` only)
538
507
  - Types: `BackendAdapter`, `BackendFactory`, `BackendSession`, `SdkBackendOptions`
539
508
 
540
509
  ## Scripts
@@ -1,2 +1,2 @@
1
- import { a as BackendEvents, i as BackendAdapter, n as createSdkBackend, o as BackendFactory, r as createFastMCPBackend, s as BackendSession, t as SdkBackendOptions } from "../index-C2wa9nmp.js";
2
- export { type BackendAdapter, type BackendEvents, type BackendFactory, type BackendSession, type SdkBackendOptions, createFastMCPBackend, createSdkBackend };
1
+ import { a as BackendFactory, i as BackendEvents, n as createSdkBackend, o as BackendSession, r as BackendAdapter, t as SdkBackendOptions } from "../index-ololl9dI.js";
2
+ export { type BackendAdapter, type BackendEvents, type BackendFactory, type BackendSession, type SdkBackendOptions, createSdkBackend };
@@ -1 +1 @@
1
- export{n as createFastMCPBackend,t as createSdkBackend}from"../backend-D0qSyfXw.js";
1
+ export{n as createSdkBackend}from"../backend-DnC4_jJ7.js";
@@ -0,0 +1 @@
1
+ import{ProtocolError as e,ProtocolErrorCode as t,ResourceNotFoundError as n,Server as r,UriTemplate as i,createMcpHandler as a}from"@modelcontextprotocol/server";import{List as o,Map as s,Option as c,Ref as l,Try as u,Tuple as d}from"functype";import{Hono as f}from"hono";import{cors as p}from"hono/cors";const m={properties:{},type:`object`},h=e=>c(e[`~standard`].jsonSchema).isSome(),g=e=>e.flatMap(e=>c(e[`~standard`].jsonSchema)).map(e=>e.input({target:`draft-2020-12`})).orElse(m),_=e=>typeof e==`string`?{content:[{text:e,type:`text`}]}:e===void 0?{content:[]}:`content`in e?e:{content:[e]},v=e=>({content:[{text:e,type:`text`}],isError:!0}),y=(e,t)=>({contents:(Array.isArray(e)?e:[e]).map(e=>({mimeType:e.mimeType??t.mimeType,uri:e.uri??t.uri,...`blob`in e?{blob:e.blob}:{text:e.text}}))}),b=e=>typeof e==`string`?{messages:[{content:{text:e,type:`text`},role:`user`}]}:e,x={values:[]},S=e=>({completion:e.orElse(x)}),C=(e,t)=>{let n=e.filter(e=>e.startsWith(t));return{total:n.length,values:n}},w=(e,t,n)=>{let r=t=>e.mcpReq.notify(t).catch(()=>{}),i=t=>(n,r)=>{e.mcpReq.log(t,r===void 0?n:{data:r,message:n}).catch(()=>{})},a=c(e.mcpReq._meta?.progressToken);return{client:{version:c(e.mcpReq.envelope?.[`io.modelcontextprotocol/clientInfo`]).map(e=>e.version).orUndefined()},log:{debug:i(`debug`),error:i(`error`),info:i(`info`),warn:i(`warning`)},reportProgress:async({progress:e,total:t})=>{await a.fold(async()=>{},n=>r({method:`notifications/progress`,params:{progress:e,progressToken:n,total:t}}))},requestId:String(e.mcpReq.id),session:t.orUndefined(),sessionId:e.sessionId,streamContent:async e=>{await r({method:`notifications/tool/streamContent`,params:{content:Array.isArray(e)?e:[e],toolName:n}})}}},T=e=>o([...e].map(([,e])=>e)),E=(...e)=>e.reduce((e,t)=>e.fold(()=>t(),()=>e),c.none()),D=e=>e instanceof Error?e.message:String(e),O=e=>`Tool '${e.name}' timed out after ${e.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`,k=(e,t)=>c(e.timeoutMs).fold(()=>t,n=>{let r=l(c.none()),i=new Promise((t,i)=>{r.set(c(setTimeout(()=>i(Error(O(e))),n)))});return Promise.race([t,i]).finally(()=>r.get().forEach(clearTimeout))}),A=(e,t)=>c(e.parameters).fold(()=>Promise.resolve({value:t}),e=>Promise.resolve(e[`~standard`].validate(t))),j=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?t.join(`,`):t])),M=(a,s,l)=>{let d=l.orUndefined(),f=e=>l.fold(()=>!(s.authRequired&&e.canAccess),t=>!e.canAccess||e.canAccess(t)),p=e=>a.tools().get(e).filter(f),m=new r({name:s.name,version:s.version},{capabilities:{completions:{},logging:{},prompts:{},resources:{},tools:{}},instructions:s.instructions});m.setRequestHandler(`tools/list`,()=>({tools:T(a.tools()).filter(f).map(e=>({...e._meta?{_meta:e._meta}:{},...e.annotations?{annotations:e.annotations}:{},description:e.description,inputSchema:g(c(e.parameters)),name:e.name,...e.outputSchema?{outputSchema:g(c(e.outputSchema))}:{},...e.annotations?.title?{title:e.annotations.title}:{}})).toArray()}));let h=async(e,t,n)=>{let r=await A(e,t);return`issues`in r?v(`Invalid arguments for tool '${e.name}': ${r.issues.map(e=>e.message).join(`; `)}`):(await u.fromPromise(k(e,e.execute(r.value,n)))).fold(e=>v(D(e)),e=>_(e))},x=n=>p(n).orThrow(()=>new e(t.InvalidParams,`Unknown tool: ${n}`));m.setRequestHandler(`tools/call`,(e,t)=>{let{arguments:n,name:r}=e.params;return h(x(r),n??{},w(t,l,r))}),m.setRequestHandler(`resources/list`,()=>({resources:T(a.resources()).map(e=>({description:e.description,mimeType:e.mimeType,name:e.name,uri:e.uri})).toArray()})),m.setRequestHandler(`resources/templates/list`,()=>({resourceTemplates:T(a.templates()).map(e=>({description:e.description,mimeType:e.mimeType,name:e.name,uriTemplate:e.uriTemplate})).toArray()}));let O=e=>E(()=>T(a.resources()).find(t=>t.uri===e).map(e=>({load:()=>e.load(d),mimeType:e.mimeType})),()=>T(a.templates()).map(t=>({template:t,variables:c(new i(t.uriTemplate).match(e))})).find(e=>e.variables.isSome()).map(({template:e,variables:t})=>({load:()=>e.load(j(t.orElse({})),d),mimeType:e.mimeType}))),M=async e=>{let t=O(e).orThrow(()=>new n(e));return y(await t.load(),{mimeType:t.mimeType,uri:e})};m.setRequestHandler(`resources/read`,e=>M(e.params.uri)),m.setRequestHandler(`prompts/list`,()=>({prompts:T(a.prompts()).map(e=>({arguments:e.arguments?.map(e=>({description:e.description,name:e.name,required:e.required})),description:e.description,name:e.name})).toArray()}));let N=async(n,r)=>{let i=a.prompts().get(n).orThrow(()=>new e(t.InvalidParams,`Unknown prompt: ${n}`)),s=o(i.arguments??[]).filter(e=>e.required===!0&&!(e.name in r));if(!s.isEmpty){let r=s.map(e=>e.name).toArray().join(`, `);throw new e(t.InvalidParams,`Prompt '${n}' is missing required arguments: ${r}`)}return b(await i.load(r,d))};m.setRequestHandler(`prompts/get`,e=>N(e.params.name,e.params.arguments??{}));let P=(e,t,n)=>{let r=a.prompts().get(e),i=r.flatMap(e=>o(e.arguments??[]).find(e=>e.name===t));return E(()=>i.flatMap(e=>c(e.complete)).map(e=>()=>e(n,d)),()=>i.flatMap(e=>c(e.enum)).map(e=>()=>Promise.resolve(C(e,n))),()=>r.flatMap(e=>c(e.complete)).map(e=>()=>e(t,n,d)))},F=(e,t,n)=>{let r=T(a.templates()).find(t=>t.uriTemplate===e),i=r.flatMap(e=>o(e.arguments).find(e=>e.name===t));return E(()=>i.flatMap(e=>c(e.complete)).map(e=>()=>e(n,d)),()=>r.flatMap(e=>c(e.complete)).map(e=>()=>e(t,n,d)))};return m.setRequestHandler(`completion/complete`,async e=>{let{argument:t,ref:n}=e.params,r=n.type===`ref/prompt`?P(n.name,t.name,t.value):F(n.uri,t.name,t.value);return S(await r.foldAsync(()=>Promise.resolve(c.none()),async e=>c(await e())))}),m},N=`somamcp/session`,P=e=>Promise.reject(/* @__PURE__ */ Error(`start({ transportType: "${e}" }) needs Node: use the createServer exported by somamcp/node, or serve requests with server.fetch().`)),F=(e,t,n)=>{let r=c(t.logger),i=e=>r.forEach(t=>t.warn(`[somamcp/sdk] ${e}`)),o=l(s.empty()),m=l(s.empty()),g=l(s.empty()),_=l(s.empty()),v={prompts:()=>_.get(),resources:()=>m.get(),templates:()=>g.get(),tools:()=>o.get()},y=(e,t,n)=>{e.set(e.get().add(d([t,n])))},b=(e,t)=>{e.set(e.get().remove(t))},x={authRequired:t.authenticate!==void 0,instructions:t.instructions,name:t.name,version:t.version},S=c(t.streamKeepalive).filter(e=>e.enabled===!0).map(e=>e.intervalMs??2e4),C=S.fold(()=>`auto`,()=>`sse`),w=e=>a(({authInfo:e})=>M(v,x,c(e).flatMap(e=>c(e.extra?.[N]))),{keepAliveMs:S.orUndefined(),onerror:e=>r.forEach(t=>t.error(`[somamcp/sdk]`,e)),responseMode:e}),T=l(C),E=l(w(T.get())),D=l(c(n).flatMap(e=>c(e.mcpPath)).orElse(`/mcp`)),O=l(c.none()),k=l(c.none()),A=l(`running`),j=(e,t)=>t instanceof Response?t:e.json({error:`Unauthorized`},401),F=(e,t)=>Promise.resolve().then(()=>e(t.orUndefined())).then(e=>c(e).fold(()=>Promise.reject(/* @__PURE__ */ Error(`Authentication required`)),e=>Promise.resolve(e))),I=async e=>{let n=e.req.raw;return t.authenticate?F(t.authenticate,c(n)).then(e=>E.get().fetch(n,{authInfo:{clientId:``,extra:{[N]:e},scopes:[],token:``}}),t=>j(e,t)):E.get().fetch(n)},L=new f;L.use(`*`,async(e,t)=>e.req.path===D.get()?O.get().fold(()=>I(e),async t=>await t(e,async()=>{e.res=await I(e)})??e.res):t());let R=e=>{c(e.endpoint).forEach(e=>D.set(e)),c(e.cors).filter(e=>e!==!1).forEach(e=>O.set(c(p(e===!0?{}:e)))),e.enableJsonResponse===!0&&(S.isSome()&&i(`enableJsonResponse disables streamKeepalive: JSON responses cannot carry keepalive frames.`),T.set(`json`),E.set(w(`json`))),e.stateless===!1&&i(`stateful sessions are not supported by the SDK backend; serving statelessly.`)},z=async e=>{let i=t.authenticate?(await u.fromPromise(F(t.authenticate,c.none()))).fold(e=>(r.forEach(t=>t.error(`[somamcp/sdk] authentication failed for stdio transport:`,e.message)),c.none()),e=>c(e)):c.none(),a=c(n).flatMap(e=>c(e.stdioTransport)).orUndefined();return e.stdio(()=>M(v,x,i),a)},B=(e,t)=>(R(t),e.http(L,t)),V=t=>e.fold(()=>t?P(t.transportType):Promise.resolve(c.none()),e=>(t?.transportType===`httpStream`?B(e,t.httpStream):z(e)).then(e=>c(e)));return{get serverState(){return A.get()},get sessions(){return[]},addPrompt:e=>{y(_,e.name,e)},addResource:e=>{y(m,e.name,e)},addResourceTemplate:e=>{y(g,e.name,e)},addTool:e=>{e.parameters&&!h(e.parameters)&&i(`tool "${e.name}": its schema has no ~standard.jsonSchema, so clients see an empty argument list. Use zod 4, ArkType, or Valibot with @valibot/to-json-schema.`),y(o,e.name,e)},fetch:async e=>L.fetch(e),getApp:()=>L,on:()=>{},removePrompt:e=>{b(_,e)},removeResource:e=>{b(m,e)},removeTool:e=>{b(o,e)},start:async e=>{if(k.get().isSome()){i(`start() called while already listening; ignoring. Call stop() first to restart.`);return}let t=await u.fromPromise(V(e));t.isFailure()&&A.set(`error`),k.set(t.orThrow()),A.set(`running`)},stop:async()=>{await E.get().close(),await k.get().fold(async()=>{},e=>e.close()),k.set(c.none()),E.set(w(T.get())),A.set(`stopped`)}}},I=(e,t)=>F(c.none(),e,t);export{I as n,F as t};