@sodiumhq/mcp-pm 0.1.0-beta.4441 → 0.1.0-beta.4448

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 CHANGED
@@ -62,6 +62,7 @@ Only enable write mode with an AI client you trust — it hands the client the a
62
62
  - **`get_practice_dashboard`** — one-call "state of the practice right now": overdue/blocked/due-soon work, what clients are sitting on, unassigned steps, unbilled time, open proposal pipeline. *"Give me my morning briefing."*
63
63
  - **`get_practice_details`** — consolidated practice overview (counts, connections, settings)
64
64
  - **`whoami`** — show the authenticated user (name, email, code), tenant (name, code, status), and practice name. Useful for verifying which account is connected.
65
+ - **`list_practices`** — list the practices you belong to. *"Which practices can I access?"* If you belong to more than one, tools take an optional `practice` code to say which one a request is about; with a single practice it is selected automatically.
65
66
 
66
67
  **Clients**
67
68
  - **`list_clients`** — list and filter clients by search, status, type, assignee, services, saved filters
package/dist/http.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-CTHTAM7g.mjs";
2
+ import { t as buildServer } from "./src-DEbTNFXg.mjs";
3
3
  import { createRequire } from "node:module";
4
- import { createHash, randomUUID } from "node:crypto";
4
+ import { createHash } 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";
@@ -140,26 +140,6 @@ const WRITES_ENABLED = writesRaw === "true" || writesRaw === "1";
140
140
  const AUTH = loadAuthConfig();
141
141
  const OBO = AUTH ? loadOboConfig(AUTH.issuer) : null;
142
142
  const ENDPOINT_PATH = AUTH ? new URL(AUTH.resourceUrl).pathname : process.env.MCP_ENDPOINT_PATH ?? "/mcp";
143
- const sessions = /* @__PURE__ */ new Map();
144
- const SESSION_IDLE_MS = Number(process.env.MCP_SESSION_IDLE_MS ?? 30 * 6e4);
145
- const SWEEP_INTERVAL_MS = 6e4;
146
- function touch(sessionId) {
147
- const session = sessions.get(sessionId);
148
- if (session) session.lastActivity = Date.now();
149
- }
150
- async function sweepIdleSessions() {
151
- const cutoff = Date.now() - SESSION_IDLE_MS;
152
- for (const [sessionId, session] of sessions) {
153
- if (session.lastActivity > cutoff) continue;
154
- sessions.delete(sessionId);
155
- console.error(`[sodium-pm-mcp-http] session ${sessionId} swept (idle)`);
156
- try {
157
- await session.transport.close();
158
- } catch (error) {
159
- console.error(`[sodium-pm-mcp-http] error closing swept session: ${String(error)}`);
160
- }
161
- }
162
- }
163
143
  const MAX_BODY_BYTES = 4 * 1024 * 1024;
164
144
  async function readJsonBody(req) {
165
145
  const chunks = [];
@@ -182,7 +162,7 @@ function sendJsonError(res, status, message) {
182
162
  id: null
183
163
  }));
184
164
  }
185
- async function createSession(req) {
165
+ async function handleMcpPost(req, res, body) {
186
166
  const authHeader = req.headers.authorization;
187
167
  if (!authHeader?.startsWith("Bearer ")) throw new HttpError(401, "Authorization: Bearer <token> header is required.");
188
168
  const bearerToken = authHeader.slice(7).trim();
@@ -195,7 +175,7 @@ async function createSession(req) {
195
175
  }
196
176
  const tenantHeader = req.headers["x-sodium-tenant"];
197
177
  const tenant = (Array.isArray(tenantHeader) ? tenantHeader[0] : tenantHeader) ?? process.env.SODIUM_TENANT;
198
- if (!tenant) throw new HttpError(400, "x-sodium-tenant header is required (or set SODIUM_TENANT).");
178
+ const isInit = isInitializeRequest(body);
199
179
  const server = await buildServer({
200
180
  context: {
201
181
  bearerToken: apiToken,
@@ -204,26 +184,17 @@ async function createSession(req) {
204
184
  writesEnabled: writesAllowed
205
185
  },
206
186
  serverName: "Sodium Practice Management",
207
- serverVersion: VERSION
187
+ serverVersion: VERSION,
188
+ includeStartupContext: isInit
208
189
  });
209
- const transport = new StreamableHTTPServerTransport({
210
- sessionIdGenerator: () => randomUUID(),
211
- onsessioninitialized: (sessionId) => {
212
- sessions.set(sessionId, {
213
- transport,
214
- lastActivity: Date.now()
215
- });
216
- console.error(`[sodium-pm-mcp-http] session ${sessionId} started (tenant: ${tenant}, live: ${sessions.size})`);
217
- }
190
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
191
+ res.on("close", () => {
192
+ transport.close();
193
+ server.close();
218
194
  });
219
- transport.onclose = () => {
220
- if (transport.sessionId) {
221
- sessions.delete(transport.sessionId);
222
- console.error(`[sodium-pm-mcp-http] session ${transport.sessionId} closed`);
223
- }
224
- };
195
+ if (isInit) console.error(`[sodium-pm-mcp-http] initialize (tenant: ${tenant || "per-call"}, writes: ${writesAllowed})`);
225
196
  await server.connect(transport);
226
- return transport;
197
+ await transport.handleRequest(req, res, body);
227
198
  }
228
199
  var HttpError = class extends Error {
229
200
  constructor(status, message) {
@@ -236,8 +207,7 @@ async function handle(req, res) {
236
207
  if (req.method === "GET" && url.pathname === "/health") {
237
208
  res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
238
209
  status: "ok",
239
- version: VERSION,
240
- sessions: sessions.size
210
+ version: VERSION
241
211
  }));
242
212
  return;
243
213
  }
@@ -249,25 +219,14 @@ async function handle(req, res) {
249
219
  sendJsonError(res, 404, `Not found. MCP endpoint is ${ENDPOINT_PATH}.`);
250
220
  return;
251
221
  }
252
- const sessionId = req.headers["mcp-session-id"];
253
- const existing = typeof sessionId === "string" ? sessions.get(sessionId) : void 0;
254
- if (existing && typeof sessionId === "string") {
255
- touch(sessionId);
256
- await existing.transport.handleRequest(req, res);
257
- return;
258
- }
259
222
  if (req.method !== "POST") {
260
- sendJsonError(res, 400, "No valid MCP session. Send an initialize request first.");
223
+ res.setHeader("Allow", "POST");
224
+ sendJsonError(res, 405, "Stateless MCP endpoint. Send requests as POST.");
261
225
  return;
262
226
  }
263
- const body = await readJsonBody(req);
264
- if (!isInitializeRequest(body)) {
265
- sendJsonError(res, 400, "No valid MCP session. Send an initialize request first.");
266
- return;
267
- }
268
- await (await createSession(req)).handleRequest(req, res, body);
227
+ await handleMcpPost(req, res, await readJsonBody(req));
269
228
  }
270
- const httpServer = createServer((req, res) => {
229
+ createServer((req, res) => {
271
230
  handle(req, res).catch((error) => {
272
231
  const message = error instanceof Error ? error.message : String(error);
273
232
  console.error(`[sodium-pm-mcp-http] ${req.method} ${req.url} failed: ${message}`);
@@ -281,11 +240,7 @@ const httpServer = createServer((req, res) => {
281
240
  if (AUTH && error instanceof HttpError && error.status === 401) res.setHeader("WWW-Authenticate", buildChallenge(AUTH));
282
241
  sendJsonError(res, error instanceof HttpError ? error.status : 500, message);
283
242
  });
284
- });
285
- setInterval(() => {
286
- sweepIdleSessions();
287
- }, SWEEP_INTERVAL_MS).unref();
288
- httpServer.listen(PORT, HOST, () => {
243
+ }).listen(PORT, HOST, () => {
289
244
  console.error(`[sodium-pm-mcp-http] v${VERSION} listening on http://${HOST}:${PORT}${ENDPOINT_PATH} (api: ${BASE_URL}, writes: ${WRITES_ENABLED ? "enabled" : "disabled"})`);
290
245
  });
291
246
  //#endregion
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-CTHTAM7g.mjs";
2
+ import { t as buildServer } from "./src-DEbTNFXg.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";