somamcp 1.0.6 → 1.0.8
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 +156 -18
- package/dist/index.d.ts +149 -5
- package/dist/index.js +8 -4
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# SomaMCP
|
|
2
2
|
|
|
3
|
-
Framework for building **MCP (Model Context Protocol)** servers with built-in telemetry, introspection, and a backend abstraction layer.
|
|
3
|
+
Framework for building **MCP (Model Context Protocol)** servers with built-in telemetry, introspection, agent feedback, and a backend abstraction layer.
|
|
4
4
|
|
|
5
|
-
somamcp wraps an underlying MCP framework (currently [FastMCP](https://github.com/punkpeye/fastmcp)) behind a `BackendAdapter` interface — giving you a stable API surface, automatic telemetry, gateway-based server composition, and HTTP artifacts out of the box.
|
|
5
|
+
somamcp wraps an underlying MCP framework (currently [FastMCP](https://github.com/punkpeye/fastmcp)) behind a `BackendAdapter` interface — giving you a stable API surface, automatic telemetry, gateway-based server composition, identity/health endpoints with sensible security defaults, and HTTP artifacts out of the box.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -11,8 +11,11 @@ somamcp wraps an underlying MCP framework (currently [FastMCP](https://github.co
|
|
|
11
11
|
- **Error classification & enrichment** — errors are auto-classified (validation / timeout / gateway / auth / not_found / internal) with actionable suggestions for LLMs
|
|
12
12
|
- **Per-tool capture config** — configure input/output capture levels, field redaction, and output size limits per tool
|
|
13
13
|
- **Gateway system** — connect to remote MCP servers and proxy their tools as local tools
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
14
|
+
- **Identity & build info** — `info` MCP tool returns name, version, build commit, runtime, and capability counts; auto-populated from `SOMAMCP_BUILD_*` env vars
|
|
15
|
+
- **Two-tier health** — public `/health` (minimal, for Docker/k8s probes) + protected `/health/detail` (full operational state)
|
|
16
|
+
- **Protected artifacts** — `protected: true` on any artifact reuses FastMCP's `authenticate` callback; dashboard and `/info` protected by default
|
|
17
|
+
- **Agent feedback tool** — `createFeedbackTool` posts agent-reported issues to GitHub or any webhook, with automatic credential/PII redaction
|
|
18
|
+
- **HTTP dashboard** — auto-generated overview at `/dashboard` (protected)
|
|
16
19
|
- **Functional style** — powered by [functype](https://github.com/jordanburke/functype) (`Ref`, `Try`, `Either`)
|
|
17
20
|
|
|
18
21
|
## Installation
|
|
@@ -44,6 +47,86 @@ server.addTool({
|
|
|
44
47
|
await server.start({ transportType: "stdio" })
|
|
45
48
|
```
|
|
46
49
|
|
|
50
|
+
## Identity & Health
|
|
51
|
+
|
|
52
|
+
SomaMCP auto-registers one MCP introspection tool and three HTTP endpoints:
|
|
53
|
+
|
|
54
|
+
| Surface | Default path / name | Protected | Returns |
|
|
55
|
+
| -------- | ------------------- | --------- | -------------------------------------------------------------------------------- |
|
|
56
|
+
| MCP tool | `info` | n/a | `{ name, version, build, runtime, capabilities: { tools, resources, prompts } }` |
|
|
57
|
+
| HTTP | `/health` | No | `{ name, status }` — minimal, status code is the real signal (200 / 503) |
|
|
58
|
+
| HTTP | `/health/detail` | Yes | Full `ServerHealth` (sessions, gateway topology) |
|
|
59
|
+
| HTTP | `/info` | Yes | Same shape as the `info` MCP tool |
|
|
60
|
+
| HTTP | `/dashboard` | Yes | Rendered HTML overview |
|
|
61
|
+
|
|
62
|
+
**Why two health tiers?** The public `/health` exists for infrastructure probes (Docker `HEALTHCHECK`, k8s liveness/readiness, load balancers) — they only need a status code. Operational details (session counts, gateway URLs) are reconnaissance signals and live behind `protected: true`.
|
|
63
|
+
|
|
64
|
+
### Build info
|
|
65
|
+
|
|
66
|
+
Set these env vars at deploy time (e.g. in your Dockerfile or CI) so `info` reflects the actual build:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
SOMAMCP_BUILD_COMMIT=abc123
|
|
70
|
+
SOMAMCP_BUILD_DATE=2026-05-22T00:00:00Z
|
|
71
|
+
SOMAMCP_BUILD_BRANCH=main
|
|
72
|
+
SOMAMCP_ENVIRONMENT=production
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or pass them programmatically (override wins over env):
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
createServer({
|
|
79
|
+
name: "my-server",
|
|
80
|
+
version: "1.0.0",
|
|
81
|
+
build: {
|
|
82
|
+
commit: process.env.GIT_SHA,
|
|
83
|
+
date: process.env.BUILD_DATE,
|
|
84
|
+
environment: "production",
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Disabling / customizing
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
createServer({
|
|
93
|
+
name: "my-server",
|
|
94
|
+
version: "1.0.0",
|
|
95
|
+
enableIntrospection: false, // remove the `info` MCP tool
|
|
96
|
+
enableHealthEndpoint: false, // remove /health and /health/detail
|
|
97
|
+
enableInfoEndpoint: false, // remove /info
|
|
98
|
+
enableDashboard: false, // remove /dashboard
|
|
99
|
+
healthPath: "/healthz", // custom paths
|
|
100
|
+
healthDetailPath: "/health/full",
|
|
101
|
+
infoPath: "/about",
|
|
102
|
+
introspectionPrefix: "soma_", // → `soma_info` tool name
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Protected Artifacts
|
|
107
|
+
|
|
108
|
+
Any artifact can opt into auth with `protected: true`. The route runs FastMCP's `authenticate` callback via Hono middleware — same auth model as MCP protocol calls. If `protected: true` is set but no `authenticate` is configured, the route returns 401 unconditionally.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
createServer({
|
|
112
|
+
name: "my-server",
|
|
113
|
+
version: "1.0.0",
|
|
114
|
+
authenticate: async (req) => {
|
|
115
|
+
const token = req.headers.authorization?.replace("Bearer ", "")
|
|
116
|
+
if (token !== process.env.OPS_TOKEN) throw new Error("denied")
|
|
117
|
+
return { user: "ops" }
|
|
118
|
+
},
|
|
119
|
+
artifacts: [
|
|
120
|
+
{
|
|
121
|
+
type: "dynamic",
|
|
122
|
+
path: "/admin/stats",
|
|
123
|
+
protected: true,
|
|
124
|
+
handler: (c) => c.json({ secret: "stuff" }),
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
47
130
|
## Telemetry
|
|
48
131
|
|
|
49
132
|
Telemetry is opt-in via the `telemetry` option. Every tool/resource/prompt call is automatically wrapped with timing, error classification, and optional input/output capture.
|
|
@@ -129,15 +212,62 @@ const server = createServer({
|
|
|
129
212
|
})
|
|
130
213
|
```
|
|
131
214
|
|
|
215
|
+
## Agent Feedback
|
|
216
|
+
|
|
217
|
+
Let agents file issues against your repo when they hit bugs or confusing behavior. The tool auto-redacts common credential patterns and PII, and includes a tool description that explicitly warns the LLM not to include proprietary data.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { createServer, createFeedbackTool, createGithubFeedback } from "somamcp"
|
|
221
|
+
|
|
222
|
+
const server = createServer({ name: "my-mcp", version: "1.0.0" })
|
|
223
|
+
|
|
224
|
+
server.addTool(
|
|
225
|
+
createFeedbackTool({
|
|
226
|
+
provider: createGithubFeedback({
|
|
227
|
+
repo: "myorg/my-mcp",
|
|
228
|
+
getToken: () => process.env.GITHUB_FEEDBACK_TOKEN,
|
|
229
|
+
defaultLabels: ["agent-feedback"],
|
|
230
|
+
}),
|
|
231
|
+
// Auto-attached to each issue (visible in the issue body)
|
|
232
|
+
enrichment: async () => ({
|
|
233
|
+
server: server.getInfo(),
|
|
234
|
+
}),
|
|
235
|
+
extraLabels: ["from-agent"],
|
|
236
|
+
}),
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**What gets redacted automatically** (best-effort, not a substitute for caution):
|
|
241
|
+
|
|
242
|
+
- GitHub PATs (`ghp_*`, `github_pat_*`)
|
|
243
|
+
- AWS access keys (`AKIA*`)
|
|
244
|
+
- Stripe keys (`sk_live_*`, `pk_test_*`, etc.)
|
|
245
|
+
- Slack tokens (`xoxb-*`, `xoxp-*`, etc.)
|
|
246
|
+
- JWTs
|
|
247
|
+
- OpenAI/Anthropic API keys
|
|
248
|
+
- Bearer tokens (`Bearer <secret>`)
|
|
249
|
+
- Email addresses
|
|
250
|
+
- Private IPv4 addresses (RFC1918)
|
|
251
|
+
- Internal hostnames (`*.internal`, `*.local`, etc.)
|
|
252
|
+
|
|
253
|
+
The tool response includes a `redactionDetails` summary so the agent knows what was scrubbed. Add custom patterns via `redactionPatterns`.
|
|
254
|
+
|
|
255
|
+
**Providers:**
|
|
256
|
+
|
|
257
|
+
- `createGithubFeedback({ repo, getToken, defaultLabels?, baseUrl? })` — posts to GitHub Issues
|
|
258
|
+
- `createWebhookFeedback({ url, headers?, transform? })` — generic JSON POST to any endpoint (Linear, Jira, Sentry, custom relay)
|
|
259
|
+
|
|
260
|
+
**Token security:** the token only needs `issues: write` scope on the target repo. Use a GitHub App for fleet-scale (auditable, scoped, rotatable).
|
|
261
|
+
|
|
132
262
|
## Artifacts & Dashboard
|
|
133
263
|
|
|
134
|
-
Mount HTTP routes on the embedded Hono app. A health dashboard is auto-mounted at `/dashboard` unless disabled.
|
|
264
|
+
Mount HTTP routes on the embedded Hono app. A protected health dashboard is auto-mounted at `/dashboard` unless disabled.
|
|
135
265
|
|
|
136
266
|
```typescript
|
|
137
267
|
const server = createServer({
|
|
138
268
|
name: "my-server",
|
|
139
269
|
version: "1.0.0",
|
|
140
|
-
enableDashboard: true, // default
|
|
270
|
+
enableDashboard: true, // default; requires `authenticate` to view
|
|
141
271
|
artifacts: [
|
|
142
272
|
{
|
|
143
273
|
type: "dynamic",
|
|
@@ -153,14 +283,6 @@ await server.start({
|
|
|
153
283
|
})
|
|
154
284
|
```
|
|
155
285
|
|
|
156
|
-
## Introspection
|
|
157
|
-
|
|
158
|
-
Three tools are auto-registered (disable with `enableIntrospection: false`):
|
|
159
|
-
|
|
160
|
-
- **`soma_health`** — server status, uptime, active sessions, gateway count
|
|
161
|
-
- **`soma_capabilities`** — registered tools, resources, prompts
|
|
162
|
-
- **`soma_connections`** — gateway connection info
|
|
163
|
-
|
|
164
286
|
## Backend Abstraction
|
|
165
287
|
|
|
166
288
|
The underlying MCP framework is accessed exclusively through the `BackendAdapter` interface. To use a non-FastMCP backend, implement `BackendFactory`:
|
|
@@ -174,7 +296,7 @@ const myBackend: BackendFactory = (config, backendOptions) => {
|
|
|
174
296
|
}
|
|
175
297
|
```
|
|
176
298
|
|
|
177
|
-
Framework-specific options (e.g. FastMCP's OAuth, ping
|
|
299
|
+
Framework-specific options (e.g. FastMCP's OAuth, ping) flow through `backendOptions`:
|
|
178
300
|
|
|
179
301
|
```typescript
|
|
180
302
|
createServer({
|
|
@@ -192,12 +314,17 @@ createServer({
|
|
|
192
314
|
**Core**
|
|
193
315
|
|
|
194
316
|
- `createServer(options)` → `SomaServerInstance`
|
|
195
|
-
- Types: `SomaServerOptions`, `SomaServerInstance`, `ServerHealth`, `ServerCapabilities`, `ToolOptions`
|
|
317
|
+
- Types: `SomaServerOptions`, `SomaServerInstance`, `ServerHealth`, `ServerInfo`, `ServerCapabilities`, `CapabilitiesSummary`, `ToolOptions`
|
|
196
318
|
|
|
197
319
|
**Primitives** (somamcp-owned, no FastMCP leakage)
|
|
198
320
|
|
|
199
321
|
- `Tool`, `Resource`, `Prompt`, `Context`, `Content`, `ContentResult`, `SessionAuth`, `UserError`
|
|
200
322
|
|
|
323
|
+
**Build info**
|
|
324
|
+
|
|
325
|
+
- `readBuildInfoFromEnv`, `resolveBuildInfo`, `getRuntimeInfo`
|
|
326
|
+
- Types: `BuildInfo`, `RuntimeInfo`
|
|
327
|
+
|
|
201
328
|
**Telemetry**
|
|
202
329
|
|
|
203
330
|
- `createConsoleTelemetry`, `createJsonFileTelemetry`, `createCompositeTelemetry`, `createLogLayerTelemetry`, `NoopTelemetry`
|
|
@@ -211,8 +338,19 @@ createServer({
|
|
|
211
338
|
|
|
212
339
|
**Artifacts**
|
|
213
340
|
|
|
214
|
-
- `registerArtifacts`, `createDashboardArtifact`
|
|
215
|
-
- Types: `StaticArtifact`, `DynamicArtifact`, `DirectoryArtifact`, `ArtifactConfig`
|
|
341
|
+
- `registerArtifacts`, `createDashboardArtifact`, `createHealthArtifact`, `createHealthDetailArtifact`, `createInfoArtifact`
|
|
342
|
+
- Types: `StaticArtifact`, `DynamicArtifact`, `DirectoryArtifact`, `ArtifactConfig`, `ArtifactAuthenticate`
|
|
343
|
+
|
|
344
|
+
**Introspection**
|
|
345
|
+
|
|
346
|
+
- `createInfoTool` (auto-registered as `info`)
|
|
347
|
+
- `createHealthTool`, `createCapabilitiesTool`, `createConnectionsTool` (legacy, exported for manual use)
|
|
348
|
+
|
|
349
|
+
**Feedback**
|
|
350
|
+
|
|
351
|
+
- `createFeedbackTool`, `createGithubFeedback`, `createWebhookFeedback`
|
|
352
|
+
- `redact`, `DEFAULT_REDACTION_PATTERNS`
|
|
353
|
+
- Types: `FeedbackProvider`, `FeedbackToolOptions`, `GithubFeedbackOptions`, `WebhookFeedbackOptions`, `RedactionPattern`, `RedactionResult`, `NormalizedFeedback`, `FeedbackSubmitResult`
|
|
216
354
|
|
|
217
355
|
**Content helpers**
|
|
218
356
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
import { Option } from "functype";
|
|
2
2
|
import { DirectLogger, DirectLogger as DirectLogger$1 } from "functype-log";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
import { Context as Context$1, Hono } from "hono";
|
|
4
5
|
import { Tool as Tool$1 } from "@modelcontextprotocol/sdk/types.js";
|
|
5
6
|
|
|
6
7
|
//#region src/artifacts/types.d.ts
|
|
7
|
-
type
|
|
8
|
+
type ArtifactCommon = {
|
|
9
|
+
protected?: boolean;
|
|
10
|
+
};
|
|
11
|
+
type StaticArtifact = ArtifactCommon & {
|
|
8
12
|
contentType: string;
|
|
9
13
|
content: string;
|
|
10
14
|
path: string;
|
|
11
15
|
type: "static";
|
|
12
16
|
};
|
|
13
|
-
type DynamicArtifact = {
|
|
17
|
+
type DynamicArtifact = ArtifactCommon & {
|
|
14
18
|
handler: (c: Context$1) => Response | Promise<Response>;
|
|
15
19
|
path: string;
|
|
16
20
|
type: "dynamic";
|
|
17
21
|
};
|
|
18
|
-
type DirectoryArtifact = {
|
|
22
|
+
type DirectoryArtifact = ArtifactCommon & {
|
|
19
23
|
directory: string;
|
|
20
24
|
path: string;
|
|
21
25
|
type: "directory";
|
|
22
26
|
};
|
|
23
27
|
type ArtifactConfig = DirectoryArtifact | DynamicArtifact | StaticArtifact;
|
|
28
|
+
type ArtifactAuthenticate = (request: unknown) => Promise<unknown>;
|
|
24
29
|
//#endregion
|
|
25
30
|
//#region src/types/core.d.ts
|
|
26
31
|
type SessionAuth = Record<string, unknown> | undefined;
|
|
@@ -236,6 +241,22 @@ type BackendFactory<T extends SessionAuth = SessionAuth> = (config: {
|
|
|
236
241
|
version: `${number}.${number}.${number}`;
|
|
237
242
|
}, backendOptions?: Record<string, unknown>) => BackendAdapter<T>;
|
|
238
243
|
//#endregion
|
|
244
|
+
//#region src/buildInfo.d.ts
|
|
245
|
+
type BuildInfo = {
|
|
246
|
+
branch?: string;
|
|
247
|
+
commit?: string;
|
|
248
|
+
date?: string;
|
|
249
|
+
environment?: string;
|
|
250
|
+
};
|
|
251
|
+
type RuntimeInfo = {
|
|
252
|
+
arch: string;
|
|
253
|
+
nodeVersion: string;
|
|
254
|
+
platform: string;
|
|
255
|
+
};
|
|
256
|
+
declare const readBuildInfoFromEnv: () => BuildInfo;
|
|
257
|
+
declare const resolveBuildInfo: (override?: BuildInfo) => BuildInfo;
|
|
258
|
+
declare const getRuntimeInfo: () => RuntimeInfo;
|
|
259
|
+
//#endregion
|
|
239
260
|
//#region src/gateway/types.d.ts
|
|
240
261
|
type GatewayConfig = {
|
|
241
262
|
id: string;
|
|
@@ -310,9 +331,16 @@ type TelemetryCollector = {
|
|
|
310
331
|
type SomaServerOptions<T extends SessionAuth = SessionAuth> = ServerConfig<T> & {
|
|
311
332
|
artifacts?: ArtifactConfig[];
|
|
312
333
|
backendOptions?: Record<string, unknown>;
|
|
334
|
+
build?: BuildInfo;
|
|
313
335
|
enableDashboard?: boolean;
|
|
336
|
+
enableHealthEndpoint?: boolean;
|
|
337
|
+
enableInfoEndpoint?: boolean;
|
|
314
338
|
enableIntrospection?: boolean;
|
|
315
339
|
gateways?: GatewayConfig[];
|
|
340
|
+
healthDetailPath?: string;
|
|
341
|
+
healthPath?: string;
|
|
342
|
+
infoPath?: string;
|
|
343
|
+
introspectionPrefix?: string;
|
|
316
344
|
logLayer?: DirectLogger$1;
|
|
317
345
|
telemetry?: TelemetryCollector;
|
|
318
346
|
};
|
|
@@ -342,6 +370,18 @@ type ServerCapabilities = {
|
|
|
342
370
|
name: string;
|
|
343
371
|
}>;
|
|
344
372
|
};
|
|
373
|
+
type CapabilitiesSummary = {
|
|
374
|
+
prompts: number;
|
|
375
|
+
resources: number;
|
|
376
|
+
tools: number;
|
|
377
|
+
};
|
|
378
|
+
type ServerInfo = {
|
|
379
|
+
build: BuildInfo;
|
|
380
|
+
capabilities: CapabilitiesSummary;
|
|
381
|
+
name: string;
|
|
382
|
+
runtime: RuntimeInfo;
|
|
383
|
+
version: string;
|
|
384
|
+
};
|
|
345
385
|
type ToolOptions<T extends SessionAuth = SessionAuth, P extends SchemaParams = SchemaParams> = Tool<T, P> & {
|
|
346
386
|
captureConfig?: ToolCaptureConfig;
|
|
347
387
|
};
|
|
@@ -360,6 +400,7 @@ type SomaServerInstance<T extends SessionAuth = SessionAuth> = {
|
|
|
360
400
|
getCapabilities: () => ServerCapabilities;
|
|
361
401
|
getGatewayManager: () => GatewayManagerInstance;
|
|
362
402
|
getHealth: () => ServerHealth;
|
|
403
|
+
getInfo: () => ServerInfo;
|
|
363
404
|
removePrompt: (name: string) => void;
|
|
364
405
|
removeResource: (name: string) => void;
|
|
365
406
|
removeTool: (name: string) => void;
|
|
@@ -425,11 +466,21 @@ declare const wrapPrompt: <T extends SessionAuth, Args extends PromptArgument<T>
|
|
|
425
466
|
declare const createDefaultLogger: (name: string) => DirectLogger$1;
|
|
426
467
|
//#endregion
|
|
427
468
|
//#region src/artifacts/ArtifactManager.d.ts
|
|
428
|
-
declare const registerArtifacts: (app: Hono, artifacts: ReadonlyArray<ArtifactConfig
|
|
469
|
+
declare const registerArtifacts: (app: Hono, artifacts: ReadonlyArray<ArtifactConfig>, authenticate?: ArtifactAuthenticate) => void;
|
|
429
470
|
//#endregion
|
|
430
471
|
//#region src/artifacts/DashboardArtifact.d.ts
|
|
431
472
|
declare const createDashboardArtifact: (getHealth: () => ServerHealth, getCapabilities: () => ServerCapabilities, getConnections: () => ReadonlyArray<GatewayInfo>) => ArtifactConfig;
|
|
432
473
|
//#endregion
|
|
474
|
+
//#region src/artifacts/HealthArtifact.d.ts
|
|
475
|
+
declare const DEFAULT_HEALTH_PATH = "/health";
|
|
476
|
+
declare const DEFAULT_HEALTH_DETAIL_PATH = "/health/detail";
|
|
477
|
+
declare const createHealthArtifact: (getHealth: () => ServerHealth, path?: string) => ArtifactConfig;
|
|
478
|
+
declare const createHealthDetailArtifact: (getHealth: () => ServerHealth, path?: string) => ArtifactConfig;
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region src/artifacts/InfoArtifact.d.ts
|
|
481
|
+
declare const DEFAULT_INFO_PATH = "/info";
|
|
482
|
+
declare const createInfoArtifact: (getInfo: () => ServerInfo, path?: string) => ArtifactConfig;
|
|
483
|
+
//#endregion
|
|
433
484
|
//#region src/introspection/capabilitiesTool.d.ts
|
|
434
485
|
declare const createCapabilitiesTool: <T extends SessionAuth>(getCapabilities: () => ServerCapabilities) => Tool<T>;
|
|
435
486
|
//#endregion
|
|
@@ -439,6 +490,99 @@ declare const createConnectionsTool: <T extends SessionAuth>(getConnections: ()
|
|
|
439
490
|
//#region src/introspection/healthTool.d.ts
|
|
440
491
|
declare const createHealthTool: <T extends SessionAuth>(getHealth: () => ServerHealth) => Tool<T>;
|
|
441
492
|
//#endregion
|
|
493
|
+
//#region src/introspection/infoTool.d.ts
|
|
494
|
+
declare const createInfoTool: <T extends SessionAuth>(getInfo: () => ServerInfo, name?: string) => Tool<T>;
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region src/feedback/redaction.d.ts
|
|
497
|
+
type RedactionPattern = {
|
|
498
|
+
description: string;
|
|
499
|
+
name: string;
|
|
500
|
+
pattern: RegExp;
|
|
501
|
+
replacement?: string;
|
|
502
|
+
};
|
|
503
|
+
type RedactionResult = {
|
|
504
|
+
matches: ReadonlyArray<{
|
|
505
|
+
name: string;
|
|
506
|
+
count: number;
|
|
507
|
+
}>;
|
|
508
|
+
redacted: boolean;
|
|
509
|
+
text: string;
|
|
510
|
+
};
|
|
511
|
+
declare const DEFAULT_REDACTION_PATTERNS: ReadonlyArray<RedactionPattern>;
|
|
512
|
+
declare const redact: (input: string, patterns?: ReadonlyArray<RedactionPattern>) => RedactionResult;
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/feedback/types.d.ts
|
|
515
|
+
type FeedbackType = "bug" | "feature" | "feedback" | "question";
|
|
516
|
+
type FeedbackSeverity = "high" | "low" | "medium";
|
|
517
|
+
type NormalizedFeedback = {
|
|
518
|
+
body: string;
|
|
519
|
+
labels: ReadonlyArray<string>;
|
|
520
|
+
title: string;
|
|
521
|
+
};
|
|
522
|
+
type FeedbackSubmitSuccess = {
|
|
523
|
+
id?: string;
|
|
524
|
+
success: true;
|
|
525
|
+
url?: string;
|
|
526
|
+
};
|
|
527
|
+
type FeedbackSubmitFailure = {
|
|
528
|
+
error: string;
|
|
529
|
+
success: false;
|
|
530
|
+
};
|
|
531
|
+
type FeedbackSubmitResult = FeedbackSubmitFailure | FeedbackSubmitSuccess;
|
|
532
|
+
type FeedbackProvider = {
|
|
533
|
+
readonly name: string;
|
|
534
|
+
submit: (payload: NormalizedFeedback) => Promise<FeedbackSubmitResult>;
|
|
535
|
+
};
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/feedback/feedbackTool.d.ts
|
|
538
|
+
type FeedbackEnrichmentContext = {
|
|
539
|
+
type: FeedbackType;
|
|
540
|
+
severity?: FeedbackSeverity;
|
|
541
|
+
};
|
|
542
|
+
type FeedbackToolOptions = {
|
|
543
|
+
description?: string;
|
|
544
|
+
enrichment?: (ctx: FeedbackEnrichmentContext) => Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
545
|
+
extraLabels?: ReadonlyArray<string>;
|
|
546
|
+
name?: string;
|
|
547
|
+
provider: FeedbackProvider;
|
|
548
|
+
redactionPatterns?: ReadonlyArray<RedactionPattern>;
|
|
549
|
+
};
|
|
550
|
+
declare const PARAMETERS: z.ZodObject<{
|
|
551
|
+
description: z.ZodString;
|
|
552
|
+
severity: z.ZodOptional<z.ZodEnum<{
|
|
553
|
+
low: "low";
|
|
554
|
+
medium: "medium";
|
|
555
|
+
high: "high";
|
|
556
|
+
}>>;
|
|
557
|
+
title: z.ZodString;
|
|
558
|
+
type: z.ZodDefault<z.ZodEnum<{
|
|
559
|
+
bug: "bug";
|
|
560
|
+
feature: "feature";
|
|
561
|
+
feedback: "feedback";
|
|
562
|
+
question: "question";
|
|
563
|
+
}>>;
|
|
564
|
+
}, z.core.$strip>;
|
|
565
|
+
declare const createFeedbackTool: <T extends SessionAuth>(options: FeedbackToolOptions) => Tool<T, typeof PARAMETERS>;
|
|
566
|
+
//#endregion
|
|
567
|
+
//#region src/feedback/providers/github.d.ts
|
|
568
|
+
type GithubFeedbackOptions = {
|
|
569
|
+
baseUrl?: string;
|
|
570
|
+
defaultLabels?: ReadonlyArray<string>;
|
|
571
|
+
fetchImpl?: typeof fetch;
|
|
572
|
+
getToken: () => string | undefined;
|
|
573
|
+
repo: `${string}/${string}`;
|
|
574
|
+
};
|
|
575
|
+
declare const createGithubFeedback: (options: GithubFeedbackOptions) => FeedbackProvider;
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/feedback/providers/webhook.d.ts
|
|
578
|
+
type WebhookFeedbackOptions = {
|
|
579
|
+
fetchImpl?: typeof fetch;
|
|
580
|
+
headers?: Record<string, string>;
|
|
581
|
+
transform?: (payload: NormalizedFeedback) => unknown;
|
|
582
|
+
url: string;
|
|
583
|
+
};
|
|
584
|
+
declare const createWebhookFeedback: (options: WebhookFeedbackOptions) => FeedbackProvider;
|
|
585
|
+
//#endregion
|
|
442
586
|
//#region src/gateway/Gateway.d.ts
|
|
443
587
|
declare const createGateway: (config: GatewayConfig, telemetry: TelemetryCollector) => GatewayInstance;
|
|
444
588
|
//#endregion
|
|
@@ -448,4 +592,4 @@ declare const createGatewayManager: (telemetry: TelemetryCollector) => GatewayMa
|
|
|
448
592
|
//#region src/gateway/toolProxy.d.ts
|
|
449
593
|
declare const createProxiedTools: <T extends SessionAuth>(gateway: GatewayInstance, telemetry?: TelemetryCollector) => ReadonlyArray<Tool<T>>;
|
|
450
594
|
//#endregion
|
|
451
|
-
export { type ArtifactConfig, type AudioContent, type BackendAdapter, type BackendFactory, type BackendSession, type CaptureLevel, type Completion, type Content, type ContentResult, type Context, type DirectLogger, type DirectoryArtifact, type DynamicArtifact, type EnrichedErrorResponse, type ErrorCategory, type GatewayConfig, type GatewayInfo, type GatewayInstance, type GatewayManagerInstance, type GatewayStatus, type ImageContent, type InferSchemaOutput, type JsonFileTelemetryOptions, type Logger, NoopTelemetry, type Progress, type Prompt, type PromptArgument, type PromptResult, type Resource, type ResourceContent, type ResourceLink, type ResourceResult, type SchemaParams, type ServerCapabilities, type ServerConfig, type ServerHealth, 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, audioContent, classifyError, createCapabilitiesTool, createCompositeTelemetry, createConnectionsTool, createConsoleTelemetry, createDashboardArtifact, createDefaultLogger, createEnrichedError, createFastMCPBackend, createGateway, createGatewayManager, createHealthTool, createJsonFileTelemetry, createLogLayerTelemetry, createProxiedTools, createServer, imageContent, registerArtifacts, wrapPrompt, wrapResource, wrapTool };
|
|
595
|
+
export { type ArtifactAuthenticate, type ArtifactConfig, type AudioContent, 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 ImageContent, type InferSchemaOutput, type JsonFileTelemetryOptions, type Logger, NoopTelemetry, type NormalizedFeedback, type Progress, type Prompt, type PromptArgument, type PromptResult, type RedactionPattern, type RedactionResult, type Resource, type ResourceContent, type ResourceLink, type ResourceResult, 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, createCapabilitiesTool, createCompositeTelemetry, createConnectionsTool, createConsoleTelemetry, createDashboardArtifact, createDefaultLogger, createEnrichedError, createFastMCPBackend, createFeedbackTool, createGateway, createGatewayManager, createGithubFeedback, createHealthArtifact, createHealthDetailArtifact, createHealthTool, createInfoArtifact, createInfoTool, createJsonFileTelemetry, createLogLayerTelemetry, createProxiedTools, createServer, createWebhookFeedback, getRuntimeInfo, imageContent, readBuildInfoFromEnv, redact, registerArtifacts, resolveBuildInfo, wrapPrompt, wrapResource, wrapTool };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{Option as e,Ref as t,Try as n}from"functype";import{FastMCP as r,ServerState as i}from"fastmcp";import{Client as a}from"@modelcontextprotocol/sdk/client/index.js";import{StreamableHTTPClientTransport as o}from"@modelcontextprotocol/sdk/client/streamableHttp.js";import{createDirectConsoleLogger as s}from"functype-log";import{appendFile as c,mkdir as l,readFile as u}from"node:fs/promises";import{dirname as d,extname as f}from"node:path";const
|
|
1
|
+
import{Option as e,Ref as t,Try as n}from"functype";import{FastMCP as r,ServerState as i}from"fastmcp";import{Client as a}from"@modelcontextprotocol/sdk/client/index.js";import{StreamableHTTPClientTransport as o}from"@modelcontextprotocol/sdk/client/streamableHttp.js";import{createDirectConsoleLogger as s}from"functype-log";import{appendFile as c,mkdir as l,readFile as u}from"node:fs/promises";import{dirname as d,extname as f}from"node:path";import{z as p}from"zod";const m=e=>e.json({error:`Unauthorized`},401),h=e=>async(t,n)=>{if(!e)return m(t);try{await e(t.env?.incoming??t.req.raw)}catch{return m(t)}await n()},g=(e,t,n)=>{t.forEach(t=>{switch(t.protected&&e.use(t.path,h(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));break;case`directory`:break}})},_=(e,t,n)=>({handler:()=>{let r=v(e(),t(),n());return new Response(r,{headers:{"Content-Type":`text/html; charset=utf-8`}})},path:`/dashboard`,protected:!0,type:`dynamic`}),v=(e,t,n)=>`<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
@@ -32,7 +32,7 @@ import{Option as e,Ref as t,Try as n}from"functype";import{FastMCP as r,ServerSt
|
|
|
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">${y(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,5 +54,9 @@ import{Option as e,Ref as t,Try as n}from"functype";import{FastMCP as r,ServerSt
|
|
|
54
54
|
</div>`:``}
|
|
55
55
|
</div>
|
|
56
56
|
</body>
|
|
57
|
-
</html>`,
|
|
58
|
-
`)}\n`;await c(n.filePath,t)},s=()=>{i.get().isSome()||i.set(e(setTimeout(()=>{i.set(e.none()),o()},n.flushIntervalMs??1e3)))};return{flush:async()=>{i.get().match({None:()=>void 0,Some:e=>clearTimeout(e)}),i.set(e.none()),await o()},recordEvent:e=>{r.update(t=>[...t,e]),s()}}},
|
|
57
|
+
</html>`,y=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`},b=`/health`,x=`/health/detail`,S=(e,t=b)=>({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`}),C=(e,t=x)=>({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`}),w=`/info`,T=(e,t=w)=>({handler:()=>new Response(JSON.stringify(e()),{headers:{"Content-Type":`application/json`},status:200}),path:t,protected:!0,type:`dynamic`}),ee=e=>{switch(e){case i.Running:return`running`;case i.Error:return`error`;default:return`stopped`}},E=(e,t)=>{let n=new r({...t,authenticate:e.authenticate,instructions:e.instructions,logger:e.logger,name:e.name,version:e.version});return{get serverState(){return ee(n.serverState)},get sessions(){return n.sessions.map(e=>({sessionId:e.sessionId}))},addPrompt:e=>{n.addPrompt(e)},addResource:e=>{n.addResource(e)},addResourceTemplate:(...e)=>{n.addResourceTemplate(...e)},addTool:e=>{n.addTool(e)},getApp:()=>n.getApp(),on:(e,t)=>{n.on(e,t)},removePrompt:e=>n.removePrompt(e),removeResource:e=>n.removeResource(e),removeTool:e=>n.removeTool(e),start:async e=>{if(!e){await n.start();return}e.transportType===`httpStream`?await n.start({httpStream:e.httpStream,transportType:`httpStream`}):await n.start({transportType:`stdio`})},stop:()=>n.stop()}},D=e=>{let t=process.env[e];return t&&t.length>0?t:void 0},O=()=>({branch:D(`SOMAMCP_BUILD_BRANCH`),commit:D(`SOMAMCP_BUILD_COMMIT`),date:D(`SOMAMCP_BUILD_DATE`),environment:D(`SOMAMCP_ENVIRONMENT`)}),k=e=>{let t=O();return{branch:e?.branch??t.branch,commit:e?.commit??t.commit,date:e?.date??t.date,environment:e?.environment??t.environment}},te=()=>({arch:process.arch,nodeVersion:process.version,platform:process.platform}),A=(r,i)=>{let s=t(e.none()),c=t(e.none()),l=t([]),u=t(`disconnected`),d=t(e.none()),f=r.name??r.id,p=()=>{let t=r.reconnectIntervalMs??5e3;d.set(e(setTimeout(()=>{m.connect()},t)))},m={get config(){return r},get info(){return{id:r.id,name:f,remoteTools:l.get().map(e=>({description:e.description,name:e.name})),status:u.get(),url:r.url}},get name(){return f},get status(){return u.get()},get tools(){return l.get()},async callTool(e,t={}){let n=s.get();if(n.isEmpty||u.get()!==`connected`)throw Error(`Gateway ${r.id} is not connected`);return n.orThrow(Error(`Gateway ${r.id} is not connected`)).callTool({arguments:t,name:e})},async connect(){if(u.get()===`connected`)return;u.set(`connecting`);let t=Date.now();(await n.fromPromise((async()=>{let t=new o(new URL(r.url));c.set(e(t));let n=new a({name:`soma-gateway-${r.id}`,version:`1.0.0`});s.set(e(n)),await n.connect(t);let i=await n.listTools();l.set(i.tools)})())).fold(e=>{u.set(`error`),i.recordEvent({data:{id:r.id,url:r.url},error:e instanceof Error?e.message:String(e),name:f,timestamp:t,type:`gateway.error`}),r.reconnect!==!1&&p()},()=>{u.set(`connected`),i.recordEvent({data:{id:r.id,remoteToolCount:l.get().length,url:r.url},durationMs:Date.now()-t,name:f,timestamp:t,type:`gateway.connect`})})},async disconnect(){d.get().match({None:()=>void 0,Some:e=>clearTimeout(e)}),d.set(e.none());let t=s.get();t.isSome()&&await n.fromPromise(t.value.close()),s.set(e.none()),c.set(e.none()),l.set([]),u.set(`disconnected`),i.recordEvent({data:{id:r.id},name:f,timestamp:Date.now(),type:`gateway.disconnect`})}};return m},j=t=>{let n=new Map;return{get connectedCount(){return[...n.values()].filter(e=>e.status===`connected`).length},get totalCount(){return n.size},add(e){let r=A(e,t);return n.set(e.id,r),r},async connectAll(){await Promise.allSettled([...n.values()].map(e=>e.connect()))},async disconnectAll(){await Promise.allSettled([...n.values()].map(e=>e.disconnect()))},get(t){return e(n.get(t))},getAll(){return[...n.values()]},getInfoAll(){return[...n.values()].map(e=>e.info)}}},M=e=>({flush:async()=>{await Promise.all(e.map(e=>e.flush?e.flush():Promise.resolve()))},recordEvent:t=>{e.forEach(e=>e.recordEvent(t))}}),N=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)}}),P=(e=`[soma]`)=>N(s({prefix:e})),F=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`},I=(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`]}},L=(e,t)=>{let n=F(e),r=e instanceof Error?e.message:String(e),i=I(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}},R=n=>{let r=t([]),i=t(e.none()),a=t(!1),o=async()=>{let e=r.getAndSet([]);if(e.length===0)return;a.get()||(await l(d(n.filePath),{recursive:!0}),a.set(!0));let t=`${e.map(e=>JSON.stringify(e)).join(`
|
|
58
|
+
`)}\n`;await c(n.filePath,t)},s=()=>{i.get().isSome()||i.set(e(setTimeout(()=>{i.set(e.none()),o()},n.flushIntervalMs??1e3)))};return{flush:async()=>{i.get().match({None:()=>void 0,Some:e=>clearTimeout(e)}),i.set(e.none()),await o()},recordEvent:e=>{r.update(t=>[...t,e]),s()}}},z={flush:()=>Promise.resolve(),recordEvent:()=>{}},B=(e,t)=>!t||t.length===0?e:Object.fromEntries(Object.entries(e).map(([e,n])=>[e,t.includes(e)?`[REDACTED]`:n])),V=(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},H=e=>n.fromPromise(e),U=(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`?B(r,n?.redactInputFields):void 0;return(await H(e.execute(r,i))).fold(n=>{let r=L(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`?V(r,n?.maxOutputSize):void 0,requestId:s,sessionId:c,timestamp:o,type:`tool.execute`}),r))}}),W=(e,t)=>({...e,load:async n=>{let r=Date.now();return(await H(e.load(n))).fold(n=>{let i=L(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))}}),G=(e,t)=>({...e,load:async(n,r)=>{let i=Date.now();return(await H(e.load(n,r))).fold(n=>{let r=L(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))}}),K=e=>n.fromPromise(e),q=(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 K(e.callTool(r.name,i))).fold(n=>{let s=L(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}`}))},J=(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}),Y=e=>{let{artifacts:n,backendOptions:r,build:i,enableDashboard:a,enableHealthEndpoint:o,enableInfoEndpoint:s,enableIntrospection:c,gateways:l,healthDetailPath:u,healthPath:d,infoPath:f,introspectionPrefix:p,logLayer:m,telemetry:h,...v}=e,y=v.name,b=v.version,x=k(i),w=te(),ee=p??``,D=h??(m?N(m):z),O=E(v,r),A=j(D),M=[],P=[],F=[],I=t(0);l?.forEach(e=>A.add(e));let L=()=>{let e=I.get(),t=e>0?Date.now()-e:0;return{activeSessions:O.sessions.length,gateways:{connected:A.connectedCount,total:A.totalCount},name:y,startedAt:e,status:O.serverState,uptime:t}},R=()=>({prompts:F,resources:P,tools:M}),B=()=>({build:x,capabilities:{prompts:F.length,resources:P.length,tools:M.length},name:y,runtime:w,version:b});c!==!1&&O.addTool(J(()=>B(),`${ee}info`));let V=[...n??[]];o!==!1&&(V.push(S(()=>L(),d)),V.push(C(()=>L(),u))),s!==!1&&V.push(T(()=>B(),f)),a!==!1&&V.push(_(()=>L(),()=>R(),()=>A.getInfoAll())),V.length>0&&g(O.getApp(),V,v.authenticate),O.on(`connect`,({session:e})=>{D.recordEvent({data:{sessionId:e.sessionId},name:y,timestamp:Date.now(),type:`session.connect`})}),O.on(`disconnect`,({session:e})=>{D.recordEvent({data:{sessionId:e.sessionId},name:y,timestamp:Date.now(),type:`session.disconnect`})});let H=e=>{let t=U(e,D,`captureConfig`in e?e.captureConfig:void 0);O.addTool(t),M.push({description:e.description,name:e.name})},K=e=>{let t=W(e,D);O.addResource(t),P.push({description:e.description,name:e.name,uri:e.uri})},Y=e=>{let t=G(e,D);O.addPrompt(t),F.push({description:e.description,name:e.name})};return{get name(){return y},get serverState(){return O.serverState},get sessions(){return O.sessions},addPrompt:Y,addPrompts:e=>{e.forEach(Y)},addResource:K,addResources:e=>{e.forEach(K)},addResourceTemplate:(...e)=>{O.addResourceTemplate(...e)},addTool:H,addTools:e=>{e.forEach(e=>H(e))},getApp:()=>O.getApp(),getCapabilities:R,getGatewayManager:()=>A,getHealth:L,getInfo:B,removePrompt:e=>{O.removePrompt(e)},removeResource:e=>{O.removeResource(e)},removeTool:e=>{O.removeTool(e)},async start(e){I.set(Date.now()),D.recordEvent({name:y,timestamp:I.get(),type:`server.start`}),await O.start(e),await A.connectAll(),A.getAll().filter(e=>e.status===`connected`&&e.config.proxyTools!==!1).forEach(e=>{q(e,D).forEach(e=>O.addTool(e))})},async stop(){await A.disconnectAll(),await O.stop(),D.recordEvent({name:y,timestamp:Date.now(),type:`server.stop`}),D.flush&&await D.flush()}}};var ne=class extends Error{constructor(e){super(e),this.name=`UserError`}};const re={".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`},ie={".aac":`audio/aac`,".flac":`audio/flac`,".m4a":`audio/mp4`,".mp3":`audio/mpeg`,".ogg":`audio/ogg`,".opus":`audio/opus`,".wav":`audio/wav`,".weba":`audio/webm`},X=(e,t,n)=>t[f(new URL(e,`file:///`).pathname).toLowerCase()]??n,Z=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 u(e.path),source:e.path}:{data:e.buffer}},ae=async e=>{let{data:t,source:n}=await Z(e,`image`),r=n?X(n,re,`image/png`):`image/png`;return{data:t.toString(`base64`),mimeType:r,type:`image`}},oe=async e=>{let{data:t,source:n}=await Z(e,`audio`),r=n?X(n,ie,`audio/mpeg`):`audio/mpeg`;return{data:t.toString(`base64`),mimeType:r,type:`audio`}},se=e=>s({prefix:`[${e}]`}).withContext({cell:e}),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,t=Q)=>{let{counts:n,text:r}=t.reduce((e,t)=>{let n=Array.from(e.text.matchAll(t.pattern)).length;return n===0?e:{counts:new Map(e.counts).set(t.name,n),text:e.text.replace(t.pattern,t.replacement??`[REDACTED]`)}},{counts:new Map,text:e}),i=Array.from(n.entries()).map(([e,t])=>({count:t,name:e}));return{matches:i,redacted:i.length>0,text:r}},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(`
|
|
59
|
+
`),fe=p.object({description:p.string().min(10,`Provide enough context to reproduce (>=10 chars)`).max(8e3).describe(`Detailed description. NO secrets, customer data, or internal URLs.`),severity:p.enum([`low`,`medium`,`high`]).optional().describe(`Severity hint (optional)`),title:p.string().min(5,`Title must be at least 5 characters`).max(200).describe(`Brief, public-safe summary`),type:p.enum([`bug`,`feature`,`feedback`,`question`]).default(`feedback`).describe(`What kind of report this is`)}),pe=(e,t)=>{if(!e.redacted&&!t.redacted)return;let n=new Map;for(let r of[...e.matches,...t.matches])n.set(r.name,(n.get(r.name)??0)+r.count);return[`⚠️ Automatic redactions applied:`,...Array.from(n.entries()).map(([e,t])=>`- ${e}: ${String(t)}`)].join(`
|
|
60
|
+
`)},me=e=>{if(!(!e||Object.keys(e).length===0))return[`---`,`**Context (auto-collected):**`,"```json",JSON.stringify(e,null,2),"```"].join(`
|
|
61
|
+
`)},he=(e,t,n,r,i)=>{let a=[];return a.push(`**Type:** ${t}`),n&&a.push(`**Severity:** ${n}`),a.push(``),a.push(e),r&&(a.push(``),a.push(r)),i&&(a.push(``),a.push(i)),a.join(`
|
|
62
|
+
`)},ge=e=>{let t=e.redactionPatterns??Q;return{annotations:{destructiveHint:!1,readOnlyHint:!1},description:e.description??de,execute:async n=>{let r=$(n.title,t),i=$(n.description,t),a=e.enrichment?await e.enrichment({severity:n.severity,type:n.type}):void 0,o=pe(r,i),s=me(a),c=he(i.text,n.type,n.severity,o,s),l=[n.type,...n.severity?[`severity:${n.severity}`]:[],...e.extraLabels??[]],u=await e.provider.submit({body:c,labels:l,title:r.text});return u.success?JSON.stringify({id:u.id,provider:e.provider.name,redacted:o!==void 0,redactionDetails:[...r.matches,...i.matches],success:!0,url:u.url},null,2):JSON.stringify({error:u.error,redacted:o!==void 0,success:!1},null,2)},name:e.name??`report_feedback`,parameters:fe}},_e=e=>{let t=e.baseUrl??`https://api.github.com`,n=e.fetchImpl??fetch;return{name:`github`,submit:async r=>{let i=e.getToken();if(!i)return{error:`No GitHub token configured`,success:!1};let a=[...e.defaultLabels??[],...r.labels],o=JSON.stringify({body:r.body,labels:Array.from(new Set(a)),title:r.title});try{let r=await n(`${t}/repos/${e.repo}/issues`,{body:o,headers:{Accept:`application/vnd.github+json`,Authorization:`Bearer ${i}`,"Content-Type":`application/json`,"X-GitHub-Api-Version":`2022-11-28`},method:`POST`}),a=await r.json().catch(()=>({}));return r.ok?{id:a.number===void 0?void 0:String(a.number),success:!0,url:a.html_url}:{error:`GitHub API ${String(r.status)}: ${a.message??r.statusText}`,success:!1}}catch(e){return{error:e instanceof Error?e.message:`Unknown error submitting to GitHub`,success:!1}}}}},ve=e=>{let t=e.fetchImpl??fetch,n=e.transform??(e=>e);return{name:`webhook`,submit:async r=>{try{let i=await t(e.url,{body:JSON.stringify(n(r)),headers:{"Content-Type":`application/json`,...e.headers??{}},method:`POST`});if(!i.ok){let e=await i.text().catch(()=>``);return{error:`Webhook ${String(i.status)}: ${e||i.statusText}`,success:!1}}return{success:!0}}catch(e){return{error:e instanceof Error?e.message:`Unknown error posting to webhook`,success:!1}}}}};export{x as DEFAULT_HEALTH_DETAIL_PATH,b as DEFAULT_HEALTH_PATH,w as DEFAULT_INFO_PATH,Q as DEFAULT_REDACTION_PATTERNS,z as NoopTelemetry,ne as UserError,oe as audioContent,F as classifyError,ce as createCapabilitiesTool,M as createCompositeTelemetry,le as createConnectionsTool,P as createConsoleTelemetry,_ as createDashboardArtifact,se as createDefaultLogger,L as createEnrichedError,E as createFastMCPBackend,ge as createFeedbackTool,A as createGateway,j as createGatewayManager,_e as createGithubFeedback,S as createHealthArtifact,C as createHealthDetailArtifact,ue as createHealthTool,T as createInfoArtifact,J as createInfoTool,R as createJsonFileTelemetry,N as createLogLayerTelemetry,q as createProxiedTools,Y as createServer,ve as createWebhookFeedback,te as getRuntimeInfo,ae as imageContent,O as readBuildInfoFromEnv,$ as redact,g as registerArtifacts,k as resolveBuildInfo,G as wrapPrompt,W as wrapResource,U as wrapTool};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "somamcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Framework for building MCP servers with telemetry, introspection, and backend abstraction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,17 +55,17 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
57
57
|
"fastmcp": "^4.0.1",
|
|
58
|
-
"functype": "^0.60.
|
|
59
|
-
"functype-log": "^0.60.
|
|
60
|
-
"hono": "^4.12.
|
|
58
|
+
"functype": "^0.60.7",
|
|
59
|
+
"functype-log": "^0.60.7",
|
|
60
|
+
"hono": "^4.12.22",
|
|
61
61
|
"zod": "^4.4.3"
|
|
62
62
|
},
|
|
63
63
|
"prettier": "ts-builds/prettier",
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@types/node": "^24.12.
|
|
65
|
+
"@types/node": "^24.12.4",
|
|
66
66
|
"get-port-please": "^3.2.0",
|
|
67
|
-
"ts-builds": "^2.
|
|
68
|
-
"tsdown": "^0.
|
|
67
|
+
"ts-builds": "^2.8.1",
|
|
68
|
+
"tsdown": "^0.22.0"
|
|
69
69
|
},
|
|
70
70
|
"packageManager": "pnpm@10.33.3+sha512.a19744364a7e248b92657a4ca5973f9354d21caf982579674b1c539f32c7420c47138ad8b1254df07aba9bc782d9b3029e3db34d5dbff974326eb74dac8ff489"
|
|
71
71
|
}
|