@thalovant/mcp-server 0.1.17 → 0.1.18

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.18
4
+
5
+ - Serialize runtime tool calls sharing a hub client identity so concurrent calls cannot replace each other's Noise sessions or HTTP message queues. Different identities remain independent.
6
+ - Use Node SDK 0.3.4 or newer for authenticated HiveMind v3 Noise across WSS, HTTPS and MQTT. HTTPS retains replica affinity and uses binary encrypted polling; MQTT reauthenticates when reconnecting.
7
+ - Derive the advertised MCP server version from the package manifest so stdio, Streamable HTTP, npm, OCI and registry metadata agree. Add transport smoke assertions for version consistency.
8
+
3
9
  ## Unreleased
4
10
 
5
11
  ### Fixed
package/README.md CHANGED
@@ -393,6 +393,24 @@ With the flag unset, a call with any `sourceType` other than `catalog` fails bef
393
393
 
394
394
  Set `THALOVANT_MCP_READONLY=1` to register only tools annotated `readOnlyHint: true`. Write and destructive tools are then never registered and never appear in `tools/list`, so an operator can run an observe-only agent without hand-writing a denylist. Like the other registration-time gates it is read when a server instance is created; `thalovant_config_status` reports the state as `readOnly`.
395
395
 
396
+ ## HiveMind Runtime Compatibility
397
+
398
+ Runtime calls sharing the same hub client identity run sequentially within one
399
+ MCP process. Use a distinct client identity for each independently running MCP
400
+ server so the hub can keep their sessions separate.
401
+
402
+ Version 0.1.18 uses `@thalovant/sdk` 0.3.4 or newer. Runtime tools support
403
+ HiveMind v3 Noise over WSS, HTTPS and MQTT over TLS. `thalovant_healthcheck`
404
+ reports readiness only after authentication; a reachable hub or broker alone
405
+ does not establish a runtime session.
406
+
407
+ Keep the SDK configuration directory persistent and private between server
408
+ restarts (`~/.config/thalovant`, or the configured XDG/Windows equivalent). It
409
+ contains the client Noise key and trusted server pins. An authentication
410
+ failure preserves those pins; replacing a server key requires an explicit,
411
+ verified trust change. HTTPS identities must advertise the hub's HTTPS plugin,
412
+ and MQTT requires the identity's broker credentials and topic prefix.
413
+
396
414
  ## Development
397
415
 
398
416
  ```bash
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { DEFAULT_CONTROL_API_URL, ThalovantClient, ThalovantControlPlane, Thalov
7
7
  import { AsyncLocalStorage } from "node:async_hooks";
8
8
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
9
9
  import { createServer as createHttpServer } from "node:http";
10
- import { realpathSync } from "node:fs";
10
+ import { readFileSync, realpathSync } from "node:fs";
11
11
  import { appendFile, readFile, writeFile } from "node:fs/promises";
12
12
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
13
  import { homedir } from "node:os";
@@ -15,7 +15,7 @@ import { mkdir } from "node:fs/promises";
15
15
  import { fileURLToPath, pathToFileURL } from "node:url";
16
16
  import { createRemoteJWKSet, jwtVerify } from "jose";
17
17
  import { z } from "zod";
18
- const VERSION = "0.1.10";
18
+ const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
19
19
  const DEFAULT_TIMEOUT_MS = 30_000;
20
20
  const MAX_TIMEOUT_MS = 300_000;
21
21
  const MAX_LIMIT = 100;
@@ -651,6 +651,33 @@ async function createRuntimeClient(options) {
651
651
  protocol,
652
652
  });
653
653
  }
654
+ // The hub HTTP/MQTT queues and Noise sessions are keyed by client identity.
655
+ // A second tool call must finish after the first closes that identity's session.
656
+ const runtimeLeases = new Map();
657
+ async function withRuntimeClient(options, run) {
658
+ const client = await createRuntimeClient(options);
659
+ const key = createHash("sha256").update(JSON.stringify([
660
+ client.identity.endpointBase(), client.identity.accessKey,
661
+ ])).digest("hex");
662
+ const previous = runtimeLeases.get(key) ?? Promise.resolve();
663
+ let release;
664
+ const current = new Promise(resolve => { release = resolve; });
665
+ runtimeLeases.set(key, current);
666
+ await previous;
667
+ try {
668
+ return await run(client);
669
+ }
670
+ finally {
671
+ try {
672
+ await client.close();
673
+ }
674
+ finally {
675
+ release();
676
+ if (runtimeLeases.get(key) === current)
677
+ runtimeLeases.delete(key);
678
+ }
679
+ }
680
+ }
654
681
  function summarizeReply(reply) {
655
682
  const displayItems = reply.displayItems({ maxTextChars: 1_000 });
656
683
  return {
@@ -1249,8 +1276,7 @@ export function createServer() {
1249
1276
  openWorldHint: false,
1250
1277
  },
1251
1278
  }, async (args) => {
1252
- const client = await createRuntimeClient(args);
1253
- try {
1279
+ return withRuntimeClient(args, async (client) => {
1254
1280
  const identity = client.identity;
1255
1281
  return jsonContent({
1256
1282
  identity: redactSecrets(identity.asObject(false)),
@@ -1259,10 +1285,7 @@ export function createServer() {
1259
1285
  supportsRequestedProtocol: identity.supportsProtocol(args.protocol),
1260
1286
  health: client.healthcheck(),
1261
1287
  });
1262
- }
1263
- finally {
1264
- await client.close();
1265
- }
1288
+ });
1266
1289
  });
1267
1290
  registerThalovantTool(server, "thalovant_healthcheck", {
1268
1291
  title: "Runtime Healthcheck",
@@ -1276,14 +1299,10 @@ export function createServer() {
1276
1299
  openWorldHint: true,
1277
1300
  },
1278
1301
  }, async ({ timeoutMs, ...runtime }) => {
1279
- const client = await createRuntimeClient(runtime);
1280
- try {
1302
+ return withRuntimeClient(runtime, async (client) => {
1281
1303
  await client.connect(clampTimeout(timeoutMs));
1282
1304
  return jsonContent(redactSecrets(client.healthcheck()));
1283
- }
1284
- finally {
1285
- await client.close();
1286
- }
1305
+ });
1287
1306
  });
1288
1307
  registerThalovantTool(server, "thalovant_ask", {
1289
1308
  title: "Ask Hub",
@@ -1306,8 +1325,7 @@ export function createServer() {
1306
1325
  openWorldHint: true,
1307
1326
  },
1308
1327
  }, async ({ text, timeoutMs, lang, sessionId, requestId, context, replySettleMs, emptyReplyWaitMs, ...runtime }) => {
1309
- const client = await createRuntimeClient(runtime);
1310
- try {
1328
+ return withRuntimeClient(runtime, async (client) => {
1311
1329
  const builtContext = context ? buildClientContext({}, context) : undefined;
1312
1330
  const reply = await client.ask(text, {
1313
1331
  timeoutMs: clampTimeout(timeoutMs),
@@ -1319,10 +1337,7 @@ export function createServer() {
1319
1337
  emptyReplyWaitMs,
1320
1338
  });
1321
1339
  return jsonContent(redactSecrets(summarizeReply(reply)));
1322
- }
1323
- finally {
1324
- await client.close();
1325
- }
1340
+ });
1326
1341
  });
1327
1342
  registerThalovantTool(server, "thalovant_send_action", {
1328
1343
  title: "Send Action",
@@ -1343,8 +1358,7 @@ export function createServer() {
1343
1358
  openWorldHint: true,
1344
1359
  },
1345
1360
  }, async ({ payload, title, lang, sessionId, requestId, context, ...runtime }) => {
1346
- const client = await createRuntimeClient(runtime);
1347
- try {
1361
+ return withRuntimeClient(runtime, async (client) => {
1348
1362
  await client.sendAction(payload, {
1349
1363
  title,
1350
1364
  lang,
@@ -1353,10 +1367,7 @@ export function createServer() {
1353
1367
  context: context ? buildClientContext({}, context) : undefined,
1354
1368
  });
1355
1369
  return textContent("Action sent.");
1356
- }
1357
- finally {
1358
- await client.close();
1359
- }
1370
+ });
1360
1371
  });
1361
1372
  registerThalovantTool(server, "thalovant_send_code", {
1362
1373
  title: "Send Code",
@@ -1378,8 +1389,7 @@ export function createServer() {
1378
1389
  openWorldHint: true,
1379
1390
  },
1380
1391
  }, async ({ value, kind, label, lang, sessionId, requestId, context, ...runtime }) => {
1381
- const client = await createRuntimeClient(runtime);
1382
- try {
1392
+ return withRuntimeClient(runtime, async (client) => {
1383
1393
  await client.sendCode(value, {
1384
1394
  kind,
1385
1395
  label,
@@ -1389,10 +1399,7 @@ export function createServer() {
1389
1399
  context: context ? buildClientContext({}, context) : undefined,
1390
1400
  });
1391
1401
  return textContent("Code sent.");
1392
- }
1393
- finally {
1394
- await client.close();
1395
- }
1402
+ });
1396
1403
  });
1397
1404
  registerThalovantTool(server, "thalovant_emit_event", {
1398
1405
  title: "Emit Event",
@@ -1410,14 +1417,10 @@ export function createServer() {
1410
1417
  openWorldHint: true,
1411
1418
  },
1412
1419
  }, async ({ eventType, data, context, ...runtime }) => {
1413
- const client = await createRuntimeClient(runtime);
1414
- try {
1420
+ return withRuntimeClient(runtime, async (client) => {
1415
1421
  await client.emit(eventType, data, context ? buildClientContext({}, context) : undefined);
1416
1422
  return textContent("Event emitted.");
1417
- }
1418
- finally {
1419
- await client.close();
1420
- }
1423
+ });
1421
1424
  });
1422
1425
  registerThalovantTool(server, "thalovant_wait_for_event", {
1423
1426
  title: "Wait For Event",
@@ -1435,8 +1438,7 @@ export function createServer() {
1435
1438
  openWorldHint: true,
1436
1439
  },
1437
1440
  }, async ({ eventName, timeoutMs, sessionId, requestId, context, ...runtime }) => {
1438
- const client = await createRuntimeClient(runtime);
1439
- try {
1441
+ return withRuntimeClient(runtime, async (client) => {
1440
1442
  const event = await client.waitForEvent(eventName, {
1441
1443
  timeoutMs: clampTimeout(timeoutMs),
1442
1444
  sessionId,
@@ -1444,10 +1446,7 @@ export function createServer() {
1444
1446
  context: context ? buildClientContext({}, context) : undefined,
1445
1447
  });
1446
1448
  return jsonContent(redactSecrets(event.asObject()));
1447
- }
1448
- finally {
1449
- await client.close();
1450
- }
1449
+ });
1451
1450
  });
1452
1451
  registerThalovantTool(server, "thalovant_get_analytics_overview", {
1453
1452
  title: "Analytics Overview",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thalovant/mcp-server",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Stdio and Streamable HTTP MCP server for Thalovant control-plane and hub runtime APIs.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@modelcontextprotocol/sdk": "^1.30.0",
67
- "@thalovant/sdk": "^0.3.3",
67
+ "@thalovant/sdk": "^0.3.4",
68
68
  "jose": "^6.2.8",
69
69
  "zod": "^3.25.76"
70
70
  },
package/server.json CHANGED
@@ -7,19 +7,19 @@
7
7
  "url": "https://github.com/thalovant/thalovant-mcp",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.1.17",
10
+ "version": "0.1.18",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@thalovant/mcp-server",
15
- "version": "0.1.17",
15
+ "version": "0.1.18",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
19
19
  },
20
20
  {
21
21
  "registryType": "oci",
22
- "identifier": "ghcr.io/thalovant/thalovant-mcp:0.1.17",
22
+ "identifier": "ghcr.io/thalovant/thalovant-mcp:0.1.18",
23
23
  "transport": {
24
24
  "type": "streamable-http",
25
25
  "url": "http://127.0.0.1:3000/mcp"