@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 CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-Bl4qYJBU.mjs";
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 { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
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
- if (typeof body === "object" && body !== null && "method" in body) {
253
- const method = String(body.method);
254
- if (method === "tools/call") {
255
- const params = body.params;
256
- return `POST tools/call ${typeof params?.name === "string" ? params.name : "?"}`;
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 "POST unknown";
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 isInit = isInitializeRequest(body);
330
- const server = await buildServer({
331
- context: {
332
- bearerToken: apiToken,
333
- tenant,
334
- baseUrl: BASE_URL,
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
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
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 (isInit) console.error(`[sodium-pm-mcp-http] initialize (tenant: ${tenant || "per-call"}, writes: ${writesAllowed})`);
351
- await server.connect(transport);
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
- }).listen(PORT, HOST, () => {
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-Bl4qYJBU.mjs";
2
+ import { t as buildServer } from "./src-DBBUms70.mjs";
3
3
  import { createRequire } from "node:module";
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
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
- async function main() {
35
+ function main() {
36
36
  const context = loadContext();
37
- const server = await buildServer({
37
+ serveStdio(() => buildServer({
38
38
  context,
39
39
  serverName: "Sodium Practice Management",
40
40
  serverVersion: VERSION
41
- });
42
- const transport = new StdioServerTransport();
43
- await server.connect(transport);
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
- main().catch((error) => {
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/sdk/server/mcp.js";
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
- }, () => handleListPractices(api));
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.5113",
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/sdk": "^1.26.0",
47
+ "@modelcontextprotocol/node": "2.0.0",
48
+ "@modelcontextprotocol/server": "2.0.0",
48
49
  "jose": "^6.2.3",
49
- "zod": "^3.25.67"
50
+ "zod": "^4.4.3"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@sodiumhq/mcp-core": "workspace:*",