@sodiumhq/mcp-pm 0.1.0-beta.5113 → 0.1.0-beta.5115
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 +60 -31
- package/dist/index.js +11 -9
- package/dist/{src-Bl4qYJBU.mjs → src-DBBUms70.mjs} +15 -9
- package/package.json +4 -3
package/dist/http.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as buildServer } from "./src-
|
|
2
|
+
import { t as buildServer } from "./src-DBBUms70.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { SUPPORTED_PROTOCOL_VERSIONS, createMcpHandler } from "@modelcontextprotocol/server";
|
|
4
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
6
|
import { createServer } from "node:http";
|
|
6
|
-
import {
|
|
7
|
-
import { SUPPORTED_PROTOCOL_VERSIONS, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
8
8
|
import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
|
|
9
9
|
import { hostname } from "node:os";
|
|
10
10
|
//#region src/obo.ts
|
|
@@ -247,17 +247,42 @@ const TELEMETRY_CONFIG = loadTelemetryConfig({
|
|
|
247
247
|
applicationVersion: VERSION
|
|
248
248
|
});
|
|
249
249
|
const TELEMETRY = TELEMETRY_CONFIG ? new TelemetryClient(TELEMETRY_CONFIG) : null;
|
|
250
|
+
const INSTRUCTION_METHODS = new Set(["initialize", "server/discover"]);
|
|
251
|
+
function requestState(ctx) {
|
|
252
|
+
const state = (ctx.authInfo?.extra)?.sodium;
|
|
253
|
+
if (!state) throw new Error("Request reached the MCP handler without authenticated state.");
|
|
254
|
+
return state;
|
|
255
|
+
}
|
|
256
|
+
const mcpHandler = createMcpHandler((ctx) => {
|
|
257
|
+
const state = requestState(ctx);
|
|
258
|
+
return buildServer({
|
|
259
|
+
context: {
|
|
260
|
+
bearerToken: state.apiToken,
|
|
261
|
+
tenant: state.tenant,
|
|
262
|
+
baseUrl: BASE_URL,
|
|
263
|
+
writesEnabled: state.writesAllowed
|
|
264
|
+
},
|
|
265
|
+
serverName: "Sodium Practice Management",
|
|
266
|
+
serverVersion: VERSION,
|
|
267
|
+
includeStartupContext: state.wantsInstructions,
|
|
268
|
+
onToolCall: state.onToolCall
|
|
269
|
+
});
|
|
270
|
+
}, { onerror: (error) => {
|
|
271
|
+
console.error(`[sodium-pm-mcp-http] handler: ${error.message}`);
|
|
272
|
+
} });
|
|
273
|
+
const serveMcp = toNodeHandler(mcpHandler);
|
|
274
|
+
function jsonRpcMethod(body) {
|
|
275
|
+
if (typeof body === "object" && body !== null && "method" in body) return String(body.method);
|
|
276
|
+
}
|
|
250
277
|
function describeRequest(body) {
|
|
251
278
|
if (Array.isArray(body)) return "POST batch";
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
return `POST ${method}`;
|
|
279
|
+
const method = jsonRpcMethod(body);
|
|
280
|
+
if (method === void 0) return "POST unknown";
|
|
281
|
+
if (method === "tools/call") {
|
|
282
|
+
const params = body.params;
|
|
283
|
+
return `POST tools/call ${typeof params?.name === "string" ? params.name : "?"}`;
|
|
259
284
|
}
|
|
260
|
-
return
|
|
285
|
+
return `POST ${method}`;
|
|
261
286
|
}
|
|
262
287
|
function unsupportedProtocolVersion(req) {
|
|
263
288
|
const header = req.headers["mcp-protocol-version"];
|
|
@@ -318,38 +343,37 @@ async function handleMcpPost(req, res, body) {
|
|
|
318
343
|
const bearerToken = authHeader.slice(7).trim();
|
|
319
344
|
let writesAllowed = WRITES_ENABLED;
|
|
320
345
|
let apiToken = bearerToken;
|
|
346
|
+
let scopes = [];
|
|
321
347
|
if (AUTH) {
|
|
322
348
|
const verified = await verifyAccessToken(AUTH, bearerToken);
|
|
323
349
|
subject = verified.subject;
|
|
350
|
+
scopes = verified.scopes;
|
|
324
351
|
writesAllowed = WRITES_ENABLED && verified.scopes.includes(AUTH.scopes.write);
|
|
325
352
|
if (OBO) apiToken = await getDownstreamToken(OBO, bearerToken);
|
|
326
353
|
}
|
|
327
354
|
const tenantHeader = req.headers["x-sodium-tenant"];
|
|
328
355
|
const tenant = (Array.isArray(tenantHeader) ? tenantHeader[0] : tenantHeader) ?? process.env.SODIUM_TENANT;
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
writesEnabled: writesAllowed
|
|
336
|
-
},
|
|
337
|
-
serverName: "Sodium Practice Management",
|
|
338
|
-
serverVersion: VERSION,
|
|
339
|
-
includeStartupContext: isInit,
|
|
356
|
+
const method = jsonRpcMethod(body);
|
|
357
|
+
const state = {
|
|
358
|
+
apiToken,
|
|
359
|
+
tenant,
|
|
360
|
+
writesAllowed,
|
|
361
|
+
wantsInstructions: method !== void 0 && INSTRUCTION_METHODS.has(method),
|
|
340
362
|
onToolCall: (info) => {
|
|
341
363
|
toolCall = info;
|
|
342
364
|
}
|
|
343
|
-
}
|
|
344
|
-
|
|
365
|
+
};
|
|
366
|
+
req.auth = {
|
|
367
|
+
token: apiToken,
|
|
368
|
+
clientId: subject ?? "local",
|
|
369
|
+
scopes,
|
|
370
|
+
extra: { sodium: state }
|
|
371
|
+
};
|
|
345
372
|
res.on("close", () => {
|
|
346
373
|
track(res.statusCode ?? 200);
|
|
347
|
-
transport.close();
|
|
348
|
-
server.close();
|
|
349
374
|
});
|
|
350
|
-
if (
|
|
351
|
-
await
|
|
352
|
-
await transport.handleRequest(req, res, body);
|
|
375
|
+
if (state.wantsInstructions) console.error(`[sodium-pm-mcp-http] ${method} (tenant: ${tenant || "per-call"}, writes: ${writesAllowed})`);
|
|
376
|
+
await serveMcp(req, res, body);
|
|
353
377
|
} catch (error) {
|
|
354
378
|
track(error instanceof HttpError || error instanceof AuthError ? error.status : 500, error instanceof Error ? error.message : String(error));
|
|
355
379
|
throw error;
|
|
@@ -385,7 +409,7 @@ async function handle(req, res) {
|
|
|
385
409
|
}
|
|
386
410
|
await handleMcpPost(req, res, await readJsonBody(req));
|
|
387
411
|
}
|
|
388
|
-
createServer((req, res) => {
|
|
412
|
+
const httpServer = createServer((req, res) => {
|
|
389
413
|
handle(req, res).catch((error) => {
|
|
390
414
|
const message = error instanceof Error ? error.message : String(error);
|
|
391
415
|
console.error(`[sodium-pm-mcp-http] ${req.method} ${req.url} failed: ${message}`);
|
|
@@ -399,7 +423,12 @@ createServer((req, res) => {
|
|
|
399
423
|
if (AUTH && error instanceof HttpError && error.status === 401) res.setHeader("WWW-Authenticate", buildChallenge(AUTH));
|
|
400
424
|
sendJsonError(res, error instanceof HttpError ? error.status : 500, message);
|
|
401
425
|
});
|
|
402
|
-
})
|
|
426
|
+
});
|
|
427
|
+
process.on("SIGTERM", () => {
|
|
428
|
+
httpServer.close();
|
|
429
|
+
mcpHandler.close();
|
|
430
|
+
});
|
|
431
|
+
httpServer.listen(PORT, HOST, () => {
|
|
403
432
|
console.error(`[sodium-pm-mcp-http] v${VERSION} listening on http://${HOST}:${PORT}${ENDPOINT_PATH} (api: ${BASE_URL}, writes: ${WRITES_ENABLED ? "enabled" : "disabled"})`);
|
|
404
433
|
});
|
|
405
434
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as buildServer } from "./src-
|
|
2
|
+
import { t as buildServer } from "./src-DBBUms70.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import {
|
|
4
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
//#region src/config.ts
|
|
7
7
|
function loadContext() {
|
|
@@ -32,21 +32,23 @@ function loadContext() {
|
|
|
32
32
|
//#endregion
|
|
33
33
|
//#region src/index.ts
|
|
34
34
|
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
35
|
-
|
|
35
|
+
function main() {
|
|
36
36
|
const context = loadContext();
|
|
37
|
-
|
|
37
|
+
serveStdio(() => buildServer({
|
|
38
38
|
context,
|
|
39
39
|
serverName: "Sodium Practice Management",
|
|
40
40
|
serverVersion: VERSION
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
}), { onerror: (error) => {
|
|
42
|
+
console.error(`[sodium-pm-mcp] ${error.message}`);
|
|
43
|
+
} });
|
|
44
44
|
console.error(`[sodium-pm-mcp] v${VERSION} ready (tenant: ${context.tenant}, writes: ${context.writesEnabled ? "enabled" : "disabled"})`);
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
try {
|
|
47
|
+
main();
|
|
48
|
+
} catch (error) {
|
|
47
49
|
const message = error instanceof Error ? error.message : String(error);
|
|
48
50
|
console.error(`[sodium-pm-mcp] fatal: ${message}`);
|
|
49
51
|
process.exit(1);
|
|
50
|
-
}
|
|
52
|
+
}
|
|
51
53
|
//#endregion
|
|
52
54
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
//#region ../mcp-core/src/generated/core/bodySerializer.gen.ts
|
|
@@ -9200,6 +9200,13 @@ async function handleCreateSavedTaskFilter(api, args) {
|
|
|
9200
9200
|
//#endregion
|
|
9201
9201
|
//#region ../mcp-core/src/server.ts
|
|
9202
9202
|
const practiceField = z.string().optional().describe("Practice code to run this call against, from list_practices. Needed only when the user belongs to more than one practice; omit when they have exactly one — it is selected automatically.");
|
|
9203
|
+
function captureClientInfo(server, api) {
|
|
9204
|
+
const info = server.server.getClientVersion();
|
|
9205
|
+
if (info?.name && info?.version) api.setMcpClientInfo({
|
|
9206
|
+
name: info.name,
|
|
9207
|
+
version: info.version
|
|
9208
|
+
});
|
|
9209
|
+
}
|
|
9203
9210
|
function registerTenantTool(server, api, name, config, cb) {
|
|
9204
9211
|
const inputSchema = {
|
|
9205
9212
|
...config.inputSchema ?? {},
|
|
@@ -9210,6 +9217,7 @@ function registerTenantTool(server, api, name, config, cb) {
|
|
|
9210
9217
|
inputSchema
|
|
9211
9218
|
}, async (args, extra) => {
|
|
9212
9219
|
const started = Date.now();
|
|
9220
|
+
captureClientInfo(server, api);
|
|
9213
9221
|
const { practice, ...rest } = args;
|
|
9214
9222
|
const resolution = await resolveTenantForCall(api, typeof practice === "string" ? practice : void 0);
|
|
9215
9223
|
if (!resolution.ok) {
|
|
@@ -9276,13 +9284,7 @@ async function buildServer(config) {
|
|
|
9276
9284
|
instructions,
|
|
9277
9285
|
capabilities: { tools: {} }
|
|
9278
9286
|
});
|
|
9279
|
-
server.server.oninitialized = () =>
|
|
9280
|
-
const info = server.server.getClientVersion();
|
|
9281
|
-
if (info?.name && info?.version) api.setMcpClientInfo({
|
|
9282
|
-
name: info.name,
|
|
9283
|
-
version: info.version
|
|
9284
|
-
});
|
|
9285
|
-
};
|
|
9287
|
+
server.server.oninitialized = () => captureClientInfo(server, api);
|
|
9286
9288
|
registerTenantTool(server, api, "get_practice_details", {
|
|
9287
9289
|
title: "Get practice details",
|
|
9288
9290
|
description: "Get a consolidated overview of the practice including name, contact details, client/service/user counts, connections, and settings. Use this when the user asks about their practice, tenant, or wants a summary of their account.",
|
|
@@ -9303,6 +9305,7 @@ async function buildServer(config) {
|
|
|
9303
9305
|
openWorldHint: true
|
|
9304
9306
|
}
|
|
9305
9307
|
}, async (args) => {
|
|
9308
|
+
captureClientInfo(server, api);
|
|
9306
9309
|
await resolveTenantForCall(api, args.practice);
|
|
9307
9310
|
return handleWhoami(api);
|
|
9308
9311
|
});
|
|
@@ -9315,7 +9318,10 @@ async function buildServer(config) {
|
|
|
9315
9318
|
idempotentHint: true,
|
|
9316
9319
|
openWorldHint: true
|
|
9317
9320
|
}
|
|
9318
|
-
}, () =>
|
|
9321
|
+
}, () => {
|
|
9322
|
+
captureClientInfo(server, api);
|
|
9323
|
+
return handleListPractices(api);
|
|
9324
|
+
});
|
|
9319
9325
|
registerTenantTool(server, api, "list_clients", {
|
|
9320
9326
|
title: "List / search / filter clients",
|
|
9321
9327
|
description: "List clients with any combination of: search (code/name/internal reference, 3+ chars), status (Active/Inactive/Prospect/LostProspect), type (PrivateLimitedCompany/PublicLimitedCompany/LimitedLiabilityPartnership/Partnership/Individual/Trust/Charity/SoleTrader), manager/partner/associate user codes, service codes, a saved filter code, sort, and pagination. Use search for 'find ACME'-style queries. Use type for 'list limited companies' (pass PrivateLimitedCompany + PublicLimitedCompany). Use status: ['Active'] to exclude prospects/inactive. Returns up to 50 clients per page — paginate via offset for more. For 'how many X?' questions, pass limit=0 to get just the total count without fetching any client data. Follow up with get_client_summary for full detail on a specific client.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sodiumhq/mcp-pm",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.5115",
|
|
4
4
|
"description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,9 +44,10 @@
|
|
|
44
44
|
"prepublishOnly": "pnpm typecheck && pnpm build"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@modelcontextprotocol/
|
|
47
|
+
"@modelcontextprotocol/node": "2.0.0",
|
|
48
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
48
49
|
"jose": "^6.2.3",
|
|
49
|
-
"zod": "^
|
|
50
|
+
"zod": "^4.4.3"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@sodiumhq/mcp-core": "workspace:*",
|