@sodiumhq/mcp-pm 0.1.0-beta.4448 → 0.1.0-beta.4465
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/dist/http.js +187 -37
- package/dist/index.js +1 -1
- package/dist/{src-DEbTNFXg.mjs → src-uJ9liBF-.mjs} +45 -8
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as buildServer } from "./src-
|
|
2
|
+
import { t as buildServer } from "./src-uJ9liBF-.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
7
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
|
|
9
|
+
import { hostname } from "node:os";
|
|
9
10
|
//#region src/obo.ts
|
|
10
11
|
const EXPIRY_MARGIN_MS = 6e4;
|
|
11
12
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -62,6 +63,105 @@ function resolveExpiry(accessToken, expiresIn) {
|
|
|
62
63
|
return Date.now() + seconds * 1e3 - EXPIRY_MARGIN_MS;
|
|
63
64
|
}
|
|
64
65
|
//#endregion
|
|
66
|
+
//#region src/telemetry.ts
|
|
67
|
+
const FLUSH_INTERVAL_MS = Number(process.env.MCP_TELEMETRY_FLUSH_MS ?? 1e4);
|
|
68
|
+
const MAX_BATCH = 20;
|
|
69
|
+
const MAX_QUEUE = 1e3;
|
|
70
|
+
const ERROR_LOG_THROTTLE_MS = 6e4;
|
|
71
|
+
function loadTelemetryConfig(options) {
|
|
72
|
+
const raw = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING?.trim();
|
|
73
|
+
if (!raw) return null;
|
|
74
|
+
const parts = new Map(raw.split(";").map((p) => p.trim()).filter(Boolean).map((p) => {
|
|
75
|
+
const eq = p.indexOf("=");
|
|
76
|
+
return [p.slice(0, eq).toLowerCase(), p.slice(eq + 1)];
|
|
77
|
+
}));
|
|
78
|
+
const instrumentationKey = parts.get("instrumentationkey");
|
|
79
|
+
const ingestionEndpoint = parts.get("ingestionendpoint");
|
|
80
|
+
if (!instrumentationKey || !ingestionEndpoint) return null;
|
|
81
|
+
return {
|
|
82
|
+
instrumentationKey,
|
|
83
|
+
ingestionEndpoint: ingestionEndpoint.replace(/\/+$/, ""),
|
|
84
|
+
roleName: options.roleName,
|
|
85
|
+
applicationVersion: options.applicationVersion
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function formatDuration(ms) {
|
|
89
|
+
const clamped = Math.max(0, Math.round(ms));
|
|
90
|
+
const h = Math.floor(clamped / 36e5);
|
|
91
|
+
const m = Math.floor(clamped % 36e5 / 6e4);
|
|
92
|
+
const s = Math.floor(clamped % 6e4 / 1e3);
|
|
93
|
+
const f = clamped % 1e3;
|
|
94
|
+
const pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
95
|
+
return `${pad(h)}:${pad(m)}:${pad(s)}.${pad(f, 3)}`;
|
|
96
|
+
}
|
|
97
|
+
var TelemetryClient = class {
|
|
98
|
+
queue = [];
|
|
99
|
+
lastErrorLogAt = 0;
|
|
100
|
+
constructor(config) {
|
|
101
|
+
this.config = config;
|
|
102
|
+
setInterval(() => void this.flush(), FLUSH_INTERVAL_MS).unref();
|
|
103
|
+
process.on("SIGTERM", () => void this.flush());
|
|
104
|
+
}
|
|
105
|
+
trackRequest(request) {
|
|
106
|
+
const properties = {};
|
|
107
|
+
if (request.practice) properties.practice = request.practice;
|
|
108
|
+
if (request.tool) properties.tool = request.tool;
|
|
109
|
+
if (request.outcome) properties.outcome = request.outcome;
|
|
110
|
+
if (request.aiClient) properties.aiClient = request.aiClient;
|
|
111
|
+
if (request.error) properties.error = request.error.slice(0, 1024);
|
|
112
|
+
this.enqueue({
|
|
113
|
+
name: `Microsoft.ApplicationInsights.${this.config.instrumentationKey.replace(/-/g, "")}.Request`,
|
|
114
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
115
|
+
iKey: this.config.instrumentationKey,
|
|
116
|
+
tags: {
|
|
117
|
+
"ai.cloud.role": this.config.roleName,
|
|
118
|
+
"ai.cloud.roleInstance": hostname(),
|
|
119
|
+
"ai.application.ver": this.config.applicationVersion,
|
|
120
|
+
"ai.operation.id": randomUUID(),
|
|
121
|
+
"ai.operation.name": request.name,
|
|
122
|
+
...request.userSubject ? { "ai.user.authUserId": request.userSubject } : {}
|
|
123
|
+
},
|
|
124
|
+
data: {
|
|
125
|
+
baseType: "RequestData",
|
|
126
|
+
baseData: {
|
|
127
|
+
ver: 2,
|
|
128
|
+
id: randomUUID().replace(/-/g, "").slice(0, 16),
|
|
129
|
+
name: request.name,
|
|
130
|
+
duration: formatDuration(request.durationMs),
|
|
131
|
+
responseCode: String(request.responseCode),
|
|
132
|
+
success: request.success,
|
|
133
|
+
properties
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
enqueue(envelope) {
|
|
139
|
+
this.queue.push(envelope);
|
|
140
|
+
if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE);
|
|
141
|
+
if (this.queue.length >= MAX_BATCH) this.flush();
|
|
142
|
+
}
|
|
143
|
+
async flush() {
|
|
144
|
+
if (this.queue.length === 0) return;
|
|
145
|
+
const batch = this.queue.splice(0, this.queue.length);
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(`${this.config.ingestionEndpoint}/v2.1/track`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: { "Content-Type": "application/x-json-stream" },
|
|
150
|
+
body: batch.map((e) => JSON.stringify(e)).join("\n")
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) this.logError(`ingestion responded ${response.status}`);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.logError(error instanceof Error ? error.message : String(error));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
logError(message) {
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
if (now - this.lastErrorLogAt < ERROR_LOG_THROTTLE_MS) return;
|
|
160
|
+
this.lastErrorLogAt = now;
|
|
161
|
+
console.error(`[sodium-pm-mcp-http] telemetry flush failed: ${message}`);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
//#endregion
|
|
65
165
|
//#region src/auth.ts
|
|
66
166
|
var AuthError = class extends Error {
|
|
67
167
|
constructor(status, code, message) {
|
|
@@ -71,11 +171,11 @@ var AuthError = class extends Error {
|
|
|
71
171
|
}
|
|
72
172
|
};
|
|
73
173
|
function loadAuthConfig() {
|
|
74
|
-
const resourceUrl = process.env.MCP_RESOURCE_URL;
|
|
174
|
+
const resourceUrl = process.env.MCP_RESOURCE_URL?.trim();
|
|
75
175
|
const issuer = process.env.AUTH0_ISSUER;
|
|
76
176
|
if (!resourceUrl || !issuer) return null;
|
|
77
177
|
return {
|
|
78
|
-
resourceUrl
|
|
178
|
+
resourceUrl,
|
|
79
179
|
issuer: issuer.endsWith("/") ? issuer : `${issuer}/`,
|
|
80
180
|
scopes: {
|
|
81
181
|
read: process.env.MCP_SCOPE_READ ?? "mcp:read",
|
|
@@ -139,7 +239,25 @@ const writesRaw = (process.env.SODIUM_ENABLE_WRITES ?? "").trim().toLowerCase();
|
|
|
139
239
|
const WRITES_ENABLED = writesRaw === "true" || writesRaw === "1";
|
|
140
240
|
const AUTH = loadAuthConfig();
|
|
141
241
|
const OBO = AUTH ? loadOboConfig(AUTH.issuer) : null;
|
|
142
|
-
const
|
|
242
|
+
const RESOURCE_PATH = AUTH ? new URL(AUTH.resourceUrl).pathname : process.env.MCP_ENDPOINT_PATH ?? "/mcp";
|
|
243
|
+
const ENDPOINT_PATH = RESOURCE_PATH.length > 1 ? RESOURCE_PATH.replace(/\/+$/, "") : RESOURCE_PATH;
|
|
244
|
+
const TELEMETRY_CONFIG = loadTelemetryConfig({
|
|
245
|
+
roleName: "sodium-pm-mcp",
|
|
246
|
+
applicationVersion: VERSION
|
|
247
|
+
});
|
|
248
|
+
const TELEMETRY = TELEMETRY_CONFIG ? new TelemetryClient(TELEMETRY_CONFIG) : null;
|
|
249
|
+
function describeRequest(body) {
|
|
250
|
+
if (Array.isArray(body)) return "POST batch";
|
|
251
|
+
if (typeof body === "object" && body !== null && "method" in body) {
|
|
252
|
+
const method = String(body.method);
|
|
253
|
+
if (method === "tools/call") {
|
|
254
|
+
const params = body.params;
|
|
255
|
+
return `POST tools/call ${typeof params?.name === "string" ? params.name : "?"}`;
|
|
256
|
+
}
|
|
257
|
+
return `POST ${method}`;
|
|
258
|
+
}
|
|
259
|
+
return "POST unknown";
|
|
260
|
+
}
|
|
143
261
|
const MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
144
262
|
async function readJsonBody(req) {
|
|
145
263
|
const chunks = [];
|
|
@@ -163,38 +281,70 @@ function sendJsonError(res, status, message) {
|
|
|
163
281
|
}));
|
|
164
282
|
}
|
|
165
283
|
async function handleMcpPost(req, res, body) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
let
|
|
170
|
-
let
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
284
|
+
const started = Date.now();
|
|
285
|
+
const requestName = describeRequest(body);
|
|
286
|
+
const userAgent = req.headers["user-agent"];
|
|
287
|
+
let subject;
|
|
288
|
+
let toolCall;
|
|
289
|
+
let tracked = false;
|
|
290
|
+
const track = (responseCode, error) => {
|
|
291
|
+
if (!TELEMETRY || tracked) return;
|
|
292
|
+
tracked = true;
|
|
293
|
+
TELEMETRY.trackRequest({
|
|
294
|
+
name: requestName,
|
|
295
|
+
durationMs: Date.now() - started,
|
|
296
|
+
responseCode,
|
|
297
|
+
success: responseCode < 400 && toolCall?.outcome !== "error",
|
|
298
|
+
userSubject: subject,
|
|
299
|
+
practice: toolCall?.practice || void 0,
|
|
300
|
+
tool: toolCall?.tool,
|
|
301
|
+
outcome: toolCall?.outcome,
|
|
302
|
+
aiClient: typeof userAgent === "string" ? userAgent : void 0,
|
|
303
|
+
error
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
try {
|
|
307
|
+
const authHeader = req.headers.authorization;
|
|
308
|
+
if (!authHeader?.startsWith("Bearer ")) throw new HttpError(401, "Authorization: Bearer <token> header is required.");
|
|
309
|
+
const bearerToken = authHeader.slice(7).trim();
|
|
310
|
+
let writesAllowed = WRITES_ENABLED;
|
|
311
|
+
let apiToken = bearerToken;
|
|
312
|
+
if (AUTH) {
|
|
313
|
+
const verified = await verifyAccessToken(AUTH, bearerToken);
|
|
314
|
+
subject = verified.subject;
|
|
315
|
+
writesAllowed = WRITES_ENABLED && verified.scopes.includes(AUTH.scopes.write);
|
|
316
|
+
if (OBO) apiToken = await getDownstreamToken(OBO, bearerToken);
|
|
317
|
+
}
|
|
318
|
+
const tenantHeader = req.headers["x-sodium-tenant"];
|
|
319
|
+
const tenant = (Array.isArray(tenantHeader) ? tenantHeader[0] : tenantHeader) ?? process.env.SODIUM_TENANT;
|
|
320
|
+
const isInit = isInitializeRequest(body);
|
|
321
|
+
const server = await buildServer({
|
|
322
|
+
context: {
|
|
323
|
+
bearerToken: apiToken,
|
|
324
|
+
tenant,
|
|
325
|
+
baseUrl: BASE_URL,
|
|
326
|
+
writesEnabled: writesAllowed
|
|
327
|
+
},
|
|
328
|
+
serverName: "Sodium Practice Management",
|
|
329
|
+
serverVersion: VERSION,
|
|
330
|
+
includeStartupContext: isInit,
|
|
331
|
+
onToolCall: (info) => {
|
|
332
|
+
toolCall = info;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
336
|
+
res.on("close", () => {
|
|
337
|
+
track(res.statusCode ?? 200);
|
|
338
|
+
transport.close();
|
|
339
|
+
server.close();
|
|
340
|
+
});
|
|
341
|
+
if (isInit) console.error(`[sodium-pm-mcp-http] initialize (tenant: ${tenant || "per-call"}, writes: ${writesAllowed})`);
|
|
342
|
+
await server.connect(transport);
|
|
343
|
+
await transport.handleRequest(req, res, body);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
track(error instanceof HttpError || error instanceof AuthError ? error.status : 500, error instanceof Error ? error.message : String(error));
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
198
348
|
}
|
|
199
349
|
var HttpError = class extends Error {
|
|
200
350
|
constructor(status, message) {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as buildServer } from "./src-
|
|
2
|
+
import { t as buildServer } from "./src-uJ9liBF-.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
@@ -2236,6 +2236,15 @@ var SodiumApiClient = class {
|
|
|
2236
2236
|
if (!this.activeTenant) throw new Error("No practice specified. Pass a practice code from list_practices in the tool's 'practice' argument.");
|
|
2237
2237
|
return this.activeTenant;
|
|
2238
2238
|
}
|
|
2239
|
+
toolCallListener = null;
|
|
2240
|
+
setToolCallListener(listener) {
|
|
2241
|
+
this.toolCallListener = listener;
|
|
2242
|
+
}
|
|
2243
|
+
notifyToolCall(info) {
|
|
2244
|
+
try {
|
|
2245
|
+
this.toolCallListener?.(info);
|
|
2246
|
+
} catch {}
|
|
2247
|
+
}
|
|
2239
2248
|
hasActiveTenant() {
|
|
2240
2249
|
return this.activeTenant.length > 0;
|
|
2241
2250
|
}
|
|
@@ -8752,16 +8761,43 @@ function registerTenantTool(server, api, name, config, cb) {
|
|
|
8752
8761
|
...config,
|
|
8753
8762
|
inputSchema
|
|
8754
8763
|
}, async (args, extra) => {
|
|
8764
|
+
const started = Date.now();
|
|
8755
8765
|
const { practice, ...rest } = args;
|
|
8756
8766
|
const resolution = await resolveTenantForCall(api, typeof practice === "string" ? practice : void 0);
|
|
8757
|
-
if (!resolution.ok)
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8767
|
+
if (!resolution.ok) {
|
|
8768
|
+
api.notifyToolCall({
|
|
8769
|
+
tool: name,
|
|
8770
|
+
practice: "",
|
|
8771
|
+
durationMs: Date.now() - started,
|
|
8772
|
+
outcome: "refused"
|
|
8773
|
+
});
|
|
8774
|
+
return {
|
|
8775
|
+
content: [{
|
|
8776
|
+
type: "text",
|
|
8777
|
+
text: resolution.message
|
|
8778
|
+
}],
|
|
8779
|
+
isError: true
|
|
8780
|
+
};
|
|
8781
|
+
}
|
|
8782
|
+
try {
|
|
8783
|
+
const result = await cb(rest, extra);
|
|
8784
|
+
const isError = typeof result === "object" && result !== null && result.isError === true;
|
|
8785
|
+
api.notifyToolCall({
|
|
8786
|
+
tool: name,
|
|
8787
|
+
practice: api.getActiveTenant(),
|
|
8788
|
+
durationMs: Date.now() - started,
|
|
8789
|
+
outcome: isError ? "error" : "ok"
|
|
8790
|
+
});
|
|
8791
|
+
return result;
|
|
8792
|
+
} catch (error) {
|
|
8793
|
+
api.notifyToolCall({
|
|
8794
|
+
tool: name,
|
|
8795
|
+
practice: api.getActiveTenant(),
|
|
8796
|
+
durationMs: Date.now() - started,
|
|
8797
|
+
outcome: "error"
|
|
8798
|
+
});
|
|
8799
|
+
throw error;
|
|
8800
|
+
}
|
|
8765
8801
|
});
|
|
8766
8802
|
}
|
|
8767
8803
|
function registerWriteTool(server, api, ctx, name, config, cb) {
|
|
@@ -8770,6 +8806,7 @@ function registerWriteTool(server, api, ctx, name, config, cb) {
|
|
|
8770
8806
|
}
|
|
8771
8807
|
async function buildServer(config) {
|
|
8772
8808
|
const api = new SodiumApiClient(config.context, { serverVersion: config.serverVersion });
|
|
8809
|
+
if (config.onToolCall) api.setToolCallListener(config.onToolCall);
|
|
8773
8810
|
let instructions;
|
|
8774
8811
|
if (config.includeStartupContext !== false) {
|
|
8775
8812
|
const selection = api.hasActiveTenant() ? { kind: "explicit" } : await resolveTenant(api);
|