somamcp 1.0.0 → 1.0.5

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 ADDED
@@ -0,0 +1,238 @@
1
+ # SomaMCP
2
+
3
+ Framework for building **MCP (Model Context Protocol)** servers with built-in telemetry, introspection, and a backend abstraction layer.
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.
6
+
7
+ ## Features
8
+
9
+ - **Backend abstraction** — framework-agnostic `BackendAdapter` interface; swap the underlying MCP framework without changing consumer code
10
+ - **Structured telemetry** — pluggable `TelemetryCollector` with console, file (NDJSON), composite, and [functype-log](https://github.com/jordanburke/functype-log) adapters
11
+ - **Error classification & enrichment** — errors are auto-classified (validation / timeout / gateway / auth / not_found / internal) with actionable suggestions for LLMs
12
+ - **Per-tool capture config** — configure input/output capture levels, field redaction, and output size limits per tool
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
16
+ - **Functional style** — powered by [functype](https://github.com/jordanburke/functype) (`Ref`, `Try`, `Either`)
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pnpm add somamcp
22
+ # or
23
+ npm install somamcp
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```typescript
29
+ import { createServer } from "somamcp"
30
+ import { z } from "zod"
31
+
32
+ const server = createServer({
33
+ name: "my-server",
34
+ version: "1.0.0",
35
+ })
36
+
37
+ server.addTool({
38
+ name: "greet",
39
+ description: "Greet someone by name",
40
+ parameters: z.object({ name: z.string() }),
41
+ execute: async ({ name }) => `Hello, ${name}!`,
42
+ })
43
+
44
+ await server.start({ transportType: "stdio" })
45
+ ```
46
+
47
+ ## Telemetry
48
+
49
+ 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.
50
+
51
+ ```typescript
52
+ import { createServer, createCompositeTelemetry, createConsoleTelemetry, createJsonFileTelemetry } from "somamcp"
53
+
54
+ const telemetry = createCompositeTelemetry([
55
+ createConsoleTelemetry(),
56
+ createJsonFileTelemetry({ filePath: "./logs/events.ndjson" }),
57
+ ])
58
+
59
+ const server = createServer({
60
+ name: "my-server",
61
+ version: "1.0.0",
62
+ telemetry,
63
+ })
64
+ ```
65
+
66
+ ### Per-tool capture configuration
67
+
68
+ ```typescript
69
+ server.addTool({
70
+ name: "processPayment",
71
+ description: "Process a payment",
72
+ parameters: z.object({ amount: z.number(), cardToken: z.string() }),
73
+ execute: async (args) => {
74
+ /* ... */
75
+ },
76
+ captureConfig: {
77
+ captureLevel: "full",
78
+ redactInputFields: ["cardToken"],
79
+ maxOutputSize: 2000,
80
+ },
81
+ })
82
+ ```
83
+
84
+ Capture levels:
85
+
86
+ - `"full"` — input + output + metadata (default)
87
+ - `"metadata"` — timing + name + ids only
88
+ - `"none"` — no telemetry
89
+
90
+ ### Error enrichment
91
+
92
+ Errors thrown from tools are automatically classified and returned as structured `ContentResult` responses with `isError: true`, including suggestions to help calling LLMs self-correct.
93
+
94
+ ```typescript
95
+ // Thrown: new Error("Request timed out after 30s")
96
+ // Returned to LLM:
97
+ {
98
+ isError: true,
99
+ content: [{
100
+ type: "text",
101
+ text: JSON.stringify({
102
+ errorCategory: "timeout",
103
+ message: "Request timed out after 30s",
104
+ suggestions: [
105
+ "The operation took too long. Try reducing the scope of the request.",
106
+ "Check if the upstream service is responsive.",
107
+ ],
108
+ }),
109
+ }],
110
+ }
111
+ ```
112
+
113
+ ## Gateways
114
+
115
+ Connect to remote MCP servers and proxy their tools as local tools.
116
+
117
+ ```typescript
118
+ const server = createServer({
119
+ name: "gateway-server",
120
+ version: "1.0.0",
121
+ gateways: [
122
+ {
123
+ id: "upstream",
124
+ url: "https://remote-mcp.example.com",
125
+ proxyTools: true, // register remote tools as local (prefixed: "upstream_toolname")
126
+ reconnect: true,
127
+ },
128
+ ],
129
+ })
130
+ ```
131
+
132
+ ## Artifacts & Dashboard
133
+
134
+ Mount HTTP routes on the embedded Hono app. A health dashboard is auto-mounted at `/dashboard` unless disabled.
135
+
136
+ ```typescript
137
+ const server = createServer({
138
+ name: "my-server",
139
+ version: "1.0.0",
140
+ enableDashboard: true, // default
141
+ artifacts: [
142
+ {
143
+ type: "dynamic",
144
+ path: "/status",
145
+ handler: (c) => c.json({ ok: true }),
146
+ },
147
+ ],
148
+ })
149
+
150
+ await server.start({
151
+ transportType: "httpStream",
152
+ httpStream: { port: 8080 },
153
+ })
154
+ ```
155
+
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
+ ## Backend Abstraction
165
+
166
+ The underlying MCP framework is accessed exclusively through the `BackendAdapter` interface. To use a non-FastMCP backend, implement `BackendFactory`:
167
+
168
+ ```typescript
169
+ import { createFastMCPBackend } from "somamcp/backend"
170
+ import type { BackendFactory } from "somamcp/backend"
171
+
172
+ const myBackend: BackendFactory = (config, backendOptions) => {
173
+ // ...
174
+ }
175
+ ```
176
+
177
+ Framework-specific options (e.g. FastMCP's OAuth, ping, health endpoints) flow through `backendOptions`:
178
+
179
+ ```typescript
180
+ createServer({
181
+ name: "my-server",
182
+ version: "1.0.0",
183
+ backendOptions: {
184
+ ping: { enabled: true, intervalMs: 30000 },
185
+ // ...any FastMCP ServerOptions fields
186
+ },
187
+ })
188
+ ```
189
+
190
+ ## API Surface
191
+
192
+ **Core**
193
+
194
+ - `createServer(options)` → `SomaServerInstance`
195
+ - Types: `SomaServerOptions`, `SomaServerInstance`, `ServerHealth`, `ServerCapabilities`, `ToolOptions`
196
+
197
+ **Primitives** (somamcp-owned, no FastMCP leakage)
198
+
199
+ - `Tool`, `Resource`, `Prompt`, `Context`, `Content`, `ContentResult`, `SessionAuth`, `UserError`
200
+
201
+ **Telemetry**
202
+
203
+ - `createConsoleTelemetry`, `createJsonFileTelemetry`, `createCompositeTelemetry`, `createLogLayerTelemetry`, `NoopTelemetry`
204
+ - `classifyError`, `createEnrichedError`
205
+ - Types: `TelemetryCollector`, `TelemetryEvent`, `CaptureLevel`, `ToolCaptureConfig`, `ErrorCategory`
206
+
207
+ **Gateway**
208
+
209
+ - `createGateway`, `createGatewayManager`, `createProxiedTools`
210
+ - Types: `GatewayConfig`, `GatewayInstance`, `GatewayManagerInstance`, `GatewayStatus`
211
+
212
+ **Artifacts**
213
+
214
+ - `registerArtifacts`, `createDashboardArtifact`
215
+ - Types: `StaticArtifact`, `DynamicArtifact`, `DirectoryArtifact`, `ArtifactConfig`
216
+
217
+ **Content helpers**
218
+
219
+ - `imageContent`, `audioContent`
220
+
221
+ **Backend** (via `somamcp/backend`)
222
+
223
+ - `createFastMCPBackend`
224
+ - Types: `BackendAdapter`, `BackendFactory`, `BackendSession`
225
+
226
+ ## Scripts
227
+
228
+ ```bash
229
+ pnpm validate # Pre-commit: format + lint + typecheck + test + build
230
+ pnpm test # Run tests
231
+ pnpm build # Build via tsdown
232
+ pnpm dev # Watch mode
233
+ pnpm typecheck # Type check only
234
+ ```
235
+
236
+ ## License
237
+
238
+ MIT © Jordan Burke
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Option } from "functype";
1
2
  import { DirectLogger, DirectLogger as DirectLogger$1 } from "functype-log";
2
3
  import { Context as Context$1, Hono } from "hono";
3
4
  import { Tool as Tool$1 } from "@modelcontextprotocol/sdk/types.js";
@@ -41,7 +42,13 @@ type SchemaParams = {
41
42
  issues: ReadonlyArray<{
42
43
  message: string;
43
44
  }>;
44
- };
45
+ } | Promise<{
46
+ value: unknown;
47
+ } | {
48
+ issues: ReadonlyArray<{
49
+ message: string;
50
+ }>;
51
+ }>;
45
52
  };
46
53
  };
47
54
  type InferSchemaOutput<P extends SchemaParams> = NonNullable<P["~standard"]["types"]>["output"] extends infer O ? [O] extends [never] ? unknown : O : unknown;
@@ -266,7 +273,7 @@ type GatewayManagerInstance = {
266
273
  add: (config: GatewayConfig) => GatewayInstance;
267
274
  connectAll: () => Promise<void>;
268
275
  disconnectAll: () => Promise<void>;
269
- get: (id: string) => GatewayInstance | undefined;
276
+ get: (id: string) => Option<GatewayInstance>;
270
277
  getAll: () => ReadonlyArray<GatewayInstance>;
271
278
  getInfoAll: () => ReadonlyArray<GatewayInfo>;
272
279
  };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
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>
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>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
@@ -32,7 +32,7 @@ import{Ref as e,Try as t}from"functype";import{FastMCP as n,ServerState as r}fro
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">${h(e.uptime)}</span></div>
35
+ <div class="metric"><span class="metric-label">Uptime:</span> <span class="metric-value">${g(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,5 @@ import{Ref as e,Try as t}from"functype";import{FastMCP as n,ServerState as r}fro
54
54
  </div>`:``}
55
55
  </div>
56
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};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "somamcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.5",
4
4
  "description": "Framework for building MCP servers with telemetry, introspection, and backend abstraction",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,6 +20,21 @@
20
20
  "files": [
21
21
  "dist"
22
22
  ],
23
+ "scripts": {
24
+ "validate": "ts-builds validate",
25
+ "format": "ts-builds format",
26
+ "format:check": "ts-builds format:check",
27
+ "lint": "ts-builds lint",
28
+ "lint:check": "ts-builds lint:check",
29
+ "typecheck": "ts-builds typecheck",
30
+ "test": "ts-builds test",
31
+ "test:watch": "ts-builds test:watch",
32
+ "test:coverage": "ts-builds test:coverage",
33
+ "build": "ts-builds build",
34
+ "dev": "ts-builds dev",
35
+ "clean": "rimraf dist",
36
+ "prepublishOnly": "pnpm validate"
37
+ },
23
38
  "keywords": [
24
39
  "MCP",
25
40
  "server",
@@ -29,33 +44,28 @@
29
44
  ],
30
45
  "author": "Jordan Burke",
31
46
  "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/sapientsai/SomaMCP.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/sapientsai/SomaMCP/issues"
53
+ },
54
+ "homepage": "https://github.com/sapientsai/SomaMCP#readme",
32
55
  "dependencies": {
33
56
  "@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",
57
+ "fastmcp": "^4.0.0",
58
+ "functype": "^0.60.0",
59
+ "functype-log": "^0.60.0",
60
+ "hono": "^4.12.14",
38
61
  "zod": "^4.3.6"
39
62
  },
40
63
  "prettier": "ts-builds/prettier",
41
64
  "devDependencies": {
42
65
  "@types/node": "^24.12.2",
43
66
  "get-port-please": "^3.2.0",
44
- "ts-builds": "^2.6.4",
45
- "tsdown": "^0.21.7"
67
+ "ts-builds": "^2.7.0",
68
+ "tsdown": "^0.21.9"
46
69
  },
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
- }
70
+ "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
71
+ }