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 +78 -1
- package/dist/backend/index.d.ts +1 -1
- package/dist/backend/index.js +1 -1
- package/dist/{backend-Bm6e2u2K.js → backend-CA7j4YCF.js} +1 -1
- package/dist/edge.d.ts +711 -0
- package/dist/edge.js +61 -0
- package/dist/{index-DizthJMe.d.ts → index-BRNcy_4O.d.ts} +7 -6
- package/dist/index.d.ts +31 -3
- package/dist/index.js +8 -8
- package/package.json +9 -3
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.
|
|
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"
|
package/dist/backend/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as BackendSession, i as BackendFactory, n as BackendAdapter, r as BackendEvents, t as createFastMCPBackend } from "../index-
|
|
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 };
|
package/dist/backend/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"../backend-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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">${
|
|
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
|
|
59
|
-
`),
|
|
60
|
-
`))},
|
|
61
|
-
`)),
|
|
62
|
-
`),
|
|
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.
|
|
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",
|