somamcp 1.5.1 → 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 +93 -124
- package/dist/backend/index.d.ts +2 -2
- package/dist/backend/index.js +1 -1
- package/dist/backend-DnC4_jJ7.js +1 -0
- package/dist/edge.d.ts +3 -767
- package/dist/edge.js +1 -61
- package/dist/index-aVpYZPUq.d.ts +421 -0
- package/dist/{index-C2wa9nmp.d.ts → index-ololl9dI.d.ts} +35 -46
- package/dist/index.d.ts +3 -474
- package/dist/index.js +1 -62
- package/dist/node/index.d.ts +28 -0
- package/dist/node/index.js +2 -0
- package/dist/src-C1b-whVb.js +69 -0
- package/package.json +21 -13
- package/dist/backend-D0qSyfXw.js +0 -1
- package/dist/listen-ZkXBwyaw.js +0 -1
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
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|
|
118
|
-
//
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
**
|
|
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
|
-
|
|
394
|
+
## The SDK Backend
|
|
400
395
|
|
|
401
|
-
|
|
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
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
```typescript
|
|
413
|
-
import { createServer } from "somamcp"
|
|
414
|
-
import { createSdkBackend } from "somamcp/backend"
|
|
401
|
+
How it behaves:
|
|
415
402
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
430
|
+
## Migrating from 1.x
|
|
460
431
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|
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`, `
|
|
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` (
|
|
512
|
-
- `streamKeepalive` on `ServerConfig` — `{ enabled, intervalMs }
|
|
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** (
|
|
504
|
+
**Backend** (`somamcp` and `somamcp/backend`)
|
|
536
505
|
|
|
537
|
-
- `
|
|
506
|
+
- `createSdkBackend`; `createNodeBackend` (`somamcp/node` only)
|
|
538
507
|
- Types: `BackendAdapter`, `BackendFactory`, `BackendSession`, `SdkBackendOptions`
|
|
539
508
|
|
|
540
509
|
## Scripts
|
package/dist/backend/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { type BackendAdapter, type BackendEvents, type BackendFactory, type BackendSession, type SdkBackendOptions,
|
|
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 };
|
package/dist/backend/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{n as
|
|
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};
|