@thalovant/mcp-server 0.1.18 → 0.1.20
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 +16 -0
- package/README.md +43 -1
- package/SECURITY.md +10 -0
- package/dist/index.js +90 -40
- package/dist/runtime-lease.d.ts +8 -0
- package/dist/runtime-lease.js +64 -0
- package/package.json +3 -3
- package/server.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.20
|
|
4
|
+
|
|
5
|
+
- Require HTTPS for credential-bearing API calls, preserving explicit loopback HTTP for local development. Reject API redirects through the SDK. Instantiate runtime clients only after acquiring their identity lease.
|
|
6
|
+
- Bind server-managed Thalovant tokens and password login to their configured API origin. Reject cross-origin tool overrides before any outbound request, including credentials for remote principals; retain configured custom origins and anonymous public discovery.
|
|
7
|
+
- Add `thalovant_query` for routed HiveMind queries, preserving query/request/session correlation and runtime identity cleanup; hide it in read-only mode. The default catalogue now contains 41 tools.
|
|
8
|
+
- Add read-only `thalovant_intent_inventory` and `thalovant_get_operation` tools for runtime discovery and asynchronous provisioning status. Inventory preserves fallback knowledge and conservative answerability.
|
|
9
|
+
- Require Node SDK 0.3.9 for aligned intent discovery, request correlation, whole-operation reply deadlines, fixed Ask settlement windows, cancellation and retained cleanup ownership, and credential-safe control-plane redirect handling.
|
|
10
|
+
- Retain the runtime identity lease until actual cleanup, even after a caller close timeout; failed cleanup blocks later identity reuse with an explicit error. Bound queued lease acquisition to six seconds without releasing pending ownership or executing expired callers.
|
|
11
|
+
- Upgrade the test runner to Vitest 4.1.11 to remove GHSA-82fw-gwwq-j7x9.
|
|
12
|
+
- Test Node 20, 22 and 24; add per-principal/shared credential regressions and exercise the inventory tool over a real HTTPS Noise session.
|
|
13
|
+
|
|
14
|
+
## 0.1.19
|
|
15
|
+
|
|
16
|
+
- Require Node SDK 0.3.5 or newer so runtime tools receive secure MQTT URL enforcement and bounded MQTT setup and HTTP failure cleanup.
|
|
17
|
+
- Keep package, registry, and container source metadata on version 0.1.19. Runtime tool names, inputs, and identity serialization remain compatible.
|
|
18
|
+
|
|
3
19
|
## 0.1.18
|
|
4
20
|
|
|
5
21
|
- 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.
|
package/README.md
CHANGED
|
@@ -93,6 +93,13 @@ npm start
|
|
|
93
93
|
|
|
94
94
|
If neither a token nor email/password is configured, authenticated control-plane tools fail with a clear error naming the supported options.
|
|
95
95
|
|
|
96
|
+
Configured API tokens and login credentials are bound to the origin of their
|
|
97
|
+
configured `apiUrl` or `THALOVANT_API_URL` (default `https://api.thalovant.com`).
|
|
98
|
+
A tool argument cannot redirect those credentials to another origin. Equivalent
|
|
99
|
+
URL spellings and paths on the same origin are allowed; custom origins must be
|
|
100
|
+
configured alongside their credentials. Anonymous public discovery may still
|
|
101
|
+
select a custom API URL.
|
|
102
|
+
|
|
96
103
|
## Local Stdio
|
|
97
104
|
|
|
98
105
|
The server speaks MCP over stdio and does not write logs to stdout.
|
|
@@ -305,8 +312,10 @@ Read-only:
|
|
|
305
312
|
- `thalovant_get_public_hub`
|
|
306
313
|
- `thalovant_list_hubs`
|
|
307
314
|
- `thalovant_get_hub`
|
|
315
|
+
- `thalovant_get_operation`
|
|
308
316
|
- `thalovant_identity_status`
|
|
309
317
|
- `thalovant_healthcheck`
|
|
318
|
+
- `thalovant_intent_inventory`
|
|
310
319
|
- `thalovant_wait_for_event`
|
|
311
320
|
- `thalovant_get_analytics_overview`
|
|
312
321
|
- `thalovant_list_memory_items`
|
|
@@ -327,6 +336,7 @@ Writes or hub events:
|
|
|
327
336
|
|
|
328
337
|
- `thalovant_create_client_identity`
|
|
329
338
|
- `thalovant_ask`
|
|
339
|
+
- `thalovant_query`
|
|
330
340
|
- `thalovant_send_action`
|
|
331
341
|
- `thalovant_send_code`
|
|
332
342
|
- `thalovant_emit_event`
|
|
@@ -355,6 +365,16 @@ Destructive, **not registered unless explicitly enabled** (see [Destructive Tool
|
|
|
355
365
|
|
|
356
366
|
Tool outputs redact credential-shaped fields. `thalovant_create_client_identity` does not return secret identity material; pass `savePath` when you want the full identity written to a local file with mode `0600`. `savePath` is confined to the server's identity directory (`THALOVANT_MCP_IDENTITY_DIR`, default `<config-dir>/thalovant/identities`): pass a plain filename, since absolute paths outside that directory and `..` traversal are rejected, so a model cannot drop a credential file into a git working tree or synced folder. `thalovant_config_status` reports the active `identityDir`.
|
|
357
367
|
|
|
368
|
+
`thalovant_intent_inventory` uses the runtime identity and accepts `languages`,
|
|
369
|
+
`describe`, `fallback`, and a per-query/batch `timeoutMs`. It returns registered
|
|
370
|
+
intents and examples, fallback skills, `fallbacks_known`, and `may_answer` by
|
|
371
|
+
requested language. A missing or denied optional fallback-skill query remains
|
|
372
|
+
unknown, rather than being reported as a known empty list. The optional probe
|
|
373
|
+
adds at most 1500ms. A silent listing can use engine manifests; disable this with
|
|
374
|
+
`fallback: false`. Both new tools are available in read-only mode and respect
|
|
375
|
+
per-principal policy. `thalovant_get_operation` reads an operation ID returned by
|
|
376
|
+
provisioning without replaying the write.
|
|
377
|
+
|
|
358
378
|
## Destructive Tools
|
|
359
379
|
|
|
360
380
|
`thalovant_delete_hub` and `thalovant_delete_runtime_group` are **disabled by default**. They are not merely blocked when called — they are never registered, so they do not appear in `tools/list` and a model cannot see or attempt them.
|
|
@@ -399,7 +419,9 @@ Runtime calls sharing the same hub client identity run sequentially within one
|
|
|
399
419
|
MCP process. Use a distinct client identity for each independently running MCP
|
|
400
420
|
server so the hub can keep their sessions separate.
|
|
401
421
|
|
|
402
|
-
Version 0.1.
|
|
422
|
+
Version 0.1.20 uses `@thalovant/sdk` `^0.3.9` (0.3.9 through versions below 0.4.0). This release enforces
|
|
423
|
+
secure effective MQTT URLs and carries a single connection deadline through
|
|
424
|
+
MQTT setup and HTTP failure cleanup. Runtime tools support
|
|
403
425
|
HiveMind v3 Noise over WSS, HTTPS and MQTT over TLS. `thalovant_healthcheck`
|
|
404
426
|
reports readiness only after authentication; a reachable hub or broker alone
|
|
405
427
|
does not establish a runtime session.
|
|
@@ -411,6 +433,13 @@ failure preserves those pins; replacing a server key requires an explicit,
|
|
|
411
433
|
verified trust change. HTTPS identities must advertise the hub's HTTPS plugin,
|
|
412
434
|
and MQTT requires the identity's broker credentials and topic prefix.
|
|
413
435
|
|
|
436
|
+
Runtime tools retain their identity lease through actual cleanup. If the close
|
|
437
|
+
caller times out, a later tool waits up to six seconds for that cleanup before opening a session.
|
|
438
|
+
A queued tool that reaches this deadline fails without executing later or releasing
|
|
439
|
+
the previous session's identity.
|
|
440
|
+
An actual cleanup failure marks the identity unavailable to later calls instead
|
|
441
|
+
of risking concurrent sessions.
|
|
442
|
+
|
|
414
443
|
## Development
|
|
415
444
|
|
|
416
445
|
```bash
|
|
@@ -427,3 +456,16 @@ npm pack --dry-run
|
|
|
427
456
|
## License
|
|
428
457
|
|
|
429
458
|
MIT. This is the right default for a public integration server: it is permissive, compatible with the MIT Thalovant Node SDK and MCP TypeScript SDK, and does not force downstream agent or enterprise users into a reciprocal licensing model.
|
|
459
|
+
|
|
460
|
+
Credential-bearing control-plane calls require HTTPS. Explicit loopback HTTP
|
|
461
|
+
(`localhost`, `127.0.0.1`, `[::1]`) remains supported for local development.
|
|
462
|
+
API redirects are rejected so password-login bodies cannot be forwarded.
|
|
463
|
+
Runtime clients are constructed only after acquiring their identity lease.
|
|
464
|
+
|
|
465
|
+
`thalovant_query` sends a routed HiveMind query; the hub may cascade it according
|
|
466
|
+
to its routing policy. It accepts `text`, `timeoutMs`, `lang`, `sessionId`,
|
|
467
|
+
`requestId`, `queryId`, `context` and `replySettleMs`, plus the runtime identity
|
|
468
|
+
options, and returns the same normalized reply shape as `thalovant_ask`. A query
|
|
469
|
+
may trigger actions, so read-only mode hides it. Conversation workflows use
|
|
470
|
+
`sessionId` with ask/query and the existing event-wait tool; MCP does not expose
|
|
471
|
+
one tool for every SDK conversation or event-listener method.
|
package/SECURITY.md
CHANGED
|
@@ -20,3 +20,13 @@ Do not open public issues for vulnerabilities. Email security reports to `hello@
|
|
|
20
20
|
- Keep `THALOVANT_ALLOW_SHARED_CREDENTIALS=false` for multi-user deployments.
|
|
21
21
|
- Set exact `MCP_HTTP_ALLOWED_HOSTS` and `MCP_HTTP_ALLOWED_ORIGINS`.
|
|
22
22
|
- Send audit logs to your log pipeline with `MCP_AUDIT_LOG=stderr` or `MCP_AUDIT_LOG_FILE`.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Server-managed Thalovant tokens and password login are bound to their configured
|
|
26
|
+
control-plane origin. Tool-provided `apiUrl` overrides with a different origin
|
|
27
|
+
are rejected before any request is sent. Configure a custom API origin together
|
|
28
|
+
with its credentials; per-principal credentials follow the same rule.
|
|
29
|
+
|
|
30
|
+
Control-plane credentials require HTTPS, with an explicit loopback HTTP exception
|
|
31
|
+
for local development (`localhost`, `127.0.0.1`, `[::1]`). API redirects are rejected,
|
|
32
|
+
including redirects that would preserve password-login request bodies.
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { withRuntimeLease } from "./runtime-lease.js";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
5
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -77,7 +78,7 @@ const controlPlaneSchema = {
|
|
|
77
78
|
.string()
|
|
78
79
|
.url()
|
|
79
80
|
.optional()
|
|
80
|
-
.describe("Control-plane base URL.
|
|
81
|
+
.describe("Control-plane base URL. Configured credentials may only be used on their configured origin; defaults to https://api.thalovant.com."),
|
|
81
82
|
};
|
|
82
83
|
const contextSchema = z
|
|
83
84
|
.object({
|
|
@@ -567,20 +568,40 @@ function redactSecrets(value) {
|
|
|
567
568
|
}
|
|
568
569
|
return output;
|
|
569
570
|
}
|
|
571
|
+
/** Keep server-managed credentials on their configured control-plane origin. */
|
|
570
572
|
async function createControlPlane(options) {
|
|
571
573
|
const principal = currentPrincipal();
|
|
572
574
|
const credential = await credentialForPrincipal(principal);
|
|
573
575
|
const canUseShared = allowSharedThalovantCredentials(principal);
|
|
574
|
-
const
|
|
576
|
+
const configuredApiUrl = credential?.control?.apiUrl ?? (canUseShared ? process.env.THALOVANT_API_URL : undefined) ?? DEFAULT_CONTROL_API_URL;
|
|
577
|
+
const apiUrl = options.apiUrl ?? configuredApiUrl;
|
|
575
578
|
const accessToken = credential?.control?.accessToken ??
|
|
576
579
|
(canUseShared ? process.env.THALOVANT_API_TOKEN ?? process.env.THALOVANT_ACCESS_TOKEN : undefined);
|
|
580
|
+
const email = canUseShared ? process.env.THALOVANT_EMAIL : undefined;
|
|
581
|
+
const password = canUseShared ? process.env.THALOVANT_PASSWORD : undefined;
|
|
582
|
+
if (accessToken || (email && password)) {
|
|
583
|
+
let endpoint;
|
|
584
|
+
let configuredEndpoint;
|
|
585
|
+
try {
|
|
586
|
+
endpoint = new URL(apiUrl);
|
|
587
|
+
configuredEndpoint = new URL(configuredApiUrl);
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
throw new Error("The configured control-plane API URL is invalid.");
|
|
591
|
+
}
|
|
592
|
+
if (endpoint.origin !== configuredEndpoint.origin) {
|
|
593
|
+
throw new Error("apiUrl must use the configured Thalovant credential origin.");
|
|
594
|
+
}
|
|
595
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname);
|
|
596
|
+
if (endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && loopback)) {
|
|
597
|
+
throw new Error("Credential-bearing control-plane requests require HTTPS (except explicit loopback HTTP).");
|
|
598
|
+
}
|
|
599
|
+
}
|
|
577
600
|
const api = new ThalovantControlPlane(apiUrl, {
|
|
578
601
|
accessToken,
|
|
579
602
|
userAgent: `thalovant-mcp/${VERSION}`,
|
|
580
603
|
});
|
|
581
604
|
if (!accessToken && canUseShared) {
|
|
582
|
-
const email = process.env.THALOVANT_EMAIL;
|
|
583
|
-
const password = process.env.THALOVANT_PASSWORD;
|
|
584
605
|
if (email && password) {
|
|
585
606
|
await api.login(email, password, { scope: options.loginScope ?? process.env.THALOVANT_SCOPE });
|
|
586
607
|
}
|
|
@@ -601,28 +622,26 @@ function controlPlaneAuthModeFromEnv() {
|
|
|
601
622
|
return "email-password";
|
|
602
623
|
return "none";
|
|
603
624
|
}
|
|
604
|
-
async function
|
|
605
|
-
const protocol = options.protocol ?? "wss";
|
|
625
|
+
async function createRuntimeIdentity(options) {
|
|
606
626
|
const principal = currentPrincipal();
|
|
607
627
|
const credential = await credentialForPrincipal(principal);
|
|
608
628
|
const runtimeCredential = credential?.runtime;
|
|
609
629
|
const canUseShared = allowSharedThalovantCredentials(principal);
|
|
610
630
|
const canUseToolPaths = canUseShared || parseBool(process.env.MCP_HTTP_ALLOW_CLIENT_CREDENTIAL_PATHS, false);
|
|
611
631
|
if (runtimeCredential?.identity) {
|
|
612
|
-
return new
|
|
632
|
+
return new ThalovantIdentity(runtimeCredential.identity);
|
|
613
633
|
}
|
|
614
634
|
if (runtimeCredential?.identityFile) {
|
|
615
|
-
return
|
|
635
|
+
return ThalovantIdentity.fromFile(runtimeCredential.identityFile);
|
|
616
636
|
}
|
|
617
637
|
if (runtimeCredential?.configPath || runtimeCredential?.profile) {
|
|
618
|
-
return
|
|
638
|
+
return ThalovantIdentity.fromConfig({
|
|
619
639
|
path: runtimeCredential.configPath,
|
|
620
640
|
profile: runtimeCredential.profile,
|
|
621
|
-
protocol,
|
|
622
641
|
});
|
|
623
642
|
}
|
|
624
643
|
if (runtimeCredential?.fromEnv && canUseShared) {
|
|
625
|
-
return
|
|
644
|
+
return ThalovantIdentity.fromEnv();
|
|
626
645
|
}
|
|
627
646
|
if (isRemotePrincipal(principal) && !canUseShared && !canUseToolPaths) {
|
|
628
647
|
throw new Error("No per-principal Thalovant runtime identity is configured for this authenticated MCP principal.");
|
|
@@ -631,52 +650,31 @@ async function createRuntimeClient(options) {
|
|
|
631
650
|
throw new Error("Runtime identity path/config arguments are disabled for remote MCP principals.");
|
|
632
651
|
}
|
|
633
652
|
if (options.identityFile) {
|
|
634
|
-
return
|
|
653
|
+
return ThalovantIdentity.fromFile(options.identityFile);
|
|
635
654
|
}
|
|
636
655
|
if (options.configPath || options.profile) {
|
|
637
|
-
return
|
|
656
|
+
return ThalovantIdentity.fromConfig({
|
|
638
657
|
path: options.configPath,
|
|
639
658
|
profile: options.profile ?? process.env.THALOVANT_PROFILE,
|
|
640
|
-
protocol,
|
|
641
659
|
});
|
|
642
660
|
}
|
|
643
661
|
if (!canUseShared) {
|
|
644
662
|
throw new Error("No per-principal Thalovant runtime identity is configured for this authenticated MCP principal.");
|
|
645
663
|
}
|
|
646
664
|
if (options.fromEnv || process.env.THALOVANT_ACCESS_KEY) {
|
|
647
|
-
return
|
|
665
|
+
return ThalovantIdentity.fromEnv();
|
|
648
666
|
}
|
|
649
|
-
return
|
|
667
|
+
return ThalovantIdentity.fromConfig({
|
|
650
668
|
profile: process.env.THALOVANT_PROFILE,
|
|
651
|
-
protocol,
|
|
652
669
|
});
|
|
653
670
|
}
|
|
654
|
-
|
|
655
|
-
// A second tool call must finish after the first closes that identity's session.
|
|
656
|
-
const runtimeLeases = new Map();
|
|
671
|
+
/** Use one identity lease through the action and actual retained cleanup. */
|
|
657
672
|
async function withRuntimeClient(options, run) {
|
|
658
|
-
const
|
|
673
|
+
const identity = await createRuntimeIdentity(options);
|
|
659
674
|
const key = createHash("sha256").update(JSON.stringify([
|
|
660
|
-
|
|
675
|
+
identity.endpointBase(), identity.accessKey,
|
|
661
676
|
])).digest("hex");
|
|
662
|
-
|
|
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
|
-
}
|
|
677
|
+
return withRuntimeLease(key, () => new ThalovantClient(identity, { protocol: options.protocol ?? "wss" }), run);
|
|
680
678
|
}
|
|
681
679
|
function summarizeReply(reply) {
|
|
682
680
|
const displayItems = reply.displayItems({ maxTextChars: 1_000 });
|
|
@@ -1209,6 +1207,16 @@ export function createServer() {
|
|
|
1209
1207
|
ensureAuthenticated(api);
|
|
1210
1208
|
return jsonContent(redactSecrets(await api.getHub(hubId)));
|
|
1211
1209
|
});
|
|
1210
|
+
registerThalovantTool(server, "thalovant_get_operation", {
|
|
1211
|
+
title: "Get Provisioning Operation",
|
|
1212
|
+
description: "Read the status of an asynchronous control-plane operation returned by provisioning.",
|
|
1213
|
+
inputSchema: { ...controlPlaneSchema, operationId: z.string().min(1).max(500) },
|
|
1214
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1215
|
+
}, async ({ operationId, ...auth }) => {
|
|
1216
|
+
const api = await createControlPlane(auth);
|
|
1217
|
+
ensureAuthenticated(api);
|
|
1218
|
+
return jsonContent(redactSecrets(await api.getOperation(operationId)));
|
|
1219
|
+
});
|
|
1212
1220
|
registerThalovantTool(server, "thalovant_create_client_identity", {
|
|
1213
1221
|
title: "Create Client Identity",
|
|
1214
1222
|
description: "Create a Thalovant client identity for a hub. The identity is secret; output is redacted unless savePath is used.",
|
|
@@ -1304,6 +1312,24 @@ export function createServer() {
|
|
|
1304
1312
|
return jsonContent(redactSecrets(client.healthcheck()));
|
|
1305
1313
|
});
|
|
1306
1314
|
});
|
|
1315
|
+
registerThalovantTool(server, "thalovant_intent_inventory", {
|
|
1316
|
+
title: "Runtime Intent Inventory",
|
|
1317
|
+
description: "Discover a hub's registered intents, language examples and fallback skills using its runtime identity. Unknown fallback support remains distinct from a known empty list; answerability is a conservative hint.",
|
|
1318
|
+
inputSchema: {
|
|
1319
|
+
...runtimeAuthSchema,
|
|
1320
|
+
languages: z.array(z.string().trim().min(1).max(100)).min(1).max(20).default(["en-us"]),
|
|
1321
|
+
describe: z.boolean().default(true),
|
|
1322
|
+
fallback: z.boolean().default(true).describe("Try engine manifests if the intent listing is refused or unanswered."),
|
|
1323
|
+
timeoutMs: z.number().int().min(1_000).max(MAX_TIMEOUT_MS).default(DEFAULT_TIMEOUT_MS).describe("Per-query/batch timeout; the optional fallback-skill probe is capped at 1500ms."),
|
|
1324
|
+
},
|
|
1325
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1326
|
+
}, async ({ languages, describe, fallback, timeoutMs, ...runtime }) => withRuntimeClient(runtime, async (client) => {
|
|
1327
|
+
const inventory = await client.intents(languages, { describe, fallback, timeoutMs: clampTimeout(timeoutMs) });
|
|
1328
|
+
return jsonContent(redactSecrets({
|
|
1329
|
+
...inventory.asObject(),
|
|
1330
|
+
may_answer: Object.fromEntries(inventory.languages.map(lang => [lang, inventory.mayAnswer(lang)])),
|
|
1331
|
+
}));
|
|
1332
|
+
}));
|
|
1307
1333
|
registerThalovantTool(server, "thalovant_ask", {
|
|
1308
1334
|
title: "Ask Hub",
|
|
1309
1335
|
description: "Send one text request to a Thalovant hub using a saved identity and return the normalized reply.",
|
|
@@ -1339,6 +1365,30 @@ export function createServer() {
|
|
|
1339
1365
|
return jsonContent(redactSecrets(summarizeReply(reply)));
|
|
1340
1366
|
});
|
|
1341
1367
|
});
|
|
1368
|
+
registerThalovantTool(server, "thalovant_query", {
|
|
1369
|
+
title: "Query Hub",
|
|
1370
|
+
description: "Send a routed HiveMind query using a saved identity and return the normalized reply. The hub may cascade the query according to its routing policy; a query can trigger actions.",
|
|
1371
|
+
inputSchema: {
|
|
1372
|
+
...runtimeAuthSchema,
|
|
1373
|
+
text: z.string().trim().min(1).max(20_000),
|
|
1374
|
+
timeoutMs: z.number().int().min(1_000).max(MAX_TIMEOUT_MS).default(DEFAULT_TIMEOUT_MS),
|
|
1375
|
+
lang: z.string().min(1).optional(),
|
|
1376
|
+
sessionId: z.string().min(1).optional(),
|
|
1377
|
+
requestId: z.string().min(1).optional(),
|
|
1378
|
+
queryId: z.string().min(1).optional(),
|
|
1379
|
+
context: contextSchema,
|
|
1380
|
+
replySettleMs: z.number().int().min(0).max(10_000).optional(),
|
|
1381
|
+
},
|
|
1382
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
1383
|
+
}, async ({ text, timeoutMs, lang, sessionId, requestId, queryId, context, replySettleMs, ...runtime }) => {
|
|
1384
|
+
return withRuntimeClient(runtime, async (client) => {
|
|
1385
|
+
const reply = await client.query(text, {
|
|
1386
|
+
timeoutMs: clampTimeout(timeoutMs), lang, sessionId, requestId, queryId,
|
|
1387
|
+
context: context ? buildClientContext({}, context) : undefined, replySettleMs,
|
|
1388
|
+
});
|
|
1389
|
+
return jsonContent(redactSecrets(summarizeReply(reply)));
|
|
1390
|
+
});
|
|
1391
|
+
});
|
|
1342
1392
|
registerThalovantTool(server, "thalovant_send_action", {
|
|
1343
1393
|
title: "Send Action",
|
|
1344
1394
|
description: "Send a structured action payload to a Thalovant hub.",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Runtime identities admit one Noise session at a time across MCP tool calls. */
|
|
2
|
+
interface RuntimeClosable {
|
|
3
|
+
close(): Promise<void>;
|
|
4
|
+
waitForClosed(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
/** A caller timeout never releases an identity before actual cleanup settles. */
|
|
7
|
+
export declare function withRuntimeLease<T, C extends RuntimeClosable>(key: string, createClient: () => C, run: (client: C) => Promise<T>, acquireTimeoutMs?: number): Promise<T>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const leases = new Map();
|
|
2
|
+
/** A caller timeout never releases an identity before actual cleanup settles. */
|
|
3
|
+
export async function withRuntimeLease(key, createClient, run, acquireTimeoutMs = 6000) {
|
|
4
|
+
if (!Number.isFinite(acquireTimeoutMs) || acquireTimeoutMs <= 0) {
|
|
5
|
+
throw new Error("Runtime identity acquisition deadline expired.");
|
|
6
|
+
}
|
|
7
|
+
const previous = leases.get(key) ?? Promise.resolve();
|
|
8
|
+
let release;
|
|
9
|
+
let fail;
|
|
10
|
+
const current = new Promise((resolve, reject) => { release = resolve; fail = reject; });
|
|
11
|
+
// A failed cleanup poisons this identity's barrier. Observe it even when no
|
|
12
|
+
// later caller arrives; later tools fail instead of replacing that session.
|
|
13
|
+
void current.catch(() => undefined);
|
|
14
|
+
leases.set(key, current);
|
|
15
|
+
const releaseIdentity = () => {
|
|
16
|
+
release();
|
|
17
|
+
if (leases.get(key) === current)
|
|
18
|
+
leases.delete(key);
|
|
19
|
+
};
|
|
20
|
+
const failIdentity = () => {
|
|
21
|
+
fail(new Error("Previous runtime session cleanup failed; this identity remains unavailable."));
|
|
22
|
+
};
|
|
23
|
+
let timer;
|
|
24
|
+
try {
|
|
25
|
+
await Promise.race([
|
|
26
|
+
previous,
|
|
27
|
+
new Promise((_, reject) => {
|
|
28
|
+
timer = setTimeout(() => reject(new Error("Runtime identity acquisition deadline expired; previous session cleanup is still pending.")), acquireTimeoutMs);
|
|
29
|
+
}),
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// An expired waiter never runs or closes its unused client. Its place in
|
|
34
|
+
// the chain remains occupied until the prior owner actually retires.
|
|
35
|
+
void previous.then(releaseIdentity, failIdentity);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
}
|
|
41
|
+
let client;
|
|
42
|
+
try {
|
|
43
|
+
client = createClient();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
releaseIdentity();
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return await run(client);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
let closing;
|
|
54
|
+
try {
|
|
55
|
+
closing = client.close();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
failIdentity();
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
void client.waitForClosed().then(releaseIdentity, failIdentity);
|
|
62
|
+
await closing;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thalovant/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
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.
|
|
67
|
+
"@thalovant/sdk": "^0.3.9",
|
|
68
68
|
"jose": "^6.2.8",
|
|
69
69
|
"zod": "^3.25.76"
|
|
70
70
|
},
|
|
@@ -74,6 +74,6 @@
|
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@types/node": "^22.18.12",
|
|
76
76
|
"typescript": "^5.9.3",
|
|
77
|
-
"vitest": "^
|
|
77
|
+
"vitest": "^4.1.11"
|
|
78
78
|
}
|
|
79
79
|
}
|
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.
|
|
10
|
+
"version": "0.1.20",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@thalovant/mcp-server",
|
|
15
|
-
"version": "0.1.
|
|
15
|
+
"version": "0.1.20",
|
|
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.
|
|
22
|
+
"identifier": "ghcr.io/thalovant/thalovant-mcp:0.1.20",
|
|
23
23
|
"transport": {
|
|
24
24
|
"type": "streamable-http",
|
|
25
25
|
"url": "http://127.0.0.1:3000/mcp"
|