@thalovant/mcp-server 0.1.19 → 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 +11 -0
- package/README.md +41 -1
- package/SECURITY.md +10 -0
- package/dist/index.js +90 -45
- 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,16 @@
|
|
|
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
|
+
|
|
3
14
|
## 0.1.19
|
|
4
15
|
|
|
5
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.
|
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,7 @@ 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
|
|
403
423
|
secure effective MQTT URLs and carries a single connection deadline through
|
|
404
424
|
MQTT setup and HTTP failure cleanup. Runtime tools support
|
|
405
425
|
HiveMind v3 Noise over WSS, HTTPS and MQTT over TLS. `thalovant_healthcheck`
|
|
@@ -413,6 +433,13 @@ failure preserves those pins; replacing a server key requires an explicit,
|
|
|
413
433
|
verified trust change. HTTPS identities must advertise the hub's HTTPS plugin,
|
|
414
434
|
and MQTT requires the identity's broker credentials and topic prefix.
|
|
415
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
|
+
|
|
416
443
|
## Development
|
|
417
444
|
|
|
418
445
|
```bash
|
|
@@ -429,3 +456,16 @@ npm pack --dry-run
|
|
|
429
456
|
## License
|
|
430
457
|
|
|
431
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,57 +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();
|
|
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
|
-
*/
|
|
671
|
+
/** Use one identity lease through the action and actual retained cleanup. */
|
|
662
672
|
async function withRuntimeClient(options, run) {
|
|
663
|
-
const
|
|
673
|
+
const identity = await createRuntimeIdentity(options);
|
|
664
674
|
const key = createHash("sha256").update(JSON.stringify([
|
|
665
|
-
|
|
675
|
+
identity.endpointBase(), identity.accessKey,
|
|
666
676
|
])).digest("hex");
|
|
667
|
-
|
|
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
|
-
}
|
|
677
|
+
return withRuntimeLease(key, () => new ThalovantClient(identity, { protocol: options.protocol ?? "wss" }), run);
|
|
685
678
|
}
|
|
686
679
|
function summarizeReply(reply) {
|
|
687
680
|
const displayItems = reply.displayItems({ maxTextChars: 1_000 });
|
|
@@ -1214,6 +1207,16 @@ export function createServer() {
|
|
|
1214
1207
|
ensureAuthenticated(api);
|
|
1215
1208
|
return jsonContent(redactSecrets(await api.getHub(hubId)));
|
|
1216
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
|
+
});
|
|
1217
1220
|
registerThalovantTool(server, "thalovant_create_client_identity", {
|
|
1218
1221
|
title: "Create Client Identity",
|
|
1219
1222
|
description: "Create a Thalovant client identity for a hub. The identity is secret; output is redacted unless savePath is used.",
|
|
@@ -1309,6 +1312,24 @@ export function createServer() {
|
|
|
1309
1312
|
return jsonContent(redactSecrets(client.healthcheck()));
|
|
1310
1313
|
});
|
|
1311
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
|
+
}));
|
|
1312
1333
|
registerThalovantTool(server, "thalovant_ask", {
|
|
1313
1334
|
title: "Ask Hub",
|
|
1314
1335
|
description: "Send one text request to a Thalovant hub using a saved identity and return the normalized reply.",
|
|
@@ -1344,6 +1365,30 @@ export function createServer() {
|
|
|
1344
1365
|
return jsonContent(redactSecrets(summarizeReply(reply)));
|
|
1345
1366
|
});
|
|
1346
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
|
+
});
|
|
1347
1392
|
registerThalovantTool(server, "thalovant_send_action", {
|
|
1348
1393
|
title: "Send Action",
|
|
1349
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"
|