somamcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Jordan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,444 @@
1
+ import { DirectLogger, DirectLogger as DirectLogger$1 } from "functype-log";
2
+ import { Context as Context$1, Hono } from "hono";
3
+ import { Tool as Tool$1 } from "@modelcontextprotocol/sdk/types.js";
4
+
5
+ //#region src/artifacts/types.d.ts
6
+ type StaticArtifact = {
7
+ contentType: string;
8
+ content: string;
9
+ path: string;
10
+ type: "static";
11
+ };
12
+ type DynamicArtifact = {
13
+ handler: (c: Context$1) => Response | Promise<Response>;
14
+ path: string;
15
+ type: "dynamic";
16
+ };
17
+ type DirectoryArtifact = {
18
+ directory: string;
19
+ path: string;
20
+ type: "directory";
21
+ };
22
+ type ArtifactConfig = DirectoryArtifact | DynamicArtifact | StaticArtifact;
23
+ //#endregion
24
+ //#region src/types/core.d.ts
25
+ type SessionAuth = Record<string, unknown> | undefined;
26
+ /**
27
+ * Structural type compatible with StandardSchemaV1 (zod, valibot, etc.)
28
+ * We define this structurally to avoid a direct dependency on @standard-schema/spec.
29
+ */
30
+ type SchemaParams = {
31
+ readonly "~standard": {
32
+ readonly version: 1;
33
+ readonly vendor: string;
34
+ readonly types?: {
35
+ readonly input: unknown;
36
+ readonly output: unknown;
37
+ } | undefined;
38
+ readonly validate: (value: unknown) => {
39
+ value: unknown;
40
+ } | {
41
+ issues: ReadonlyArray<{
42
+ message: string;
43
+ }>;
44
+ };
45
+ };
46
+ };
47
+ type InferSchemaOutput<P extends SchemaParams> = NonNullable<P["~standard"]["types"]>["output"] extends infer O ? [O] extends [never] ? unknown : O : unknown;
48
+ type TextContent = {
49
+ text: string;
50
+ type: "text";
51
+ };
52
+ type ImageContent = {
53
+ data: string;
54
+ mimeType: string;
55
+ type: "image";
56
+ };
57
+ type AudioContent = {
58
+ data: string;
59
+ mimeType: string;
60
+ type: "audio";
61
+ };
62
+ type ResourceContent = {
63
+ resource: {
64
+ blob?: string;
65
+ mimeType?: string;
66
+ text?: string;
67
+ uri: string;
68
+ };
69
+ type: "resource";
70
+ };
71
+ type ResourceLink = {
72
+ mimeType?: string;
73
+ name?: string;
74
+ type: "resource_link";
75
+ uri: string;
76
+ };
77
+ type Content = AudioContent | ImageContent | ResourceContent | ResourceLink | TextContent;
78
+ type ContentResult = {
79
+ content: Content[];
80
+ isError?: boolean;
81
+ };
82
+ type Progress = {
83
+ progress: number;
84
+ total?: number;
85
+ };
86
+ type Context<T extends SessionAuth = SessionAuth> = {
87
+ client: {
88
+ version: unknown;
89
+ };
90
+ log: {
91
+ debug: (message: string, data?: unknown) => void;
92
+ error: (message: string, data?: unknown) => void;
93
+ info: (message: string, data?: unknown) => void;
94
+ warn: (message: string, data?: unknown) => void;
95
+ };
96
+ reportProgress: (progress: Progress) => Promise<void>;
97
+ requestId?: string;
98
+ session: T | undefined;
99
+ sessionId?: string;
100
+ streamContent: (content: Content | Content[]) => Promise<void>;
101
+ };
102
+ type ToolAnnotations = {
103
+ destructiveHint?: boolean;
104
+ idempotentHint?: boolean;
105
+ openWorldHint?: boolean;
106
+ readOnlyHint?: boolean;
107
+ streamingHint?: boolean;
108
+ title?: string;
109
+ };
110
+ type Tool<T extends SessionAuth = SessionAuth, P extends SchemaParams = SchemaParams> = {
111
+ _meta?: {
112
+ [key: string]: unknown;
113
+ ui?: {
114
+ resourceUri?: string;
115
+ };
116
+ };
117
+ annotations?: ToolAnnotations;
118
+ canAccess?: (auth: T) => boolean;
119
+ description?: string;
120
+ execute: (args: InferSchemaOutput<P>, context: Context<T>) => Promise<Content | ContentResult | string | void>;
121
+ name: string;
122
+ outputSchema?: SchemaParams;
123
+ parameters?: P;
124
+ timeoutMs?: number;
125
+ };
126
+ type Completion = {
127
+ hasMore?: boolean;
128
+ total?: number;
129
+ values: string[];
130
+ };
131
+ type ResourceResult = {
132
+ blob: string;
133
+ mimeType?: string;
134
+ uri?: string;
135
+ } | {
136
+ mimeType?: string;
137
+ text: string;
138
+ uri?: string;
139
+ };
140
+ type Resource<T extends SessionAuth = SessionAuth> = {
141
+ complete?: (name: string, value: string, auth?: T) => Promise<Completion>;
142
+ description?: string;
143
+ load: (auth?: T) => Promise<ResourceResult | ResourceResult[]>;
144
+ mimeType?: string;
145
+ name: string;
146
+ uri: string;
147
+ };
148
+ type PromptArgument<T extends SessionAuth = SessionAuth> = Readonly<{
149
+ complete?: (value: string, auth?: T) => Promise<Completion>;
150
+ description?: string;
151
+ enum?: string[];
152
+ name: string;
153
+ required?: boolean;
154
+ }>;
155
+ type PromptResult = {
156
+ messages: unknown[];
157
+ } | string;
158
+ type Prompt<T extends SessionAuth = SessionAuth, Args extends PromptArgument<T>[] = PromptArgument<T>[]> = {
159
+ arguments?: Args;
160
+ complete?: (name: string, value: string, auth?: T) => Promise<Completion>;
161
+ description?: string;
162
+ load: (args: Record<string, string | undefined>, auth?: T) => Promise<PromptResult>;
163
+ name: string;
164
+ };
165
+ type ServerStatus = "error" | "running" | "stopped";
166
+ declare class UserError extends Error {
167
+ constructor(message: string);
168
+ }
169
+ //#endregion
170
+ //#region src/types/server.d.ts
171
+ type Logger = {
172
+ debug: (...args: unknown[]) => void;
173
+ error: (...args: unknown[]) => void;
174
+ info: (...args: unknown[]) => void;
175
+ log: (...args: unknown[]) => void;
176
+ warn: (...args: unknown[]) => void;
177
+ };
178
+ type ServerConfig<T extends SessionAuth = SessionAuth> = {
179
+ authenticate?: (request: unknown) => Promise<T>;
180
+ instructions?: string;
181
+ logger?: Logger;
182
+ name: string;
183
+ version: `${number}.${number}.${number}`;
184
+ };
185
+ type TransportConfig = {
186
+ transportType: "stdio";
187
+ } | {
188
+ httpStream: {
189
+ enableJsonResponse?: boolean;
190
+ endpoint?: `/${string}`;
191
+ host?: string;
192
+ port: number;
193
+ };
194
+ transportType: "httpStream";
195
+ };
196
+ //#endregion
197
+ //#region src/backend/adapter.d.ts
198
+ type BackendSession<T extends SessionAuth = SessionAuth> = {
199
+ sessionId?: string;
200
+ auth?: T;
201
+ };
202
+ type BackendEvents<T extends SessionAuth> = {
203
+ connect: (event: {
204
+ session: BackendSession<T>;
205
+ }) => void;
206
+ disconnect: (event: {
207
+ session: BackendSession<T>;
208
+ }) => void;
209
+ };
210
+ type BackendAdapter<T extends SessionAuth = SessionAuth> = {
211
+ readonly serverState: ServerStatus;
212
+ readonly sessions: ReadonlyArray<BackendSession<T>>;
213
+ addPrompt: <Args extends PromptArgument<T>[]>(prompt: Prompt<T, Args>) => void;
214
+ addResource: (resource: Resource<T>) => void;
215
+ addResourceTemplate: (...args: ReadonlyArray<unknown>) => void;
216
+ addTool: <P extends SchemaParams>(tool: Tool<T, P>) => void;
217
+ getApp: () => Hono;
218
+ on: <E extends keyof BackendEvents<T>>(event: E, handler: BackendEvents<T>[E]) => void;
219
+ removePrompt: (name: string) => void;
220
+ removeResource: (name: string) => void;
221
+ removeTool: (name: string) => void;
222
+ start: (transport?: TransportConfig) => Promise<void>;
223
+ stop: () => Promise<void>;
224
+ };
225
+ type BackendFactory<T extends SessionAuth = SessionAuth> = (config: {
226
+ authenticate?: (request: unknown) => Promise<T>;
227
+ instructions?: string;
228
+ name: string;
229
+ version: `${number}.${number}.${number}`;
230
+ }, backendOptions?: Record<string, unknown>) => BackendAdapter<T>;
231
+ //#endregion
232
+ //#region src/gateway/types.d.ts
233
+ type GatewayConfig = {
234
+ id: string;
235
+ name?: string;
236
+ proxyTools?: boolean;
237
+ reconnect?: boolean;
238
+ reconnectIntervalMs?: number;
239
+ toolPrefix?: string;
240
+ url: string;
241
+ };
242
+ type GatewayStatus = "connected" | "connecting" | "disconnected" | "error";
243
+ type GatewayInfo = {
244
+ id: string;
245
+ name: string;
246
+ remoteTools: ReadonlyArray<{
247
+ description?: string;
248
+ name: string;
249
+ }>;
250
+ status: GatewayStatus;
251
+ url: string;
252
+ };
253
+ type GatewayInstance = {
254
+ readonly config: GatewayConfig;
255
+ readonly info: GatewayInfo;
256
+ readonly name: string;
257
+ readonly status: GatewayStatus;
258
+ readonly tools: ReadonlyArray<Tool$1>;
259
+ callTool: (name: string, args?: Record<string, unknown>) => Promise<unknown>;
260
+ connect: () => Promise<void>;
261
+ disconnect: () => Promise<void>;
262
+ };
263
+ type GatewayManagerInstance = {
264
+ readonly connectedCount: number;
265
+ readonly totalCount: number;
266
+ add: (config: GatewayConfig) => GatewayInstance;
267
+ connectAll: () => Promise<void>;
268
+ disconnectAll: () => Promise<void>;
269
+ get: (id: string) => GatewayInstance | undefined;
270
+ getAll: () => ReadonlyArray<GatewayInstance>;
271
+ getInfoAll: () => ReadonlyArray<GatewayInfo>;
272
+ };
273
+ //#endregion
274
+ //#region src/telemetry/TelemetryCollector.d.ts
275
+ type TelemetryEventType = "server.start" | "server.stop" | "tool.execute" | "tool.error" | "resource.load" | "resource.error" | "prompt.load" | "prompt.error" | "gateway.connect" | "gateway.disconnect" | "gateway.error" | "session.connect" | "session.disconnect";
276
+ type ErrorCategory = "validation" | "timeout" | "gateway" | "internal" | "auth" | "not_found";
277
+ type CaptureLevel = "full" | "metadata" | "none";
278
+ type ToolCaptureConfig = {
279
+ captureLevel?: CaptureLevel;
280
+ maxOutputSize?: number;
281
+ redactInputFields?: ReadonlyArray<string>;
282
+ };
283
+ type TelemetryEvent = {
284
+ data?: Record<string, unknown>;
285
+ durationMs?: number;
286
+ error?: string;
287
+ errorCategory?: ErrorCategory;
288
+ errorStack?: string;
289
+ input?: Record<string, unknown>;
290
+ name: string;
291
+ output?: unknown;
292
+ requestId?: string;
293
+ sessionId?: string;
294
+ timestamp: number;
295
+ type: TelemetryEventType;
296
+ };
297
+ type TelemetryCollector = {
298
+ flush?: () => Promise<void>;
299
+ recordEvent: (event: TelemetryEvent) => void;
300
+ };
301
+ //#endregion
302
+ //#region src/types.d.ts
303
+ type SomaServerOptions<T extends SessionAuth = SessionAuth> = ServerConfig<T> & {
304
+ artifacts?: ArtifactConfig[];
305
+ backendOptions?: Record<string, unknown>;
306
+ enableDashboard?: boolean;
307
+ enableIntrospection?: boolean;
308
+ gateways?: GatewayConfig[];
309
+ logLayer?: DirectLogger$1;
310
+ telemetry?: TelemetryCollector;
311
+ };
312
+ type ServerHealth = {
313
+ activeSessions: number;
314
+ gateways: {
315
+ connected: number;
316
+ total: number;
317
+ };
318
+ name: string;
319
+ startedAt: number;
320
+ status: ServerStatus;
321
+ uptime: number;
322
+ };
323
+ type ServerCapabilities = {
324
+ prompts: ReadonlyArray<{
325
+ description?: string;
326
+ name: string;
327
+ }>;
328
+ resources: ReadonlyArray<{
329
+ description?: string;
330
+ name: string;
331
+ uri: string;
332
+ }>;
333
+ tools: ReadonlyArray<{
334
+ description?: string;
335
+ name: string;
336
+ }>;
337
+ };
338
+ type ToolOptions<T extends SessionAuth = SessionAuth, P extends SchemaParams = SchemaParams> = Tool<T, P> & {
339
+ captureConfig?: ToolCaptureConfig;
340
+ };
341
+ type SomaServerInstance<T extends SessionAuth = SessionAuth> = {
342
+ readonly name: string;
343
+ readonly serverState: ServerStatus;
344
+ readonly sessions: ReadonlyArray<BackendSession<T>>;
345
+ addPrompt: <Args extends PromptArgument<T>[]>(prompt: Prompt<T, Args>) => void;
346
+ addPrompts: <Args extends PromptArgument<T>[]>(prompts: Prompt<T, Args>[]) => void;
347
+ addResource: (resource: Resource<T>) => void;
348
+ addResources: (resources: Resource<T>[]) => void;
349
+ addResourceTemplate: (...args: ReadonlyArray<unknown>) => void;
350
+ addTool: <P extends SchemaParams>(tool: Tool<T, P>) => void;
351
+ addTools: <P extends SchemaParams>(tools: Tool<T, P>[]) => void;
352
+ getApp: () => Hono;
353
+ getCapabilities: () => ServerCapabilities;
354
+ getGatewayManager: () => GatewayManagerInstance;
355
+ getHealth: () => ServerHealth;
356
+ removePrompt: (name: string) => void;
357
+ removeResource: (name: string) => void;
358
+ removeTool: (name: string) => void;
359
+ start: (options?: TransportConfig) => Promise<void>;
360
+ stop: () => Promise<void>;
361
+ };
362
+ //#endregion
363
+ //#region src/Server.d.ts
364
+ declare const createServer: <T extends SessionAuth = SessionAuth>(options: SomaServerOptions<T>) => SomaServerInstance<T>;
365
+ //#endregion
366
+ //#region src/content/helpers.d.ts
367
+ type ContentInput = {
368
+ buffer: Buffer;
369
+ } | {
370
+ path: string;
371
+ } | {
372
+ url: string;
373
+ };
374
+ declare const imageContent: (input: ContentInput) => Promise<ImageContent>;
375
+ declare const audioContent: (input: ContentInput) => Promise<AudioContent>;
376
+ //#endregion
377
+ //#region src/backend/fastmcp.d.ts
378
+ declare const createFastMCPBackend: <T extends SessionAuth = SessionAuth>(config: ServerConfig<T>, backendOptions?: Record<string, unknown>) => BackendAdapter<T>;
379
+ //#endregion
380
+ //#region src/telemetry/CompositeTelemetry.d.ts
381
+ declare const createCompositeTelemetry: (collectors: ReadonlyArray<TelemetryCollector>) => TelemetryCollector;
382
+ //#endregion
383
+ //#region src/telemetry/ConsoleTelemetry.d.ts
384
+ declare const createConsoleTelemetry: (prefix?: string) => TelemetryCollector;
385
+ //#endregion
386
+ //#region src/telemetry/EnrichedError.d.ts
387
+ type EnrichedErrorResponse = {
388
+ errorCategory: ErrorCategory;
389
+ message: string;
390
+ relatedState?: Record<string, unknown>;
391
+ suggestions?: ReadonlyArray<string>;
392
+ };
393
+ declare const classifyError: (error: unknown) => ErrorCategory;
394
+ declare const createEnrichedError: (error: unknown, context?: {
395
+ gatewayStatus?: string;
396
+ toolName?: string;
397
+ }) => EnrichedErrorResponse;
398
+ //#endregion
399
+ //#region src/telemetry/JsonFileTelemetry.d.ts
400
+ type JsonFileTelemetryOptions = {
401
+ filePath: string;
402
+ flushIntervalMs?: number;
403
+ };
404
+ declare const createJsonFileTelemetry: (options: JsonFileTelemetryOptions) => TelemetryCollector;
405
+ //#endregion
406
+ //#region src/telemetry/LogLayerTelemetry.d.ts
407
+ declare const createLogLayerTelemetry: (logger: DirectLogger$1) => TelemetryCollector;
408
+ //#endregion
409
+ //#region src/telemetry/NoopTelemetry.d.ts
410
+ declare const NoopTelemetry: TelemetryCollector;
411
+ //#endregion
412
+ //#region src/telemetry/telemetryWrapper.d.ts
413
+ declare const wrapTool: <T extends SessionAuth, P extends SchemaParams>(tool: Tool<T, P>, telemetry: TelemetryCollector, captureConfig?: ToolCaptureConfig) => Tool<T, P>;
414
+ declare const wrapResource: <T extends SessionAuth>(resource: Resource<T>, telemetry: TelemetryCollector) => Resource<T>;
415
+ declare const wrapPrompt: <T extends SessionAuth, Args extends PromptArgument<T>[] = PromptArgument<T>[]>(prompt: Prompt<T, Args>, telemetry: TelemetryCollector) => Prompt<T, Args>;
416
+ //#endregion
417
+ //#region src/logging.d.ts
418
+ declare const createDefaultLogger: (name: string) => DirectLogger$1;
419
+ //#endregion
420
+ //#region src/artifacts/ArtifactManager.d.ts
421
+ declare const registerArtifacts: (app: Hono, artifacts: ReadonlyArray<ArtifactConfig>) => void;
422
+ //#endregion
423
+ //#region src/artifacts/DashboardArtifact.d.ts
424
+ declare const createDashboardArtifact: (getHealth: () => ServerHealth, getCapabilities: () => ServerCapabilities, getConnections: () => ReadonlyArray<GatewayInfo>) => ArtifactConfig;
425
+ //#endregion
426
+ //#region src/introspection/capabilitiesTool.d.ts
427
+ declare const createCapabilitiesTool: <T extends SessionAuth>(getCapabilities: () => ServerCapabilities) => Tool<T>;
428
+ //#endregion
429
+ //#region src/introspection/connectionsTool.d.ts
430
+ declare const createConnectionsTool: <T extends SessionAuth>(getConnections: () => ReadonlyArray<GatewayInfo>) => Tool<T>;
431
+ //#endregion
432
+ //#region src/introspection/healthTool.d.ts
433
+ declare const createHealthTool: <T extends SessionAuth>(getHealth: () => ServerHealth) => Tool<T>;
434
+ //#endregion
435
+ //#region src/gateway/Gateway.d.ts
436
+ declare const createGateway: (config: GatewayConfig, telemetry: TelemetryCollector) => GatewayInstance;
437
+ //#endregion
438
+ //#region src/gateway/GatewayManager.d.ts
439
+ declare const createGatewayManager: (telemetry: TelemetryCollector) => GatewayManagerInstance;
440
+ //#endregion
441
+ //#region src/gateway/toolProxy.d.ts
442
+ declare const createProxiedTools: <T extends SessionAuth>(gateway: GatewayInstance, telemetry?: TelemetryCollector) => ReadonlyArray<Tool<T>>;
443
+ //#endregion
444
+ 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 };
package/dist/index.js ADDED
@@ -0,0 +1,58 @@
1
+ import{Ref as e,Try as t}from"functype";import{FastMCP as n,ServerState as r}from"fastmcp";import{Client as i}from"@modelcontextprotocol/sdk/client/index.js";import{StreamableHTTPClientTransport as a}from"@modelcontextprotocol/sdk/client/streamableHttp.js";import{appendFile as o,mkdir as s,readFile as c}from"node:fs/promises";import{dirname as l,extname as u}from"node:path";import{createDirectConsoleLogger as d}from"functype-log";const f=(e,t)=>{for(let n of t)switch(n.type){case`static`:e.get(n.path,e=>e.body(n.content,{headers:{"Content-Type":n.contentType}}));break;case`dynamic`:e.get(n.path,e=>n.handler(e));break;case`directory`:break}},p=(e,t,n)=>({handler:()=>{let r=m(e(),t(),n());return new Response(r,{headers:{"Content-Type":`text/html; charset=utf-8`}})},path:`/dashboard`,type:`dynamic`}),m=(e,t,n)=>`<!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>${e.name} - SomaMCP Dashboard</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: system-ui, -apple-system, sans-serif; background: #0a0a0a; color: #e0e0e0; padding: 2rem; }
10
+ h1 { font-size: 1.5rem; margin-bottom: 1.5rem; color: #fff; }
11
+ h2 { font-size: 1.1rem; margin-bottom: 0.75rem; color: #a0a0a0; text-transform: uppercase; letter-spacing: 0.05em; }
12
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; }
13
+ .card { background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 8px; padding: 1.25rem; }
14
+ .status { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85rem; font-weight: 600; }
15
+ .status.running { background: #0a3d0a; color: #4ade80; }
16
+ .status.stopped { background: #3d0a0a; color: #f87171; }
17
+ .status.error { background: #3d2a0a; color: #fbbf24; }
18
+ .status.connected { background: #0a3d0a; color: #4ade80; }
19
+ .status.disconnected { background: #3d0a0a; color: #f87171; }
20
+ .metric { margin: 0.5rem 0; }
21
+ .metric-label { color: #888; font-size: 0.85rem; }
22
+ .metric-value { color: #fff; font-weight: 500; }
23
+ ul { list-style: none; }
24
+ li { padding: 0.35rem 0; border-bottom: 1px solid #222; font-size: 0.9rem; }
25
+ li:last-child { border-bottom: none; }
26
+ .desc { color: #888; font-size: 0.8rem; }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <h1>${e.name}</h1>
31
+ <div class="grid">
32
+ <div class="card">
33
+ <h2>Health</h2>
34
+ <div class="metric"><span class="metric-label">Status:</span> <span class="status ${e.status}">${e.status}</span></div>
35
+ <div class="metric"><span class="metric-label">Uptime:</span> <span class="metric-value">${h(e.uptime)}</span></div>
36
+ <div class="metric"><span class="metric-label">Sessions:</span> <span class="metric-value">${e.activeSessions}</span></div>
37
+ <div class="metric"><span class="metric-label">Gateways:</span> <span class="metric-value">${e.gateways.connected}/${e.gateways.total}</span></div>
38
+ </div>
39
+ <div class="card">
40
+ <h2>Tools (${t.tools.length})</h2>
41
+ <ul>${t.tools.map(e=>`<li><strong>${e.name}</strong>${e.description?` <span class="desc">${e.description}</span>`:``}</li>`).join(``)}</ul>
42
+ </div>
43
+ <div class="card">
44
+ <h2>Resources (${t.resources.length})</h2>
45
+ <ul>${t.resources.map(e=>`<li><strong>${e.name}</strong> <span class="desc">${e.uri}</span></li>`).join(``)}</ul>
46
+ </div>
47
+ <div class="card">
48
+ <h2>Prompts (${t.prompts.length})</h2>
49
+ <ul>${t.prompts.map(e=>`<li><strong>${e.name}</strong>${e.description?` <span class="desc">${e.description}</span>`:``}</li>`).join(``)}</ul>
50
+ </div>
51
+ ${n.length>0?`<div class="card">
52
+ <h2>Gateways (${n.length})</h2>
53
+ <ul>${n.map(e=>`<li><strong>${e.name}</strong> <span class="status ${e.status}">${e.status}</span> <span class="desc">${e.url}</span></li>`).join(``)}</ul>
54
+ </div>`:``}
55
+ </div>
56
+ </body>
57
+ </html>`,h=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`},g=e=>{switch(e){case r.Running:return`running`;case r.Error:return`error`;default:return`stopped`}},_=(e,t)=>{let r=new n({...t,authenticate:e.authenticate,instructions:e.instructions,logger:e.logger,name:e.name,version:e.version});return{get serverState(){return g(r.serverState)},get sessions(){return r.sessions.map(e=>({sessionId:e.sessionId}))},addPrompt:e=>{r.addPrompt(e)},addResource:e=>{r.addResource(e)},addResourceTemplate:(...e)=>{r.addResourceTemplate(...e)},addTool:e=>{r.addTool(e)},getApp:()=>r.getApp(),on:(e,t)=>{r.on(e,t)},removePrompt:e=>r.removePrompt(e),removeResource:e=>r.removeResource(e),removeTool:e=>r.removeTool(e),start:async e=>{if(!e){await r.start();return}e.transportType===`httpStream`?await r.start({httpStream:e.httpStream,transportType:`httpStream`}):await r.start({transportType:`stdio`})},stop:()=>r.stop()}},v=(t,n)=>{let r=e(void 0),o=e(void 0),s=e([]),c=e(`disconnected`),l=e(void 0),u=t.name??t.id,d=()=>{let e=t.reconnectIntervalMs??5e3;l.set(setTimeout(()=>{f.connect()},e))},f={get config(){return t},get info(){return{id:t.id,name:u,remoteTools:s.get().map(e=>({description:e.description,name:e.name})),status:c.get(),url:t.url}},get name(){return u},get status(){return c.get()},get tools(){return s.get()},async callTool(e,n={}){let i=r.get();if(!i||c.get()!==`connected`)throw Error(`Gateway ${t.id} is not connected`);return await i.callTool({arguments:n,name:e})},async connect(){if(c.get()===`connected`)return;c.set(`connecting`);let e=Date.now();try{let l=new a(new URL(t.url));o.set(l);let d=new i({name:`soma-gateway-${t.id}`,version:`1.0.0`});r.set(d),await d.connect(l);let f=await d.listTools();s.set(f.tools),c.set(`connected`),n.recordEvent({data:{id:t.id,remoteToolCount:s.get().length,url:t.url},durationMs:Date.now()-e,name:u,timestamp:e,type:`gateway.connect`})}catch(r){c.set(`error`),n.recordEvent({data:{id:t.id,url:t.url},error:r instanceof Error?r.message:String(r),name:u,timestamp:e,type:`gateway.error`}),t.reconnect!==!1&&d()}},async disconnect(){let e=l.get();e&&(clearTimeout(e),l.set(void 0));let i=r.get();if(i)try{await i.close()}catch{}r.set(void 0),o.set(void 0),s.set([]),c.set(`disconnected`),n.recordEvent({data:{id:t.id},name:u,timestamp:Date.now(),type:`gateway.disconnect`})}};return f},y=e=>{let t=new Map;return{get connectedCount(){return[...t.values()].filter(e=>e.status===`connected`).length},get totalCount(){return t.size},add(n){let r=v(n,e);return t.set(n.id,r),r},async connectAll(){await Promise.allSettled([...t.values()].map(e=>e.connect()))},async disconnectAll(){await Promise.allSettled([...t.values()].map(e=>e.disconnect()))},get(e){return t.get(e)},getAll(){return[...t.values()]},getInfoAll(){return[...t.values()].map(e=>e.info)}}},b=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`},x=(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`]}},S=(e,t)=>{let n=b(e),r=e instanceof Error?e.message:String(e),i=x(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}},C=e=>t.fromPromise(e),w=(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 C(e.callTool(r.name,i))).fold(n=>{let s=S(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}`}))},T=e=>({annotations:{readOnlyHint:!0},description:`Lists all tools, resources, and prompts registered on this server`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:`soma_capabilities`}),E=e=>({annotations:{readOnlyHint:!0},description:`Lists all gateway connections to remote servers with their status and available tools`,execute:()=>Promise.resolve(JSON.stringify(e(),null,2)),name:`soma_connections`}),D=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`}),O=e=>({flush:()=>Promise.resolve(),recordEvent:t=>{let n={durationMs:t.durationMs,eventName:t.name,timestamp:new Date(t.timestamp).toISOString(),type:t.type,...t.sessionId&&{sessionId:t.sessionId},...t.requestId&&{requestId:t.requestId},...t.errorCategory&&{errorCategory:t.errorCategory},...t.data};t.error?e.withError(Error(t.error)).error(t.type,n):e.info(t.type,n)}}),k={flush:()=>Promise.resolve(),recordEvent:()=>{}},A=(e,t)=>!t||t.length===0?e:Object.fromEntries(Object.entries(e).map(([e,n])=>[e,t.includes(e)?`[REDACTED]`:n])),j=(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},M=e=>t.fromPromise(e),N=(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`?A(r,n?.redactInputFields):void 0;return(await M(e.execute(r,i))).fold(n=>{let r=S(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`?j(r,n?.maxOutputSize):void 0,requestId:s,sessionId:c,timestamp:o,type:`tool.execute`}),r))}}),P=(e,t)=>({...e,load:async n=>{let r=Date.now();return(await M(e.load(n))).fold(n=>{let i=S(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))}}),F=(e,t)=>({...e,load:async(n,r)=>{let i=Date.now();return(await M(e.load(n,r))).fold(n=>{let r=S(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))}}),I=t=>{let{artifacts:n,backendOptions:r,enableDashboard:i,enableIntrospection:a,gateways:o,logLayer:s,telemetry:c,...l}=t,u=l.name,d=c??(s?O(s):k),m=_(l,r),h=y(d),g=[],v=[],b=[],x=e(0);if(o)for(let e of o)h.add(e);let S=()=>{let e=x.get(),t=e>0?Date.now()-e:0;return{activeSessions:m.sessions.length,gateways:{connected:h.connectedCount,total:h.totalCount},name:u,startedAt:e,status:m.serverState,uptime:t}},C=()=>({prompts:b,resources:v,tools:g});a!==!1&&(m.addTool(D(()=>S())),m.addTool(T(()=>C())),m.addTool(E(()=>h.getInfoAll())));let A=[...n??[]];i!==!1&&A.push(p(()=>S(),()=>C(),()=>h.getInfoAll())),A.length>0&&f(m.getApp(),A),m.on(`connect`,({session:e})=>{d.recordEvent({data:{sessionId:e.sessionId},name:u,timestamp:Date.now(),type:`session.connect`})}),m.on(`disconnect`,({session:e})=>{d.recordEvent({data:{sessionId:e.sessionId},name:u,timestamp:Date.now(),type:`session.disconnect`})});let j=e=>{let t=N(e,d,`captureConfig`in e?e.captureConfig:void 0);m.addTool(t),g.push({description:e.description,name:e.name})},M=e=>{let t=P(e,d);m.addResource(t),v.push({description:e.description,name:e.name,uri:e.uri})},I=e=>{let t=F(e,d);m.addPrompt(t),b.push({description:e.description,name:e.name})};return{get name(){return u},get serverState(){return m.serverState},get sessions(){return m.sessions},addPrompt:I,addPrompts:e=>{for(let t of e)I(t)},addResource:M,addResources:e=>{for(let t of e)M(t)},addResourceTemplate:(...e)=>{m.addResourceTemplate(...e)},addTool:j,addTools:e=>{for(let t of e)j(t)},getApp:()=>m.getApp(),getCapabilities:C,getGatewayManager:()=>h,getHealth:S,removePrompt:e=>{m.removePrompt(e)},removeResource:e=>{m.removeResource(e)},removeTool:e=>{m.removeTool(e)},async start(e){x.set(Date.now()),d.recordEvent({name:u,timestamp:x.get(),type:`server.start`}),await m.start(e),await h.connectAll();for(let e of h.getAll())if(e.status===`connected`&&e.config.proxyTools!==!1){let t=w(e,d);for(let e of t)m.addTool(e)}},async stop(){await h.disconnectAll(),await m.stop(),d.recordEvent({name:u,timestamp:Date.now(),type:`server.stop`}),d.flush&&await d.flush()}}};var L=class extends Error{constructor(e){super(e),this.name=`UserError`}};const R={".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`},z={".aac":`audio/aac`,".flac":`audio/flac`,".m4a":`audio/mp4`,".mp3":`audio/mpeg`,".ogg":`audio/ogg`,".opus":`audio/opus`,".wav":`audio/wav`,".weba":`audio/webm`},B=(e,t,n)=>t[u(new URL(e,`file:///`).pathname).toLowerCase()]??n,V=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 c(e.path),source:e.path}:{data:e.buffer}},H=async e=>{let{data:t,source:n}=await V(e,`image`),r=n?B(n,R,`image/png`):`image/png`;return{data:t.toString(`base64`),mimeType:r,type:`image`}},U=async e=>{let{data:t,source:n}=await V(e,`audio`),r=n?B(n,z,`audio/mpeg`):`audio/mpeg`;return{data:t.toString(`base64`),mimeType:r,type:`audio`}},W=e=>({flush:async()=>{await Promise.all(e.map(e=>e.flush?e.flush():Promise.resolve()))},recordEvent:t=>{e.forEach(e=>e.recordEvent(t))}}),G=(e=`[soma]`)=>O(d({prefix:e})),K=t=>{let n=e([]),r=e(void 0),i=e(!1),a=async()=>{let e=n.getAndSet([]);if(e.length===0)return;i.get()||(await s(l(t.filePath),{recursive:!0}),i.set(!0));let r=`${e.map(e=>JSON.stringify(e)).join(`
58
+ `)}\n`;await o(t.filePath,r)},c=()=>{r.get()||r.set(setTimeout(()=>{r.set(void 0),a()},t.flushIntervalMs??1e3))};return{flush:async()=>{let e=r.get();e&&(clearTimeout(e),r.set(void 0)),await a()},recordEvent:e=>{n.update(t=>[...t,e]),c()}}},q=e=>d({prefix:`[${e}]`}).withContext({cell:e});export{k as NoopTelemetry,L as UserError,U as audioContent,b as classifyError,T as createCapabilitiesTool,W as createCompositeTelemetry,E as createConnectionsTool,G as createConsoleTelemetry,p as createDashboardArtifact,q as createDefaultLogger,S as createEnrichedError,_ as createFastMCPBackend,v as createGateway,y as createGatewayManager,D as createHealthTool,K as createJsonFileTelemetry,O as createLogLayerTelemetry,w as createProxiedTools,I as createServer,H as imageContent,f as registerArtifacts,F as wrapPrompt,P as wrapResource,N as wrapTool};
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "somamcp",
3
+ "version": "1.0.0",
4
+ "description": "Framework for building MCP servers with telemetry, introspection, and backend abstraction",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./backend": {
16
+ "types": "./dist/backend/index.d.ts",
17
+ "import": "./dist/backend/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "keywords": [
24
+ "MCP",
25
+ "server",
26
+ "telemetry",
27
+ "introspection",
28
+ "backend-adapter"
29
+ ],
30
+ "author": "Jordan Burke",
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.29.0",
34
+ "fastmcp": "^3.35.0",
35
+ "functype": "^0.56.0",
36
+ "functype-log": "^0.2.0",
37
+ "hono": "^4.12.12",
38
+ "zod": "^4.3.6"
39
+ },
40
+ "prettier": "ts-builds/prettier",
41
+ "devDependencies": {
42
+ "@types/node": "^24.12.2",
43
+ "get-port-please": "^3.2.0",
44
+ "ts-builds": "^2.6.4",
45
+ "tsdown": "^0.21.7"
46
+ },
47
+ "scripts": {
48
+ "validate": "ts-builds validate",
49
+ "format": "ts-builds format",
50
+ "format:check": "ts-builds format:check",
51
+ "lint": "ts-builds lint",
52
+ "lint:check": "ts-builds lint:check",
53
+ "typecheck": "ts-builds typecheck",
54
+ "test": "ts-builds test",
55
+ "test:watch": "ts-builds test:watch",
56
+ "test:coverage": "ts-builds test:coverage",
57
+ "build": "ts-builds build",
58
+ "dev": "ts-builds dev",
59
+ "clean": "rimraf dist"
60
+ }
61
+ }