@thalovant/mcp-server 0.1.17 → 0.1.19

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.19
4
+
5
+ - Require Node SDK 0.3.5 or newer so runtime tools receive secure MQTT URL enforcement and bounded MQTT setup and HTTP failure cleanup.
6
+ - Keep package, registry, and container source metadata on version 0.1.19. Runtime tool names, inputs, and identity serialization remain compatible.
7
+
8
+ ## 0.1.18
9
+
10
+ - 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.
11
+ - 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.
12
+ - 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.
13
+
3
14
  ## Unreleased
4
15
 
5
16
  ### Fixed
package/README.md CHANGED
@@ -393,6 +393,26 @@ 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.19 uses `@thalovant/sdk` 0.3.5 or newer. This release enforces
403
+ secure effective MQTT URLs and carries a single connection deadline through
404
+ MQTT setup and HTTP failure cleanup. Runtime tools support
405
+ HiveMind v3 Noise over WSS, HTTPS and MQTT over TLS. `thalovant_healthcheck`
406
+ reports readiness only after authentication; a reachable hub or broker alone
407
+ does not establish a runtime session.
408
+
409
+ Keep the SDK configuration directory persistent and private between server
410
+ restarts (`~/.config/thalovant`, or the configured XDG/Windows equivalent). It
411
+ contains the client Noise key and trusted server pins. An authentication
412
+ failure preserves those pins; replacing a server key requires an explicit,
413
+ verified trust change. HTTPS identities must advertise the hub's HTTPS plugin,
414
+ and MQTT requires the identity's broker credentials and topic prefix.
415
+
396
416
  ## Development
397
417
 
398
418
  ```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,38 @@ 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
+ /**
658
+ * Hold an identity lease through both the tool action and client cleanup.
659
+ * This keeps HTTP/MQTT admissions from replacing another call's Noise session;
660
+ * a failed action still closes its client before the next lease starts.
661
+ */
662
+ async function withRuntimeClient(options, run) {
663
+ const client = await createRuntimeClient(options);
664
+ const key = createHash("sha256").update(JSON.stringify([
665
+ client.identity.endpointBase(), client.identity.accessKey,
666
+ ])).digest("hex");
667
+ const previous = runtimeLeases.get(key) ?? Promise.resolve();
668
+ let release;
669
+ const current = new Promise(resolve => { release = resolve; });
670
+ runtimeLeases.set(key, current);
671
+ await previous;
672
+ try {
673
+ return await run(client);
674
+ }
675
+ finally {
676
+ try {
677
+ await client.close();
678
+ }
679
+ finally {
680
+ release();
681
+ if (runtimeLeases.get(key) === current)
682
+ runtimeLeases.delete(key);
683
+ }
684
+ }
685
+ }
654
686
  function summarizeReply(reply) {
655
687
  const displayItems = reply.displayItems({ maxTextChars: 1_000 });
656
688
  return {
@@ -1249,8 +1281,7 @@ export function createServer() {
1249
1281
  openWorldHint: false,
1250
1282
  },
1251
1283
  }, async (args) => {
1252
- const client = await createRuntimeClient(args);
1253
- try {
1284
+ return withRuntimeClient(args, async (client) => {
1254
1285
  const identity = client.identity;
1255
1286
  return jsonContent({
1256
1287
  identity: redactSecrets(identity.asObject(false)),
@@ -1259,10 +1290,7 @@ export function createServer() {
1259
1290
  supportsRequestedProtocol: identity.supportsProtocol(args.protocol),
1260
1291
  health: client.healthcheck(),
1261
1292
  });
1262
- }
1263
- finally {
1264
- await client.close();
1265
- }
1293
+ });
1266
1294
  });
1267
1295
  registerThalovantTool(server, "thalovant_healthcheck", {
1268
1296
  title: "Runtime Healthcheck",
@@ -1276,14 +1304,10 @@ export function createServer() {
1276
1304
  openWorldHint: true,
1277
1305
  },
1278
1306
  }, async ({ timeoutMs, ...runtime }) => {
1279
- const client = await createRuntimeClient(runtime);
1280
- try {
1307
+ return withRuntimeClient(runtime, async (client) => {
1281
1308
  await client.connect(clampTimeout(timeoutMs));
1282
1309
  return jsonContent(redactSecrets(client.healthcheck()));
1283
- }
1284
- finally {
1285
- await client.close();
1286
- }
1310
+ });
1287
1311
  });
1288
1312
  registerThalovantTool(server, "thalovant_ask", {
1289
1313
  title: "Ask Hub",
@@ -1306,8 +1330,7 @@ export function createServer() {
1306
1330
  openWorldHint: true,
1307
1331
  },
1308
1332
  }, async ({ text, timeoutMs, lang, sessionId, requestId, context, replySettleMs, emptyReplyWaitMs, ...runtime }) => {
1309
- const client = await createRuntimeClient(runtime);
1310
- try {
1333
+ return withRuntimeClient(runtime, async (client) => {
1311
1334
  const builtContext = context ? buildClientContext({}, context) : undefined;
1312
1335
  const reply = await client.ask(text, {
1313
1336
  timeoutMs: clampTimeout(timeoutMs),
@@ -1319,10 +1342,7 @@ export function createServer() {
1319
1342
  emptyReplyWaitMs,
1320
1343
  });
1321
1344
  return jsonContent(redactSecrets(summarizeReply(reply)));
1322
- }
1323
- finally {
1324
- await client.close();
1325
- }
1345
+ });
1326
1346
  });
1327
1347
  registerThalovantTool(server, "thalovant_send_action", {
1328
1348
  title: "Send Action",
@@ -1343,8 +1363,7 @@ export function createServer() {
1343
1363
  openWorldHint: true,
1344
1364
  },
1345
1365
  }, async ({ payload, title, lang, sessionId, requestId, context, ...runtime }) => {
1346
- const client = await createRuntimeClient(runtime);
1347
- try {
1366
+ return withRuntimeClient(runtime, async (client) => {
1348
1367
  await client.sendAction(payload, {
1349
1368
  title,
1350
1369
  lang,
@@ -1353,10 +1372,7 @@ export function createServer() {
1353
1372
  context: context ? buildClientContext({}, context) : undefined,
1354
1373
  });
1355
1374
  return textContent("Action sent.");
1356
- }
1357
- finally {
1358
- await client.close();
1359
- }
1375
+ });
1360
1376
  });
1361
1377
  registerThalovantTool(server, "thalovant_send_code", {
1362
1378
  title: "Send Code",
@@ -1378,8 +1394,7 @@ export function createServer() {
1378
1394
  openWorldHint: true,
1379
1395
  },
1380
1396
  }, async ({ value, kind, label, lang, sessionId, requestId, context, ...runtime }) => {
1381
- const client = await createRuntimeClient(runtime);
1382
- try {
1397
+ return withRuntimeClient(runtime, async (client) => {
1383
1398
  await client.sendCode(value, {
1384
1399
  kind,
1385
1400
  label,
@@ -1389,10 +1404,7 @@ export function createServer() {
1389
1404
  context: context ? buildClientContext({}, context) : undefined,
1390
1405
  });
1391
1406
  return textContent("Code sent.");
1392
- }
1393
- finally {
1394
- await client.close();
1395
- }
1407
+ });
1396
1408
  });
1397
1409
  registerThalovantTool(server, "thalovant_emit_event", {
1398
1410
  title: "Emit Event",
@@ -1410,14 +1422,10 @@ export function createServer() {
1410
1422
  openWorldHint: true,
1411
1423
  },
1412
1424
  }, async ({ eventType, data, context, ...runtime }) => {
1413
- const client = await createRuntimeClient(runtime);
1414
- try {
1425
+ return withRuntimeClient(runtime, async (client) => {
1415
1426
  await client.emit(eventType, data, context ? buildClientContext({}, context) : undefined);
1416
1427
  return textContent("Event emitted.");
1417
- }
1418
- finally {
1419
- await client.close();
1420
- }
1428
+ });
1421
1429
  });
1422
1430
  registerThalovantTool(server, "thalovant_wait_for_event", {
1423
1431
  title: "Wait For Event",
@@ -1435,8 +1443,7 @@ export function createServer() {
1435
1443
  openWorldHint: true,
1436
1444
  },
1437
1445
  }, async ({ eventName, timeoutMs, sessionId, requestId, context, ...runtime }) => {
1438
- const client = await createRuntimeClient(runtime);
1439
- try {
1446
+ return withRuntimeClient(runtime, async (client) => {
1440
1447
  const event = await client.waitForEvent(eventName, {
1441
1448
  timeoutMs: clampTimeout(timeoutMs),
1442
1449
  sessionId,
@@ -1444,10 +1451,7 @@ export function createServer() {
1444
1451
  context: context ? buildClientContext({}, context) : undefined,
1445
1452
  });
1446
1453
  return jsonContent(redactSecrets(event.asObject()));
1447
- }
1448
- finally {
1449
- await client.close();
1450
- }
1454
+ });
1451
1455
  });
1452
1456
  registerThalovantTool(server, "thalovant_get_analytics_overview", {
1453
1457
  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.19",
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.5",
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.19",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@thalovant/mcp-server",
15
- "version": "0.1.17",
15
+ "version": "0.1.19",
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.19",
23
23
  "transport": {
24
24
  "type": "streamable-http",
25
25
  "url": "http://127.0.0.1:3000/mcp"