somamcp 1.1.3 → 1.2.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
@@ -328,9 +328,86 @@ await server.start({
328
328
  })
329
329
  ```
330
330
 
331
+ ## Edge Runtimes (Cloudflare Workers, Deno Deploy, Bun)
332
+
333
+ Import from `somamcp/edge` instead of `somamcp`. The API is identical — `backend` just defaults to `createEdgeBackend` rather than the Node FastMCP backend.
334
+
335
+ ```typescript
336
+ import { createServer } from "somamcp/edge"
337
+ import { z } from "zod"
338
+
339
+ const server = createServer({
340
+ name: "my-worker",
341
+ version: "1.0.0",
342
+ // Workers has no process.env — pass build info from the fetch handler's `env`.
343
+ build: { commit: "abc123", environment: "production" },
344
+ })
345
+
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
+ }
356
+ ```
357
+
358
+ `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
+
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
+ | `addResourceTemplate` | ✅ | ⚠️ warns, does nothing |
383
+
384
+ 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.
385
+
386
+ **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.
387
+
388
+ **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.
389
+
390
+ **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.
391
+
392
+ **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.
393
+
394
+ **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.
395
+
396
+ **Runtime reporting.** `getInfo().runtime` reports the runtime _family_: `"node"` whenever a Node-compatible `process` global is present. Bun, and Workers with `nodejs_compat` enabled, therefore report `"node"` even under `somamcp/edge`.
397
+
331
398
  ## Backend Abstraction
332
399
 
333
- The underlying MCP framework is accessed exclusively through the `BackendAdapter` interface. To use a non-FastMCP backend, implement `BackendFactory`:
400
+ 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:
401
+
402
+ ```typescript
403
+ import { createServer } from "somamcp"
404
+ import { createEdgeBackend } from "somamcp/edge"
405
+
406
+ // equivalent to importing createServer from "somamcp/edge"
407
+ const server = createServer({ name: "my-server", version: "1.0.0", backend: createEdgeBackend })
408
+ ```
409
+
410
+ To use a different backend entirely, implement `BackendFactory`:
334
411
 
335
412
  ```typescript
336
413
  import { createFastMCPBackend } from "somamcp/backend"
@@ -1,2 +1,2 @@
1
- import { a as BackendSession, i as BackendFactory, n as BackendAdapter, r as BackendEvents, t as createFastMCPBackend } from "../index-DizthJMe.js";
1
+ import { a as BackendSession, i as BackendFactory, n as BackendAdapter, r as BackendEvents, t as createFastMCPBackend } from "../index-BRNcy_4O.js";
2
2
  export { type BackendAdapter, type BackendEvents, type BackendFactory, type BackendSession, createFastMCPBackend };
@@ -1 +1 @@
1
- import{t as e}from"../backend-Bm6e2u2K.js";export{e as createFastMCPBackend};
1
+ import{t as e}from"../backend-CA7j4YCF.js";export{e as createFastMCPBackend};
@@ -1 +1 @@
1
- import{FastMCP as e,ServerState as t}from"fastmcp";const n=e=>{switch(e){case t.Running:return`running`;case t.Error:return`error`;default:return`stopped`}},r=(t,r)=>{let i={...r,authenticate:t.authenticate,instructions:t.instructions,logger:t.logger,name:t.name,version:t.version},a=new e(i);return{get serverState(){return n(a.serverState)},get sessions(){return a.sessions.map(e=>({sessionId:e.sessionId}))},addPrompt:e=>{a.addPrompt(e)},addResource:e=>{a.addResource(e)},addResourceTemplate:(...e)=>{a.addResourceTemplate(...e)},addTool:e=>{a.addTool(e)},getApp:()=>a.getApp(),on:(e,t)=>{a.on(e,t)},removePrompt:e=>a.removePrompt(e),removeResource:e=>a.removeResource(e),removeTool:e=>a.removeTool(e),start:async e=>{if(!e){await a.start();return}e.transportType===`httpStream`?await a.start({httpStream:e.httpStream,transportType:`httpStream`}):await a.start({transportType:`stdio`})},stop:()=>a.stop()}};export{r as t};
1
+ import{FastMCP as e,ServerState as t}from"fastmcp";const n=e=>{switch(e){case t.Running:return`running`;case t.Error:return`error`;default:return`stopped`}},r=(t,r)=>{let i={...r,authenticate:t.authenticate,instructions:t.instructions,logger:t.logger,name:t.name,version:t.version},a=new e(i);return{get serverState(){return n(a.serverState)},get sessions(){return a.sessions.map(e=>({sessionId:e.sessionId}))},addPrompt:e=>{a.addPrompt(e)},addResource:e=>{a.addResource(e)},addResourceTemplate:(...e)=>{a.addResourceTemplate(...e)},addTool:e=>{a.addTool(e)},fetch:async e=>a.getApp().fetch(e),getApp:()=>a.getApp(),on:(e,t)=>{a.on(e,t)},removePrompt:e=>a.removePrompt(e),removeResource:e=>a.removeResource(e),removeTool:e=>a.removeTool(e),start:async e=>{if(!e){await a.start();return}e.transportType===`httpStream`?await a.start({httpStream:e.httpStream,transportType:`httpStream`}):await a.start({transportType:`stdio`})},stop:()=>a.stop()}};export{r as t};
package/dist/edge.d.ts ADDED
@@ -0,0 +1,711 @@
1
+ import { Option } from "functype";
2
+ import { Context as Context$1, Hono, MiddlewareHandler } from "hono";
3
+ import { DirectLogger } from "functype-log";
4
+ import { z } from "zod";
5
+ import { Tool as Tool$1 } from "@modelcontextprotocol/sdk/types.js";
6
+ //#region src/artifacts/types.d.ts
7
+ type ArtifactCommon = {
8
+ protected?: boolean;
9
+ };
10
+ type StaticArtifact = ArtifactCommon & {
11
+ contentType: string;
12
+ content: string;
13
+ path: string;
14
+ type: "static";
15
+ };
16
+ type DynamicArtifact = ArtifactCommon & {
17
+ handler: (c: Context$1) => Response | Promise<Response>;
18
+ path: string;
19
+ type: "dynamic";
20
+ };
21
+ type DirectoryArtifact = ArtifactCommon & {
22
+ directory: string;
23
+ path: string;
24
+ type: "directory";
25
+ };
26
+ type ArtifactConfig = DirectoryArtifact | DynamicArtifact | StaticArtifact;
27
+ type ArtifactAuthenticate = (request: unknown) => Promise<unknown>;
28
+ //#endregion
29
+ //#region src/types/core.d.ts
30
+ type SessionAuth = Record<string, unknown> | undefined;
31
+ /**
32
+ * Structural type compatible with StandardSchemaV1 (zod, valibot, etc.)
33
+ * We define this structurally to avoid a direct dependency on @standard-schema/spec.
34
+ */
35
+ type SchemaParams = {
36
+ readonly "~standard": {
37
+ readonly version: 1;
38
+ readonly vendor: string;
39
+ readonly types?: {
40
+ readonly input: unknown;
41
+ readonly output: unknown;
42
+ } | undefined;
43
+ readonly validate: (value: unknown) => {
44
+ value: unknown;
45
+ } | {
46
+ issues: ReadonlyArray<{
47
+ message: string;
48
+ }>;
49
+ } | Promise<{
50
+ value: unknown;
51
+ } | {
52
+ issues: ReadonlyArray<{
53
+ message: string;
54
+ }>;
55
+ }>;
56
+ };
57
+ };
58
+ type InferSchemaOutput<P extends SchemaParams> = NonNullable<P["~standard"]["types"]>["output"] extends (infer O) ? [O] extends [never] ? unknown : O : unknown;
59
+ type TextContent = {
60
+ text: string;
61
+ type: "text";
62
+ };
63
+ type ImageContent = {
64
+ data: string;
65
+ mimeType: string;
66
+ type: "image";
67
+ };
68
+ type AudioContent = {
69
+ data: string;
70
+ mimeType: string;
71
+ type: "audio";
72
+ };
73
+ type ResourceContent = {
74
+ resource: {
75
+ blob?: string;
76
+ mimeType?: string;
77
+ text?: string;
78
+ uri: string;
79
+ };
80
+ type: "resource";
81
+ };
82
+ type ResourceLink = {
83
+ mimeType?: string;
84
+ name?: string;
85
+ type: "resource_link";
86
+ uri: string;
87
+ };
88
+ type Content = AudioContent | ImageContent | ResourceContent | ResourceLink | TextContent;
89
+ type ContentResult = {
90
+ content: Content[];
91
+ isError?: boolean;
92
+ };
93
+ type Progress = {
94
+ progress: number;
95
+ total?: number;
96
+ };
97
+ type Context<T extends SessionAuth = SessionAuth> = {
98
+ client: {
99
+ version: unknown;
100
+ };
101
+ log: {
102
+ debug: (message: string, data?: unknown) => void;
103
+ error: (message: string, data?: unknown) => void;
104
+ info: (message: string, data?: unknown) => void;
105
+ warn: (message: string, data?: unknown) => void;
106
+ };
107
+ reportProgress: (progress: Progress) => Promise<void>;
108
+ requestId?: string;
109
+ session: T | undefined;
110
+ sessionId?: string;
111
+ streamContent: (content: Content | Content[]) => Promise<void>;
112
+ };
113
+ type ToolAnnotations = {
114
+ destructiveHint?: boolean;
115
+ idempotentHint?: boolean;
116
+ openWorldHint?: boolean;
117
+ readOnlyHint?: boolean;
118
+ streamingHint?: boolean;
119
+ title?: string;
120
+ };
121
+ type Tool<T extends SessionAuth = SessionAuth, P extends SchemaParams = SchemaParams> = {
122
+ _meta?: {
123
+ [key: string]: unknown;
124
+ ui?: {
125
+ resourceUri?: string;
126
+ };
127
+ };
128
+ annotations?: ToolAnnotations;
129
+ canAccess?: (auth: T) => boolean;
130
+ description?: string;
131
+ execute: (args: InferSchemaOutput<P>, context: Context<T>) => Promise<Content | ContentResult | string | void>;
132
+ name: string;
133
+ outputSchema?: SchemaParams;
134
+ parameters?: P;
135
+ timeoutMs?: number;
136
+ };
137
+ type Completion = {
138
+ hasMore?: boolean;
139
+ total?: number;
140
+ values: string[];
141
+ };
142
+ type ResourceResult = {
143
+ blob: string;
144
+ mimeType?: string;
145
+ uri?: string;
146
+ } | {
147
+ mimeType?: string;
148
+ text: string;
149
+ uri?: string;
150
+ };
151
+ type Resource<T extends SessionAuth = SessionAuth> = {
152
+ complete?: (name: string, value: string, auth?: T) => Promise<Completion>;
153
+ description?: string;
154
+ load: (auth?: T) => Promise<ResourceResult | ResourceResult[]>;
155
+ mimeType?: string;
156
+ name: string;
157
+ uri: string;
158
+ };
159
+ type PromptArgument<T extends SessionAuth = SessionAuth> = Readonly<{
160
+ complete?: (value: string, auth?: T) => Promise<Completion>;
161
+ description?: string;
162
+ enum?: string[];
163
+ name: string;
164
+ required?: boolean;
165
+ }>;
166
+ type PromptResult = {
167
+ messages: unknown[];
168
+ } | string;
169
+ type Prompt<T extends SessionAuth = SessionAuth, Args extends PromptArgument<T>[] = PromptArgument<T>[]> = {
170
+ arguments?: Args;
171
+ complete?: (name: string, value: string, auth?: T) => Promise<Completion>;
172
+ description?: string;
173
+ load: (args: Record<string, string | undefined>, auth?: T) => Promise<PromptResult>;
174
+ name: string;
175
+ };
176
+ type ServerStatus = "error" | "running" | "stopped";
177
+ declare class UserError extends Error {
178
+ constructor(message: string);
179
+ }
180
+ //#endregion
181
+ //#region src/types/server.d.ts
182
+ type Logger = {
183
+ debug: (...args: unknown[]) => void;
184
+ error: (...args: unknown[]) => void;
185
+ info: (...args: unknown[]) => void;
186
+ log: (...args: unknown[]) => void;
187
+ warn: (...args: unknown[]) => void;
188
+ };
189
+ type ServerConfig<T extends SessionAuth = SessionAuth> = {
190
+ authenticate?: (request: unknown) => Promise<T>;
191
+ instructions?: string;
192
+ logger?: Logger;
193
+ name: string;
194
+ version: `${number}.${number}.${number}`;
195
+ };
196
+ /**
197
+ * httpStream transport options. All fields are passed through to the underlying
198
+ * MCP backend unchanged; `port` is the only one required.
199
+ *
200
+ * - `cors` — `true` for permissive defaults, or a config object forwarded to the
201
+ * backend's CORS implementation. Only relevant for browser-facing servers.
202
+ * - `stateless` — treat each request independently (no session state). Useful
203
+ * for horizontally-scaled or Lambda-style deployments.
204
+ * - `eventStore` — pluggable store enabling resumable event streams; the
205
+ * backend's `EventStore` interface, opaque to somamcp.
206
+ * - `sslCert` / `sslKey` / `sslCa` — inline PEM strings enabling TLS termination
207
+ * at the server. Omit for plain HTTP behind a proxy.
208
+ */
209
+ type HttpStreamConfig = {
210
+ cors?: boolean | Record<string, unknown>;
211
+ enableJsonResponse?: boolean;
212
+ endpoint?: `/${string}`;
213
+ eventStore?: unknown;
214
+ host?: string;
215
+ port: number;
216
+ sslCa?: string;
217
+ sslCert?: string;
218
+ sslKey?: string;
219
+ stateless?: boolean;
220
+ };
221
+ type TransportConfig = {
222
+ transportType: "stdio";
223
+ } | {
224
+ httpStream: HttpStreamConfig;
225
+ transportType: "httpStream";
226
+ };
227
+ //#endregion
228
+ //#region src/backend/adapter.d.ts
229
+ type BackendSession<T extends SessionAuth = SessionAuth> = {
230
+ sessionId?: string;
231
+ auth?: T;
232
+ };
233
+ type BackendEvents<T extends SessionAuth> = {
234
+ connect: (event: {
235
+ session: BackendSession<T>;
236
+ }) => void;
237
+ disconnect: (event: {
238
+ session: BackendSession<T>;
239
+ }) => void;
240
+ };
241
+ type BackendAdapter<T extends SessionAuth = SessionAuth> = {
242
+ readonly serverState: ServerStatus;
243
+ readonly sessions: ReadonlyArray<BackendSession<T>>;
244
+ addPrompt: <Args extends PromptArgument<T>[]>(prompt: Prompt<T, Args>) => void;
245
+ addResource: (resource: Resource<T>) => void;
246
+ addResourceTemplate: (...args: ReadonlyArray<unknown>) => void;
247
+ addTool: <P extends SchemaParams>(tool: Tool<T, P>) => void;
248
+ /**
249
+ * Handle a web-standard Request. Optional: when absent, callers fall back to
250
+ * `getApp().fetch`. Edge backends implement this as their primary entry point;
251
+ * Node backends provide it for parity so `SomaServerInstance.fetch` always works.
252
+ */
253
+ fetch?: (request: Request) => Promise<Response>;
254
+ getApp: () => Hono;
255
+ on: <E extends keyof BackendEvents<T>>(event: E, handler: BackendEvents<T>[E]) => void;
256
+ removePrompt: (name: string) => void;
257
+ removeResource: (name: string) => void;
258
+ removeTool: (name: string) => void;
259
+ start: (transport?: TransportConfig) => Promise<void>;
260
+ stop: () => Promise<void>;
261
+ };
262
+ type BackendFactory<T extends SessionAuth = SessionAuth> = (config: ServerConfig<T>, backendOptions?: Record<string, unknown>) => BackendAdapter<T>;
263
+ //#endregion
264
+ //#region src/buildInfo.d.ts
265
+ type BuildInfo = {
266
+ branch?: string;
267
+ commit?: string;
268
+ date?: string;
269
+ environment?: string;
270
+ };
271
+ type RuntimeInfo = {
272
+ arch: string;
273
+ nodeVersion: string;
274
+ platform: string;
275
+ /** Which runtime family reported the fields above. Absent on pre-1.2 builds. */
276
+ runtime?: "edge" | "node";
277
+ };
278
+ declare const readBuildInfoFromEnv: () => BuildInfo;
279
+ declare const resolveBuildInfo: (override?: BuildInfo) => BuildInfo;
280
+ declare const getRuntimeInfo: () => RuntimeInfo;
281
+ //#endregion
282
+ //#region src/gateway/types.d.ts
283
+ type GatewayConfig = {
284
+ id: string;
285
+ name?: string;
286
+ proxyTools?: boolean;
287
+ reconnect?: boolean;
288
+ reconnectIntervalMs?: number;
289
+ toolPrefix?: string;
290
+ url: string;
291
+ };
292
+ type GatewayStatus = "connected" | "connecting" | "disconnected" | "error";
293
+ type GatewayInfo = {
294
+ id: string;
295
+ name: string;
296
+ remoteTools: ReadonlyArray<{
297
+ description?: string;
298
+ name: string;
299
+ }>;
300
+ status: GatewayStatus;
301
+ url: string;
302
+ };
303
+ type GatewayInstance = {
304
+ readonly config: GatewayConfig;
305
+ readonly info: GatewayInfo;
306
+ readonly name: string;
307
+ readonly status: GatewayStatus;
308
+ readonly tools: ReadonlyArray<Tool$1>;
309
+ callTool: (name: string, args?: Record<string, unknown>) => Promise<unknown>;
310
+ connect: () => Promise<void>;
311
+ disconnect: () => Promise<void>;
312
+ };
313
+ type GatewayManagerInstance = {
314
+ readonly connectedCount: number;
315
+ readonly totalCount: number;
316
+ add: (config: GatewayConfig) => GatewayInstance;
317
+ connectAll: () => Promise<void>;
318
+ disconnectAll: () => Promise<void>;
319
+ get: (id: string) => Option<GatewayInstance>;
320
+ getAll: () => ReadonlyArray<GatewayInstance>;
321
+ getInfoAll: () => ReadonlyArray<GatewayInfo>;
322
+ };
323
+ //#endregion
324
+ //#region src/telemetry/TelemetryCollector.d.ts
325
+ type TelemetryEventType = "server.start" | "server.stop" | "tool.execute" | "tool.error" | "resource.load" | "resource.error" | "prompt.load" | "prompt.error" | "gateway.connect" | "gateway.disconnect" | "gateway.error" | "session.connect" | "session.disconnect";
326
+ type ErrorCategory = "validation" | "timeout" | "gateway" | "internal" | "auth" | "not_found";
327
+ type CaptureLevel = "full" | "metadata" | "none";
328
+ type ToolCaptureConfig = {
329
+ captureLevel?: CaptureLevel;
330
+ maxOutputSize?: number;
331
+ redactInputFields?: ReadonlyArray<string>;
332
+ };
333
+ type TelemetryEvent = {
334
+ data?: Record<string, unknown>;
335
+ durationMs?: number;
336
+ error?: string;
337
+ errorCategory?: ErrorCategory;
338
+ errorStack?: string;
339
+ input?: Record<string, unknown>;
340
+ name: string;
341
+ output?: unknown;
342
+ requestId?: string;
343
+ sessionId?: string;
344
+ timestamp: number;
345
+ type: TelemetryEventType;
346
+ };
347
+ type TelemetryCollector = {
348
+ flush?: () => Promise<void>;
349
+ recordEvent: (event: TelemetryEvent) => void;
350
+ };
351
+ //#endregion
352
+ //#region src/auth/middleware.d.ts
353
+ /**
354
+ * Callback invoked to authenticate an incoming request. Receives either a Hono
355
+ * `Request` (custom routes / protected artifacts) or an `http.IncomingMessage`
356
+ * (MCP transport). Use `getRequestHeader` to hide the shape difference.
357
+ */
358
+ type Authenticate = (request: unknown) => Promise<unknown>;
359
+ /** Response builder invoked when auth is missing or fails. Defaults to `401 { error: "Unauthorized" }`. */
360
+ type OnUnauthorized = (c: Context$1) => Response | Promise<Response>;
361
+ type AuthMiddlewareConfig = {
362
+ authenticate?: Authenticate;
363
+ onUnauthorized?: OnUnauthorized;
364
+ };
365
+ declare const createAuthMiddleware: ({ authenticate, onUnauthorized }: AuthMiddlewareConfig) => MiddlewareHandler;
366
+ //#endregion
367
+ //#region src/auth/request.d.ts
368
+ /**
369
+ * Extract a header from either shape somamcp's `authenticate` callback may receive:
370
+ *
371
+ * - **Hono `Request`** (`c.req.raw`) — used on protected artifact and custom routes.
372
+ * Headers implement the WHATWG `Headers` interface (`.get(name)`).
373
+ * - **`http.IncomingMessage`** — used on the MCP transport path (FastMCP).
374
+ * Headers are a plain lowercased object; values may be `string | string[]`.
375
+ *
376
+ * Case-insensitive on the header name. Returns `undefined` when the header is
377
+ * absent or the request shape is not recognized — matches the `Headers.get`
378
+ * shape callers already write against.
379
+ */
380
+ declare const getRequestHeader: (request: unknown, name: string) => string | undefined;
381
+ //#endregion
382
+ //#region src/types/routes.d.ts
383
+ type RouteMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
384
+ /**
385
+ * Config for a custom HTTP route registered on the server's Hono app via
386
+ * `SomaServerInstance.addRoute`.
387
+ *
388
+ * When `protected: true`, the server's `authenticate` callback runs first — a
389
+ * throw or rejection short-circuits to `onUnauthorized` (default `401`).
390
+ *
391
+ * ### Route ordering
392
+ *
393
+ * Hono dispatches by registration order. `addRoute` registers immediately, so
394
+ * routes added **before** `start()` land alongside artifacts and introspection
395
+ * routes in call order. For concrete non-overlapping paths this doesn't matter;
396
+ * for wildcards or overlapping prefixes, register earlier to take precedence.
397
+ */
398
+ type RouteConfig = {
399
+ method: RouteMethod | ReadonlyArray<RouteMethod>;
400
+ path: `/${string}`;
401
+ protected?: boolean;
402
+ handler: (c: Context$1) => Response | Promise<Response>;
403
+ onUnauthorized?: OnUnauthorized;
404
+ };
405
+ //#endregion
406
+ //#region src/types.d.ts
407
+ type SomaServerOptions<T extends SessionAuth = SessionAuth> = ServerConfig<T> & {
408
+ artifacts?: ArtifactConfig[];
409
+ /**
410
+ * Backend implementation. Defaults to `createFastMCPBackend` (Node).
411
+ * Pass `createEdgeBackend` from `somamcp/edge` to target Cloudflare Workers,
412
+ * Deno Deploy, or Bun.
413
+ */
414
+ backend?: BackendFactory<T>;
415
+ backendOptions?: Record<string, unknown>;
416
+ build?: BuildInfo;
417
+ enableDashboard?: boolean;
418
+ enableHealthEndpoint?: boolean;
419
+ enableInfoEndpoint?: boolean;
420
+ enableIntrospection?: boolean;
421
+ gateways?: GatewayConfig[];
422
+ healthDetailPath?: string;
423
+ healthPath?: string;
424
+ infoPath?: string;
425
+ introspectionPrefix?: string;
426
+ logLayer?: DirectLogger;
427
+ telemetry?: TelemetryCollector;
428
+ };
429
+ type ServerHealth = {
430
+ activeSessions: number;
431
+ gateways: {
432
+ connected: number;
433
+ total: number;
434
+ };
435
+ name: string;
436
+ startedAt: number;
437
+ status: ServerStatus;
438
+ uptime: number;
439
+ };
440
+ type ServerCapabilities = {
441
+ prompts: ReadonlyArray<{
442
+ description?: string;
443
+ name: string;
444
+ }>;
445
+ resources: ReadonlyArray<{
446
+ description?: string;
447
+ name: string;
448
+ uri: string;
449
+ }>;
450
+ tools: ReadonlyArray<{
451
+ description?: string;
452
+ name: string;
453
+ }>;
454
+ };
455
+ type CapabilitiesSummary = {
456
+ prompts: number;
457
+ resources: number;
458
+ tools: number;
459
+ };
460
+ type ServerInfo = {
461
+ build: BuildInfo;
462
+ capabilities: CapabilitiesSummary;
463
+ name: string;
464
+ runtime: RuntimeInfo;
465
+ version: string;
466
+ };
467
+ type ToolOptions<T extends SessionAuth = SessionAuth, P extends SchemaParams = SchemaParams> = Tool<T, P> & {
468
+ captureConfig?: ToolCaptureConfig;
469
+ };
470
+ type SomaServerInstance<T extends SessionAuth = SessionAuth> = {
471
+ readonly name: string;
472
+ readonly serverState: ServerStatus;
473
+ readonly sessions: ReadonlyArray<BackendSession<T>>;
474
+ addPrompt: <Args extends PromptArgument<T>[]>(prompt: Prompt<T, Args>) => void;
475
+ addPrompts: <Args extends PromptArgument<T>[]>(prompts: Prompt<T, Args>[]) => void;
476
+ addResource: (resource: Resource<T>) => void;
477
+ addResources: (resources: Resource<T>[]) => void;
478
+ addResourceTemplate: (...args: ReadonlyArray<unknown>) => void;
479
+ addRoute: (route: RouteConfig) => void;
480
+ addTool: <P extends SchemaParams>(tool: Tool<T, P>) => void;
481
+ addTools: <P extends SchemaParams>(tools: Tool<T, P>[]) => void;
482
+ /**
483
+ * Web-standard request handler covering both MCP calls and registered
484
+ * artifacts/routes. This is the entry point for edge runtimes:
485
+ * `export default { fetch: (req) => server.fetch(req) }`.
486
+ */
487
+ fetch: (request: Request) => Promise<Response>;
488
+ getApp: () => Hono;
489
+ getCapabilities: () => ServerCapabilities;
490
+ getGatewayManager: () => GatewayManagerInstance;
491
+ getHealth: () => ServerHealth;
492
+ getInfo: () => ServerInfo;
493
+ removePrompt: (name: string) => void;
494
+ removeResource: (name: string) => void;
495
+ removeTool: (name: string) => void;
496
+ start: (options?: TransportConfig) => Promise<void>;
497
+ stop: () => Promise<void>;
498
+ };
499
+ //#endregion
500
+ //#region src/Server.d.ts
501
+ /**
502
+ * Backend-agnostic server core.
503
+ *
504
+ * `backend` is required here on purpose. Defaulting it would mean a static import
505
+ * of the Node FastMCP backend, which lands in every bundle that reaches this
506
+ * module — including `somamcp/edge`. The entry barrels supply the default instead:
507
+ * `somamcp` binds createFastMCPBackend, `somamcp/edge` binds createEdgeBackend.
508
+ */
509
+ declare const createServerCore: <T extends SessionAuth = SessionAuth>(options: SomaServerOptions<T> & {
510
+ backend: BackendFactory<T>;
511
+ }) => SomaServerInstance<T>;
512
+ //#endregion
513
+ //#region src/backend/edge.d.ts
514
+ type EdgeBackendOptions = {
515
+ /** Base path for the MCP endpoint. Defaults to "/mcp". */
516
+ mcpPath?: string;
517
+ };
518
+ /**
519
+ * Edge-runtime backend for Cloudflare Workers, Deno Deploy, and Bun.
520
+ *
521
+ * Routing: somamcp owns the outer Hono app so artifacts and routes registered
522
+ * via `createServer` take precedence. Anything unmatched falls through to
523
+ * EdgeFastMCP, which serves the MCP endpoint. This ordering matters — EdgeFastMCP
524
+ * registers its own `/health` at construction, and Hono is first-match-wins, so
525
+ * mounting it first would shadow somamcp's health artifact.
526
+ *
527
+ * The prefer-either disable below is for the nested tool-execute throw, which
528
+ * EdgeFastMCP requires; this factory itself never throws.
529
+ */
530
+ declare const createEdgeBackend: <T extends SessionAuth = SessionAuth>(config: ServerConfig<T>, backendOptions?: Record<string, unknown>) => BackendAdapter<T>;
531
+ //#endregion
532
+ //#region src/telemetry/CompositeTelemetry.d.ts
533
+ declare const createCompositeTelemetry: (collectors: ReadonlyArray<TelemetryCollector>) => TelemetryCollector;
534
+ //#endregion
535
+ //#region src/telemetry/ConsoleTelemetry.d.ts
536
+ declare const createConsoleTelemetry: (prefix?: string) => TelemetryCollector;
537
+ //#endregion
538
+ //#region src/telemetry/EnrichedError.d.ts
539
+ type EnrichedErrorResponse = {
540
+ errorCategory: ErrorCategory;
541
+ message: string;
542
+ relatedState?: Record<string, unknown>;
543
+ suggestions?: ReadonlyArray<string>;
544
+ };
545
+ declare const classifyError: (error: unknown) => ErrorCategory;
546
+ declare const createEnrichedError: (error: unknown, context?: {
547
+ gatewayStatus?: string;
548
+ toolName?: string;
549
+ }) => EnrichedErrorResponse;
550
+ //#endregion
551
+ //#region src/telemetry/NoopTelemetry.d.ts
552
+ declare const NoopTelemetry: TelemetryCollector;
553
+ //#endregion
554
+ //#region src/telemetry/telemetryWrapper.d.ts
555
+ /**
556
+ * Wrap a tool's `execute` in telemetry + error enrichment.
557
+ *
558
+ * The success path returns the tool's return value **unchanged**, so any of the
559
+ * supported shapes reach the backend intact: a plain string, a `ContentResult`
560
+ * (`{ content: [...], isError? }`), or a content-array with multimodal parts
561
+ * (`{ type: "image", data, mimeType }`, `audio`, `resource`, etc.). Use the
562
+ * exported `imageContent` / `audioContent` helpers to build parts without
563
+ * reaching into the backend.
564
+ *
565
+ * The failure path converts an error to a `ContentResult` with `isError: true`
566
+ * and the enriched error payload as the text content.
567
+ */
568
+ declare const wrapTool: <T extends SessionAuth, P extends SchemaParams>(tool: Tool<T, P>, telemetry: TelemetryCollector, captureConfig?: ToolCaptureConfig) => Tool<T, P>;
569
+ declare const wrapResource: <T extends SessionAuth>(resource: Resource<T>, telemetry: TelemetryCollector) => Resource<T>;
570
+ declare const wrapPrompt: <T extends SessionAuth, Args extends PromptArgument<T>[] = PromptArgument<T>[]>(prompt: Prompt<T, Args>, telemetry: TelemetryCollector) => Prompt<T, Args>;
571
+ //#endregion
572
+ //#region src/artifacts/ArtifactManager.d.ts
573
+ declare const registerArtifacts: (app: Hono, artifacts: ReadonlyArray<ArtifactConfig>, authenticate?: ArtifactAuthenticate) => void;
574
+ //#endregion
575
+ //#region src/artifacts/DashboardArtifact.d.ts
576
+ declare const createDashboardArtifact: (getHealth: () => ServerHealth, getCapabilities: () => ServerCapabilities, getConnections: () => ReadonlyArray<GatewayInfo>) => ArtifactConfig;
577
+ //#endregion
578
+ //#region src/artifacts/HealthArtifact.d.ts
579
+ declare const DEFAULT_HEALTH_PATH = "/health";
580
+ declare const DEFAULT_HEALTH_DETAIL_PATH = "/health/detail";
581
+ declare const createHealthArtifact: (getHealth: () => ServerHealth, path?: string) => ArtifactConfig;
582
+ declare const createHealthDetailArtifact: (getHealth: () => ServerHealth, path?: string) => ArtifactConfig;
583
+ //#endregion
584
+ //#region src/artifacts/InfoArtifact.d.ts
585
+ declare const DEFAULT_INFO_PATH = "/info";
586
+ declare const createInfoArtifact: (getInfo: () => ServerInfo, path?: string) => ArtifactConfig;
587
+ //#endregion
588
+ //#region src/introspection/capabilitiesTool.d.ts
589
+ declare const createCapabilitiesTool: <T extends SessionAuth>(getCapabilities: () => ServerCapabilities) => Tool<T>;
590
+ //#endregion
591
+ //#region src/introspection/connectionsTool.d.ts
592
+ declare const createConnectionsTool: <T extends SessionAuth>(getConnections: () => ReadonlyArray<GatewayInfo>) => Tool<T>;
593
+ //#endregion
594
+ //#region src/introspection/healthTool.d.ts
595
+ declare const createHealthTool: <T extends SessionAuth>(getHealth: () => ServerHealth) => Tool<T>;
596
+ //#endregion
597
+ //#region src/introspection/infoTool.d.ts
598
+ declare const createInfoTool: <T extends SessionAuth>(getInfo: () => ServerInfo, name?: string) => Tool<T>;
599
+ //#endregion
600
+ //#region src/feedback/redaction.d.ts
601
+ type RedactionPattern = {
602
+ description: string;
603
+ name: string;
604
+ pattern: RegExp;
605
+ replacement?: string;
606
+ };
607
+ type RedactionResult = {
608
+ matches: ReadonlyArray<{
609
+ name: string;
610
+ count: number;
611
+ }>;
612
+ redacted: boolean;
613
+ text: string;
614
+ };
615
+ declare const DEFAULT_REDACTION_PATTERNS: ReadonlyArray<RedactionPattern>;
616
+ declare const redact: (input: string, patterns?: ReadonlyArray<RedactionPattern>) => RedactionResult;
617
+ //#endregion
618
+ //#region src/feedback/types.d.ts
619
+ type FeedbackType = "bug" | "feature" | "feedback" | "question";
620
+ type FeedbackSeverity = "high" | "low" | "medium";
621
+ type NormalizedFeedback = {
622
+ body: string;
623
+ labels: ReadonlyArray<string>;
624
+ title: string;
625
+ };
626
+ type FeedbackSubmitSuccess = {
627
+ id?: string;
628
+ success: true;
629
+ url?: string;
630
+ };
631
+ type FeedbackSubmitFailure = {
632
+ error: string;
633
+ success: false;
634
+ };
635
+ type FeedbackSubmitResult = FeedbackSubmitFailure | FeedbackSubmitSuccess;
636
+ type FeedbackProvider = {
637
+ readonly name: string;
638
+ submit: (payload: NormalizedFeedback) => Promise<FeedbackSubmitResult>;
639
+ };
640
+ //#endregion
641
+ //#region src/feedback/feedbackTool.d.ts
642
+ type FeedbackEnrichmentContext = {
643
+ type: FeedbackType;
644
+ severity?: FeedbackSeverity;
645
+ };
646
+ type FeedbackToolOptions = {
647
+ description?: string;
648
+ enrichment?: (ctx: FeedbackEnrichmentContext) => Promise<Record<string, unknown>> | Record<string, unknown>;
649
+ extraLabels?: ReadonlyArray<string>;
650
+ name?: string;
651
+ provider: FeedbackProvider;
652
+ redactionPatterns?: ReadonlyArray<RedactionPattern>;
653
+ };
654
+ declare const PARAMETERS: z.ZodObject<{
655
+ description: z.ZodString;
656
+ severity: z.ZodOptional<z.ZodEnum<{
657
+ low: "low";
658
+ medium: "medium";
659
+ high: "high";
660
+ }>>;
661
+ title: z.ZodString;
662
+ type: z.ZodDefault<z.ZodEnum<{
663
+ bug: "bug";
664
+ feature: "feature";
665
+ feedback: "feedback";
666
+ question: "question";
667
+ }>>;
668
+ }, z.core.$strip>;
669
+ declare const createFeedbackTool: <T extends SessionAuth>(options: FeedbackToolOptions) => Tool<T, typeof PARAMETERS>;
670
+ //#endregion
671
+ //#region src/feedback/providers/github.d.ts
672
+ type GithubFeedbackOptions = {
673
+ baseUrl?: string;
674
+ defaultLabels?: ReadonlyArray<string>;
675
+ fetchImpl?: typeof fetch;
676
+ getToken: () => Option<string>;
677
+ repo: `${string}/${string}`;
678
+ };
679
+ declare const createGithubFeedback: (options: GithubFeedbackOptions) => FeedbackProvider;
680
+ //#endregion
681
+ //#region src/feedback/providers/webhook.d.ts
682
+ type WebhookFeedbackOptions = {
683
+ fetchImpl?: typeof fetch;
684
+ headers?: Record<string, string>;
685
+ transform?: (payload: NormalizedFeedback) => unknown;
686
+ url: string;
687
+ };
688
+ declare const createWebhookFeedback: (options: WebhookFeedbackOptions) => FeedbackProvider;
689
+ //#endregion
690
+ //#region src/gateway/Gateway.d.ts
691
+ declare const createGateway: (config: GatewayConfig, telemetry: TelemetryCollector) => GatewayInstance;
692
+ //#endregion
693
+ //#region src/gateway/GatewayManager.d.ts
694
+ declare const createGatewayManager: (telemetry: TelemetryCollector) => GatewayManagerInstance;
695
+ //#endregion
696
+ //#region src/gateway/toolProxy.d.ts
697
+ declare const createProxiedTools: <T extends SessionAuth>(gateway: GatewayInstance, telemetry?: TelemetryCollector) => ReadonlyArray<Tool<T>>;
698
+ //#endregion
699
+ //#region src/edge.d.ts
700
+ /**
701
+ * Create a server backed by the edge runtime adapter. Identical to the `somamcp`
702
+ * `createServer` except that `backend` defaults to `createEdgeBackend`.
703
+ *
704
+ * ```ts
705
+ * const server = createServer({ name: "my-server", version: "1.0.0" })
706
+ * export default { fetch: (req: Request) => server.fetch(req) }
707
+ * ```
708
+ */
709
+ declare const createServer: <T extends SessionAuth = SessionAuth>(options: SomaServerOptions<T>) => SomaServerInstance<T>;
710
+ //#endregion
711
+ export { type ArtifactAuthenticate, type ArtifactConfig, type AudioContent, type AuthMiddlewareConfig, type Authenticate, type BackendAdapter, type BackendFactory, type BackendSession, type BuildInfo, type CapabilitiesSummary, type CaptureLevel, type Completion, type Content, type ContentResult, type Context, DEFAULT_HEALTH_DETAIL_PATH, DEFAULT_HEALTH_PATH, DEFAULT_INFO_PATH, DEFAULT_REDACTION_PATTERNS, type DirectoryArtifact, type DynamicArtifact, type EdgeBackendOptions, type EnrichedErrorResponse, type ErrorCategory, type FeedbackEnrichmentContext, type FeedbackProvider, type FeedbackSeverity, type FeedbackSubmitFailure, type FeedbackSubmitResult, type FeedbackSubmitSuccess, type FeedbackToolOptions, type FeedbackType, type GatewayConfig, type GatewayInfo, type GatewayInstance, type GatewayManagerInstance, type GatewayStatus, type GithubFeedbackOptions, type HttpStreamConfig, type ImageContent, type InferSchemaOutput, type Logger, NoopTelemetry, type NormalizedFeedback, type OnUnauthorized, type Progress, type Prompt, type PromptArgument, type PromptResult, type RedactionPattern, type RedactionResult, type Resource, type ResourceContent, type ResourceLink, type ResourceResult, type RouteConfig, type RouteMethod, type RuntimeInfo, type SchemaParams, type ServerCapabilities, type ServerConfig, type ServerHealth, type ServerInfo, type ServerStatus, type SessionAuth, type SomaServerInstance, type SomaServerOptions, type StaticArtifact, type TelemetryCollector, type TelemetryEvent, type TelemetryEventType, type TextContent, type Tool, type ToolAnnotations, type ToolCaptureConfig, type ToolOptions, type TransportConfig, UserError, type WebhookFeedbackOptions, classifyError, createAuthMiddleware, createCapabilitiesTool, createCompositeTelemetry, createConnectionsTool, createConsoleTelemetry, createDashboardArtifact, createEdgeBackend, createEnrichedError, createFeedbackTool, createGateway, createGatewayManager, createGithubFeedback, createHealthArtifact, createHealthDetailArtifact, createHealthTool, createInfoArtifact, createInfoTool, createProxiedTools, createServer, createServerCore, createWebhookFeedback, getRequestHeader, getRuntimeInfo, readBuildInfoFromEnv, redact, registerArtifacts, resolveBuildInfo, wrapPrompt, wrapResource, wrapTool };
package/dist/edge.js ADDED
@@ -0,0 +1,61 @@
1
+ import{EdgeFastMCP as e}from"fastmcp/edge";import{Map as t,Option as n,Ref as r,Set as i,Try as a,Tuple as o}from"functype";import{Hono as s}from"hono";import{Client as c}from"@modelcontextprotocol/sdk/client/index.js";import{StreamableHTTPClientTransport as l}from"@modelcontextprotocol/sdk/client/streamableHttp.js";import{createDirectConsoleLogger as u}from"functype-log";import{z as d}from"zod";const f=e=>e.json({error:`Unauthorized`},401),p=({authenticate:e,onUnauthorized:t})=>{let r=t??f;return async(t,i)=>{if(!e)return r(t);let o=n(t.env).flatMap(e=>n(e.incoming)).orElse(t.req.raw);if((await a.fromPromise(e(o))).isFailure())return r(t);await i()}},m=(e,t)=>typeof e.get==`function`?n(e.get(t)):n.none(),h=(e,t,r)=>{let i=e;return n(i[t]??i[r]).map(e=>Array.isArray(e)?e[0]??``:e)},g=(e,t,r)=>m(e,t).fold(()=>h(e,t,r),e=>n(e)),_=e=>n(e).flatMap(e=>n(e.headers)),v=(e,t)=>{let n=t.toLowerCase();return _(e).flatMap(e=>g(e,n,t)).orUndefined()},y=e=>typeof e==`string`?e:e===void 0?{content:[]}:`content`in e?e:{content:[e]},b=e=>e.content.map(e=>e.type===`text`?e.text:`[${e.type}]`).join(` `).trim()||`Tool execution failed`,x=e=>({client:{version:void 0},log:{debug:(t,n)=>e.forEach(e=>e.debug(t,n)),error:(t,n)=>e.forEach(e=>e.error(t,n)),info:(t,n)=>e.forEach(e=>e.info(t,n)),warn:(t,n)=>e.forEach(e=>e.warn(t,n))},reportProgress:async()=>{},session:void 0,streamContent:async()=>{}}),S=(t,r)=>{let{mcpPath:i}=r??{},a=i??`/mcp`,o=n(t.logger),c=new e({logger:o.orUndefined(),mcpPath:a,name:t.name,version:t.version}),l=new s;n(t.authenticate).forEach(e=>{l.use(a,p({authenticate:e}))}),l.notFound(e=>c.fetch(e.req.raw));let u=e=>o.forEach(t=>t.warn(`[somamcp/edge] ${e}`)),d=e=>u(`${e} is not supported by the edge backend; ignoring.`);return{get serverState(){return`running`},get sessions(){return[]},addPrompt:e=>{c.addPrompt({arguments:e.arguments?.map(e=>({description:e.description,name:e.name,required:e.required})),description:e.description,load:async t=>await e.load(t),name:e.name})},addResource:e=>{c.addResource({description:e.description,load:async()=>{let t=await e.load();return Array.isArray(t)?(t.length>1&&u(`resource "${e.name}" returned ${t.length} results; only the first is served.`),t[0]??{text:``}):t},mimeType:e.mimeType,name:e.name,uri:e.uri})},addResourceTemplate:()=>d(`addResourceTemplate`),addTool:e=>{let t=x(o);c.addTool({description:e.description??``,execute:async n=>{let r=y(await e.execute(n,t));if(typeof r!=`string`&&r.isError===!0)throw Error(b(r));return r},name:e.name,parameters:e.parameters})},fetch:async e=>l.fetch(e),getApp:()=>l,on:()=>{},removePrompt:()=>d(`removePrompt`),removeResource:()=>d(`removeResource`),removeTool:()=>d(`removeTool`),start:async()=>{},stop:async()=>{}}},C=(e,t,n)=>{t.forEach(t=>{switch(t.protected&&e.use(t.path,p({authenticate:n})),t.type){case`static`:e.get(t.path,e=>e.body(t.content,{headers:{"Content-Type":t.contentType}}));break;case`dynamic`:e.get(t.path,e=>t.handler(e))}})},w=(e,t,n)=>({handler:()=>{let r=e(),i=t(),a=n(),o=ee(r,i,a);return new Response(o,{headers:{"Content-Type":`text/html; charset=utf-8`}})},path:`/dashboard`,protected:!0,type:`dynamic`}),ee=(e,t,n)=>`<!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>${e.name} - SomaMCP Dashboard</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: system-ui, -apple-system, sans-serif; background: #0a0a0a; color: #e0e0e0; padding: 2rem; }
10
+ h1 { font-size: 1.5rem; margin-bottom: 1.5rem; color: #fff; }
11
+ h2 { font-size: 1.1rem; margin-bottom: 0.75rem; color: #a0a0a0; text-transform: uppercase; letter-spacing: 0.05em; }
12
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; }
13
+ .card { background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 8px; padding: 1.25rem; }
14
+ .status { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85rem; font-weight: 600; }
15
+ .status.running { background: #0a3d0a; color: #4ade80; }
16
+ .status.stopped { background: #3d0a0a; color: #f87171; }
17
+ .status.error { background: #3d2a0a; color: #fbbf24; }
18
+ .status.connected { background: #0a3d0a; color: #4ade80; }
19
+ .status.disconnected { background: #3d0a0a; color: #f87171; }
20
+ .metric { margin: 0.5rem 0; }
21
+ .metric-label { color: #888; font-size: 0.85rem; }
22
+ .metric-value { color: #fff; font-weight: 500; }
23
+ ul { list-style: none; }
24
+ li { padding: 0.35rem 0; border-bottom: 1px solid #222; font-size: 0.9rem; }
25
+ li:last-child { border-bottom: none; }
26
+ .desc { color: #888; font-size: 0.8rem; }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <h1>${e.name}</h1>
31
+ <div class="grid">
32
+ <div class="card">
33
+ <h2>Health</h2>
34
+ <div class="metric"><span class="metric-label">Status:</span> <span class="status ${e.status}">${e.status}</span></div>
35
+ <div class="metric"><span class="metric-label">Uptime:</span> <span class="metric-value">${T(e.uptime)}</span></div>
36
+ <div class="metric"><span class="metric-label">Sessions:</span> <span class="metric-value">${e.activeSessions}</span></div>
37
+ <div class="metric"><span class="metric-label">Gateways:</span> <span class="metric-value">${e.gateways.connected}/${e.gateways.total}</span></div>
38
+ </div>
39
+ <div class="card">
40
+ <h2>Tools (${t.tools.length})</h2>
41
+ <ul>${t.tools.map(e=>`<li><strong>${e.name}</strong>${e.description?` <span class="desc">${e.description}</span>`:``}</li>`).join(``)}</ul>
42
+ </div>
43
+ <div class="card">
44
+ <h2>Resources (${t.resources.length})</h2>
45
+ <ul>${t.resources.map(e=>`<li><strong>${e.name}</strong> <span class="desc">${e.uri}</span></li>`).join(``)}</ul>
46
+ </div>
47
+ <div class="card">
48
+ <h2>Prompts (${t.prompts.length})</h2>
49
+ <ul>${t.prompts.map(e=>`<li><strong>${e.name}</strong>${e.description?` <span class="desc">${e.description}</span>`:``}</li>`).join(``)}</ul>
50
+ </div>
51
+ ${n.length>0?`<div class="card">
52
+ <h2>Gateways (${n.length})</h2>
53
+ <ul>${n.map(e=>`<li><strong>${e.name}</strong> <span class="status ${e.status}">${e.status}</span> <span class="desc">${e.url}</span></li>`).join(``)}</ul>
54
+ </div>`:``}
55
+ </div>
56
+ </body>
57
+ </html>`,T=e=>{let t=Math.floor(e/1e3),n=Math.floor(t/60),r=Math.floor(n/60),i=Math.floor(r/24);return i>0?`${String(i)}d ${String(r%24)}h`:r>0?`${String(r)}h ${String(n%60)}m`:n>0?`${String(n)}m ${String(t%60)}s`:`${String(t)}s`},E=`/health`,D=`/health/detail`,te=(e,t=E)=>({handler:()=>{let t=e(),n=t.status===`running`?200:503;return new Response(JSON.stringify({name:t.name,status:t.status}),{headers:{"Content-Type":`application/json`},status:n})},path:t,type:`dynamic`}),O=(e,t=D)=>({handler:()=>{let t=e(),n=t.status===`running`?200:503;return new Response(JSON.stringify(t),{headers:{"Content-Type":`application/json`},status:n})},path:t,protected:!0,type:`dynamic`}),k=`/info`,A=(e,t=k)=>({handler:()=>new Response(JSON.stringify(e()),{headers:{"Content-Type":`application/json`},status:200}),path:t,protected:!0,type:`dynamic`}),j=()=>n(globalThis.process),M=()=>j().flatMap(e=>n(e.versions?.node)).isEmpty===!1,N=e=>j().flatMap(t=>n(t.env?.[e])).filter(e=>e.length>0),P=()=>({branch:N(`SOMAMCP_BUILD_BRANCH`).orUndefined(),commit:N(`SOMAMCP_BUILD_COMMIT`).orUndefined(),date:N(`SOMAMCP_BUILD_DATE`).orUndefined(),environment:N(`SOMAMCP_ENVIRONMENT`).orUndefined()}),ne=e=>{let t=P();return{branch:e?.branch??t.branch,commit:e?.commit??t.commit,date:e?.date??t.date,environment:e?.environment??t.environment}},F=`unknown`,I=()=>M()?{arch:j().flatMap(e=>n(e.arch)).orElse(F),nodeVersion:j().flatMap(e=>n(e.version)).orElse(F),platform:j().flatMap(e=>n(e.platform)).orElse(F),runtime:`node`}:{arch:F,nodeVersion:F,platform:n(globalThis.navigator?.userAgent).orElse(F),runtime:`edge`},L=(e,t)=>{let i=r(n.none()),o=r(n.none()),s=r([]),u=r(`disconnected`),d=r(n.none()),f=e.name??e.id,p=()=>{let t=e.reconnectIntervalMs??5e3;d.set(n(setTimeout(()=>{m.connect()},t)))},m={get config(){return e},get info(){return{id:e.id,name:f,remoteTools:s.get().map(e=>({description:e.description,name:e.name})),status:u.get(),url:e.url}},get name(){return f},get status(){return u.get()},get tools(){return s.get()},async callTool(t,n={}){let r=i.get();if(r.isEmpty||u.get()!==`connected`)throw Error(`Gateway ${e.id} is not connected`);return r.orThrow(Error(`Gateway ${e.id} is not connected`)).callTool({arguments:n,name:t})},async connect(){if(u.get()===`connected`)return;u.set(`connecting`);let r=Date.now();(await a.fromPromise((async()=>{let t=new l(new URL(e.url));o.set(n(t));let r=new c({name:`soma-gateway-${e.id}`,version:`1.0.0`});i.set(n(r)),await r.connect(t);let a=await r.listTools();s.set(a.tools)})())).fold(n=>{u.set(`error`),t.recordEvent({data:{id:e.id,url:e.url},error:n instanceof Error?n.message:String(n),name:f,timestamp:r,type:`gateway.error`}),e.reconnect!==!1&&p()},()=>{u.set(`connected`),t.recordEvent({data:{id:e.id,remoteToolCount:s.get().length,url:e.url},durationMs:Date.now()-r,name:f,timestamp:r,type:`gateway.connect`})})},async disconnect(){d.get().match({None:()=>void 0,Some:e=>clearTimeout(e)}),d.set(n.none());let r=i.get();r.isSome()&&await a.fromPromise(r.value.close()),i.set(n.none()),o.set(n.none()),s.set([]),u.set(`disconnected`),t.recordEvent({data:{id:e.id},name:f,timestamp:Date.now(),type:`gateway.disconnect`})}};return m},R=e=>{let n=r(t.empty()),i=()=>[...n.get()].map(([,e])=>e);return{get connectedCount(){return i().filter(e=>e.status===`connected`).length},get totalCount(){return n.get().size},add(t){let r=L(t,e);return n.set(n.get().add(o([t.id,r]))),r},async connectAll(){await Promise.allSettled(i().map(e=>e.connect()))},async disconnectAll(){await Promise.allSettled(i().map(e=>e.disconnect()))},get(e){return n.get().get(e)},getAll(){return i()},getInfoAll(){return i().map(e=>e.info)}}},z=e=>{if(e instanceof Error){let t=e.message.toLowerCase();if(t.includes(`validation`)||t.includes(`invalid`)||t.includes(`schema`))return`validation`;if(t.includes(`timeout`)||t.includes(`timed out`)||t.includes(`etimedout`))return`timeout`;if(t.includes(`gateway`)||t.includes(`not connected`)||t.includes(`econnrefused`)||t.includes(`connection refused`))return`gateway`;if(t.includes(`auth`)||t.includes(`unauthorized`)||t.includes(`forbidden`))return`auth`;if(t.includes(`not found`)||t.includes(`404`))return`not_found`}return typeof e==`object`&&e&&`name`in e&&e.name===`UserError`?`validation`:`internal`},B=(e,t)=>{switch(e){case`validation`:return[`Check the tool's parameter schema and provide valid input`];case`timeout`:return[`The operation timed out. Try again or reduce the scope of the request`];case`gateway`:return[`Gateway is ${t?.gatewayStatus??`unavailable`}. The remote service may be down`,`Try again later or check the remote service status`];case`auth`:return[`Authentication failed. Check credentials or permissions`];case`not_found`:return[`The requested resource was not found. Verify the identifier`];case`internal`:return[`An internal error occurred. The tool maintainer should investigate`]}},V=(e,t)=>{let n=z(e),r=e instanceof Error?e.message:String(e),i=B(n,t),a={...t?.gatewayStatus&&{gatewayStatus:t.gatewayStatus},...t?.toolName&&{toolName:t.toolName}};return{errorCategory:n,message:r,...Object.keys(a).length>0&&{relatedState:a},suggestions:i}},H=e=>a.fromPromise(e),U=(e,t)=>{let n=e.config.toolPrefix??`${e.config.id}_`;return e.tools.map(r=>({annotations:{openWorldHint:!0,readOnlyHint:!0},description:`[${e.name}] ${r.description??`Remote tool`}`,execute:async i=>{let a=Date.now(),o=`${n}${r.name}`;return(await H(e.callTool(r.name,i))).fold(n=>{let s=V(n,{gatewayStatus:e.status,toolName:r.name});return t?.recordEvent({data:{gateway:e.config.id,remoteTool:r.name},durationMs:Date.now()-a,error:s.message,errorCategory:s.errorCategory,errorStack:n.stack,input:i,name:o,timestamp:a,type:`tool.error`}),{content:[{text:JSON.stringify(s,null,2),type:`text`}],isError:!0}},n=>(t?.recordEvent({data:{gateway:e.config.id,remoteTool:r.name},durationMs:Date.now()-a,input:i,name:o,output:n,timestamp:a,type:`tool.execute`}),JSON.stringify(n,null,2)))},name:`${n}${r.name}`}))},W=(e,t=`info`)=>({annotations:{readOnlyHint:!0},description:`Returns identity and build information for this server (name, version, build commit, runtime, capability counts)`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:t}),G=e=>({flush:()=>Promise.resolve(),recordEvent:t=>{let n={durationMs:t.durationMs,eventName:t.name,timestamp:new Date(t.timestamp).toISOString(),type:t.type,...t.sessionId&&{sessionId:t.sessionId},...t.requestId&&{requestId:t.requestId},...t.errorCategory&&{errorCategory:t.errorCategory},...t.data};t.error?e.withError(Error(t.error)).error(t.type,n):e.info(t.type,n)}}),K={flush:()=>Promise.resolve(),recordEvent:()=>{}},q=(e,t)=>!t||t.length===0?e:Object.fromEntries(Object.entries(e).map(([e,n])=>[e,t.includes(e)?`[REDACTED]`:n])),re=(e,t=1e4)=>{let n=typeof e==`string`?e:JSON.stringify(e);return n&&n.length>t?`${n.slice(0,t)}...[truncated, ${n.length} total chars]`:e},J=e=>a.fromPromise(e),Y=(e,t,n)=>({...e,execute:async(r,i)=>{let a=n?.captureLevel??`full`;if(a===`none`)return e.execute(r,i);let o=Date.now(),{requestId:s,sessionId:c}=i,l=a===`full`?q(r,n?.redactInputFields):void 0;return(await J(e.execute(r,i))).fold(n=>{let r=V(n,{toolName:e.name});return t.recordEvent({data:{name:e.name},durationMs:Date.now()-o,error:r.message,errorCategory:r.errorCategory,errorStack:n.stack,input:l,name:e.name,requestId:s,sessionId:c,timestamp:o,type:`tool.error`}),{content:[{text:JSON.stringify(r,null,2),type:`text`}],isError:!0}},r=>(t.recordEvent({data:{name:e.name},durationMs:Date.now()-o,input:l,name:e.name,output:a===`full`?re(r,n?.maxOutputSize):void 0,requestId:s,sessionId:c,timestamp:o,type:`tool.execute`}),r))}}),X=(e,t)=>({...e,load:async n=>{let r=Date.now();return(await J(e.load(n))).fold(n=>{let i=V(n);throw t.recordEvent({data:{name:e.name,uri:e.uri},durationMs:Date.now()-r,error:i.message,errorCategory:i.errorCategory,errorStack:n.stack,name:e.name,timestamp:r,type:`resource.error`}),n},n=>(t.recordEvent({data:{name:e.name,uri:e.uri},durationMs:Date.now()-r,name:e.name,timestamp:r,type:`resource.load`}),n))}}),Z=(e,t)=>({...e,load:async(n,r)=>{let i=Date.now();return(await J(e.load(n,r))).fold(n=>{let r=V(n);throw t.recordEvent({data:{name:e.name},durationMs:Date.now()-i,error:r.message,errorCategory:r.errorCategory,errorStack:n.stack,name:e.name,timestamp:i,type:`prompt.error`}),n},n=>(t.recordEvent({data:{name:e.name},durationMs:Date.now()-i,name:e.name,timestamp:i,type:`prompt.load`}),n))}}),ie=e=>{let{artifacts:t,backend:i,backendOptions:a,build:o,enableDashboard:s,enableHealthEndpoint:c,enableInfoEndpoint:l,enableIntrospection:u,gateways:d,healthDetailPath:f,healthPath:m,infoPath:h,introspectionPrefix:g,logLayer:_,telemetry:v,...y}=e,b=y.name,x=y.version,S=ne(o),ee=I(),T=g??``,E=v??(_?G(_):K),D=i(y,a),k=R(E),j=[],M=[],N=[],P=r(0);d?.forEach(e=>k.add(e));let F=()=>{let e=P.get(),t=e>0?Date.now()-e:0;return{activeSessions:D.sessions.length,gateways:{connected:k.connectedCount,total:k.totalCount},name:b,startedAt:e,status:D.serverState,uptime:t}},L=()=>({prompts:N,resources:M,tools:j}),z=()=>({build:S,capabilities:{prompts:N.length,resources:M.length,tools:j.length},name:b,runtime:ee,version:x});u!==!1&&D.addTool(W(()=>z(),`${T}info`));let B=[...t??[]];c!==!1&&(B.push(te(()=>F(),m)),B.push(O(()=>F(),f))),l!==!1&&B.push(A(()=>z(),h)),s!==!1&&B.push(w(()=>F(),()=>L(),()=>k.getInfoAll())),B.length>0&&C(D.getApp(),B,n(y.authenticate).orUndefined()),D.on(`connect`,({session:e})=>{E.recordEvent({data:{sessionId:e.sessionId},name:b,timestamp:Date.now(),type:`session.connect`})}),D.on(`disconnect`,({session:e})=>{E.recordEvent({data:{sessionId:e.sessionId},name:b,timestamp:Date.now(),type:`session.disconnect`})});let V=e=>{let t=`captureConfig`in e?e.captureConfig:void 0,n=Y(e,E,t);D.addTool(n),j.push({description:e.description,name:e.name})},H=e=>{let t=X(e,E);D.addResource(t),M.push({description:e.description,name:e.name,uri:e.uri})},q=e=>{let t=Z(e,E);D.addPrompt(t),N.push({description:e.description,name:e.name})};return{get name(){return b},get serverState(){return D.serverState},get sessions(){return D.sessions},addPrompt:q,addPrompts:e=>{e.forEach(q)},addResource:H,addResources:e=>{e.forEach(H)},addResourceTemplate:(...e)=>{D.addResourceTemplate(...e)},addRoute:e=>{let t=D.getApp(),r=Array.isArray(e.method)?[...e.method]:[e.method];if(e.protected){let r=n(y.authenticate).orUndefined();t.use(e.path,p({authenticate:r,onUnauthorized:e.onUnauthorized}))}t.on(r,e.path,t=>e.handler(t))},addTool:V,addTools:e=>{e.forEach(e=>V(e))},fetch:async e=>D.fetch?D.fetch(e):D.getApp().fetch(e),getApp:()=>D.getApp(),getCapabilities:L,getGatewayManager:()=>k,getHealth:F,getInfo:z,removePrompt:e=>{D.removePrompt(e)},removeResource:e=>{D.removeResource(e)},removeTool:e=>{D.removeTool(e)},async start(e){P.set(Date.now()),E.recordEvent({name:b,timestamp:P.get(),type:`server.start`}),await D.start(e),await k.connectAll(),k.getAll().filter(e=>e.status===`connected`&&e.config.proxyTools!==!1).forEach(e=>{U(e,E).forEach(e=>D.addTool(e))})},async stop(){await k.disconnectAll(),await D.stop(),E.recordEvent({name:b,timestamp:Date.now(),type:`server.stop`}),E.flush&&await E.flush()}}};var ae=class extends Error{constructor(e){super(e),this.name=`UserError`}};const oe=e=>({flush:async()=>{await Promise.all(e.map(e=>e.flush?e.flush():Promise.resolve()))},recordEvent:t=>{e.forEach(e=>e.recordEvent(t))}}),se=(e=`[soma]`)=>{let t=u({prefix:e,stream:`stderr`});return G(t)},ce=e=>({annotations:{readOnlyHint:!0},description:`Lists all tools, resources, and prompts registered on this server`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:`soma_capabilities`}),le=e=>({annotations:{readOnlyHint:!0},description:`Lists all gateway connections to remote servers with their status and available tools`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:`soma_connections`}),ue=e=>({annotations:{readOnlyHint:!0},description:`Returns health and status information for this server including uptime, sessions, and gateway status`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:`soma_health`}),Q=[{description:`GitHub personal access token`,name:`github_pat`,pattern:/\b(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{20,}\b/g},{description:`AWS access key ID`,name:`aws_access_key`,pattern:/\bAKIA[0-9A-Z]{16}\b/g},{description:`AWS secret access key (heuristic)`,name:`aws_secret_key`,pattern:/(?<![A-Za-z0-9/+])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+])/g},{description:`Stripe secret/publishable key`,name:`stripe_key`,pattern:/\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{20,}\b/g},{description:`Slack token`,name:`slack_token`,pattern:/\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g},{description:`JSON Web Token`,name:`jwt`,pattern:/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+\b/g},{description:`OpenAI / Anthropic style API key`,name:`ai_api_key`,pattern:/\b(?:sk-ant-|sk-)[A-Za-z0-9_-]{20,}\b/g},{description:`Generic bearer-style secret prefix`,name:`bearer_authorization`,pattern:/\b[Bb]earer\s+[A-Za-z0-9._\-+/]{16,}\b/g},{description:`Email address`,name:`email`,pattern:/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g},{description:`Private IPv4 address (RFC1918)`,name:`private_ipv4`,pattern:/\b(?:10(?:\.\d{1,3}){3}|192\.168(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2})\b/g},{description:`URL pointing at internal/private hostname`,name:`internal_url`,pattern:/\bhttps?:\/\/(?:[a-z0-9-]+\.)*(?:internal|intranet|corp|local|lan)(?:[/:][^\s]*)?/gi}],$=(e,n=Q)=>{let{counts:r,text:i}=n.reduce((e,t)=>{let n=Array.from(e.text.matchAll(t.pattern)).length;return n===0?e:{counts:e.counts.add(o([t.name,n])),text:e.text.replace(t.pattern,t.replacement??`[REDACTED]`)}},{counts:t.empty(),text:e}),a=[...r].map(([e,t])=>({count:t,name:e}));return{matches:a,redacted:a.length>0,text:i}},de=[`Submit a feedback report or issue to this server's maintainers.`,``,`IMPORTANT: Treat all input as potentially public.`,`- Do NOT include API keys, tokens, passwords, or credentials`,`- Do NOT include customer data, PII, or proprietary information`,`- Do NOT include internal URLs, hostnames, or system topology`,`- Describe the behavior and how to reproduce it, not the data involved`,``,`Common credential patterns are automatically redacted before submission,`,`but redaction is best-effort and is not a substitute for caution.`].join(`
58
+ `),fe=d.object({description:d.string().min(10,`Provide enough context to reproduce (>=10 chars)`).max(8e3).describe(`Detailed description. NO secrets, customer data, or internal URLs.`),severity:d.enum([`low`,`medium`,`high`]).optional().describe(`Severity hint (optional)`),title:d.string().min(5,`Title must be at least 5 characters`).max(200).describe(`Brief, public-safe summary`),type:d.enum([`bug`,`feature`,`feedback`,`question`]).default(`feedback`).describe(`What kind of report this is`)}),pe=(e,r)=>{if(!e.redacted&&!r.redacted)return n.none();let i=[...[...e.matches,...r.matches].reduce((e,t)=>e.add(o([t.name,e.get(t.name).orElse(0)+t.count])),t.empty())].map(([e,t])=>`- ${e}: ${String(t)}`);return n([`⚠️ Automatic redactions applied:`,...i].join(`
59
+ `))},me=e=>e.filter(e=>Object.keys(e).length>0).map(e=>[`---`,`**Context (auto-collected):**`,"```json",JSON.stringify(e,null,2),"```"].join(`
60
+ `)),he=(e,t,n,r,i)=>[`**Type:** ${t}`,...n.map(e=>`**Severity:** ${e}`).toArray(),``,e,...r.map(e=>[``,e]).toArray().flat(),...i.map(e=>[``,e]).toArray().flat()].join(`
61
+ `),ge=e=>{let t=e.redactionPatterns??Q;return{annotations:{destructiveHint:!1,readOnlyHint:!1},description:e.description??de,execute:async r=>{let i=$(r.title,t),a=$(r.description,t),o=e.enrichment?n(await e.enrichment({severity:r.severity,type:r.type})):n.none(),s=pe(i,a),c=me(o),l=he(a.text,r.type,n(r.severity),s,c),u=[r.type,...r.severity?[`severity:${r.severity}`]:[],...e.extraLabels??[]],d=await e.provider.submit({body:l,labels:u,title:i.text});return d.success?JSON.stringify({id:d.id,provider:e.provider.name,redacted:s.isSome(),redactionDetails:[...i.matches,...a.matches],success:!0,url:d.url},null,2):JSON.stringify({error:d.error,redacted:s.isSome(),success:!1},null,2)},name:e.name??`report_feedback`,parameters:fe}},_e=e=>{let t=e.baseUrl??`https://api.github.com`,r=e.fetchImpl??fetch;return{name:`github`,submit:async o=>{let s=e.getToken();if(s.isEmpty)return{error:`No GitHub token configured`,success:!1};let c=s.orThrow(Error(`unreachable`)),l=[...e.defaultLabels??[],...o.labels],u=JSON.stringify({body:o.body,labels:i(l).toArray(),title:o.title});return(await a.fromPromise(r(`${t}/repos/${e.repo}/issues`,{body:u,headers:{Accept:`application/vnd.github+json`,Authorization:`Bearer ${c}`,"Content-Type":`application/json`,"X-GitHub-Api-Version":`2022-11-28`},method:`POST`}))).foldAsync(e=>({error:e instanceof Error?e.message:`Unknown error submitting to GitHub`,success:!1}),async e=>{let t=await e.json().catch(()=>({}));return e.ok?{id:n(t.number).fold(()=>void 0,e=>String(e)),success:!0,url:t.html_url}:{error:`GitHub API ${String(e.status)}: ${t.message??e.statusText}`,success:!1}})}}},ve=e=>{let t=e.fetchImpl??fetch,n=e.transform??(e=>e);return{name:`webhook`,submit:async r=>(await a.fromPromise(t(e.url,{body:JSON.stringify(n(r)),headers:{"Content-Type":`application/json`,...e.headers??{}},method:`POST`}))).foldAsync(e=>({error:e instanceof Error?e.message:`Unknown error posting to webhook`,success:!1}),async e=>{if(!e.ok){let t=await e.text().catch(()=>``);return{error:`Webhook ${String(e.status)}: ${t||e.statusText}`,success:!1}}return{success:!0}})}},ye=e=>ie({...e,backend:e.backend??S});export{D as DEFAULT_HEALTH_DETAIL_PATH,E as DEFAULT_HEALTH_PATH,k as DEFAULT_INFO_PATH,Q as DEFAULT_REDACTION_PATTERNS,K as NoopTelemetry,ae as UserError,z as classifyError,p as createAuthMiddleware,ce as createCapabilitiesTool,oe as createCompositeTelemetry,le as createConnectionsTool,se as createConsoleTelemetry,w as createDashboardArtifact,S as createEdgeBackend,V as createEnrichedError,ge as createFeedbackTool,L as createGateway,R as createGatewayManager,_e as createGithubFeedback,te as createHealthArtifact,O as createHealthDetailArtifact,ue as createHealthTool,A as createInfoArtifact,W as createInfoTool,U as createProxiedTools,ye as createServer,ie as createServerCore,ve as createWebhookFeedback,v as getRequestHeader,I as getRuntimeInfo,P as readBuildInfoFromEnv,$ as redact,C as registerArtifacts,ne as resolveBuildInfo,Z as wrapPrompt,X as wrapResource,Y as wrapTool};
@@ -218,6 +218,12 @@ type BackendAdapter<T extends SessionAuth = SessionAuth> = {
218
218
  addResource: (resource: Resource<T>) => void;
219
219
  addResourceTemplate: (...args: ReadonlyArray<unknown>) => void;
220
220
  addTool: <P extends SchemaParams>(tool: Tool<T, P>) => void;
221
+ /**
222
+ * Handle a web-standard Request. Optional: when absent, callers fall back to
223
+ * `getApp().fetch`. Edge backends implement this as their primary entry point;
224
+ * Node backends provide it for parity so `SomaServerInstance.fetch` always works.
225
+ */
226
+ fetch?: (request: Request) => Promise<Response>;
221
227
  getApp: () => Hono;
222
228
  on: <E extends keyof BackendEvents<T>>(event: E, handler: BackendEvents<T>[E]) => void;
223
229
  removePrompt: (name: string) => void;
@@ -226,12 +232,7 @@ type BackendAdapter<T extends SessionAuth = SessionAuth> = {
226
232
  start: (transport?: TransportConfig) => Promise<void>;
227
233
  stop: () => Promise<void>;
228
234
  };
229
- type BackendFactory<T extends SessionAuth = SessionAuth> = (config: {
230
- authenticate?: (request: unknown) => Promise<T>;
231
- instructions?: string;
232
- name: string;
233
- version: `${number}.${number}.${number}`;
234
- }, backendOptions?: Record<string, unknown>) => BackendAdapter<T>;
235
+ type BackendFactory<T extends SessionAuth = SessionAuth> = (config: ServerConfig<T>, backendOptions?: Record<string, unknown>) => BackendAdapter<T>;
235
236
  //#endregion
236
237
  //#region src/backend/fastmcp.d.ts
237
238
  declare const createFastMCPBackend: <T extends SessionAuth = SessionAuth>(config: ServerConfig<T>, backendOptions?: Record<string, unknown>) => BackendAdapter<T>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as ToolAnnotations, C as ResourceLink, D as SessionAuth, E as ServerStatus, O as TextContent, S as ResourceContent, T as SchemaParams, _ as Progress, a as BackendSession, b as PromptResult, c as ServerConfig, d as Completion, f as Content, g as InferSchemaOutput, h as ImageContent, i as BackendFactory, j as UserError, k as Tool, l as TransportConfig, m as Context, n as BackendAdapter, o as HttpStreamConfig, p as ContentResult, s as Logger, t as createFastMCPBackend, u as AudioContent, v as Prompt, w as ResourceResult, x as Resource, y as PromptArgument } from "./index-DizthJMe.js";
1
+ import { A as ToolAnnotations, C as ResourceLink, D as SessionAuth, E as ServerStatus, O as TextContent, S as ResourceContent, T as SchemaParams, _ as Progress, a as BackendSession, b as PromptResult, c as ServerConfig, d as Completion, f as Content, g as InferSchemaOutput, h as ImageContent, i as BackendFactory, j as UserError, k as Tool, l as TransportConfig, m as Context, n as BackendAdapter, o as HttpStreamConfig, p as ContentResult, s as Logger, t as createFastMCPBackend, u as AudioContent, v as Prompt, w as ResourceResult, x as Resource, y as PromptArgument } from "./index-BRNcy_4O.js";
2
2
  import { Option } from "functype";
3
3
  import { DirectLogger, DirectLogger as DirectLogger$1 } from "functype-log";
4
4
  import { z } from "zod";
@@ -38,6 +38,8 @@ type RuntimeInfo = {
38
38
  arch: string;
39
39
  nodeVersion: string;
40
40
  platform: string;
41
+ /** Which runtime family reported the fields above. Absent on pre-1.2 builds. */
42
+ runtime?: "edge" | "node";
41
43
  };
42
44
  declare const readBuildInfoFromEnv: () => BuildInfo;
43
45
  declare const resolveBuildInfo: (override?: BuildInfo) => BuildInfo;
@@ -170,6 +172,12 @@ type RouteConfig = {
170
172
  //#region src/types.d.ts
171
173
  type SomaServerOptions<T extends SessionAuth = SessionAuth> = ServerConfig<T> & {
172
174
  artifacts?: ArtifactConfig[];
175
+ /**
176
+ * Backend implementation. Defaults to `createFastMCPBackend` (Node).
177
+ * Pass `createEdgeBackend` from `somamcp/edge` to target Cloudflare Workers,
178
+ * Deno Deploy, or Bun.
179
+ */
180
+ backend?: BackendFactory<T>;
173
181
  backendOptions?: Record<string, unknown>;
174
182
  build?: BuildInfo;
175
183
  enableDashboard?: boolean;
@@ -237,6 +245,12 @@ type SomaServerInstance<T extends SessionAuth = SessionAuth> = {
237
245
  addRoute: (route: RouteConfig) => void;
238
246
  addTool: <P extends SchemaParams>(tool: Tool<T, P>) => void;
239
247
  addTools: <P extends SchemaParams>(tools: Tool<T, P>[]) => void;
248
+ /**
249
+ * Web-standard request handler covering both MCP calls and registered
250
+ * artifacts/routes. This is the entry point for edge runtimes:
251
+ * `export default { fetch: (req) => server.fetch(req) }`.
252
+ */
253
+ fetch: (request: Request) => Promise<Response>;
240
254
  getApp: () => Hono;
241
255
  getCapabilities: () => ServerCapabilities;
242
256
  getGatewayManager: () => GatewayManagerInstance;
@@ -250,7 +264,17 @@ type SomaServerInstance<T extends SessionAuth = SessionAuth> = {
250
264
  };
251
265
  //#endregion
252
266
  //#region src/Server.d.ts
253
- declare const createServer: <T extends SessionAuth = SessionAuth>(options: SomaServerOptions<T>) => SomaServerInstance<T>;
267
+ /**
268
+ * Backend-agnostic server core.
269
+ *
270
+ * `backend` is required here on purpose. Defaulting it would mean a static import
271
+ * of the Node FastMCP backend, which lands in every bundle that reaches this
272
+ * module — including `somamcp/edge`. The entry barrels supply the default instead:
273
+ * `somamcp` binds createFastMCPBackend, `somamcp/edge` binds createEdgeBackend.
274
+ */
275
+ declare const createServerCore: <T extends SessionAuth = SessionAuth>(options: SomaServerOptions<T> & {
276
+ backend: BackendFactory<T>;
277
+ }) => SomaServerInstance<T>;
254
278
  //#endregion
255
279
  //#region src/content/helpers.d.ts
256
280
  type ContentInput = {
@@ -443,4 +467,8 @@ declare const createGatewayManager: (telemetry: TelemetryCollector) => GatewayMa
443
467
  //#region src/gateway/toolProxy.d.ts
444
468
  declare const createProxiedTools: <T extends SessionAuth>(gateway: GatewayInstance, telemetry?: TelemetryCollector) => ReadonlyArray<Tool<T>>;
445
469
  //#endregion
446
- export { type ArtifactAuthenticate, type ArtifactConfig, type AudioContent, type AuthMiddlewareConfig, type Authenticate, type BackendAdapter, type BackendFactory, type BackendSession, type BuildInfo, type CapabilitiesSummary, type CaptureLevel, type Completion, type Content, type ContentResult, type Context, DEFAULT_HEALTH_DETAIL_PATH, DEFAULT_HEALTH_PATH, DEFAULT_INFO_PATH, DEFAULT_REDACTION_PATTERNS, type DirectLogger, type DirectoryArtifact, type DynamicArtifact, type EnrichedErrorResponse, type ErrorCategory, type FeedbackEnrichmentContext, type FeedbackProvider, type FeedbackSeverity, type FeedbackSubmitFailure, type FeedbackSubmitResult, type FeedbackSubmitSuccess, type FeedbackToolOptions, type FeedbackType, type GatewayConfig, type GatewayInfo, type GatewayInstance, type GatewayManagerInstance, type GatewayStatus, type GithubFeedbackOptions, type HttpStreamConfig, type ImageContent, type InferSchemaOutput, type JsonFileTelemetryOptions, type Logger, NoopTelemetry, type NormalizedFeedback, type OnUnauthorized, type Progress, type Prompt, type PromptArgument, type PromptResult, type RedactionPattern, type RedactionResult, type Resource, type ResourceContent, type ResourceLink, type ResourceResult, type RouteConfig, type RouteMethod, type RuntimeInfo, type SchemaParams, type ServerCapabilities, type ServerConfig, type ServerHealth, type ServerInfo, type ServerStatus, type SessionAuth, type SomaServerInstance, type SomaServerOptions, type StaticArtifact, type TelemetryCollector, type TelemetryEvent, type TelemetryEventType, type TextContent, type Tool, type ToolAnnotations, type ToolCaptureConfig, type ToolOptions, type TransportConfig, UserError, type WebhookFeedbackOptions, audioContent, classifyError, createAuthMiddleware, createCapabilitiesTool, createCompositeTelemetry, createConnectionsTool, createConsoleTelemetry, createDashboardArtifact, createDefaultLogger, createEnrichedError, createFastMCPBackend, createFeedbackTool, createGateway, createGatewayManager, createGithubFeedback, createHealthArtifact, createHealthDetailArtifact, createHealthTool, createInfoArtifact, createInfoTool, createJsonFileTelemetry, createLogLayerTelemetry, createProxiedTools, createServer, createWebhookFeedback, getRequestHeader, getRuntimeInfo, imageContent, readBuildInfoFromEnv, redact, registerArtifacts, resolveBuildInfo, wrapPrompt, wrapResource, wrapTool };
470
+ //#region src/index.d.ts
471
+ /** Create a server backed by FastMCP (Node). For edge runtimes, use `somamcp/edge`. */
472
+ declare const createServer: <T extends SessionAuth = SessionAuth>(options: SomaServerOptions<T>) => SomaServerInstance<T>;
473
+ //#endregion
474
+ export { type ArtifactAuthenticate, type ArtifactConfig, type AudioContent, type AuthMiddlewareConfig, type Authenticate, type BackendAdapter, type BackendFactory, type BackendSession, type BuildInfo, type CapabilitiesSummary, type CaptureLevel, type Completion, type Content, type ContentResult, type Context, DEFAULT_HEALTH_DETAIL_PATH, DEFAULT_HEALTH_PATH, DEFAULT_INFO_PATH, DEFAULT_REDACTION_PATTERNS, type DirectLogger, type DirectoryArtifact, type DynamicArtifact, type EnrichedErrorResponse, type ErrorCategory, type FeedbackEnrichmentContext, type FeedbackProvider, type FeedbackSeverity, type FeedbackSubmitFailure, type FeedbackSubmitResult, type FeedbackSubmitSuccess, type FeedbackToolOptions, type FeedbackType, type GatewayConfig, type GatewayInfo, type GatewayInstance, type GatewayManagerInstance, type GatewayStatus, type GithubFeedbackOptions, type HttpStreamConfig, type ImageContent, type InferSchemaOutput, type JsonFileTelemetryOptions, type Logger, NoopTelemetry, type NormalizedFeedback, type OnUnauthorized, type Progress, type Prompt, type PromptArgument, type PromptResult, type RedactionPattern, type RedactionResult, type Resource, type ResourceContent, type ResourceLink, type ResourceResult, type RouteConfig, type RouteMethod, type RuntimeInfo, type SchemaParams, type ServerCapabilities, type ServerConfig, type ServerHealth, type ServerInfo, type ServerStatus, type SessionAuth, type SomaServerInstance, type SomaServerOptions, type StaticArtifact, type TelemetryCollector, type TelemetryEvent, type TelemetryEventType, type TextContent, type Tool, type ToolAnnotations, type ToolCaptureConfig, type ToolOptions, type TransportConfig, UserError, type WebhookFeedbackOptions, audioContent, classifyError, createAuthMiddleware, createCapabilitiesTool, createCompositeTelemetry, createConnectionsTool, createConsoleTelemetry, createDashboardArtifact, createDefaultLogger, createEnrichedError, createFastMCPBackend, createFeedbackTool, createGateway, createGatewayManager, createGithubFeedback, createHealthArtifact, createHealthDetailArtifact, createHealthTool, createInfoArtifact, createInfoTool, createJsonFileTelemetry, createLogLayerTelemetry, createProxiedTools, createServer, createServerCore, createWebhookFeedback, getRequestHeader, getRuntimeInfo, imageContent, readBuildInfoFromEnv, redact, registerArtifacts, resolveBuildInfo, wrapPrompt, wrapResource, wrapTool };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import{t as e}from"./backend-Bm6e2u2K.js";import{Map as t,Option as n,Ref as r,Set as i,Try as a,Tuple as o}from"functype";import{Client as s}from"@modelcontextprotocol/sdk/client/index.js";import{StreamableHTTPClientTransport as c}from"@modelcontextprotocol/sdk/client/streamableHttp.js";import{createDirectConsoleLogger as l}from"functype-log";import{appendFile as u,mkdir as d,readFile as f}from"node:fs/promises";import{dirname as p,extname as m}from"node:path";import{z as h}from"zod";const g=e=>e.json({error:`Unauthorized`},401),_=({authenticate:e,onUnauthorized:t})=>{let r=t??g;return async(t,i)=>{if(!e)return r(t);let o=n(t.env).flatMap(e=>n(e.incoming)).orElse(t.req.raw);if((await a.fromPromise(e(o))).isFailure())return r(t);await i()}},v=(e,t)=>typeof e.get==`function`?n(e.get(t)):n.none(),y=(e,t,r)=>{let i=e;return n(i[t]??i[r]).map(e=>Array.isArray(e)?e[0]??``:e)},b=(e,t,r)=>v(e,t).fold(()=>y(e,t,r),e=>n(e)),x=e=>n(e).flatMap(e=>n(e.headers)),S=(e,t)=>{let n=t.toLowerCase();return x(e).flatMap(e=>b(e,n,t)).orUndefined()},C=(e,t,n)=>{t.forEach(t=>{switch(t.protected&&e.use(t.path,_({authenticate:n})),t.type){case`static`:e.get(t.path,e=>e.body(t.content,{headers:{"Content-Type":t.contentType}}));break;case`dynamic`:e.get(t.path,e=>t.handler(e))}})},w=(e,t,n)=>({handler:()=>{let r=e(),i=t(),a=n(),o=T(r,i,a);return new Response(o,{headers:{"Content-Type":`text/html; charset=utf-8`}})},path:`/dashboard`,protected:!0,type:`dynamic`}),T=(e,t,n)=>`<!DOCTYPE html>
1
+ import{t as e}from"./backend-CA7j4YCF.js";import{Map as t,Option as n,Ref as r,Set as i,Try as a,Tuple as o}from"functype";import{Client as s}from"@modelcontextprotocol/sdk/client/index.js";import{StreamableHTTPClientTransport as c}from"@modelcontextprotocol/sdk/client/streamableHttp.js";import{appendFile as l,mkdir as u,readFile as d}from"node:fs/promises";import{dirname as f,extname as p}from"node:path";import{createDirectConsoleLogger as m}from"functype-log";import{z as h}from"zod";const g=e=>e.json({error:`Unauthorized`},401),_=({authenticate:e,onUnauthorized:t})=>{let r=t??g;return async(t,i)=>{if(!e)return r(t);let o=n(t.env).flatMap(e=>n(e.incoming)).orElse(t.req.raw);if((await a.fromPromise(e(o))).isFailure())return r(t);await i()}},v=(e,t)=>typeof e.get==`function`?n(e.get(t)):n.none(),y=(e,t,r)=>{let i=e;return n(i[t]??i[r]).map(e=>Array.isArray(e)?e[0]??``:e)},b=(e,t,r)=>v(e,t).fold(()=>y(e,t,r),e=>n(e)),x=e=>n(e).flatMap(e=>n(e.headers)),S=(e,t)=>{let n=t.toLowerCase();return x(e).flatMap(e=>b(e,n,t)).orUndefined()},ee=(e,t,n)=>{t.forEach(t=>{switch(t.protected&&e.use(t.path,_({authenticate:n})),t.type){case`static`:e.get(t.path,e=>e.body(t.content,{headers:{"Content-Type":t.contentType}}));break;case`dynamic`:e.get(t.path,e=>t.handler(e))}})},C=(e,t,n)=>({handler:()=>{let r=e(),i=t(),a=n(),o=w(r,i,a);return new Response(o,{headers:{"Content-Type":`text/html; charset=utf-8`}})},path:`/dashboard`,protected:!0,type:`dynamic`}),w=(e,t,n)=>`<!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
@@ -32,7 +32,7 @@ import{t as e}from"./backend-Bm6e2u2K.js";import{Map as t,Option as n,Ref as r,S
32
32
  <div class="card">
33
33
  <h2>Health</h2>
34
34
  <div class="metric"><span class="metric-label">Status:</span> <span class="status ${e.status}">${e.status}</span></div>
35
- <div class="metric"><span class="metric-label">Uptime:</span> <span class="metric-value">${E(e.uptime)}</span></div>
35
+ <div class="metric"><span class="metric-label">Uptime:</span> <span class="metric-value">${T(e.uptime)}</span></div>
36
36
  <div class="metric"><span class="metric-label">Sessions:</span> <span class="metric-value">${e.activeSessions}</span></div>
37
37
  <div class="metric"><span class="metric-label">Gateways:</span> <span class="metric-value">${e.gateways.connected}/${e.gateways.total}</span></div>
38
38
  </div>
@@ -54,9 +54,9 @@ import{t as e}from"./backend-Bm6e2u2K.js";import{Map as t,Option as n,Ref as r,S
54
54
  </div>`:``}
55
55
  </div>
56
56
  </body>
57
- </html>`,E=e=>{let t=Math.floor(e/1e3),n=Math.floor(t/60),r=Math.floor(n/60),i=Math.floor(r/24);return i>0?`${String(i)}d ${String(r%24)}h`:r>0?`${String(r)}h ${String(n%60)}m`:n>0?`${String(n)}m ${String(t%60)}s`:`${String(t)}s`},D=`/health`,O=`/health/detail`,ee=(e,t=D)=>({handler:()=>{let t=e(),n=t.status===`running`?200:503;return new Response(JSON.stringify({name:t.name,status:t.status}),{headers:{"Content-Type":`application/json`},status:n})},path:t,type:`dynamic`}),k=(e,t=O)=>({handler:()=>{let t=e(),n=t.status===`running`?200:503;return new Response(JSON.stringify(t),{headers:{"Content-Type":`application/json`},status:n})},path:t,protected:!0,type:`dynamic`}),A=`/info`,j=(e,t=A)=>({handler:()=>new Response(JSON.stringify(e()),{headers:{"Content-Type":`application/json`},status:200}),path:t,protected:!0,type:`dynamic`}),M=e=>n(process.env[e]).filter(e=>e.length>0),N=()=>({branch:M(`SOMAMCP_BUILD_BRANCH`).orUndefined(),commit:M(`SOMAMCP_BUILD_COMMIT`).orUndefined(),date:M(`SOMAMCP_BUILD_DATE`).orUndefined(),environment:M(`SOMAMCP_ENVIRONMENT`).orUndefined()}),P=e=>{let t=N();return{branch:e?.branch??t.branch,commit:e?.commit??t.commit,date:e?.date??t.date,environment:e?.environment??t.environment}},F=()=>({arch:process.arch,nodeVersion:process.version,platform:process.platform}),I=(e,t)=>{let i=r(n.none()),o=r(n.none()),l=r([]),u=r(`disconnected`),d=r(n.none()),f=e.name??e.id,p=()=>{let t=e.reconnectIntervalMs??5e3;d.set(n(setTimeout(()=>{m.connect()},t)))},m={get config(){return e},get info(){return{id:e.id,name:f,remoteTools:l.get().map(e=>({description:e.description,name:e.name})),status:u.get(),url:e.url}},get name(){return f},get status(){return u.get()},get tools(){return l.get()},async callTool(t,n={}){let r=i.get();if(r.isEmpty||u.get()!==`connected`)throw Error(`Gateway ${e.id} is not connected`);return r.orThrow(Error(`Gateway ${e.id} is not connected`)).callTool({arguments:n,name:t})},async connect(){if(u.get()===`connected`)return;u.set(`connecting`);let r=Date.now();(await a.fromPromise((async()=>{let t=new c(new URL(e.url));o.set(n(t));let r=new s({name:`soma-gateway-${e.id}`,version:`1.0.0`});i.set(n(r)),await r.connect(t);let a=await r.listTools();l.set(a.tools)})())).fold(n=>{u.set(`error`),t.recordEvent({data:{id:e.id,url:e.url},error:n instanceof Error?n.message:String(n),name:f,timestamp:r,type:`gateway.error`}),e.reconnect!==!1&&p()},()=>{u.set(`connected`),t.recordEvent({data:{id:e.id,remoteToolCount:l.get().length,url:e.url},durationMs:Date.now()-r,name:f,timestamp:r,type:`gateway.connect`})})},async disconnect(){d.get().match({None:()=>void 0,Some:e=>clearTimeout(e)}),d.set(n.none());let r=i.get();r.isSome()&&await a.fromPromise(r.value.close()),i.set(n.none()),o.set(n.none()),l.set([]),u.set(`disconnected`),t.recordEvent({data:{id:e.id},name:f,timestamp:Date.now(),type:`gateway.disconnect`})}};return m},L=e=>{let n=r(t.empty()),i=()=>[...n.get()].map(([,e])=>e);return{get connectedCount(){return i().filter(e=>e.status===`connected`).length},get totalCount(){return n.get().size},add(t){let r=I(t,e);return n.set(n.get().add(o([t.id,r]))),r},async connectAll(){await Promise.allSettled(i().map(e=>e.connect()))},async disconnectAll(){await Promise.allSettled(i().map(e=>e.disconnect()))},get(e){return n.get().get(e)},getAll(){return i()},getInfoAll(){return i().map(e=>e.info)}}},R=e=>({flush:async()=>{await Promise.all(e.map(e=>e.flush?e.flush():Promise.resolve()))},recordEvent:t=>{e.forEach(e=>e.recordEvent(t))}}),z=e=>({flush:()=>Promise.resolve(),recordEvent:t=>{let n={durationMs:t.durationMs,eventName:t.name,timestamp:new Date(t.timestamp).toISOString(),type:t.type,...t.sessionId&&{sessionId:t.sessionId},...t.requestId&&{requestId:t.requestId},...t.errorCategory&&{errorCategory:t.errorCategory},...t.data};t.error?e.withError(Error(t.error)).error(t.type,n):e.info(t.type,n)}}),B=(e=`[soma]`)=>{let t=l({prefix:e,stream:`stderr`});return z(t)},V=e=>{if(e instanceof Error){let t=e.message.toLowerCase();if(t.includes(`validation`)||t.includes(`invalid`)||t.includes(`schema`))return`validation`;if(t.includes(`timeout`)||t.includes(`timed out`)||t.includes(`etimedout`))return`timeout`;if(t.includes(`gateway`)||t.includes(`not connected`)||t.includes(`econnrefused`)||t.includes(`connection refused`))return`gateway`;if(t.includes(`auth`)||t.includes(`unauthorized`)||t.includes(`forbidden`))return`auth`;if(t.includes(`not found`)||t.includes(`404`))return`not_found`}return typeof e==`object`&&e&&`name`in e&&e.name===`UserError`?`validation`:`internal`},H=(e,t)=>{switch(e){case`validation`:return[`Check the tool's parameter schema and provide valid input`];case`timeout`:return[`The operation timed out. Try again or reduce the scope of the request`];case`gateway`:return[`Gateway is ${t?.gatewayStatus??`unavailable`}. The remote service may be down`,`Try again later or check the remote service status`];case`auth`:return[`Authentication failed. Check credentials or permissions`];case`not_found`:return[`The requested resource was not found. Verify the identifier`];case`internal`:return[`An internal error occurred. The tool maintainer should investigate`]}},U=(e,t)=>{let n=V(e),r=e instanceof Error?e.message:String(e),i=H(n,t),a={...t?.gatewayStatus&&{gatewayStatus:t.gatewayStatus},...t?.toolName&&{toolName:t.toolName}};return{errorCategory:n,message:r,...Object.keys(a).length>0&&{relatedState:a},suggestions:i}},W=e=>{let t=r([]),i=r(n.none()),a=r(!1),o=async()=>{let n=t.getAndSet([]);if(n.length===0)return;a.get()||(await d(p(e.filePath),{recursive:!0}),a.set(!0));let r=`${n.map(e=>JSON.stringify(e)).join(`
58
- `)}\n`;await u(e.filePath,r)},s=()=>{i.get().isSome()||i.set(n(setTimeout(()=>{i.set(n.none()),o()},e.flushIntervalMs??1e3)))};return{flush:async()=>{i.get().match({None:()=>void 0,Some:e=>clearTimeout(e)}),i.set(n.none()),await o()},recordEvent:e=>{t.update(t=>[...t,e]),s()}}},G={flush:()=>Promise.resolve(),recordEvent:()=>{}},K=(e,t)=>!t||t.length===0?e:Object.fromEntries(Object.entries(e).map(([e,n])=>[e,t.includes(e)?`[REDACTED]`:n])),q=(e,t=1e4)=>{let n=typeof e==`string`?e:JSON.stringify(e);return n&&n.length>t?`${n.slice(0,t)}...[truncated, ${n.length} total chars]`:e},J=e=>a.fromPromise(e),Y=(e,t,n)=>({...e,execute:async(r,i)=>{let a=n?.captureLevel??`full`;if(a===`none`)return e.execute(r,i);let o=Date.now(),{requestId:s,sessionId:c}=i,l=a===`full`?K(r,n?.redactInputFields):void 0;return(await J(e.execute(r,i))).fold(n=>{let r=U(n,{toolName:e.name});return t.recordEvent({data:{name:e.name},durationMs:Date.now()-o,error:r.message,errorCategory:r.errorCategory,errorStack:n.stack,input:l,name:e.name,requestId:s,sessionId:c,timestamp:o,type:`tool.error`}),{content:[{text:JSON.stringify(r,null,2),type:`text`}],isError:!0}},r=>(t.recordEvent({data:{name:e.name},durationMs:Date.now()-o,input:l,name:e.name,output:a===`full`?q(r,n?.maxOutputSize):void 0,requestId:s,sessionId:c,timestamp:o,type:`tool.execute`}),r))}}),X=(e,t)=>({...e,load:async n=>{let r=Date.now();return(await J(e.load(n))).fold(n=>{let i=U(n);throw t.recordEvent({data:{name:e.name,uri:e.uri},durationMs:Date.now()-r,error:i.message,errorCategory:i.errorCategory,errorStack:n.stack,name:e.name,timestamp:r,type:`resource.error`}),n},n=>(t.recordEvent({data:{name:e.name,uri:e.uri},durationMs:Date.now()-r,name:e.name,timestamp:r,type:`resource.load`}),n))}}),Z=(e,t)=>({...e,load:async(n,r)=>{let i=Date.now();return(await J(e.load(n,r))).fold(n=>{let r=U(n);throw t.recordEvent({data:{name:e.name},durationMs:Date.now()-i,error:r.message,errorCategory:r.errorCategory,errorStack:n.stack,name:e.name,timestamp:i,type:`prompt.error`}),n},n=>(t.recordEvent({data:{name:e.name},durationMs:Date.now()-i,name:e.name,timestamp:i,type:`prompt.load`}),n))}}),te=e=>a.fromPromise(e),ne=(e,t)=>{let n=e.config.toolPrefix??`${e.config.id}_`;return e.tools.map(r=>({annotations:{openWorldHint:!0,readOnlyHint:!0},description:`[${e.name}] ${r.description??`Remote tool`}`,execute:async i=>{let a=Date.now(),o=`${n}${r.name}`;return(await te(e.callTool(r.name,i))).fold(n=>{let s=U(n,{gatewayStatus:e.status,toolName:r.name});return t?.recordEvent({data:{gateway:e.config.id,remoteTool:r.name},durationMs:Date.now()-a,error:s.message,errorCategory:s.errorCategory,errorStack:n.stack,input:i,name:o,timestamp:a,type:`tool.error`}),{content:[{text:JSON.stringify(s,null,2),type:`text`}],isError:!0}},n=>(t?.recordEvent({data:{gateway:e.config.id,remoteTool:r.name},durationMs:Date.now()-a,input:i,name:o,output:n,timestamp:a,type:`tool.execute`}),JSON.stringify(n,null,2)))},name:`${n}${r.name}`}))},re=(e,t=`info`)=>({annotations:{readOnlyHint:!0},description:`Returns identity and build information for this server (name, version, build commit, runtime, capability counts)`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:t}),ie=t=>{let{artifacts:i,backendOptions:a,build:o,enableDashboard:s,enableHealthEndpoint:c,enableInfoEndpoint:l,enableIntrospection:u,gateways:d,healthDetailPath:f,healthPath:p,infoPath:m,introspectionPrefix:h,logLayer:g,telemetry:v,...y}=t,b=y.name,x=y.version,S=P(o),T=F(),E=h??``,D=v??(g?z(g):G),O=e(y,a),A=L(D),M=[],N=[],I=[],R=r(0);d?.forEach(e=>A.add(e));let B=()=>{let e=R.get(),t=e>0?Date.now()-e:0;return{activeSessions:O.sessions.length,gateways:{connected:A.connectedCount,total:A.totalCount},name:b,startedAt:e,status:O.serverState,uptime:t}},V=()=>({prompts:I,resources:N,tools:M}),H=()=>({build:S,capabilities:{prompts:I.length,resources:N.length,tools:M.length},name:b,runtime:T,version:x});u!==!1&&O.addTool(re(()=>H(),`${E}info`));let U=[...i??[]];c!==!1&&(U.push(ee(()=>B(),p)),U.push(k(()=>B(),f))),l!==!1&&U.push(j(()=>H(),m)),s!==!1&&U.push(w(()=>B(),()=>V(),()=>A.getInfoAll())),U.length>0&&C(O.getApp(),U,n(y.authenticate).orUndefined()),O.on(`connect`,({session:e})=>{D.recordEvent({data:{sessionId:e.sessionId},name:b,timestamp:Date.now(),type:`session.connect`})}),O.on(`disconnect`,({session:e})=>{D.recordEvent({data:{sessionId:e.sessionId},name:b,timestamp:Date.now(),type:`session.disconnect`})});let W=e=>{let t=`captureConfig`in e?e.captureConfig:void 0,n=Y(e,D,t);O.addTool(n),M.push({description:e.description,name:e.name})},K=e=>{let t=X(e,D);O.addResource(t),N.push({description:e.description,name:e.name,uri:e.uri})},q=e=>{let t=Z(e,D);O.addPrompt(t),I.push({description:e.description,name:e.name})};return{get name(){return b},get serverState(){return O.serverState},get sessions(){return O.sessions},addPrompt:q,addPrompts:e=>{e.forEach(q)},addResource:K,addResources:e=>{e.forEach(K)},addResourceTemplate:(...e)=>{O.addResourceTemplate(...e)},addRoute:e=>{let t=O.getApp(),r=Array.isArray(e.method)?[...e.method]:[e.method];if(e.protected){let r=n(y.authenticate).orUndefined();t.use(e.path,_({authenticate:r,onUnauthorized:e.onUnauthorized}))}t.on(r,e.path,t=>e.handler(t))},addTool:W,addTools:e=>{e.forEach(e=>W(e))},getApp:()=>O.getApp(),getCapabilities:V,getGatewayManager:()=>A,getHealth:B,getInfo:H,removePrompt:e=>{O.removePrompt(e)},removeResource:e=>{O.removeResource(e)},removeTool:e=>{O.removeTool(e)},async start(e){R.set(Date.now()),D.recordEvent({name:b,timestamp:R.get(),type:`server.start`}),await O.start(e),await A.connectAll(),A.getAll().filter(e=>e.status===`connected`&&e.config.proxyTools!==!1).forEach(e=>{ne(e,D).forEach(e=>O.addTool(e))})},async stop(){await A.disconnectAll(),await O.stop(),D.recordEvent({name:b,timestamp:Date.now(),type:`server.stop`}),D.flush&&await D.flush()}}};var ae=class extends Error{constructor(e){super(e),this.name=`UserError`}};const oe={".avif":`image/avif`,".bmp":`image/bmp`,".gif":`image/gif`,".ico":`image/x-icon`,".jpeg":`image/jpeg`,".jpg":`image/jpeg`,".png":`image/png`,".svg":`image/svg+xml`,".tiff":`image/tiff`,".webp":`image/webp`},se={".aac":`audio/aac`,".flac":`audio/flac`,".m4a":`audio/mp4`,".mp3":`audio/mpeg`,".ogg":`audio/ogg`,".opus":`audio/opus`,".wav":`audio/wav`,".weba":`audio/webm`},ce=(e,t,n)=>t[m(new URL(e,`file:///`).pathname).toLowerCase()]??n,le=async(e,t)=>{if(`url`in e){let n=await fetch(e.url);if(!n.ok)throw Error(`Failed to fetch ${t} from URL (${e.url}): ${n.status} ${n.statusText}`);return{data:Buffer.from(await n.arrayBuffer()),source:e.url}}return`path`in e?{data:await f(e.path),source:e.path}:{data:e.buffer}},ue=async e=>{let{data:t,source:n}=await le(e,`image`),r=n?ce(n,oe,`image/png`):`image/png`;return{data:t.toString(`base64`),mimeType:r,type:`image`}},de=async e=>{let{data:t,source:n}=await le(e,`audio`),r=n?ce(n,se,`audio/mpeg`):`audio/mpeg`;return{data:t.toString(`base64`),mimeType:r,type:`audio`}},fe=e=>l({prefix:`[${e}]`,stream:`stderr`}).withContext({cell:e}),pe=e=>({annotations:{readOnlyHint:!0},description:`Lists all tools, resources, and prompts registered on this server`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:`soma_capabilities`}),me=e=>({annotations:{readOnlyHint:!0},description:`Lists all gateway connections to remote servers with their status and available tools`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:`soma_connections`}),he=e=>({annotations:{readOnlyHint:!0},description:`Returns health and status information for this server including uptime, sessions, and gateway status`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:`soma_health`}),Q=[{description:`GitHub personal access token`,name:`github_pat`,pattern:/\b(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{20,}\b/g},{description:`AWS access key ID`,name:`aws_access_key`,pattern:/\bAKIA[0-9A-Z]{16}\b/g},{description:`AWS secret access key (heuristic)`,name:`aws_secret_key`,pattern:/(?<![A-Za-z0-9/+])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+])/g},{description:`Stripe secret/publishable key`,name:`stripe_key`,pattern:/\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{20,}\b/g},{description:`Slack token`,name:`slack_token`,pattern:/\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g},{description:`JSON Web Token`,name:`jwt`,pattern:/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+\b/g},{description:`OpenAI / Anthropic style API key`,name:`ai_api_key`,pattern:/\b(?:sk-ant-|sk-)[A-Za-z0-9_-]{20,}\b/g},{description:`Generic bearer-style secret prefix`,name:`bearer_authorization`,pattern:/\b[Bb]earer\s+[A-Za-z0-9._\-+/]{16,}\b/g},{description:`Email address`,name:`email`,pattern:/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g},{description:`Private IPv4 address (RFC1918)`,name:`private_ipv4`,pattern:/\b(?:10(?:\.\d{1,3}){3}|192\.168(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2})\b/g},{description:`URL pointing at internal/private hostname`,name:`internal_url`,pattern:/\bhttps?:\/\/(?:[a-z0-9-]+\.)*(?:internal|intranet|corp|local|lan)(?:[/:][^\s]*)?/gi}],$=(e,n=Q)=>{let{counts:r,text:i}=n.reduce((e,t)=>{let n=Array.from(e.text.matchAll(t.pattern)).length;return n===0?e:{counts:e.counts.add(o([t.name,n])),text:e.text.replace(t.pattern,t.replacement??`[REDACTED]`)}},{counts:t.empty(),text:e}),a=[...r].map(([e,t])=>({count:t,name:e}));return{matches:a,redacted:a.length>0,text:i}},ge=[`Submit a feedback report or issue to this server's maintainers.`,``,`IMPORTANT: Treat all input as potentially public.`,`- Do NOT include API keys, tokens, passwords, or credentials`,`- Do NOT include customer data, PII, or proprietary information`,`- Do NOT include internal URLs, hostnames, or system topology`,`- Describe the behavior and how to reproduce it, not the data involved`,``,`Common credential patterns are automatically redacted before submission,`,`but redaction is best-effort and is not a substitute for caution.`].join(`
59
- `),_e=h.object({description:h.string().min(10,`Provide enough context to reproduce (>=10 chars)`).max(8e3).describe(`Detailed description. NO secrets, customer data, or internal URLs.`),severity:h.enum([`low`,`medium`,`high`]).optional().describe(`Severity hint (optional)`),title:h.string().min(5,`Title must be at least 5 characters`).max(200).describe(`Brief, public-safe summary`),type:h.enum([`bug`,`feature`,`feedback`,`question`]).default(`feedback`).describe(`What kind of report this is`)}),ve=(e,r)=>{if(!e.redacted&&!r.redacted)return n.none();let i=[...[...e.matches,...r.matches].reduce((e,t)=>e.add(o([t.name,e.get(t.name).orElse(0)+t.count])),t.empty())].map(([e,t])=>`- ${e}: ${String(t)}`);return n([`⚠️ Automatic redactions applied:`,...i].join(`
60
- `))},ye=e=>e.filter(e=>Object.keys(e).length>0).map(e=>[`---`,`**Context (auto-collected):**`,"```json",JSON.stringify(e,null,2),"```"].join(`
61
- `)),be=(e,t,n,r,i)=>[`**Type:** ${t}`,...n.map(e=>`**Severity:** ${e}`).toArray(),``,e,...r.map(e=>[``,e]).toArray().flat(),...i.map(e=>[``,e]).toArray().flat()].join(`
62
- `),xe=e=>{let t=e.redactionPatterns??Q;return{annotations:{destructiveHint:!1,readOnlyHint:!1},description:e.description??ge,execute:async r=>{let i=$(r.title,t),a=$(r.description,t),o=e.enrichment?n(await e.enrichment({severity:r.severity,type:r.type})):n.none(),s=ve(i,a),c=ye(o),l=be(a.text,r.type,n(r.severity),s,c),u=[r.type,...r.severity?[`severity:${r.severity}`]:[],...e.extraLabels??[]],d=await e.provider.submit({body:l,labels:u,title:i.text});return d.success?JSON.stringify({id:d.id,provider:e.provider.name,redacted:s.isSome(),redactionDetails:[...i.matches,...a.matches],success:!0,url:d.url},null,2):JSON.stringify({error:d.error,redacted:s.isSome(),success:!1},null,2)},name:e.name??`report_feedback`,parameters:_e}},Se=e=>{let t=e.baseUrl??`https://api.github.com`,r=e.fetchImpl??fetch;return{name:`github`,submit:async o=>{let s=e.getToken();if(s.isEmpty)return{error:`No GitHub token configured`,success:!1};let c=s.orThrow(Error(`unreachable`)),l=[...e.defaultLabels??[],...o.labels],u=JSON.stringify({body:o.body,labels:i(l).toArray(),title:o.title});return(await a.fromPromise(r(`${t}/repos/${e.repo}/issues`,{body:u,headers:{Accept:`application/vnd.github+json`,Authorization:`Bearer ${c}`,"Content-Type":`application/json`,"X-GitHub-Api-Version":`2022-11-28`},method:`POST`}))).foldAsync(e=>({error:e instanceof Error?e.message:`Unknown error submitting to GitHub`,success:!1}),async e=>{let t=await e.json().catch(()=>({}));return e.ok?{id:n(t.number).fold(()=>void 0,e=>String(e)),success:!0,url:t.html_url}:{error:`GitHub API ${String(e.status)}: ${t.message??e.statusText}`,success:!1}})}}},Ce=e=>{let t=e.fetchImpl??fetch,n=e.transform??(e=>e);return{name:`webhook`,submit:async r=>(await a.fromPromise(t(e.url,{body:JSON.stringify(n(r)),headers:{"Content-Type":`application/json`,...e.headers??{}},method:`POST`}))).foldAsync(e=>({error:e instanceof Error?e.message:`Unknown error posting to webhook`,success:!1}),async e=>{if(!e.ok){let t=await e.text().catch(()=>``);return{error:`Webhook ${String(e.status)}: ${t||e.statusText}`,success:!1}}return{success:!0}})}};export{O as DEFAULT_HEALTH_DETAIL_PATH,D as DEFAULT_HEALTH_PATH,A as DEFAULT_INFO_PATH,Q as DEFAULT_REDACTION_PATTERNS,G as NoopTelemetry,ae as UserError,de as audioContent,V as classifyError,_ as createAuthMiddleware,pe as createCapabilitiesTool,R as createCompositeTelemetry,me as createConnectionsTool,B as createConsoleTelemetry,w as createDashboardArtifact,fe as createDefaultLogger,U as createEnrichedError,e as createFastMCPBackend,xe as createFeedbackTool,I as createGateway,L as createGatewayManager,Se as createGithubFeedback,ee as createHealthArtifact,k as createHealthDetailArtifact,he as createHealthTool,j as createInfoArtifact,re as createInfoTool,W as createJsonFileTelemetry,z as createLogLayerTelemetry,ne as createProxiedTools,ie as createServer,Ce as createWebhookFeedback,S as getRequestHeader,F as getRuntimeInfo,ue as imageContent,N as readBuildInfoFromEnv,$ as redact,C as registerArtifacts,P as resolveBuildInfo,Z as wrapPrompt,X as wrapResource,Y as wrapTool};
57
+ </html>`,T=e=>{let t=Math.floor(e/1e3),n=Math.floor(t/60),r=Math.floor(n/60),i=Math.floor(r/24);return i>0?`${String(i)}d ${String(r%24)}h`:r>0?`${String(r)}h ${String(n%60)}m`:n>0?`${String(n)}m ${String(t%60)}s`:`${String(t)}s`},E=`/health`,D=`/health/detail`,te=(e,t=E)=>({handler:()=>{let t=e(),n=t.status===`running`?200:503;return new Response(JSON.stringify({name:t.name,status:t.status}),{headers:{"Content-Type":`application/json`},status:n})},path:t,type:`dynamic`}),O=(e,t=D)=>({handler:()=>{let t=e(),n=t.status===`running`?200:503;return new Response(JSON.stringify(t),{headers:{"Content-Type":`application/json`},status:n})},path:t,protected:!0,type:`dynamic`}),k=`/info`,A=(e,t=k)=>({handler:()=>new Response(JSON.stringify(e()),{headers:{"Content-Type":`application/json`},status:200}),path:t,protected:!0,type:`dynamic`}),j=()=>n(globalThis.process),M=()=>j().flatMap(e=>n(e.versions?.node)).isEmpty===!1,N=e=>j().flatMap(t=>n(t.env?.[e])).filter(e=>e.length>0),P=()=>({branch:N(`SOMAMCP_BUILD_BRANCH`).orUndefined(),commit:N(`SOMAMCP_BUILD_COMMIT`).orUndefined(),date:N(`SOMAMCP_BUILD_DATE`).orUndefined(),environment:N(`SOMAMCP_ENVIRONMENT`).orUndefined()}),F=e=>{let t=P();return{branch:e?.branch??t.branch,commit:e?.commit??t.commit,date:e?.date??t.date,environment:e?.environment??t.environment}},I=`unknown`,ne=()=>M()?{arch:j().flatMap(e=>n(e.arch)).orElse(I),nodeVersion:j().flatMap(e=>n(e.version)).orElse(I),platform:j().flatMap(e=>n(e.platform)).orElse(I),runtime:`node`}:{arch:I,nodeVersion:I,platform:n(globalThis.navigator?.userAgent).orElse(I),runtime:`edge`},L=(e,t)=>{let i=r(n.none()),o=r(n.none()),l=r([]),u=r(`disconnected`),d=r(n.none()),f=e.name??e.id,p=()=>{let t=e.reconnectIntervalMs??5e3;d.set(n(setTimeout(()=>{m.connect()},t)))},m={get config(){return e},get info(){return{id:e.id,name:f,remoteTools:l.get().map(e=>({description:e.description,name:e.name})),status:u.get(),url:e.url}},get name(){return f},get status(){return u.get()},get tools(){return l.get()},async callTool(t,n={}){let r=i.get();if(r.isEmpty||u.get()!==`connected`)throw Error(`Gateway ${e.id} is not connected`);return r.orThrow(Error(`Gateway ${e.id} is not connected`)).callTool({arguments:n,name:t})},async connect(){if(u.get()===`connected`)return;u.set(`connecting`);let r=Date.now();(await a.fromPromise((async()=>{let t=new c(new URL(e.url));o.set(n(t));let r=new s({name:`soma-gateway-${e.id}`,version:`1.0.0`});i.set(n(r)),await r.connect(t);let a=await r.listTools();l.set(a.tools)})())).fold(n=>{u.set(`error`),t.recordEvent({data:{id:e.id,url:e.url},error:n instanceof Error?n.message:String(n),name:f,timestamp:r,type:`gateway.error`}),e.reconnect!==!1&&p()},()=>{u.set(`connected`),t.recordEvent({data:{id:e.id,remoteToolCount:l.get().length,url:e.url},durationMs:Date.now()-r,name:f,timestamp:r,type:`gateway.connect`})})},async disconnect(){d.get().match({None:()=>void 0,Some:e=>clearTimeout(e)}),d.set(n.none());let r=i.get();r.isSome()&&await a.fromPromise(r.value.close()),i.set(n.none()),o.set(n.none()),l.set([]),u.set(`disconnected`),t.recordEvent({data:{id:e.id},name:f,timestamp:Date.now(),type:`gateway.disconnect`})}};return m},R=e=>{let n=r(t.empty()),i=()=>[...n.get()].map(([,e])=>e);return{get connectedCount(){return i().filter(e=>e.status===`connected`).length},get totalCount(){return n.get().size},add(t){let r=L(t,e);return n.set(n.get().add(o([t.id,r]))),r},async connectAll(){await Promise.allSettled(i().map(e=>e.connect()))},async disconnectAll(){await Promise.allSettled(i().map(e=>e.disconnect()))},get(e){return n.get().get(e)},getAll(){return i()},getInfoAll(){return i().map(e=>e.info)}}},z=e=>{if(e instanceof Error){let t=e.message.toLowerCase();if(t.includes(`validation`)||t.includes(`invalid`)||t.includes(`schema`))return`validation`;if(t.includes(`timeout`)||t.includes(`timed out`)||t.includes(`etimedout`))return`timeout`;if(t.includes(`gateway`)||t.includes(`not connected`)||t.includes(`econnrefused`)||t.includes(`connection refused`))return`gateway`;if(t.includes(`auth`)||t.includes(`unauthorized`)||t.includes(`forbidden`))return`auth`;if(t.includes(`not found`)||t.includes(`404`))return`not_found`}return typeof e==`object`&&e&&`name`in e&&e.name===`UserError`?`validation`:`internal`},B=(e,t)=>{switch(e){case`validation`:return[`Check the tool's parameter schema and provide valid input`];case`timeout`:return[`The operation timed out. Try again or reduce the scope of the request`];case`gateway`:return[`Gateway is ${t?.gatewayStatus??`unavailable`}. The remote service may be down`,`Try again later or check the remote service status`];case`auth`:return[`Authentication failed. Check credentials or permissions`];case`not_found`:return[`The requested resource was not found. Verify the identifier`];case`internal`:return[`An internal error occurred. The tool maintainer should investigate`]}},V=(e,t)=>{let n=z(e),r=e instanceof Error?e.message:String(e),i=B(n,t),a={...t?.gatewayStatus&&{gatewayStatus:t.gatewayStatus},...t?.toolName&&{toolName:t.toolName}};return{errorCategory:n,message:r,...Object.keys(a).length>0&&{relatedState:a},suggestions:i}},H=e=>a.fromPromise(e),U=(e,t)=>{let n=e.config.toolPrefix??`${e.config.id}_`;return e.tools.map(r=>({annotations:{openWorldHint:!0,readOnlyHint:!0},description:`[${e.name}] ${r.description??`Remote tool`}`,execute:async i=>{let a=Date.now(),o=`${n}${r.name}`;return(await H(e.callTool(r.name,i))).fold(n=>{let s=V(n,{gatewayStatus:e.status,toolName:r.name});return t?.recordEvent({data:{gateway:e.config.id,remoteTool:r.name},durationMs:Date.now()-a,error:s.message,errorCategory:s.errorCategory,errorStack:n.stack,input:i,name:o,timestamp:a,type:`tool.error`}),{content:[{text:JSON.stringify(s,null,2),type:`text`}],isError:!0}},n=>(t?.recordEvent({data:{gateway:e.config.id,remoteTool:r.name},durationMs:Date.now()-a,input:i,name:o,output:n,timestamp:a,type:`tool.execute`}),JSON.stringify(n,null,2)))},name:`${n}${r.name}`}))},W=(e,t=`info`)=>({annotations:{readOnlyHint:!0},description:`Returns identity and build information for this server (name, version, build commit, runtime, capability counts)`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:t}),G=e=>({flush:()=>Promise.resolve(),recordEvent:t=>{let n={durationMs:t.durationMs,eventName:t.name,timestamp:new Date(t.timestamp).toISOString(),type:t.type,...t.sessionId&&{sessionId:t.sessionId},...t.requestId&&{requestId:t.requestId},...t.errorCategory&&{errorCategory:t.errorCategory},...t.data};t.error?e.withError(Error(t.error)).error(t.type,n):e.info(t.type,n)}}),K={flush:()=>Promise.resolve(),recordEvent:()=>{}},q=(e,t)=>!t||t.length===0?e:Object.fromEntries(Object.entries(e).map(([e,n])=>[e,t.includes(e)?`[REDACTED]`:n])),re=(e,t=1e4)=>{let n=typeof e==`string`?e:JSON.stringify(e);return n&&n.length>t?`${n.slice(0,t)}...[truncated, ${n.length} total chars]`:e},J=e=>a.fromPromise(e),Y=(e,t,n)=>({...e,execute:async(r,i)=>{let a=n?.captureLevel??`full`;if(a===`none`)return e.execute(r,i);let o=Date.now(),{requestId:s,sessionId:c}=i,l=a===`full`?q(r,n?.redactInputFields):void 0;return(await J(e.execute(r,i))).fold(n=>{let r=V(n,{toolName:e.name});return t.recordEvent({data:{name:e.name},durationMs:Date.now()-o,error:r.message,errorCategory:r.errorCategory,errorStack:n.stack,input:l,name:e.name,requestId:s,sessionId:c,timestamp:o,type:`tool.error`}),{content:[{text:JSON.stringify(r,null,2),type:`text`}],isError:!0}},r=>(t.recordEvent({data:{name:e.name},durationMs:Date.now()-o,input:l,name:e.name,output:a===`full`?re(r,n?.maxOutputSize):void 0,requestId:s,sessionId:c,timestamp:o,type:`tool.execute`}),r))}}),X=(e,t)=>({...e,load:async n=>{let r=Date.now();return(await J(e.load(n))).fold(n=>{let i=V(n);throw t.recordEvent({data:{name:e.name,uri:e.uri},durationMs:Date.now()-r,error:i.message,errorCategory:i.errorCategory,errorStack:n.stack,name:e.name,timestamp:r,type:`resource.error`}),n},n=>(t.recordEvent({data:{name:e.name,uri:e.uri},durationMs:Date.now()-r,name:e.name,timestamp:r,type:`resource.load`}),n))}}),Z=(e,t)=>({...e,load:async(n,r)=>{let i=Date.now();return(await J(e.load(n,r))).fold(n=>{let r=V(n);throw t.recordEvent({data:{name:e.name},durationMs:Date.now()-i,error:r.message,errorCategory:r.errorCategory,errorStack:n.stack,name:e.name,timestamp:i,type:`prompt.error`}),n},n=>(t.recordEvent({data:{name:e.name},durationMs:Date.now()-i,name:e.name,timestamp:i,type:`prompt.load`}),n))}}),ie=e=>{let{artifacts:t,backend:i,backendOptions:a,build:o,enableDashboard:s,enableHealthEndpoint:c,enableInfoEndpoint:l,enableIntrospection:u,gateways:d,healthDetailPath:f,healthPath:p,infoPath:m,introspectionPrefix:h,logLayer:g,telemetry:v,...y}=e,b=y.name,x=y.version,S=F(o),w=ne(),T=h??``,E=v??(g?G(g):K),D=i(y,a),k=R(E),j=[],M=[],N=[],P=r(0);d?.forEach(e=>k.add(e));let I=()=>{let e=P.get(),t=e>0?Date.now()-e:0;return{activeSessions:D.sessions.length,gateways:{connected:k.connectedCount,total:k.totalCount},name:b,startedAt:e,status:D.serverState,uptime:t}},L=()=>({prompts:N,resources:M,tools:j}),z=()=>({build:S,capabilities:{prompts:N.length,resources:M.length,tools:j.length},name:b,runtime:w,version:x});u!==!1&&D.addTool(W(()=>z(),`${T}info`));let B=[...t??[]];c!==!1&&(B.push(te(()=>I(),p)),B.push(O(()=>I(),f))),l!==!1&&B.push(A(()=>z(),m)),s!==!1&&B.push(C(()=>I(),()=>L(),()=>k.getInfoAll())),B.length>0&&ee(D.getApp(),B,n(y.authenticate).orUndefined()),D.on(`connect`,({session:e})=>{E.recordEvent({data:{sessionId:e.sessionId},name:b,timestamp:Date.now(),type:`session.connect`})}),D.on(`disconnect`,({session:e})=>{E.recordEvent({data:{sessionId:e.sessionId},name:b,timestamp:Date.now(),type:`session.disconnect`})});let V=e=>{let t=`captureConfig`in e?e.captureConfig:void 0,n=Y(e,E,t);D.addTool(n),j.push({description:e.description,name:e.name})},H=e=>{let t=X(e,E);D.addResource(t),M.push({description:e.description,name:e.name,uri:e.uri})},q=e=>{let t=Z(e,E);D.addPrompt(t),N.push({description:e.description,name:e.name})};return{get name(){return b},get serverState(){return D.serverState},get sessions(){return D.sessions},addPrompt:q,addPrompts:e=>{e.forEach(q)},addResource:H,addResources:e=>{e.forEach(H)},addResourceTemplate:(...e)=>{D.addResourceTemplate(...e)},addRoute:e=>{let t=D.getApp(),r=Array.isArray(e.method)?[...e.method]:[e.method];if(e.protected){let r=n(y.authenticate).orUndefined();t.use(e.path,_({authenticate:r,onUnauthorized:e.onUnauthorized}))}t.on(r,e.path,t=>e.handler(t))},addTool:V,addTools:e=>{e.forEach(e=>V(e))},fetch:async e=>D.fetch?D.fetch(e):D.getApp().fetch(e),getApp:()=>D.getApp(),getCapabilities:L,getGatewayManager:()=>k,getHealth:I,getInfo:z,removePrompt:e=>{D.removePrompt(e)},removeResource:e=>{D.removeResource(e)},removeTool:e=>{D.removeTool(e)},async start(e){P.set(Date.now()),E.recordEvent({name:b,timestamp:P.get(),type:`server.start`}),await D.start(e),await k.connectAll(),k.getAll().filter(e=>e.status===`connected`&&e.config.proxyTools!==!1).forEach(e=>{U(e,E).forEach(e=>D.addTool(e))})},async stop(){await k.disconnectAll(),await D.stop(),E.recordEvent({name:b,timestamp:Date.now(),type:`server.stop`}),E.flush&&await E.flush()}}};var ae=class extends Error{constructor(e){super(e),this.name=`UserError`}};const oe={".avif":`image/avif`,".bmp":`image/bmp`,".gif":`image/gif`,".ico":`image/x-icon`,".jpeg":`image/jpeg`,".jpg":`image/jpeg`,".png":`image/png`,".svg":`image/svg+xml`,".tiff":`image/tiff`,".webp":`image/webp`},se={".aac":`audio/aac`,".flac":`audio/flac`,".m4a":`audio/mp4`,".mp3":`audio/mpeg`,".ogg":`audio/ogg`,".opus":`audio/opus`,".wav":`audio/wav`,".weba":`audio/webm`},ce=(e,t,n)=>t[p(new URL(e,`file:///`).pathname).toLowerCase()]??n,le=async(e,t)=>{if(`url`in e){let n=await fetch(e.url);if(!n.ok)throw Error(`Failed to fetch ${t} from URL (${e.url}): ${n.status} ${n.statusText}`);return{data:Buffer.from(await n.arrayBuffer()),source:e.url}}return`path`in e?{data:await d(e.path),source:e.path}:{data:e.buffer}},ue=async e=>{let{data:t,source:n}=await le(e,`image`),r=n?ce(n,oe,`image/png`):`image/png`;return{data:t.toString(`base64`),mimeType:r,type:`image`}},de=async e=>{let{data:t,source:n}=await le(e,`audio`),r=n?ce(n,se,`audio/mpeg`):`audio/mpeg`;return{data:t.toString(`base64`),mimeType:r,type:`audio`}},fe=e=>({flush:async()=>{await Promise.all(e.map(e=>e.flush?e.flush():Promise.resolve()))},recordEvent:t=>{e.forEach(e=>e.recordEvent(t))}}),pe=(e=`[soma]`)=>{let t=m({prefix:e,stream:`stderr`});return G(t)},me=e=>{let t=r([]),i=r(n.none()),a=r(!1),o=async()=>{let n=t.getAndSet([]);if(n.length===0)return;a.get()||(await u(f(e.filePath),{recursive:!0}),a.set(!0));let r=`${n.map(e=>JSON.stringify(e)).join(`
58
+ `)}\n`;await l(e.filePath,r)},s=()=>{i.get().isSome()||i.set(n(setTimeout(()=>{i.set(n.none()),o()},e.flushIntervalMs??1e3)))};return{flush:async()=>{i.get().match({None:()=>void 0,Some:e=>clearTimeout(e)}),i.set(n.none()),await o()},recordEvent:e=>{t.update(t=>[...t,e]),s()}}},he=e=>m({prefix:`[${e}]`,stream:`stderr`}).withContext({cell:e}),ge=e=>({annotations:{readOnlyHint:!0},description:`Lists all tools, resources, and prompts registered on this server`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:`soma_capabilities`}),_e=e=>({annotations:{readOnlyHint:!0},description:`Lists all gateway connections to remote servers with their status and available tools`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:`soma_connections`}),ve=e=>({annotations:{readOnlyHint:!0},description:`Returns health and status information for this server including uptime, sessions, and gateway status`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:`soma_health`}),Q=[{description:`GitHub personal access token`,name:`github_pat`,pattern:/\b(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{20,}\b/g},{description:`AWS access key ID`,name:`aws_access_key`,pattern:/\bAKIA[0-9A-Z]{16}\b/g},{description:`AWS secret access key (heuristic)`,name:`aws_secret_key`,pattern:/(?<![A-Za-z0-9/+])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+])/g},{description:`Stripe secret/publishable key`,name:`stripe_key`,pattern:/\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{20,}\b/g},{description:`Slack token`,name:`slack_token`,pattern:/\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g},{description:`JSON Web Token`,name:`jwt`,pattern:/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+\b/g},{description:`OpenAI / Anthropic style API key`,name:`ai_api_key`,pattern:/\b(?:sk-ant-|sk-)[A-Za-z0-9_-]{20,}\b/g},{description:`Generic bearer-style secret prefix`,name:`bearer_authorization`,pattern:/\b[Bb]earer\s+[A-Za-z0-9._\-+/]{16,}\b/g},{description:`Email address`,name:`email`,pattern:/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g},{description:`Private IPv4 address (RFC1918)`,name:`private_ipv4`,pattern:/\b(?:10(?:\.\d{1,3}){3}|192\.168(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2})\b/g},{description:`URL pointing at internal/private hostname`,name:`internal_url`,pattern:/\bhttps?:\/\/(?:[a-z0-9-]+\.)*(?:internal|intranet|corp|local|lan)(?:[/:][^\s]*)?/gi}],$=(e,n=Q)=>{let{counts:r,text:i}=n.reduce((e,t)=>{let n=Array.from(e.text.matchAll(t.pattern)).length;return n===0?e:{counts:e.counts.add(o([t.name,n])),text:e.text.replace(t.pattern,t.replacement??`[REDACTED]`)}},{counts:t.empty(),text:e}),a=[...r].map(([e,t])=>({count:t,name:e}));return{matches:a,redacted:a.length>0,text:i}},ye=[`Submit a feedback report or issue to this server's maintainers.`,``,`IMPORTANT: Treat all input as potentially public.`,`- Do NOT include API keys, tokens, passwords, or credentials`,`- Do NOT include customer data, PII, or proprietary information`,`- Do NOT include internal URLs, hostnames, or system topology`,`- Describe the behavior and how to reproduce it, not the data involved`,``,`Common credential patterns are automatically redacted before submission,`,`but redaction is best-effort and is not a substitute for caution.`].join(`
59
+ `),be=h.object({description:h.string().min(10,`Provide enough context to reproduce (>=10 chars)`).max(8e3).describe(`Detailed description. NO secrets, customer data, or internal URLs.`),severity:h.enum([`low`,`medium`,`high`]).optional().describe(`Severity hint (optional)`),title:h.string().min(5,`Title must be at least 5 characters`).max(200).describe(`Brief, public-safe summary`),type:h.enum([`bug`,`feature`,`feedback`,`question`]).default(`feedback`).describe(`What kind of report this is`)}),xe=(e,r)=>{if(!e.redacted&&!r.redacted)return n.none();let i=[...[...e.matches,...r.matches].reduce((e,t)=>e.add(o([t.name,e.get(t.name).orElse(0)+t.count])),t.empty())].map(([e,t])=>`- ${e}: ${String(t)}`);return n([`⚠️ Automatic redactions applied:`,...i].join(`
60
+ `))},Se=e=>e.filter(e=>Object.keys(e).length>0).map(e=>[`---`,`**Context (auto-collected):**`,"```json",JSON.stringify(e,null,2),"```"].join(`
61
+ `)),Ce=(e,t,n,r,i)=>[`**Type:** ${t}`,...n.map(e=>`**Severity:** ${e}`).toArray(),``,e,...r.map(e=>[``,e]).toArray().flat(),...i.map(e=>[``,e]).toArray().flat()].join(`
62
+ `),we=e=>{let t=e.redactionPatterns??Q;return{annotations:{destructiveHint:!1,readOnlyHint:!1},description:e.description??ye,execute:async r=>{let i=$(r.title,t),a=$(r.description,t),o=e.enrichment?n(await e.enrichment({severity:r.severity,type:r.type})):n.none(),s=xe(i,a),c=Se(o),l=Ce(a.text,r.type,n(r.severity),s,c),u=[r.type,...r.severity?[`severity:${r.severity}`]:[],...e.extraLabels??[]],d=await e.provider.submit({body:l,labels:u,title:i.text});return d.success?JSON.stringify({id:d.id,provider:e.provider.name,redacted:s.isSome(),redactionDetails:[...i.matches,...a.matches],success:!0,url:d.url},null,2):JSON.stringify({error:d.error,redacted:s.isSome(),success:!1},null,2)},name:e.name??`report_feedback`,parameters:be}},Te=e=>{let t=e.baseUrl??`https://api.github.com`,r=e.fetchImpl??fetch;return{name:`github`,submit:async o=>{let s=e.getToken();if(s.isEmpty)return{error:`No GitHub token configured`,success:!1};let c=s.orThrow(Error(`unreachable`)),l=[...e.defaultLabels??[],...o.labels],u=JSON.stringify({body:o.body,labels:i(l).toArray(),title:o.title});return(await a.fromPromise(r(`${t}/repos/${e.repo}/issues`,{body:u,headers:{Accept:`application/vnd.github+json`,Authorization:`Bearer ${c}`,"Content-Type":`application/json`,"X-GitHub-Api-Version":`2022-11-28`},method:`POST`}))).foldAsync(e=>({error:e instanceof Error?e.message:`Unknown error submitting to GitHub`,success:!1}),async e=>{let t=await e.json().catch(()=>({}));return e.ok?{id:n(t.number).fold(()=>void 0,e=>String(e)),success:!0,url:t.html_url}:{error:`GitHub API ${String(e.status)}: ${t.message??e.statusText}`,success:!1}})}}},Ee=e=>{let t=e.fetchImpl??fetch,n=e.transform??(e=>e);return{name:`webhook`,submit:async r=>(await a.fromPromise(t(e.url,{body:JSON.stringify(n(r)),headers:{"Content-Type":`application/json`,...e.headers??{}},method:`POST`}))).foldAsync(e=>({error:e instanceof Error?e.message:`Unknown error posting to webhook`,success:!1}),async e=>{if(!e.ok){let t=await e.text().catch(()=>``);return{error:`Webhook ${String(e.status)}: ${t||e.statusText}`,success:!1}}return{success:!0}})}},De=t=>ie({...t,backend:t.backend??e});export{D as DEFAULT_HEALTH_DETAIL_PATH,E as DEFAULT_HEALTH_PATH,k as DEFAULT_INFO_PATH,Q as DEFAULT_REDACTION_PATTERNS,K as NoopTelemetry,ae as UserError,de as audioContent,z as classifyError,_ as createAuthMiddleware,ge as createCapabilitiesTool,fe as createCompositeTelemetry,_e as createConnectionsTool,pe as createConsoleTelemetry,C as createDashboardArtifact,he as createDefaultLogger,V as createEnrichedError,e as createFastMCPBackend,we as createFeedbackTool,L as createGateway,R as createGatewayManager,Te as createGithubFeedback,te as createHealthArtifact,O as createHealthDetailArtifact,ve as createHealthTool,A as createInfoArtifact,W as createInfoTool,me as createJsonFileTelemetry,G as createLogLayerTelemetry,U as createProxiedTools,De as createServer,ie as createServerCore,Ee as createWebhookFeedback,S as getRequestHeader,ne as getRuntimeInfo,ue as imageContent,P as readBuildInfoFromEnv,$ as redact,ee as registerArtifacts,F as resolveBuildInfo,Z as wrapPrompt,X as wrapResource,Y as wrapTool};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "somamcp",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "Framework for building MCP servers with telemetry, introspection, and backend abstraction",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,6 +12,11 @@
12
12
  "import": "./dist/index.js",
13
13
  "default": "./dist/index.js"
14
14
  },
15
+ "./edge": {
16
+ "types": "./dist/edge.d.ts",
17
+ "import": "./dist/edge.js",
18
+ "default": "./dist/edge.js"
19
+ },
15
20
  "./backend": {
16
21
  "types": "./dist/backend/index.d.ts",
17
22
  "import": "./dist/backend/index.js"
@@ -30,10 +35,11 @@
30
35
  "test": "ts-builds test",
31
36
  "test:watch": "ts-builds test:watch",
32
37
  "test:coverage": "ts-builds test:coverage",
33
- "build": "ts-builds build",
38
+ "build": "ts-builds build && pnpm check:edge",
39
+ "check:edge": "node scripts/check-edge-bundle.mjs",
34
40
  "dev": "ts-builds dev",
35
41
  "clean": "rimraf dist",
36
- "prepublishOnly": "pnpm validate"
42
+ "prepublishOnly": "pnpm validate && pnpm check:edge"
37
43
  },
38
44
  "keywords": [
39
45
  "MCP",