@thalovant/mcp-server 0.1.24 → 0.3.0
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 +53 -6
- package/dist/index.js +44 -30
- package/dist/reply-output.d.ts +45 -0
- package/dist/reply-output.js +42 -0
- package/package.json +2 -2
- package/server.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-09-12
|
|
4
|
+
|
|
5
|
+
- Use Node SDK 0.5.0 request hints, bounded embedded audio replies, and speakable intent examples. Keep embedded audio out of model text; offer explicit audio/resource content blocks through includeAudio.
|
|
6
|
+
- Default runtime configuration tools to revision-guarded merging with at most three attempts and only HTTP 412 retries. Preserve explicit replacement with merge: false. Guarded merging requires hubs:read and paid hubs:write.
|
|
7
|
+
- Cover actual encrypted runtime hints/audio and default conditional-write conflict handling through MCP tool calls. Preserve the existing tool catalog and align npm/OCI/registry versions.
|
|
8
|
+
|
|
9
|
+
## 0.2.0 — 2026-09-12
|
|
10
|
+
|
|
11
|
+
- Add a read-only hub skill history tool using Node SDK 0.4.0. The catalog now has 46 default tools, 23 in read-only mode, and 48 with destructive tools enabled.
|
|
12
|
+
- Correct shared-runtime skill scope and token requirements.
|
|
13
|
+
|
|
3
14
|
## 0.1.24
|
|
4
15
|
|
|
5
16
|
- Require Node SDK 0.3.16 and use its public hub-skill methods instead of the temporary duplicate HTTP and polling implementation. Preserve tool schemas, accepted-operation output, API guidance and MCP cancellation checks.
|
package/README.md
CHANGED
|
@@ -56,8 +56,8 @@ Minimum scopes for the full control-plane tool surface:
|
|
|
56
56
|
|
|
57
57
|
| Scope | Used by |
|
|
58
58
|
|-------|---------|
|
|
59
|
-
| `hubs:read` | `thalovant_list_hubs`, `thalovant_get_hub`, `thalovant_get_analytics_overview`, `thalovant_list_marketplace_skills`, `thalovant_list_runtime_groups`, `thalovant_get_runtime_group`, `thalovant_get_runtime_group_config`, and the hub lookup inside `thalovant_create_client_identity` |
|
|
60
|
-
| `hubs:inspect` | `thalovant_get_hub_runtime_capabilities`, `thalovant_list_runtime_group_marketplace`, `thalovant_list_runtime_group_inventory`, `thalovant_list_hub_skills` |
|
|
59
|
+
| `hubs:read` | `thalovant_list_hubs`, `thalovant_get_hub`, `thalovant_get_analytics_overview`, `thalovant_list_marketplace_skills`, `thalovant_list_runtime_groups`, `thalovant_get_runtime_group`, `thalovant_get_runtime_group_config`, the guarded merge read in `thalovant_update_runtime_group_config`, and the hub lookup inside `thalovant_create_client_identity` |
|
|
60
|
+
| `hubs:inspect` | `thalovant_get_hub_runtime_capabilities`, `thalovant_list_runtime_group_marketplace`, `thalovant_list_runtime_group_inventory`, `thalovant_list_hub_skills`, `thalovant_list_hub_skill_history` |
|
|
61
61
|
| `hubs:write` | All hub and runtime-group provisioning: `thalovant_create_hub`, `thalovant_update_hub`, `thalovant_release_hub`, `thalovant_create_runtime_group`, `thalovant_update_runtime_group`, `thalovant_update_runtime_group_config`, `thalovant_release_runtime_group`, `thalovant_install_runtime_group_skill`, `thalovant_uninstall_runtime_group_skill`, the per-hub `thalovant_install_hub_skill`, `thalovant_update_hub_skill`, `thalovant_remove_hub_skill`, the hub rating tools, and the opt-in delete tools |
|
|
62
62
|
| `clients:write` | `thalovant_create_client_identity` (`POST /v1/clients`) |
|
|
63
63
|
| `memory:read` | `thalovant_list_memory_items`, `thalovant_get_memory_summary`, `thalovant_get_memory_item` |
|
|
@@ -81,7 +81,7 @@ Grant fewer scopes for narrower deployments: a read-only assistant needs only `h
|
|
|
81
81
|
|
|
82
82
|
### Hub Skills
|
|
83
83
|
|
|
84
|
-
`thalovant_list_hub_skills`, `thalovant_install_hub_skill`, `thalovant_update_hub_skill`, and `thalovant_remove_hub_skill`
|
|
84
|
+
`thalovant_list_hub_skills`, `thalovant_install_hub_skill`, `thalovant_update_hub_skill`, and `thalovant_remove_hub_skill` select a hub's attached runtime group; all hubs sharing it are affected. A hub can start with no skills at all and gain them one at a time; a change applies live on the hub in about 15 seconds with no restart. `hubId` must be the hub UUID — the authenticated hub routes reject slugs.
|
|
85
85
|
|
|
86
86
|
`thalovant_list_hub_skills` returns the whole `GET /v1/hubs/{hub_id}/skills` envelope: `hub_id`, `runtime_group_id`, `observed_at`, `source`, the runtime's phase and message, and `data`, one row per skill (possibly empty) with `skill`, `title`, `marketplace_skill_id`, `package_name`, `source_type`, `install_source`, `version`, `version_pin`, `installed_version`, `observed_version`, `previous_version`, `latest_version`, `available_version`, `update_available`, `changelog`, `active`, `state`, the runtime's phase, message and last error, and `last_transition_at`. `state` is one of `pending`, `installed`, `failed`, `removing`, `drifted`, `quarantined`, or `unmanaged`; a change in progress shows as `pending`.
|
|
87
87
|
|
|
@@ -368,9 +368,10 @@ Hub and runtime-group provisioning:
|
|
|
368
368
|
- `thalovant_install_runtime_group_skill`
|
|
369
369
|
- `thalovant_uninstall_runtime_group_skill`
|
|
370
370
|
|
|
371
|
-
Hub skills, acting on
|
|
371
|
+
Hub skills, acting on the hub’s shared runtime (see [Hub Skills](#hub-skills); the list tool is read-only):
|
|
372
372
|
|
|
373
373
|
- `thalovant_list_hub_skills`
|
|
374
|
+
- `thalovant_list_hub_skill_history`
|
|
374
375
|
- `thalovant_install_hub_skill`
|
|
375
376
|
- `thalovant_update_hub_skill`
|
|
376
377
|
- `thalovant_remove_hub_skill`
|
|
@@ -383,7 +384,7 @@ Destructive, **not registered unless explicitly enabled** (see [Destructive Tool
|
|
|
383
384
|
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`.
|
|
384
385
|
|
|
385
386
|
`thalovant_intent_inventory` uses the runtime identity and accepts `languages`,
|
|
386
|
-
`describe`, `fallback`, and a per-query/batch `timeoutMs`. It returns registered
|
|
387
|
+
`describe`, `fallback`, `speakable`, `slots`, `exampleLimit`, and a per-query/batch `timeoutMs`. It returns registered
|
|
387
388
|
intents and examples, fallback skills, `fallbacks_known`, and `may_answer` by
|
|
388
389
|
requested language. A missing or denied optional fallback-skill query remains
|
|
389
390
|
unknown, rather than being reported as a known empty list. The optional probe
|
|
@@ -392,6 +393,33 @@ adds at most 1500ms. A silent listing can use engine manifests; disable this wit
|
|
|
392
393
|
per-principal policy. `thalovant_get_operation` reads an operation ID returned by
|
|
393
394
|
provisioning without replaying the write.
|
|
394
395
|
|
|
396
|
+
## Request hints, embedded audio, and configuration merging
|
|
397
|
+
|
|
398
|
+
`thalovant_ask` accepts `sttLang`, an ordered `pipeline`, and `location`
|
|
399
|
+
with required `city` and optional `region`, `country`, `timezone`, `latitude`,
|
|
400
|
+
and `longitude`. Hints apply to the current request. Omit unused hints;
|
|
401
|
+
an empty pipeline is ignored, and blank language or city strings are rejected.
|
|
402
|
+
|
|
403
|
+
Ask and Query return language, ordered event metadata, `hasAudio` and
|
|
404
|
+
`droppedMedia`. Embedded hex stays out of text output. Opt into separate MCP
|
|
405
|
+
audio blocks with `includeAudio: true`; unknown formats become embedded binary
|
|
406
|
+
resource blocks. Clip and reply limits are 4 MiB and 16 MiB respectively,
|
|
407
|
+
checked by the SDK before retention. The client owns playback. No skill-supplied
|
|
408
|
+
path or URL is fetched. Inventory `speakable: true` renders examples without
|
|
409
|
+
changing the raw patterns; `slots` supplies illustrative slot values, and
|
|
410
|
+
`exampleLimit` limits examples per language to 1–20 (default 2).
|
|
411
|
+
|
|
412
|
+
Configuration updates default to `merge: true`: GET the revision, deep-merge
|
|
413
|
+
the caller's delta and PUT with `expected_revision`. Only HTTP 412 retries,
|
|
414
|
+
with a fresh read and at most three total attempts. This requires `hubs:read`,
|
|
415
|
+
`hubs:write`, and a paid plan. An older API without revision support fails before
|
|
416
|
+
writing. There is no unconditional fallback. Supplied personas replace;
|
|
417
|
+
omitted personas stay unchanged. For an intentional full replacement, pass
|
|
418
|
+
`merge: false`, requiring `hubs:write`, a paid plan, and coordination with other writers.
|
|
419
|
+
Guarded merges reject integers outside JavaScript's safe integer range in the
|
|
420
|
+
delta or stored configuration before writing. Store large identifiers as strings
|
|
421
|
+
or use an SDK that preserves large JSON integers.
|
|
422
|
+
|
|
395
423
|
## Destructive Tools
|
|
396
424
|
|
|
397
425
|
`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.
|
|
@@ -436,7 +464,7 @@ Runtime calls sharing the same hub client identity run sequentially within one
|
|
|
436
464
|
MCP process. Use a distinct client identity for each independently running MCP
|
|
437
465
|
server so the hub can keep their sessions separate.
|
|
438
466
|
|
|
439
|
-
Version 0.
|
|
467
|
+
Version 0.3.0 requires `@thalovant/sdk` `^0.5.1` (0.5.1 through versions below 0.6.0). This release enforces
|
|
440
468
|
secure effective MQTT URLs and carries a single connection deadline through
|
|
441
469
|
MQTT setup and HTTP failure cleanup. Runtime tools support
|
|
442
470
|
HiveMind v3 Noise over WSS, HTTPS and MQTT over TLS. `thalovant_healthcheck`
|
|
@@ -510,3 +538,22 @@ options, and returns the same normalized reply shape as `thalovant_ask`. A query
|
|
|
510
538
|
may trigger actions, so read-only mode hides it. Conversation workflows use
|
|
511
539
|
`sessionId` with ask/query and the existing event-wait tool; MCP does not expose
|
|
512
540
|
one tool for every SDK conversation or event-listener method.
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
### Shared-runtime skill management
|
|
544
|
+
|
|
545
|
+
The catalog has 46 default tools, 23 in read-only mode, and 48 with destructive tools enabled.
|
|
546
|
+
|
|
547
|
+
Hub-addressed skill methods select the runtime group attached to the hub UUID.
|
|
548
|
+
Every hub sharing that group sees the same skill changes and history. The API
|
|
549
|
+
requires a restricted token to cover all served hubs. Reads need `hubs:inspect`
|
|
550
|
+
(`hubs:read` implies it); writes need `hubs:write`, an eligible paid plan and ownership.
|
|
551
|
+
|
|
552
|
+
The history response contains newest-first `event` and `operation` entries,
|
|
553
|
+
including nullable actor/version fields. Its limit is 1–200 (50 where omitted).
|
|
554
|
+
An accepted mutation is not proof the skill is ready. Optional waiting polls the
|
|
555
|
+
operation, with a 120-second default timeout and two-second interval. Polling
|
|
556
|
+
never repeats an accepted mutation and starts no new read after its deadline;
|
|
557
|
+
an already-running HTTP request retains its normal request timeout.
|
|
558
|
+
|
|
559
|
+
Read history with `thalovant_list_hub_skill_history` (`hubId`, optional `limit`). This tool remains available in read-only mode.
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { runtimeReplyContent } from "./reply-output.js";
|
|
2
3
|
import { throwIfRuntimeCancelled, withRuntimeLease } from "./runtime-lease.js";
|
|
3
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
6
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
7
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
-
import { DEFAULT_CONTROL_API_URL, ThalovantClient, ThalovantControlPlane, ThalovantIdentity, buildClientContext, } from "@thalovant/sdk";
|
|
8
|
+
import { DEFAULT_CONTROL_API_URL, ThalovantClient, ThalovantControlPlane, ThalovantIdentity, buildClientContext, buildLocation, } from "@thalovant/sdk";
|
|
8
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
10
|
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
10
11
|
import { createServer as createHttpServer } from "node:http";
|
|
@@ -718,21 +719,6 @@ async function withRuntimeClient(options, run) {
|
|
|
718
719
|
])).digest("hex");
|
|
719
720
|
return withRuntimeLease(key, () => new ThalovantClient(identity, { protocol: options.protocol ?? "wss" }), client => run(client, signal), 6000, signal);
|
|
720
721
|
}
|
|
721
|
-
function summarizeReply(reply) {
|
|
722
|
-
const displayItems = reply.displayItems({ maxTextChars: 1_000 });
|
|
723
|
-
return {
|
|
724
|
-
text: reply.text,
|
|
725
|
-
displayText: reply.displayText,
|
|
726
|
-
utterances: reply.utterances,
|
|
727
|
-
handled: reply.handled,
|
|
728
|
-
ok: reply.ok,
|
|
729
|
-
sessionId: reply.sessionId,
|
|
730
|
-
requestId: reply.requestId,
|
|
731
|
-
displayItems,
|
|
732
|
-
events: reply.events.map((event) => redactSecrets(event.asObject())),
|
|
733
|
-
failureEvent: reply.failureEvent ? redactSecrets(reply.failureEvent.asObject()) : undefined,
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
722
|
/**
|
|
737
723
|
* Base directory that saved client-identity files are confined to.
|
|
738
724
|
*
|
|
@@ -1360,17 +1346,23 @@ export function createServer() {
|
|
|
1360
1346
|
inputSchema: {
|
|
1361
1347
|
...runtimeAuthSchema,
|
|
1362
1348
|
languages: z.array(z.string().trim().min(1).max(100)).min(1).max(20).default(["en-us"]),
|
|
1349
|
+
speakable: z.boolean().default(false).describe("Render intent patterns into example sentences; preserve whole-sentence priority."),
|
|
1350
|
+
slots: z.record(z.string().max(500)).optional().describe("Example values for named intent slots."),
|
|
1351
|
+
exampleLimit: z.number().int().min(1).max(20).default(2),
|
|
1363
1352
|
describe: z.boolean().default(true),
|
|
1364
1353
|
fallback: z.boolean().default(true).describe("Try engine manifests if the intent listing is refused or unanswered."),
|
|
1365
1354
|
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."),
|
|
1366
1355
|
},
|
|
1367
1356
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1368
|
-
}, async ({ languages, describe, fallback, timeoutMs, ...runtime }) => withRuntimeClient(runtime, async (client, signal) => {
|
|
1357
|
+
}, async ({ languages, describe, fallback, timeoutMs, speakable, slots, exampleLimit, ...runtime }) => withRuntimeClient(runtime, async (client, signal) => {
|
|
1369
1358
|
await client.connect(undefined, signal);
|
|
1370
1359
|
throwIfRuntimeCancelled(signal);
|
|
1371
1360
|
const inventory = await client.intents(languages, { describe, fallback, timeoutMs: clampTimeout(timeoutMs) });
|
|
1372
1361
|
return jsonContent(redactSecrets({
|
|
1373
1362
|
...inventory.asObject(),
|
|
1363
|
+
examples: inventory.intents.map(intent => ({ id: intent.id,
|
|
1364
|
+
languages: Object.fromEntries(inventory.languages.map(lang => [lang, intent.examples(lang, exampleLimit, { speakable, slots })])),
|
|
1365
|
+
})),
|
|
1374
1366
|
may_answer: Object.fromEntries(inventory.languages.map(lang => [lang, inventory.mayAnswer(lang)])),
|
|
1375
1367
|
}));
|
|
1376
1368
|
}));
|
|
@@ -1381,6 +1373,12 @@ export function createServer() {
|
|
|
1381
1373
|
...runtimeAuthSchema,
|
|
1382
1374
|
text: z.string().min(1).max(20_000),
|
|
1383
1375
|
timeoutMs: z.number().int().min(1_000).max(MAX_TIMEOUT_MS).default(DEFAULT_TIMEOUT_MS),
|
|
1376
|
+
sttLang: z.string().trim().min(1).max(100).optional().describe("Language recognized by speech-to-text; the hub validates this hint."),
|
|
1377
|
+
pipeline: z.array(z.string().trim().min(1).max(200)).max(32).optional(),
|
|
1378
|
+
location: z.object({ city: z.string().trim().min(1).max(500), region: z.string().max(500).optional(), country: z.string().max(100).optional(),
|
|
1379
|
+
latitude: z.number().finite().min(-90).max(90).optional(), longitude: z.number().finite().min(-180).max(180).optional(), timezone: z.string().max(200).optional(),
|
|
1380
|
+
}).optional().describe("Request location, preferred over the hub's configured city."),
|
|
1381
|
+
includeAudio: z.boolean().default(false).describe("Include bounded embedded skill clips as MCP audio/resources; default returns metadata without placing audio hex in text."),
|
|
1384
1382
|
lang: z.string().min(1).optional(),
|
|
1385
1383
|
sessionId: z.string().min(1).optional(),
|
|
1386
1384
|
requestId: z.string().min(1).optional(),
|
|
@@ -1394,7 +1392,7 @@ export function createServer() {
|
|
|
1394
1392
|
idempotentHint: false,
|
|
1395
1393
|
openWorldHint: true,
|
|
1396
1394
|
},
|
|
1397
|
-
}, async ({ text, timeoutMs, lang, sessionId, requestId, context, replySettleMs, emptyReplyWaitMs, ...runtime }) => {
|
|
1395
|
+
}, async ({ text, timeoutMs, lang, sessionId, requestId, context, replySettleMs, emptyReplyWaitMs, sttLang, pipeline, location, includeAudio, ...runtime }) => {
|
|
1398
1396
|
return withRuntimeClient(runtime, async (client, signal) => {
|
|
1399
1397
|
const builtContext = context ? buildClientContext({}, context) : undefined;
|
|
1400
1398
|
const reply = await client.ask(text, {
|
|
@@ -1404,10 +1402,12 @@ export function createServer() {
|
|
|
1404
1402
|
sessionId,
|
|
1405
1403
|
requestId,
|
|
1406
1404
|
context: builtContext,
|
|
1405
|
+
sttLang, pipeline, location: location ? buildLocation(location) : undefined,
|
|
1407
1406
|
replySettleMs,
|
|
1408
1407
|
emptyReplyWaitMs,
|
|
1409
1408
|
});
|
|
1410
|
-
|
|
1409
|
+
const output = runtimeReplyContent(reply, includeAudio);
|
|
1410
|
+
return { content: [...jsonContent(redactSecrets(output.summary)).content, ...output.clips] };
|
|
1411
1411
|
});
|
|
1412
1412
|
});
|
|
1413
1413
|
registerThalovantTool(server, "thalovant_query", {
|
|
@@ -1417,6 +1417,7 @@ export function createServer() {
|
|
|
1417
1417
|
...runtimeAuthSchema,
|
|
1418
1418
|
text: z.string().trim().min(1).max(20_000),
|
|
1419
1419
|
timeoutMs: z.number().int().min(1_000).max(MAX_TIMEOUT_MS).default(DEFAULT_TIMEOUT_MS),
|
|
1420
|
+
includeAudio: z.boolean().default(false).describe("Include bounded embedded skill clips as MCP audio/resources."),
|
|
1420
1421
|
lang: z.string().min(1).optional(),
|
|
1421
1422
|
sessionId: z.string().min(1).optional(),
|
|
1422
1423
|
requestId: z.string().min(1).optional(),
|
|
@@ -1425,14 +1426,15 @@ export function createServer() {
|
|
|
1425
1426
|
replySettleMs: z.number().int().min(0).max(10_000).optional(),
|
|
1426
1427
|
},
|
|
1427
1428
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
1428
|
-
}, async ({ text, timeoutMs, lang, sessionId, requestId, queryId, context, replySettleMs, ...runtime }) => {
|
|
1429
|
+
}, async ({ text, timeoutMs, lang, sessionId, requestId, queryId, context, replySettleMs, includeAudio, ...runtime }) => {
|
|
1429
1430
|
return withRuntimeClient(runtime, async (client, signal) => {
|
|
1430
1431
|
const reply = await client.query(text, {
|
|
1431
1432
|
signal,
|
|
1432
1433
|
timeoutMs: clampTimeout(timeoutMs), lang, sessionId, requestId, queryId,
|
|
1433
1434
|
context: context ? buildClientContext({}, context) : undefined, replySettleMs,
|
|
1434
1435
|
});
|
|
1435
|
-
|
|
1436
|
+
const output = runtimeReplyContent(reply, includeAudio);
|
|
1437
|
+
return { content: [...jsonContent(redactSecrets(output.summary)).content, ...output.clips] };
|
|
1436
1438
|
});
|
|
1437
1439
|
});
|
|
1438
1440
|
registerThalovantTool(server, "thalovant_send_action", {
|
|
@@ -2030,7 +2032,7 @@ export function createServer() {
|
|
|
2030
2032
|
});
|
|
2031
2033
|
registerThalovantTool(server, "thalovant_get_runtime_group_config", {
|
|
2032
2034
|
title: "Get Runtime Group Config",
|
|
2033
|
-
description: "Read a Thalovant runtime group's runtime configuration and personas. Requires the hubs:read scope.
|
|
2035
|
+
description: "Read a Thalovant runtime group's runtime configuration and personas. Requires the hubs:read scope. The revision returned by this call is used by safe configuration merges.",
|
|
2034
2036
|
inputSchema: {
|
|
2035
2037
|
...controlPlaneSchema,
|
|
2036
2038
|
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
@@ -2046,23 +2048,24 @@ export function createServer() {
|
|
|
2046
2048
|
});
|
|
2047
2049
|
registerThalovantTool(server, "thalovant_update_runtime_group_config", {
|
|
2048
2050
|
title: "Update Runtime Group Config",
|
|
2049
|
-
description: "Merge runtime configuration into a Thalovant runtime group. The
|
|
2051
|
+
description: "Merge runtime configuration into a Thalovant runtime group. The SDK reads and deep-merges config, then writes with a revision precondition. Only explicit HTTP 412 conflicts retry, at most three attempts; older APIs fail before a write. Set merge=false for explicit unconditional replacement. personas, when provided, is REPLACED wholesale rather than merged; omit it to leave stored personas untouched. Requires hubs:read and paid hubs:write for merging; replacement needs paid hubs:write.",
|
|
2050
2052
|
inputSchema: {
|
|
2051
2053
|
...controlPlaneSchema,
|
|
2052
2054
|
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
2055
|
+
merge: z.boolean().default(true).describe("False explicitly replaces the full configuration without a revision precondition."),
|
|
2053
2056
|
config: jsonRecordSchema.describe("Configuration object merged into the stored configuration. Required."),
|
|
2054
2057
|
personas: jsonRecordSchema.optional().describe("Replaces the stored personas outright. Left untouched when omitted."),
|
|
2055
2058
|
},
|
|
2056
2059
|
annotations: {
|
|
2057
2060
|
readOnlyHint: false,
|
|
2058
|
-
destructiveHint:
|
|
2061
|
+
destructiveHint: true,
|
|
2059
2062
|
idempotentHint: false,
|
|
2060
2063
|
openWorldHint: true,
|
|
2061
2064
|
},
|
|
2062
|
-
}, async ({ runtimeGroupId, config, personas, ...auth }) => {
|
|
2065
|
+
}, async ({ runtimeGroupId, config, personas, merge, ...auth }) => {
|
|
2063
2066
|
const api = await createControlPlane(auth);
|
|
2064
2067
|
ensureAuthenticated(api);
|
|
2065
|
-
return jsonContent(redactSecrets(await callControlPlane("write", () => api.updateRuntimeGroupConfig(runtimeGroupId, config, { personas }))));
|
|
2068
|
+
return jsonContent(redactSecrets(await callControlPlane("write", () => api.updateRuntimeGroupConfig(runtimeGroupId, config, { personas, merge }))));
|
|
2066
2069
|
});
|
|
2067
2070
|
registerThalovantTool(server, "thalovant_release_runtime_group", {
|
|
2068
2071
|
title: "Release Runtime Group",
|
|
@@ -2152,9 +2155,20 @@ export function createServer() {
|
|
|
2152
2155
|
await callControlPlane("write", () => api.uninstallRuntimeGroupSkill(runtimeGroupId, skillId));
|
|
2153
2156
|
return textContent("Runtime group skill uninstalled.");
|
|
2154
2157
|
});
|
|
2158
|
+
registerThalovantTool(server, "thalovant_list_hub_skill_history", {
|
|
2159
|
+
title: "List Hub Skill History",
|
|
2160
|
+
description: "Read newest-first skill events and operations for the runtime group attached to a hub. This history is shared by all hubs using that runtime. Requires hubs:inspect (hubs:read implies it); restricted tokens must cover all served hubs. Returns event/operation kind, timestamps, versions, actors and outcome fields. No changes are made.",
|
|
2161
|
+
inputSchema: { ...controlPlaneSchema, hubId: z.string().uuid().describe("Hub UUID (not the slug)."), limit: z.number().int().min(1).max(200).default(50) },
|
|
2162
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
2163
|
+
}, async ({ hubId, limit, ...auth }) => {
|
|
2164
|
+
const api = await createControlPlane(auth);
|
|
2165
|
+
ensureAuthenticated(api);
|
|
2166
|
+
throwIfRuntimeCancelled(requestContext.getStore()?.signal);
|
|
2167
|
+
return jsonContent(redactSecrets(await callControlPlane("read", () => api.listHubSkillHistory(hubId, { limit }))));
|
|
2168
|
+
});
|
|
2155
2169
|
registerThalovantTool(server, "thalovant_list_hub_skills", {
|
|
2156
2170
|
title: "List Hub Skills",
|
|
2157
|
-
description: "List the skills installed on
|
|
2171
|
+
description: "List the skills installed on the runtime group attached to a Thalovant hub. Returns the whole envelope: hub_id, runtime_group_id, observed_at, source, the runtime's phase and message, and data — one row per skill with version, version_pin, installed_version, observed_version, previous_version, latest_version, available_version, update_available, changelog, active, state (pending, installed, failed, removing, drifted, quarantined, unmanaged; a change in progress shows as pending), the runtime's last error and last_transition_at. The hub UUID selects its attached runtime group. Skills and changes are shared by every hub using that group; restricted tokens must cover all served hubs. hubId must be the hub UUID — the authenticated hub routes reject slugs; a hub with no runtime group attached fails 404 with code hub_without_runtime_group. Requires the hubs:inspect scope (hubs:read implies it); hub-restricted tokens only see the hubs in their allowlist.",
|
|
2158
2172
|
inputSchema: {
|
|
2159
2173
|
...controlPlaneSchema,
|
|
2160
2174
|
hubId: z.string().min(1).describe("Hub UUID (not the slug)."),
|
|
@@ -2168,7 +2182,7 @@ export function createServer() {
|
|
|
2168
2182
|
});
|
|
2169
2183
|
registerThalovantTool(server, "thalovant_install_hub_skill", {
|
|
2170
2184
|
title: "Install Hub Skill",
|
|
2171
|
-
description: "Install a skill on
|
|
2185
|
+
description: "Install a skill on the runtime group attached to a Thalovant hub. All hubs sharing that runtime group are affected. The change applies live on the hub in about 15 seconds, with no hub restart; a hub may start with no skills and gain them one at a time. Discover the skill name with thalovant_list_marketplace_skills. The API answers 202 with an operation_id and state \"installing\": pass wait: true to poll the operation until it converges (state \"installed\", or an error carrying the operation's failure message), with a 120 s default timeout; otherwise follow it with thalovant_get_operation. The 202 body also carries hub_id, runtime_group_id and previous_version. Installing a skill that is already installed at ANOTHER version performs an update; the SAME version fails 409 with code skill_version_already_installed. Fails 404 with code hub_without_runtime_group when the hub has no runtime group attached yet, and 422 when \"latest\" cannot be resolved or the version string is invalid. hubId must be the hub UUID — the authenticated hub routes reject slugs. Requires the hubs:write scope and a paid plan; scope is checked before plan, so a free-plan token sees 403, never 402. A hub-restricted token must cover every hub sharing the affected runtime group.",
|
|
2172
2186
|
inputSchema: {
|
|
2173
2187
|
...controlPlaneSchema,
|
|
2174
2188
|
hubId: z.string().min(1).describe("Hub UUID (not the slug)."),
|
|
@@ -2194,7 +2208,7 @@ export function createServer() {
|
|
|
2194
2208
|
});
|
|
2195
2209
|
registerThalovantTool(server, "thalovant_update_hub_skill", {
|
|
2196
2210
|
title: "Update Hub Skill",
|
|
2197
|
-
description: "Move a skill already installed on
|
|
2211
|
+
description: "Move a skill already installed on the runtime group attached to a Thalovant hub to a specific version (not a runtime group or skill set). The change applies live on the hub in about 15 seconds, with no hub restart. version is required — read the current version and latest_version from thalovant_list_hub_skills. The API answers 202 with an operation_id, hub_id, runtime_group_id, previous_version and state \"updating\": pass wait: true to poll the operation until it converges (state \"installed\", or an error carrying the operation's failure message), with a 120 s default timeout; otherwise follow it with thalovant_get_operation. Fails 404 when the skill is not installed on the hub (code hub_without_runtime_group when the hub has no runtime group), 409 with code skill_version_already_installed when it is already at that version, and 422 for an invalid version. hubId must be the hub UUID — the authenticated hub routes reject slugs. Requires the hubs:write scope and a paid plan; scope is checked before plan, so a free-plan token sees 403, never 402. A hub-restricted token must cover every hub sharing the affected runtime group.",
|
|
2198
2212
|
inputSchema: {
|
|
2199
2213
|
...controlPlaneSchema,
|
|
2200
2214
|
hubId: z.string().min(1).describe("Hub UUID (not the slug)."),
|
|
@@ -2218,7 +2232,7 @@ export function createServer() {
|
|
|
2218
2232
|
});
|
|
2219
2233
|
registerThalovantTool(server, "thalovant_remove_hub_skill", {
|
|
2220
2234
|
title: "Remove Hub Skill",
|
|
2221
|
-
description: "Remove one skill from
|
|
2235
|
+
description: "Remove one skill from the runtime group attached to a Thalovant hub. Every hub sharing the runtime loses this skill; other skill attachments are unchanged, and the change applies live on the hub in about 15 seconds with no hub restart. The API answers 202 with an operation_id, hub_id, runtime_group_id, previous_version (version is null) and state \"removing\": pass wait: true to poll the operation until it converges (state \"removed\", or an error carrying the operation's failure message), with a 120 s default timeout; otherwise follow it with thalovant_get_operation. Fails 404 when the skill is not installed on the hub (code hub_without_runtime_group when the hub has no runtime group). hubId must be the hub UUID — the authenticated hub routes reject slugs. Requires the hubs:write scope and a paid plan; scope is checked before plan, so a free-plan token sees 403, never 402. A hub-restricted token must cover every hub sharing the affected runtime group.",
|
|
2222
2236
|
inputSchema: {
|
|
2223
2237
|
...controlPlaneSchema,
|
|
2224
2238
|
hubId: z.string().min(1).describe("Hub UUID (not the slug)."),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ThalovantReply } from "@thalovant/sdk";
|
|
2
|
+
/** Keep embedded skill bytes out of the language model's text context. */
|
|
3
|
+
export declare function runtimeReplyContent(reply: ThalovantReply, includeAudio?: boolean): {
|
|
4
|
+
summary: {
|
|
5
|
+
text: string;
|
|
6
|
+
displayText: string;
|
|
7
|
+
utterances: string[];
|
|
8
|
+
handled: boolean;
|
|
9
|
+
ok: boolean;
|
|
10
|
+
sessionId: string | undefined;
|
|
11
|
+
requestId: string | undefined;
|
|
12
|
+
lang: string | undefined;
|
|
13
|
+
hasAudio: boolean;
|
|
14
|
+
droppedMedia: number;
|
|
15
|
+
displayItems: import("@thalovant/sdk").ThalovantDisplayItem[];
|
|
16
|
+
events: {
|
|
17
|
+
data: {
|
|
18
|
+
[x: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
}[];
|
|
21
|
+
media: {
|
|
22
|
+
eventIndex: number;
|
|
23
|
+
byteLength?: number;
|
|
24
|
+
mimeType?: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
}[];
|
|
27
|
+
failureEvent: {
|
|
28
|
+
data: {
|
|
29
|
+
[x: string]: unknown;
|
|
30
|
+
};
|
|
31
|
+
} | undefined;
|
|
32
|
+
};
|
|
33
|
+
clips: ({
|
|
34
|
+
type: "audio";
|
|
35
|
+
data: string;
|
|
36
|
+
mimeType: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: "resource";
|
|
39
|
+
resource: {
|
|
40
|
+
uri: string;
|
|
41
|
+
mimeType: string;
|
|
42
|
+
blob: string;
|
|
43
|
+
};
|
|
44
|
+
})[];
|
|
45
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
function eventSummary(event) {
|
|
3
|
+
const { binary_data: _encoded, ...data } = event.data;
|
|
4
|
+
return { ...event.asObject(), data };
|
|
5
|
+
}
|
|
6
|
+
/** Keep embedded skill bytes out of the language model's text context. */
|
|
7
|
+
export function runtimeReplyContent(reply, includeAudio = false) {
|
|
8
|
+
const clips = [];
|
|
9
|
+
const media = [];
|
|
10
|
+
const events = reply.events.map((event, eventIndex) => {
|
|
11
|
+
const output = eventSummary(event);
|
|
12
|
+
if (!event.isAudio)
|
|
13
|
+
return output;
|
|
14
|
+
try {
|
|
15
|
+
const bytes = Buffer.from(event.audioBytes());
|
|
16
|
+
const mimeType = bytes.subarray(0, 4).toString() === "RIFF" && bytes.subarray(8, 12).toString() === "WAVE" ? "audio/wav"
|
|
17
|
+
: bytes.subarray(0, 4).toString() === "OggS" ? "audio/ogg"
|
|
18
|
+
: bytes.subarray(0, 4).toString() === "fLaC" ? "audio/flac"
|
|
19
|
+
: bytes.subarray(0, 3).toString() === "ID3" ? "audio/mpeg" : "application/octet-stream";
|
|
20
|
+
media.push({ eventIndex, byteLength: bytes.length, mimeType });
|
|
21
|
+
if (includeAudio) {
|
|
22
|
+
const encoded = bytes.toString("base64");
|
|
23
|
+
clips.push(mimeType.startsWith("audio/") ? { type: "audio", data: encoded, mimeType } : {
|
|
24
|
+
type: "resource", resource: { uri: `thalovant-audio://sha256/${createHash("sha256").update(bytes).digest("hex")}`, mimeType, blob: encoded },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
media.push({ eventIndex, error: "Missing, invalid or oversized embedded audio." });
|
|
30
|
+
}
|
|
31
|
+
return output;
|
|
32
|
+
});
|
|
33
|
+
return { summary: {
|
|
34
|
+
text: reply.text, displayText: reply.displayText, utterances: reply.utterances,
|
|
35
|
+
handled: reply.handled, ok: reply.ok, sessionId: reply.sessionId, requestId: reply.requestId,
|
|
36
|
+
lang: reply.lang, hasAudio: reply.hasAudio ?? false, droppedMedia: reply.droppedMedia ?? 0,
|
|
37
|
+
displayItems: reply.displayItems({ maxTextChars: 1_000 }), events, media,
|
|
38
|
+
// A failure reference must not bypass text sanitization or add a clip
|
|
39
|
+
// outside the SDK's admitted event sequence and aggregate media budget.
|
|
40
|
+
failureEvent: reply.failureEvent ? eventSummary(reply.failureEvent) : undefined,
|
|
41
|
+
}, clips };
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thalovant/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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.
|
|
67
|
+
"@thalovant/sdk": "^0.5.1",
|
|
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.
|
|
10
|
+
"version": "0.3.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@thalovant/mcp-server",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.3.0",
|
|
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.
|
|
22
|
+
"identifier": "ghcr.io/thalovant/thalovant-mcp:0.3.0",
|
|
23
23
|
"transport": {
|
|
24
24
|
"type": "streamable-http",
|
|
25
25
|
"url": "http://127.0.0.1:3000/mcp"
|