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 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
- - **Auto-introspection** — `soma_health`, `soma_capabilities`, `soma_connections` tools registered by default
15
- - **HTTP artifacts** — mount static or dynamic routes on the embedded [Hono](https://hono.dev) app, including an auto-generated dashboard
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, health endpoints) flow through `backendOptions`:
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 StaticArtifact = {
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>) => void;
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 p=(e,t)=>{t.forEach(t=>{switch(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}})},m=(e,t,n)=>({handler:()=>{let r=h(e(),t(),n());return new Response(r,{headers:{"Content-Type":`text/html; charset=utf-8`}})},path:`/dashboard`,type:`dynamic`}),h=(e,t,n)=>`<!DOCTYPE html>
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">${g(e.uptime)}</span></div>
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>`,g=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=>{switch(e){case i.Running:return`running`;case i.Error:return`error`;default:return`stopped`}},v=(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 _(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()}},y=(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},b=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=y(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)}}},x=e=>({flush:async()=>{await Promise.all(e.map(e=>e.flush?e.flush():Promise.resolve()))},recordEvent:t=>{e.forEach(e=>e.recordEvent(t))}}),S=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)}}),C=(e=`[soma]`)=>S(s({prefix:e})),w=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`},T=(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`]}},E=(e,t)=>{let n=w(e),r=e instanceof Error?e.message:String(e),i=T(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}},D=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()}}},O={flush:()=>Promise.resolve(),recordEvent:()=>{}},k=(e,t)=>!t||t.length===0?e:Object.fromEntries(Object.entries(e).map(([e,n])=>[e,t.includes(e)?`[REDACTED]`:n])),A=(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=>n.fromPromise(e),M=(e,t,n)=>({...e,execute:async(r,i)=>{let a=n?.captureLevel??`full`;if(a===`none`)return e.execute(r,i);let o=Date.now(),{requestId:s,sessionId:c}=i,l=a===`full`?k(r,n?.redactInputFields):void 0;return(await j(e.execute(r,i))).fold(n=>{let r=E(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`?A(r,n?.maxOutputSize):void 0,requestId:s,sessionId:c,timestamp:o,type:`tool.execute`}),r))}}),N=(e,t)=>({...e,load:async n=>{let r=Date.now();return(await j(e.load(n))).fold(n=>{let i=E(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))}}),P=(e,t)=>({...e,load:async(n,r)=>{let i=Date.now();return(await j(e.load(n,r))).fold(n=>{let r=E(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))}}),F=e=>n.fromPromise(e),I=(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 F(e.callTool(r.name,i))).fold(n=>{let s=E(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}`}))},L=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`}),R=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`}),z=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`}),B=e=>{let{artifacts:n,backendOptions:r,enableDashboard:i,enableIntrospection:a,gateways:o,logLayer:s,telemetry:c,...l}=e,u=l.name,d=c??(s?S(s):O),f=v(l,r),h=b(d),g=[],_=[],y=[],x=t(0);o?.forEach(e=>h.add(e));let C=()=>{let e=x.get(),t=e>0?Date.now()-e:0;return{activeSessions:f.sessions.length,gateways:{connected:h.connectedCount,total:h.totalCount},name:u,startedAt:e,status:f.serverState,uptime:t}},w=()=>({prompts:y,resources:_,tools:g});a!==!1&&(f.addTool(z(()=>C())),f.addTool(L(()=>w())),f.addTool(R(()=>h.getInfoAll())));let T=[...n??[]];i!==!1&&T.push(m(()=>C(),()=>w(),()=>h.getInfoAll())),T.length>0&&p(f.getApp(),T),f.on(`connect`,({session:e})=>{d.recordEvent({data:{sessionId:e.sessionId},name:u,timestamp:Date.now(),type:`session.connect`})}),f.on(`disconnect`,({session:e})=>{d.recordEvent({data:{sessionId:e.sessionId},name:u,timestamp:Date.now(),type:`session.disconnect`})});let E=e=>{let t=M(e,d,`captureConfig`in e?e.captureConfig:void 0);f.addTool(t),g.push({description:e.description,name:e.name})},D=e=>{let t=N(e,d);f.addResource(t),_.push({description:e.description,name:e.name,uri:e.uri})},k=e=>{let t=P(e,d);f.addPrompt(t),y.push({description:e.description,name:e.name})};return{get name(){return u},get serverState(){return f.serverState},get sessions(){return f.sessions},addPrompt:k,addPrompts:e=>{e.forEach(k)},addResource:D,addResources:e=>{e.forEach(D)},addResourceTemplate:(...e)=>{f.addResourceTemplate(...e)},addTool:E,addTools:e=>{e.forEach(e=>E(e))},getApp:()=>f.getApp(),getCapabilities:w,getGatewayManager:()=>h,getHealth:C,removePrompt:e=>{f.removePrompt(e)},removeResource:e=>{f.removeResource(e)},removeTool:e=>{f.removeTool(e)},async start(e){x.set(Date.now()),d.recordEvent({name:u,timestamp:x.get(),type:`server.start`}),await f.start(e),await h.connectAll(),h.getAll().filter(e=>e.status===`connected`&&e.config.proxyTools!==!1).forEach(e=>{I(e,d).forEach(e=>f.addTool(e))})},async stop(){await h.disconnectAll(),await f.stop(),d.recordEvent({name:u,timestamp:Date.now(),type:`server.stop`}),d.flush&&await d.flush()}}};var V=class extends Error{constructor(e){super(e),this.name=`UserError`}};const H={".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`},U={".aac":`audio/aac`,".flac":`audio/flac`,".m4a":`audio/mp4`,".mp3":`audio/mpeg`,".ogg":`audio/ogg`,".opus":`audio/opus`,".wav":`audio/wav`,".weba":`audio/webm`},W=(e,t,n)=>t[f(new URL(e,`file:///`).pathname).toLowerCase()]??n,G=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}},K=async e=>{let{data:t,source:n}=await G(e,`image`),r=n?W(n,H,`image/png`):`image/png`;return{data:t.toString(`base64`),mimeType:r,type:`image`}},q=async e=>{let{data:t,source:n}=await G(e,`audio`),r=n?W(n,U,`audio/mpeg`):`audio/mpeg`;return{data:t.toString(`base64`),mimeType:r,type:`audio`}},J=e=>s({prefix:`[${e}]`}).withContext({cell:e});export{O as NoopTelemetry,V as UserError,q as audioContent,w as classifyError,L as createCapabilitiesTool,x as createCompositeTelemetry,R as createConnectionsTool,C as createConsoleTelemetry,m as createDashboardArtifact,J as createDefaultLogger,E as createEnrichedError,v as createFastMCPBackend,y as createGateway,b as createGatewayManager,z as createHealthTool,D as createJsonFileTelemetry,S as createLogLayerTelemetry,I as createProxiedTools,B as createServer,K as imageContent,p as registerArtifacts,P as wrapPrompt,N as wrapResource,M as wrapTool};
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.6",
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.3",
59
- "functype-log": "^0.60.0",
60
- "hono": "^4.12.16",
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.2",
65
+ "@types/node": "^24.12.4",
66
66
  "get-port-please": "^3.2.0",
67
- "ts-builds": "^2.7.2",
68
- "tsdown": "^0.21.10"
67
+ "ts-builds": "^2.8.1",
68
+ "tsdown": "^0.22.0"
69
69
  },
70
70
  "packageManager": "pnpm@10.33.3+sha512.a19744364a7e248b92657a4ca5973f9354d21caf982579674b1c539f32c7420c47138ad8b1254df07aba9bc782d9b3029e3db34d5dbff974326eb74dac8ff489"
71
71
  }