@thalovant/mcp-server 0.1.9 → 0.1.10
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/.env.example +10 -1
- package/CHANGELOG.md +11 -0
- package/README.md +73 -2
- package/dist/index.js +571 -1
- package/examples/principal-credentials.sample.json +1 -1
- package/package.json +2 -2
- package/server.json +3 -3
package/.env.example
CHANGED
|
@@ -4,7 +4,11 @@ THALOVANT_API_URL=https://api.thalovant.com
|
|
|
4
4
|
|
|
5
5
|
# Use one control-plane auth option for private/admin tools.
|
|
6
6
|
# Recommended: a scoped, revocable API token (dashboard or device flow).
|
|
7
|
-
# Minimum scopes for the full tool surface: hubs:read,
|
|
7
|
+
# Minimum scopes for the full tool surface: hubs:read, hubs:write, clients:write,
|
|
8
|
+
# memory:read, memory:write. hubs:write implies hubs:read, which implies hubs:inspect.
|
|
9
|
+
# Discovery only (browse the skill catalog, never provision): hubs:read alone.
|
|
10
|
+
# Hub and runtime-group provisioning also requires a PAID plan; the marketplace
|
|
11
|
+
# catalog is not paid-gated, so free-tier tokens can browse but not install.
|
|
8
12
|
# THALOVANT_API_TOKEN=
|
|
9
13
|
# THALOVANT_ACCESS_TOKEN=
|
|
10
14
|
# THALOVANT_EMAIL=
|
|
@@ -45,6 +49,11 @@ MCP_HTTP_AUTH_TOKEN=
|
|
|
45
49
|
# THALOVANT_PRINCIPAL_CREDENTIALS_DIR=/run/secrets/thalovant-principals
|
|
46
50
|
# THALOVANT_ALLOW_SHARED_CREDENTIALS=false
|
|
47
51
|
|
|
52
|
+
# Destructive control-plane tools (thalovant_delete_hub, thalovant_delete_runtime_group)
|
|
53
|
+
# are NOT registered unless this is explicitly enabled. Leave it unset in any
|
|
54
|
+
# deployment where a model drives the tools with a long-lived token.
|
|
55
|
+
# THALOVANT_ENABLE_DESTRUCTIVE_TOOLS=false
|
|
56
|
+
|
|
48
57
|
# Optional policy and audit controls.
|
|
49
58
|
# MCP_TOOL_ALLOWLIST=thalovant_*
|
|
50
59
|
# MCP_TOOL_DENYLIST=thalovant_delete_memory_item
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.10
|
|
4
|
+
|
|
5
|
+
- Added hub-provisioning and skill-discovery tools so an agent can discover skills and provision hubs conversationally, all delegating to `@thalovant/sdk` (now `^0.2.28`).
|
|
6
|
+
- Discovery tools: `thalovant_list_marketplace_skills`, `thalovant_list_runtime_group_marketplace`, `thalovant_list_runtime_group_inventory`, `thalovant_list_runtime_groups`, `thalovant_get_runtime_group`, `thalovant_get_runtime_group_config`, `thalovant_get_hub_runtime_capabilities`.
|
|
7
|
+
- Provisioning tools: `thalovant_create_hub`, `thalovant_update_hub`, `thalovant_release_hub`, `thalovant_set_hub_rating`, `thalovant_clear_hub_rating`, `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`.
|
|
8
|
+
- `thalovant_delete_hub` and `thalovant_delete_runtime_group` are **disabled by default**: they are not registered at all unless the operator sets `THALOVANT_ENABLE_DESTRUCTIVE_TOOLS`, so they never appear in `tools/list`. The existing allow/deny tool policy is a call-time filter that cannot express a default-off tool, so this is a separate registration-time gate; once enabled, the delete tools remain subject to that policy. `thalovant_config_status` now reports `destructiveToolsEnabled`.
|
|
9
|
+
- Mirrored the control-plane API's exact constraints in the new input schemas so invalid values fail client-side instead of as an opaque 422: hub `spec.version` required, `name` max 128, `slug` 1-191 matching `^[a-z0-9]+(?:-[a-z0-9]+)*$`, `namespace` max 128, `domain` max 255, `visibility` max 32, `capacity_profile` restricted to `standard`/`autoscaling`, hub rating 1-5, runtime group `name` max 128 and `description` max 255, runtime group `spec.replicas` 1-20, skill `skill_id` max 191, `source_ref` max 255, `version_pin` max 64, and `marketplace_skill_id` a UUID. `source_type` is intentionally left a free-form 1-32 string because the API does not constrain it to an enum.
|
|
10
|
+
- `thalovant_update_hub` requires an `etag` and no longer accepts `name`, `namespace`, or `domain`, which are immutable after creation; the description directs agents to read the etag from the hub resource body, since the API sends no `ETag` response header. `thalovant_delete_hub` requires an `etag` for the same reason.
|
|
11
|
+
- Control-plane failures on the new tools now carry actionable guidance that distinguishes `403 Insufficient scopes` (checked before the plan gate, so it can mask a plan problem), the `402 API access requires a paid plan.` gate, the distinct `402 This skill requires paid marketplace access for the tenant plan.` at skill install, `412 ETag mismatch`, and `409` idempotency-key reuse.
|
|
12
|
+
- Documented the scope map for the new surface — `hubs:read` for the catalog, `hubs:inspect` for runtime-group and hub runtime views, `hubs:write` plus a paid plan for provisioning — and noted that catalog browsing is not paid-gated, so a free-tier token can browse the marketplace but cannot install.
|
|
13
|
+
|
|
3
14
|
## 0.1.9
|
|
4
15
|
|
|
5
16
|
- Added first-class scoped API token auth for the control plane: set `THALOVANT_API_TOKEN` and the server authenticates with the token directly and never calls the login endpoint. Precedence is `THALOVANT_API_TOKEN`, then `THALOVANT_ACCESS_TOKEN`, then the `THALOVANT_EMAIL`/`THALOVANT_PASSWORD` login fallback.
|
package/README.md
CHANGED
|
@@ -56,12 +56,28 @@ 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`, and the hub lookup inside `thalovant_create_client_identity` |
|
|
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` |
|
|
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 hub rating tools, and the opt-in delete tools |
|
|
60
62
|
| `clients:write` | `thalovant_create_client_identity` (`POST /v1/clients`) |
|
|
61
63
|
| `memory:read` | `thalovant_list_memory_items`, `thalovant_get_memory_summary`, `thalovant_get_memory_item` |
|
|
62
64
|
| `memory:write` | `thalovant_create_memory_item`, `thalovant_update_memory_item`, `thalovant_delete_memory_item` |
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
The hub scopes imply one another: `hubs:write` grants `hubs:read`, which grants `hubs:inspect` and `hubs:preview`. Minting a token with `hubs:read` is therefore enough for every discovery tool in the table above.
|
|
67
|
+
|
|
68
|
+
Scope is not the whole story for provisioning. Every hub and runtime-group write also requires a **paid plan**, and the API checks scope *before* the plan, so the two failure modes are ordered:
|
|
69
|
+
|
|
70
|
+
- A token missing the scope fails `403 Insufficient scopes`. Free-plan API tokens are capped at `hubs:read`, `clients:read`, and `clients:write`, so on the free tier provisioning fails with this 403 and never reaches the 402.
|
|
71
|
+
- A correctly scoped token on a free plan fails `402 API access requires a paid plan.`
|
|
72
|
+
- `thalovant_install_runtime_group_skill` can fail with a **second, distinct** 402, `This skill requires paid marketplace access for the tenant plan.`, when the plan is paid but does not include `access_tier: paid` catalog entries.
|
|
73
|
+
|
|
74
|
+
Discovery is deliberately not paid-gated: **a free-tier token can browse the marketplace catalog and set hub ratings, but cannot install skills or provision hubs.** Use `thalovant_list_runtime_group_marketplace` before installing — it reports `installable`, `purchase_required`, and `access_message` per skill, which turns an opaque 402 into a decision you can make up front.
|
|
75
|
+
|
|
76
|
+
Grant fewer scopes for narrower deployments: a read-only assistant needs only `hubs:read` and `memory:read`, and a discovery-only agent that browses skills but never provisions needs `hubs:read` alone. `thalovant_get_analytics_overview` with `admin: true` additionally requires an admin account with `admin:analytics`, which API tokens for regular use should not carry. Runtime hub tools (`thalovant_ask`, `thalovant_send_action`, and friends) use Thalovant client identities, not control-plane tokens.
|
|
77
|
+
|
|
78
|
+
### Hub Etags
|
|
79
|
+
|
|
80
|
+
`thalovant_update_hub` and `thalovant_delete_hub` use optimistic locking and require the hub's current etag, sent as `If-Match`. The etag is only available in the **body** of the hub resource — the API sends no `ETag` response header — so an agent must call `thalovant_get_hub` first and pass the `etag` field from that response. A missing or stale value fails `412 ETag mismatch` and changes nothing; re-fetch and retry. `name`, `namespace`, and `domain` are immutable after creation, so `thalovant_update_hub` does not accept them at all; send only the fields you are changing rather than round-tripping a whole hub resource. Runtime-group writes do not use etags.
|
|
65
81
|
|
|
66
82
|
### Login Fallback
|
|
67
83
|
|
|
@@ -201,6 +217,8 @@ export MCP_TOOL_DENYLIST="thalovant_delete_memory_item"
|
|
|
201
217
|
|
|
202
218
|
Per-principal credential files may also include `allowedTools` and `deniedTools`.
|
|
203
219
|
|
|
220
|
+
Both are call-time filters, and an empty allowlist means "allow everything". They cannot make a tool default-off or hide it from `tools/list`, which is why the two destructive control-plane tools are gated separately by `THALOVANT_ENABLE_DESTRUCTIVE_TOOLS`. See [Destructive Tools](#destructive-tools).
|
|
221
|
+
|
|
204
222
|
Audit logs:
|
|
205
223
|
|
|
206
224
|
```bash
|
|
@@ -295,6 +313,16 @@ Read-only:
|
|
|
295
313
|
- `thalovant_get_memory_summary`
|
|
296
314
|
- `thalovant_get_memory_item`
|
|
297
315
|
|
|
316
|
+
Skill and runtime-group discovery (read-only):
|
|
317
|
+
|
|
318
|
+
- `thalovant_list_marketplace_skills`
|
|
319
|
+
- `thalovant_list_runtime_group_marketplace`
|
|
320
|
+
- `thalovant_list_runtime_group_inventory`
|
|
321
|
+
- `thalovant_list_runtime_groups`
|
|
322
|
+
- `thalovant_get_runtime_group`
|
|
323
|
+
- `thalovant_get_runtime_group_config`
|
|
324
|
+
- `thalovant_get_hub_runtime_capabilities`
|
|
325
|
+
|
|
298
326
|
Writes or hub events:
|
|
299
327
|
|
|
300
328
|
- `thalovant_create_client_identity`
|
|
@@ -306,8 +334,51 @@ Writes or hub events:
|
|
|
306
334
|
- `thalovant_update_memory_item`
|
|
307
335
|
- `thalovant_delete_memory_item`
|
|
308
336
|
|
|
337
|
+
Hub and runtime-group provisioning:
|
|
338
|
+
|
|
339
|
+
- `thalovant_create_hub`
|
|
340
|
+
- `thalovant_update_hub`
|
|
341
|
+
- `thalovant_release_hub`
|
|
342
|
+
- `thalovant_set_hub_rating`
|
|
343
|
+
- `thalovant_clear_hub_rating`
|
|
344
|
+
- `thalovant_create_runtime_group`
|
|
345
|
+
- `thalovant_update_runtime_group`
|
|
346
|
+
- `thalovant_update_runtime_group_config`
|
|
347
|
+
- `thalovant_release_runtime_group`
|
|
348
|
+
- `thalovant_install_runtime_group_skill`
|
|
349
|
+
- `thalovant_uninstall_runtime_group_skill`
|
|
350
|
+
|
|
351
|
+
Destructive, **not registered unless explicitly enabled** (see [Destructive Tools](#destructive-tools)):
|
|
352
|
+
|
|
353
|
+
- `thalovant_delete_hub`
|
|
354
|
+
- `thalovant_delete_runtime_group`
|
|
355
|
+
|
|
309
356
|
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`.
|
|
310
357
|
|
|
358
|
+
## Destructive Tools
|
|
359
|
+
|
|
360
|
+
`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.
|
|
361
|
+
|
|
362
|
+
A long-lived control-plane token combined with an always-available delete tool is a categorically different risk from a read or update tool: deleting a hub also deletes its dependent clients and ACLs, and none of it is reversible. So these two are opt-in:
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
export THALOVANT_ENABLE_DESTRUCTIVE_TOOLS="true"
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Accepted true values are `1`, `true`, `yes`, and `on`; anything else, including unset, leaves the tools off. The flag is read when a server instance is created, so restart the server (or, in Streamable HTTP mode, start a new session) after changing it. `thalovant_config_status` reports the current state as `destructiveToolsEnabled` and lists the tools the flag controls.
|
|
369
|
+
|
|
370
|
+
This is a separate mechanism from the existing tool policy, deliberately. `MCP_TOOL_ALLOWLIST` / `MCP_TOOL_DENYLIST` and the per-principal `allowedTools` / `deniedTools` are call-time filters where an empty allowlist means "allow everything"; they cannot express a tool that is off until an operator turns it on, and they cannot hide a tool from `tools/list`. Once `THALOVANT_ENABLE_DESTRUCTIVE_TOOLS` is set the delete tools are ordinary tools again and remain subject to that policy, so the two layers compose:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
# Enable deletes server-wide, but deny them to everyone except trusted principals.
|
|
374
|
+
export THALOVANT_ENABLE_DESTRUCTIVE_TOOLS="true"
|
|
375
|
+
export MCP_TOOL_DENYLIST="thalovant_delete_hub,thalovant_delete_runtime_group"
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
with the trusted principal's credential file granting them back via `allowedTools`.
|
|
379
|
+
|
|
380
|
+
Deleting a hub still requires a current etag (`412` otherwise), and deleting a runtime group fails with `409` while it is the workspace default or still has hubs attached.
|
|
381
|
+
|
|
311
382
|
## Development
|
|
312
383
|
|
|
313
384
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
15
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
16
16
|
import { z } from "zod";
|
|
17
|
-
const VERSION = "0.1.
|
|
17
|
+
const VERSION = "0.1.10";
|
|
18
18
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
19
19
|
const MAX_TIMEOUT_MS = 300_000;
|
|
20
20
|
const MAX_LIMIT = 100;
|
|
@@ -106,6 +106,108 @@ const runtimeAuthSchema = {
|
|
|
106
106
|
.describe("Load the identity from Thalovant SDK environment variables."),
|
|
107
107
|
protocol: protocolSchema.default("wss").describe("Runtime transport protocol."),
|
|
108
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* Tools that destroy control-plane resources. They are never registered unless
|
|
111
|
+
* an operator opts in with THALOVANT_ENABLE_DESTRUCTIVE_TOOLS, so by default
|
|
112
|
+
* they are absent from tools/list entirely rather than merely failing when
|
|
113
|
+
* called. See destructiveToolsEnabled().
|
|
114
|
+
*/
|
|
115
|
+
const DESTRUCTIVE_TOOLS = ["thalovant_delete_hub", "thalovant_delete_runtime_group"];
|
|
116
|
+
/**
|
|
117
|
+
* Destructive control-plane tools are opt-in.
|
|
118
|
+
*
|
|
119
|
+
* The per-principal tool policy in authorizeTool() is an allow/deny filter
|
|
120
|
+
* evaluated at call time: an empty allowlist means "allow everything", so it
|
|
121
|
+
* cannot express a tool that is off until an operator turns it on, and it
|
|
122
|
+
* cannot hide a tool from tools/list. A long-lived token plus an always-listed
|
|
123
|
+
* delete tool is a categorically different risk from a read or update tool, so
|
|
124
|
+
* these two are gated at registration time by this env flag instead. When the
|
|
125
|
+
* flag is on they are registered like any other tool and remain subject to the
|
|
126
|
+
* global and per-principal policy, which can still deny them.
|
|
127
|
+
*/
|
|
128
|
+
function destructiveToolsEnabled() {
|
|
129
|
+
return parseBool(process.env.THALOVANT_ENABLE_DESTRUCTIVE_TOOLS, false);
|
|
130
|
+
}
|
|
131
|
+
const capacityProfileSchema = z
|
|
132
|
+
.enum(["standard", "autoscaling"])
|
|
133
|
+
.describe('Hub capacity profile. Only "standard" (the API default, 1 replica) and "autoscaling" (2-32 replicas) are accepted. Omit the key entirely rather than sending null — an explicit null fails 422 INVALID_HUB_CAPACITY_PROFILE. "autoscaling" is plan-gated: 403 HUB_AUTOSCALING_NOT_INCLUDED when the plan has no autoscaling, 409 AUTOSCALING_HUB_LIMIT_REACHED when its autoscaling slots are already used.');
|
|
134
|
+
const hubSpecSchema = z
|
|
135
|
+
.object({ version: z.string().min(1).describe("Required. Hub spec version, e.g. \"1\".") })
|
|
136
|
+
.passthrough()
|
|
137
|
+
.describe('Hub spec object. `version` is REQUIRED and must be a non-empty string — {"version": "1"} is the minimum valid spec; omitting it fails 422 "Schema validation failed". Other keys pass through, and the API injects defaults for replicas, resources, catalog, and protocols.');
|
|
138
|
+
const hubEtagSchema = z
|
|
139
|
+
.string()
|
|
140
|
+
.min(1)
|
|
141
|
+
.describe("Required. The hub's current etag, sent as If-Match for optimistic locking. Read it from the `etag` field in the hub resource BODY returned by thalovant_get_hub — the API does not send an ETag response header, so you must fetch the hub first. A missing or stale value fails with HTTP 412 and changes nothing: re-fetch the hub, take the new etag, and retry.");
|
|
142
|
+
const runtimeGroupResourceLimitsSchema = z
|
|
143
|
+
.object({
|
|
144
|
+
requests: z.record(z.string()).optional(),
|
|
145
|
+
limits: z.record(z.string()).optional(),
|
|
146
|
+
})
|
|
147
|
+
.optional();
|
|
148
|
+
const runtimeGroupSpecSchema = z
|
|
149
|
+
.object({
|
|
150
|
+
replicas: z.number().int().min(1).max(20).optional().describe("Replica count, 1-20."),
|
|
151
|
+
resources: z
|
|
152
|
+
.object({
|
|
153
|
+
core: runtimeGroupResourceLimitsSchema,
|
|
154
|
+
messagebus: runtimeGroupResourceLimitsSchema,
|
|
155
|
+
})
|
|
156
|
+
.optional()
|
|
157
|
+
.describe("Container resource requests/limits, as string-valued maps (for example {\"cpu\": \"500m\"})."),
|
|
158
|
+
})
|
|
159
|
+
.describe("Patches replicas and container resources only. Note the replica ceiling here is 20, separate from a hub's autoscaling ceiling of 32.");
|
|
160
|
+
const releaseOptionsSchema = {
|
|
161
|
+
channel: z.string().min(1).optional().describe("Release channel. Falls back to the workspace release policy when omitted."),
|
|
162
|
+
mode: z.string().min(1).optional().describe('Release mode. Passing images without mode switches to "custom".'),
|
|
163
|
+
version: z.string().min(1).optional().describe("Pinned release version."),
|
|
164
|
+
images: z.record(z.string().min(1)).optional().describe("Explicit image overrides. Switches to custom mode unless mode is also set."),
|
|
165
|
+
reason: z.string().min(1).optional().describe("Audit reason recorded with the release."),
|
|
166
|
+
};
|
|
167
|
+
function releaseOptionsFrom(args) {
|
|
168
|
+
return { channel: args.channel, mode: args.mode, version: args.version, images: args.images, reason: args.reason };
|
|
169
|
+
}
|
|
170
|
+
const SCOPE_HINT_403 = 'HTTP 403 "Insufficient scopes" — the token lacks the scope this route needs: hubs:write for provisioning, hubs:read for the marketplace catalog, hubs:inspect for runtime-group and hub runtime views (hubs:write implies hubs:read, which implies hubs:inspect and hubs:preview). The API checks scope BEFORE the paid-plan gate, so a 403 can mask a plan problem and granting the scope may surface a 402 next. Free-plan API tokens are capped at hubs:read, clients:read, and clients:write, so a free-tier API token fails provisioning with this 403 and never reaches the 402. A 403 here can also mean the caller does not own the resource ("Ownership required"), or a plan restriction such as HUB_AUTOSCALING_NOT_INCLUDED or a custom hub domain the plan does not allow.';
|
|
171
|
+
const PLAN_HINT_402 = 'HTTP 402 "API access requires a paid plan." — the token is valid and correctly scoped but the tenant is on the free plan. Free-tier callers can browse the catalog (thalovant_list_marketplace_skills) and set hub ratings, but cannot create, update, release, or delete hubs and runtime groups, and cannot install skills.';
|
|
172
|
+
const MARKETPLACE_HINT_402 = 'HTTP 402 "This skill requires paid marketplace access for the tenant plan." — a DIFFERENT 402 from the plan gate: the plan is paid enough to provision, but this catalog entry has access_tier "paid" and the plan does not include paid marketplace skills. thalovant_list_runtime_group_marketplace reports this per skill as purchase_required, installable, and access_message; check it before installing.';
|
|
173
|
+
const ETAG_HINT_412 = 'HTTP 412 "ETag mismatch" — the If-Match etag was missing or stale and nothing changed. Re-fetch the hub with thalovant_get_hub, read the `etag` field from the response BODY (there is no ETag response header), and retry with that exact value. The comparison is exact string equality, so do not rewrite or weaken the value.';
|
|
174
|
+
function controlPlaneErrorHint(profile, status, body) {
|
|
175
|
+
switch (status) {
|
|
176
|
+
case 402:
|
|
177
|
+
return profile === "skillInstall" && /marketplace/i.test(body) ? MARKETPLACE_HINT_402 : PLAN_HINT_402;
|
|
178
|
+
case 403:
|
|
179
|
+
return SCOPE_HINT_403;
|
|
180
|
+
case 409:
|
|
181
|
+
if (profile === "hubWrite") {
|
|
182
|
+
return 'HTTP 409 — for a hub create this is either a duplicate ("Hub with name ... already exists for this owner" / "Hub slug ... already exists"), an autoscaling slot limit (AUTOSCALING_HUB_LIMIT_REACHED), or "Idempotency key re-used with different payload". Only hub create honors Idempotency-Key: retrying an identical create is safe and returns the original hub, but reusing a key with a changed body conflicts — use a new idempotencyKey for a genuinely different hub.';
|
|
183
|
+
}
|
|
184
|
+
return "HTTP 409 — the resource is not in a state that allows this call: a hub with no connected client cannot report runtime capabilities, a runtime group cannot be deleted while it is the workspace default or still has hubs attached, and a deactivated marketplace skill cannot be installed.";
|
|
185
|
+
case 412:
|
|
186
|
+
return ETAG_HINT_412;
|
|
187
|
+
default:
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Run a control-plane call and, on failure, append actionable guidance.
|
|
193
|
+
*
|
|
194
|
+
* The SDK raises ThalovantApiError with the status embedded in the message
|
|
195
|
+
* ("Thalovant API request failed with HTTP 412: ..."), so the status is
|
|
196
|
+
* recovered by parsing rather than from a structured field.
|
|
197
|
+
*/
|
|
198
|
+
async function callControlPlane(profile, run) {
|
|
199
|
+
try {
|
|
200
|
+
return await run();
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
204
|
+
const status = Number(/HTTP (\d{3})/.exec(message)?.[1]);
|
|
205
|
+
const hint = Number.isFinite(status) ? controlPlaneErrorHint(profile, status, message) : undefined;
|
|
206
|
+
if (!hint)
|
|
207
|
+
throw error;
|
|
208
|
+
throw new Error(`${message}\n\n${hint}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
109
211
|
class HttpError extends Error {
|
|
110
212
|
status;
|
|
111
213
|
constructor(status, message) {
|
|
@@ -913,6 +1015,8 @@ export function createServer() {
|
|
|
913
1015
|
profile: process.env.THALOVANT_PROFILE,
|
|
914
1016
|
hasRuntimeEnvIdentity: Boolean(process.env.THALOVANT_ACCESS_KEY || process.env.THALOVANT_IDENTITY),
|
|
915
1017
|
defaultProtocol: "wss",
|
|
1018
|
+
destructiveToolsEnabled: destructiveToolsEnabled(),
|
|
1019
|
+
destructiveTools: destructiveToolsEnabled() ? [...DESTRUCTIVE_TOOLS] : [],
|
|
916
1020
|
secretHandling: "Secrets are redacted in tool output. Identity files are only written when savePath is provided.",
|
|
917
1021
|
}));
|
|
918
1022
|
registerThalovantTool(server, "thalovant_list_public_hubs", {
|
|
@@ -1412,6 +1516,472 @@ export function createServer() {
|
|
|
1412
1516
|
await api.deleteMemoryItem(memoryId);
|
|
1413
1517
|
return textContent("Memory item deleted.");
|
|
1414
1518
|
});
|
|
1519
|
+
registerThalovantTool(server, "thalovant_list_marketplace_skills", {
|
|
1520
|
+
title: "List Marketplace Skills",
|
|
1521
|
+
description: "Browse the Thalovant marketplace skill catalog. Start here when discovering what a hub could run: each entry carries the fields an install needs (skill_id, source_type, source_ref, package_name, version compatibility, config_schema, secret_schema) plus category, tags, verified, access_tier, and billing_sku. Requires the hubs:read scope and is NOT paid-gated, so a free-tier token can browse the catalog even though it cannot install from it. The response is not paginated — the whole catalog comes back at once.",
|
|
1522
|
+
inputSchema: {
|
|
1523
|
+
...controlPlaneSchema,
|
|
1524
|
+
ownerId: z
|
|
1525
|
+
.string()
|
|
1526
|
+
.min(1)
|
|
1527
|
+
.optional()
|
|
1528
|
+
.describe("Admin tokens only. A non-admin caller is silently scoped to their own tenant instead of being rejected, so this is a no-op for ordinary tokens."),
|
|
1529
|
+
includeInactive: z
|
|
1530
|
+
.boolean()
|
|
1531
|
+
.optional()
|
|
1532
|
+
.describe("Admin tokens only. A non-admin caller silently gets active entries only, with no error."),
|
|
1533
|
+
forceRefresh: z
|
|
1534
|
+
.boolean()
|
|
1535
|
+
.optional()
|
|
1536
|
+
.describe("Re-sync the global catalog from its source before answering. Open to all callers; noticeably slower."),
|
|
1537
|
+
},
|
|
1538
|
+
annotations: {
|
|
1539
|
+
readOnlyHint: true,
|
|
1540
|
+
openWorldHint: true,
|
|
1541
|
+
},
|
|
1542
|
+
}, async ({ ownerId, includeInactive, forceRefresh, ...auth }) => {
|
|
1543
|
+
const api = await createControlPlane(auth);
|
|
1544
|
+
ensureAuthenticated(api);
|
|
1545
|
+
return jsonContent(redactSecrets(await callControlPlane("read", () => api.listMarketplaceSkills({ ownerId, includeInactive, forceRefresh }))));
|
|
1546
|
+
});
|
|
1547
|
+
registerThalovantTool(server, "thalovant_list_runtime_group_marketplace", {
|
|
1548
|
+
title: "List Runtime Group Marketplace",
|
|
1549
|
+
description: "List the marketplace catalog resolved against one runtime group. This is the view to read immediately before installing: every catalog entry comes back with the group's own state folded in — whether the skill is desired (active, version_pin, source_type), whether it was observed running (observed_source, observed_at, intent counts), operator status, and the plan verdict (purchase_required, installable, access_message). Check installable and purchase_required here to avoid a 402 at install time. Requires the hubs:inspect scope; browsing needs no paid plan. Answers 404 for an unknown group and 403 when the caller does not own it.",
|
|
1550
|
+
inputSchema: {
|
|
1551
|
+
...controlPlaneSchema,
|
|
1552
|
+
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
1553
|
+
refreshInventory: z
|
|
1554
|
+
.boolean()
|
|
1555
|
+
.optional()
|
|
1556
|
+
.describe("Force a live read from the runtime operator. Without it the envelope source is runtime-group-cache (or runtime-group-cache-empty), never a live read."),
|
|
1557
|
+
},
|
|
1558
|
+
annotations: {
|
|
1559
|
+
readOnlyHint: true,
|
|
1560
|
+
openWorldHint: true,
|
|
1561
|
+
},
|
|
1562
|
+
}, async ({ runtimeGroupId, refreshInventory, ...auth }) => {
|
|
1563
|
+
const api = await createControlPlane(auth);
|
|
1564
|
+
ensureAuthenticated(api);
|
|
1565
|
+
return jsonContent(redactSecrets(await callControlPlane("read", () => api.listRuntimeGroupMarketplace(runtimeGroupId, { refreshInventory }))));
|
|
1566
|
+
});
|
|
1567
|
+
registerThalovantTool(server, "thalovant_list_runtime_group_inventory", {
|
|
1568
|
+
title: "List Runtime Group Inventory",
|
|
1569
|
+
description: "List the skills a runtime group is actually observed running right now. Where thalovant_list_runtime_group_marketplace answers 'what could be installed here', this answers 'what is loaded'. Each entry carries skill_id, version, source, active, adapt_intents, padatious_intents, total_intents, and observed_at; the envelope reports source (ovos-runtime-operator, runtime-group-cache, or ovos-runtime-operator-pending), operator_phase, and operator_message. Unlike thalovant_get_hub_runtime_capabilities this never fails with 409 when nothing is reporting — it returns an empty list with a pending source. Requires the hubs:inspect scope; no paid plan needed.",
|
|
1570
|
+
inputSchema: {
|
|
1571
|
+
...controlPlaneSchema,
|
|
1572
|
+
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
1573
|
+
refresh: z
|
|
1574
|
+
.boolean()
|
|
1575
|
+
.optional()
|
|
1576
|
+
.describe("Force a live operator read. The API also refreshes on its own when it holds no cached snapshot."),
|
|
1577
|
+
},
|
|
1578
|
+
annotations: {
|
|
1579
|
+
readOnlyHint: true,
|
|
1580
|
+
openWorldHint: true,
|
|
1581
|
+
},
|
|
1582
|
+
}, async ({ runtimeGroupId, refresh, ...auth }) => {
|
|
1583
|
+
const api = await createControlPlane(auth);
|
|
1584
|
+
ensureAuthenticated(api);
|
|
1585
|
+
return jsonContent(redactSecrets(await callControlPlane("read", () => api.listRuntimeGroupInventory(runtimeGroupId, { refresh }))));
|
|
1586
|
+
});
|
|
1587
|
+
registerThalovantTool(server, "thalovant_create_hub", {
|
|
1588
|
+
title: "Create Hub",
|
|
1589
|
+
description: "Create a Thalovant hub. Requires the hubs:write scope AND a paid plan: a token without the scope fails 403, and because the scope is checked before the plan, granting the scope can then surface a 402 'API access requires a paid plan'. The create is idempotent — the SDK sends an Idempotency-Key, so a retried create returns the first hub rather than making a second one; reusing a key with a DIFFERENT body fails 409.",
|
|
1590
|
+
inputSchema: {
|
|
1591
|
+
...controlPlaneSchema,
|
|
1592
|
+
name: z.string().min(1).max(128).describe("Hub name, 1-128 characters. Required. Immutable once created."),
|
|
1593
|
+
spec: hubSpecSchema,
|
|
1594
|
+
slug: z
|
|
1595
|
+
.string()
|
|
1596
|
+
.min(1)
|
|
1597
|
+
.max(191)
|
|
1598
|
+
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Slug must be lowercase alphanumeric segments separated by single hyphens.")
|
|
1599
|
+
.optional()
|
|
1600
|
+
.describe("1-191 chars, lowercase alphanumeric segments separated by single hyphens. Defaults to a slug derived from name. Unlike name, slug stays mutable."),
|
|
1601
|
+
namespace: z.string().min(1).max(128).optional().describe("1-128 characters. Resolved server-side when omitted. Immutable once created."),
|
|
1602
|
+
domain: z
|
|
1603
|
+
.string()
|
|
1604
|
+
.min(1)
|
|
1605
|
+
.max(255)
|
|
1606
|
+
.optional()
|
|
1607
|
+
.describe("Max 255 characters. Immutable once created. Plans without custom domains are rejected with 403; when omitted, a managed subdomain is generated if the plan provides one."),
|
|
1608
|
+
active: z.boolean().optional().describe("Defaults to true."),
|
|
1609
|
+
visibility: z
|
|
1610
|
+
.string()
|
|
1611
|
+
.min(1)
|
|
1612
|
+
.max(32)
|
|
1613
|
+
.optional()
|
|
1614
|
+
.describe('1-32 characters, defaults to "private". Only "public" is special-cased by the API, and public listing is plan-gated.'),
|
|
1615
|
+
ownerId: z.string().min(1).optional().describe("Defaults to the caller. Setting another owner requires admin."),
|
|
1616
|
+
runtimeGroupId: z.string().min(1).optional().describe("Runtime group UUID to attach the hub to."),
|
|
1617
|
+
capacityProfile: capacityProfileSchema.optional(),
|
|
1618
|
+
isLocked: z.boolean().optional(),
|
|
1619
|
+
idempotencyKey: z
|
|
1620
|
+
.string()
|
|
1621
|
+
.min(1)
|
|
1622
|
+
.optional()
|
|
1623
|
+
.describe("Reuse the same key to safely retry one create. Reusing it with a different body fails 409."),
|
|
1624
|
+
},
|
|
1625
|
+
annotations: {
|
|
1626
|
+
readOnlyHint: false,
|
|
1627
|
+
destructiveHint: false,
|
|
1628
|
+
idempotentHint: true,
|
|
1629
|
+
openWorldHint: true,
|
|
1630
|
+
},
|
|
1631
|
+
}, async (args) => {
|
|
1632
|
+
const { loginScope, apiUrl, idempotencyKey, ...payload } = args;
|
|
1633
|
+
const api = await createControlPlane({ loginScope, apiUrl });
|
|
1634
|
+
ensureAuthenticated(api);
|
|
1635
|
+
return jsonContent(redactSecrets(await callControlPlane("hubWrite", () => api.createHub(payload, { idempotencyKey }))));
|
|
1636
|
+
});
|
|
1637
|
+
registerThalovantTool(server, "thalovant_update_hub", {
|
|
1638
|
+
title: "Update Hub",
|
|
1639
|
+
description: "Partially update a Thalovant hub. Send ONLY the fields you are changing — do not round-trip a whole hub resource. name, namespace, and domain are immutable and are deliberately not accepted here; the API rejects a changed value with HTTP 400. This route uses optimistic locking and requires the hub's current etag, which lives in the hub resource BODY (there is no ETag response header), so call thalovant_get_hub first and pass the etag field from its response. A missing or stale etag fails 412 and changes nothing. Requires the hubs:write scope and a paid plan.",
|
|
1640
|
+
inputSchema: {
|
|
1641
|
+
...controlPlaneSchema,
|
|
1642
|
+
hubId: z.string().min(1).describe("Hub UUID. This authenticated route rejects slugs."),
|
|
1643
|
+
etag: hubEtagSchema,
|
|
1644
|
+
active: z.boolean().optional(),
|
|
1645
|
+
visibility: z
|
|
1646
|
+
.string()
|
|
1647
|
+
.min(1)
|
|
1648
|
+
.max(32)
|
|
1649
|
+
.optional()
|
|
1650
|
+
.describe('1-32 characters. Switching to "public" is plan-gated and fails 403 on plans that keep hubs private.'),
|
|
1651
|
+
slug: z
|
|
1652
|
+
.string()
|
|
1653
|
+
.min(1)
|
|
1654
|
+
.max(191)
|
|
1655
|
+
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Slug must be lowercase alphanumeric segments separated by single hyphens.")
|
|
1656
|
+
.optional()
|
|
1657
|
+
.describe("Mutable, unlike name/namespace/domain. Fails 409 if the slug is already taken."),
|
|
1658
|
+
spec: hubSpecSchema.optional(),
|
|
1659
|
+
ownerId: z.string().min(1).optional(),
|
|
1660
|
+
runtimeGroupId: z.string().min(1).optional(),
|
|
1661
|
+
capacityProfile: capacityProfileSchema.optional(),
|
|
1662
|
+
isLocked: z.boolean().optional().describe("Admin only: a non-admin gets 403, and a non-admin cannot update a locked hub at all."),
|
|
1663
|
+
},
|
|
1664
|
+
annotations: {
|
|
1665
|
+
readOnlyHint: false,
|
|
1666
|
+
destructiveHint: false,
|
|
1667
|
+
idempotentHint: false,
|
|
1668
|
+
openWorldHint: true,
|
|
1669
|
+
},
|
|
1670
|
+
}, async (args) => {
|
|
1671
|
+
const { loginScope, apiUrl, hubId, etag, ...payload } = args;
|
|
1672
|
+
const api = await createControlPlane({ loginScope, apiUrl });
|
|
1673
|
+
ensureAuthenticated(api);
|
|
1674
|
+
return jsonContent(redactSecrets(await callControlPlane("hubWrite", () => api.updateHub(hubId, payload, { etag }))));
|
|
1675
|
+
});
|
|
1676
|
+
registerThalovantTool(server, "thalovant_release_hub", {
|
|
1677
|
+
title: "Release Hub",
|
|
1678
|
+
description: "Apply a release policy to a hub and return the updated hub. Every option is optional; omitted fields fall back to the workspace release policy. Passing images switches the hub to custom mode unless mode is also set. Requires the hubs:write scope and a paid plan. No etag is needed.",
|
|
1679
|
+
inputSchema: {
|
|
1680
|
+
...controlPlaneSchema,
|
|
1681
|
+
hubId: z.string().min(1).describe("Hub UUID."),
|
|
1682
|
+
...releaseOptionsSchema,
|
|
1683
|
+
},
|
|
1684
|
+
annotations: {
|
|
1685
|
+
readOnlyHint: false,
|
|
1686
|
+
destructiveHint: false,
|
|
1687
|
+
idempotentHint: false,
|
|
1688
|
+
openWorldHint: true,
|
|
1689
|
+
},
|
|
1690
|
+
}, async ({ hubId, channel, mode, version, images, reason, ...auth }) => {
|
|
1691
|
+
const api = await createControlPlane(auth);
|
|
1692
|
+
ensureAuthenticated(api);
|
|
1693
|
+
return jsonContent(redactSecrets(await callControlPlane("hubWrite", () => api.releaseHub(hubId, releaseOptionsFrom({ channel, mode, version, images, reason })))));
|
|
1694
|
+
});
|
|
1695
|
+
registerThalovantTool(server, "thalovant_set_hub_rating", {
|
|
1696
|
+
title: "Set Hub Rating",
|
|
1697
|
+
description: 'Rate a public Thalovant hub from 1 to 5 and return the updated hub. Requires the hubs:write scope; no paid plan is needed, so free-tier tokens can rate. Rating a non-public hub fails 400 "Only public hubs can be rated." and rating your own hub fails 400 "Hub owners cannot rate their own public hubs."',
|
|
1698
|
+
inputSchema: {
|
|
1699
|
+
...controlPlaneSchema,
|
|
1700
|
+
hubId: z.string().min(1).describe("Hub UUID."),
|
|
1701
|
+
rating: z.number().int().min(1).max(5).describe("Rating, an integer from 1 to 5."),
|
|
1702
|
+
},
|
|
1703
|
+
annotations: {
|
|
1704
|
+
readOnlyHint: false,
|
|
1705
|
+
destructiveHint: false,
|
|
1706
|
+
idempotentHint: true,
|
|
1707
|
+
openWorldHint: true,
|
|
1708
|
+
},
|
|
1709
|
+
}, async ({ hubId, rating, ...auth }) => {
|
|
1710
|
+
const api = await createControlPlane(auth);
|
|
1711
|
+
ensureAuthenticated(api);
|
|
1712
|
+
return jsonContent(redactSecrets(await callControlPlane("write", () => api.setHubRating(hubId, rating))));
|
|
1713
|
+
});
|
|
1714
|
+
registerThalovantTool(server, "thalovant_clear_hub_rating", {
|
|
1715
|
+
title: "Clear Hub Rating",
|
|
1716
|
+
description: "Remove the caller's own rating from a public Thalovant hub and return the hub. This clears only the caller's rating, not the hub. Requires the hubs:write scope; no paid plan is needed.",
|
|
1717
|
+
inputSchema: {
|
|
1718
|
+
...controlPlaneSchema,
|
|
1719
|
+
hubId: z.string().min(1).describe("Hub UUID."),
|
|
1720
|
+
},
|
|
1721
|
+
annotations: {
|
|
1722
|
+
readOnlyHint: false,
|
|
1723
|
+
destructiveHint: false,
|
|
1724
|
+
idempotentHint: true,
|
|
1725
|
+
openWorldHint: true,
|
|
1726
|
+
},
|
|
1727
|
+
}, async ({ hubId, ...auth }) => {
|
|
1728
|
+
const api = await createControlPlane(auth);
|
|
1729
|
+
ensureAuthenticated(api);
|
|
1730
|
+
return jsonContent(redactSecrets(await callControlPlane("write", () => api.clearHubRating(hubId))));
|
|
1731
|
+
});
|
|
1732
|
+
registerThalovantTool(server, "thalovant_get_hub_runtime_capabilities", {
|
|
1733
|
+
title: "Get Hub Runtime Capabilities",
|
|
1734
|
+
description: "Read the live skill and intent inventory a hub runtime exposes. Requires the hubs:inspect scope. The API answers HTTP 409 when the hub has no connected client that can report inventory — for a group-level view that returns an empty list instead of failing, use thalovant_list_runtime_group_inventory.",
|
|
1735
|
+
inputSchema: {
|
|
1736
|
+
...controlPlaneSchema,
|
|
1737
|
+
hubId: z.string().min(1).describe("Hub UUID."),
|
|
1738
|
+
},
|
|
1739
|
+
annotations: {
|
|
1740
|
+
readOnlyHint: true,
|
|
1741
|
+
openWorldHint: true,
|
|
1742
|
+
},
|
|
1743
|
+
}, async ({ hubId, ...auth }) => {
|
|
1744
|
+
const api = await createControlPlane(auth);
|
|
1745
|
+
ensureAuthenticated(api);
|
|
1746
|
+
return jsonContent(redactSecrets(await callControlPlane("read", () => api.getHubRuntimeCapabilities(hubId))));
|
|
1747
|
+
});
|
|
1748
|
+
registerThalovantTool(server, "thalovant_list_runtime_groups", {
|
|
1749
|
+
title: "List Runtime Groups",
|
|
1750
|
+
description: "List the Thalovant runtime groups visible to the authenticated account. Requires the hubs:read scope. The response is not paginated — every visible group is returned at once.",
|
|
1751
|
+
inputSchema: {
|
|
1752
|
+
...controlPlaneSchema,
|
|
1753
|
+
ownerId: z
|
|
1754
|
+
.string()
|
|
1755
|
+
.min(1)
|
|
1756
|
+
.optional()
|
|
1757
|
+
.describe("Admin tokens only. Unlike the marketplace catalog, which silently ignores this for non-admins, passing another account's id here fails 403 \"Ownership required\". Omit it to list your own groups."),
|
|
1758
|
+
},
|
|
1759
|
+
annotations: {
|
|
1760
|
+
readOnlyHint: true,
|
|
1761
|
+
openWorldHint: true,
|
|
1762
|
+
},
|
|
1763
|
+
}, async ({ ownerId, ...auth }) => {
|
|
1764
|
+
const api = await createControlPlane(auth);
|
|
1765
|
+
ensureAuthenticated(api);
|
|
1766
|
+
return jsonContent(redactSecrets(await callControlPlane("read", () => api.listRuntimeGroups({ ownerId }))));
|
|
1767
|
+
});
|
|
1768
|
+
registerThalovantTool(server, "thalovant_get_runtime_group", {
|
|
1769
|
+
title: "Get Runtime Group",
|
|
1770
|
+
description: "Fetch one Thalovant runtime group. Requires the hubs:read scope.",
|
|
1771
|
+
inputSchema: {
|
|
1772
|
+
...controlPlaneSchema,
|
|
1773
|
+
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
1774
|
+
},
|
|
1775
|
+
annotations: {
|
|
1776
|
+
readOnlyHint: true,
|
|
1777
|
+
openWorldHint: true,
|
|
1778
|
+
},
|
|
1779
|
+
}, async ({ runtimeGroupId, ...auth }) => {
|
|
1780
|
+
const api = await createControlPlane(auth);
|
|
1781
|
+
ensureAuthenticated(api);
|
|
1782
|
+
return jsonContent(redactSecrets(await callControlPlane("read", () => api.getRuntimeGroup(runtimeGroupId))));
|
|
1783
|
+
});
|
|
1784
|
+
registerThalovantTool(server, "thalovant_create_runtime_group", {
|
|
1785
|
+
title: "Create Runtime Group",
|
|
1786
|
+
description: "Create a Thalovant runtime group — the unit that hosts hubs and holds installed skills. Requires the hubs:write scope and a paid plan. Unlike the hub write routes this one takes no etag.",
|
|
1787
|
+
inputSchema: {
|
|
1788
|
+
...controlPlaneSchema,
|
|
1789
|
+
name: z.string().min(1).max(128).describe("Runtime group name, 1-128 characters. Required."),
|
|
1790
|
+
description: z.string().max(255).optional().describe("Max 255 characters."),
|
|
1791
|
+
environment: z.string().min(1).max(32).optional().describe("1-32 characters. Lowercased server-side; defaults from server settings."),
|
|
1792
|
+
ownerId: z.string().min(1).optional().describe("Defaults to the caller. Setting another owner requires admin."),
|
|
1793
|
+
cloneFromDefault: z
|
|
1794
|
+
.boolean()
|
|
1795
|
+
.optional()
|
|
1796
|
+
.describe("Seed the new group from the workspace default group instead of starting empty."),
|
|
1797
|
+
},
|
|
1798
|
+
annotations: {
|
|
1799
|
+
readOnlyHint: false,
|
|
1800
|
+
destructiveHint: false,
|
|
1801
|
+
idempotentHint: false,
|
|
1802
|
+
openWorldHint: true,
|
|
1803
|
+
},
|
|
1804
|
+
}, async (args) => {
|
|
1805
|
+
const { loginScope, apiUrl, ...payload } = args;
|
|
1806
|
+
const api = await createControlPlane({ loginScope, apiUrl });
|
|
1807
|
+
ensureAuthenticated(api);
|
|
1808
|
+
return jsonContent(redactSecrets(await callControlPlane("write", () => api.createRuntimeGroup(payload))));
|
|
1809
|
+
});
|
|
1810
|
+
registerThalovantTool(server, "thalovant_update_runtime_group", {
|
|
1811
|
+
title: "Update Runtime Group",
|
|
1812
|
+
description: "Update a Thalovant runtime group's name, description, or spec. spec patches replicas and container resources. This route does NOT use If-Match, so no etag is required. Requires the hubs:write scope and a paid plan.",
|
|
1813
|
+
inputSchema: {
|
|
1814
|
+
...controlPlaneSchema,
|
|
1815
|
+
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
1816
|
+
name: z.string().min(1).max(128).optional().describe("1-128 characters."),
|
|
1817
|
+
description: z.string().max(255).optional().describe("Max 255 characters."),
|
|
1818
|
+
spec: runtimeGroupSpecSchema.optional(),
|
|
1819
|
+
},
|
|
1820
|
+
annotations: {
|
|
1821
|
+
readOnlyHint: false,
|
|
1822
|
+
destructiveHint: false,
|
|
1823
|
+
idempotentHint: false,
|
|
1824
|
+
openWorldHint: true,
|
|
1825
|
+
},
|
|
1826
|
+
}, async (args) => {
|
|
1827
|
+
const { loginScope, apiUrl, runtimeGroupId, ...payload } = args;
|
|
1828
|
+
const api = await createControlPlane({ loginScope, apiUrl });
|
|
1829
|
+
ensureAuthenticated(api);
|
|
1830
|
+
return jsonContent(redactSecrets(await callControlPlane("write", () => api.updateRuntimeGroup(runtimeGroupId, payload))));
|
|
1831
|
+
});
|
|
1832
|
+
registerThalovantTool(server, "thalovant_get_runtime_group_config", {
|
|
1833
|
+
title: "Get Runtime Group Config",
|
|
1834
|
+
description: "Read a Thalovant runtime group's runtime configuration and personas. Requires the hubs:read scope. Read this before thalovant_update_runtime_group_config so you know what the merge will land on.",
|
|
1835
|
+
inputSchema: {
|
|
1836
|
+
...controlPlaneSchema,
|
|
1837
|
+
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
1838
|
+
},
|
|
1839
|
+
annotations: {
|
|
1840
|
+
readOnlyHint: true,
|
|
1841
|
+
openWorldHint: true,
|
|
1842
|
+
},
|
|
1843
|
+
}, async ({ runtimeGroupId, ...auth }) => {
|
|
1844
|
+
const api = await createControlPlane(auth);
|
|
1845
|
+
ensureAuthenticated(api);
|
|
1846
|
+
return jsonContent(redactSecrets(await callControlPlane("read", () => api.getRuntimeGroupConfig(runtimeGroupId))));
|
|
1847
|
+
});
|
|
1848
|
+
registerThalovantTool(server, "thalovant_update_runtime_group_config", {
|
|
1849
|
+
title: "Update Runtime Group Config",
|
|
1850
|
+
description: "Merge runtime configuration into a Thalovant runtime group. The API MERGES config into the stored configuration rather than replacing it, and marks the group pending so the runtime operator reconciles the change. personas, when provided, is REPLACED wholesale rather than merged; omit it to leave stored personas untouched. Requires the hubs:write scope and a paid plan.",
|
|
1851
|
+
inputSchema: {
|
|
1852
|
+
...controlPlaneSchema,
|
|
1853
|
+
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
1854
|
+
config: jsonRecordSchema.describe("Configuration object merged into the stored configuration. Required."),
|
|
1855
|
+
personas: jsonRecordSchema.optional().describe("Replaces the stored personas outright. Left untouched when omitted."),
|
|
1856
|
+
},
|
|
1857
|
+
annotations: {
|
|
1858
|
+
readOnlyHint: false,
|
|
1859
|
+
destructiveHint: false,
|
|
1860
|
+
idempotentHint: false,
|
|
1861
|
+
openWorldHint: true,
|
|
1862
|
+
},
|
|
1863
|
+
}, async ({ runtimeGroupId, config, personas, ...auth }) => {
|
|
1864
|
+
const api = await createControlPlane(auth);
|
|
1865
|
+
ensureAuthenticated(api);
|
|
1866
|
+
return jsonContent(redactSecrets(await callControlPlane("write", () => api.updateRuntimeGroupConfig(runtimeGroupId, config, { personas }))));
|
|
1867
|
+
});
|
|
1868
|
+
registerThalovantTool(server, "thalovant_release_runtime_group", {
|
|
1869
|
+
title: "Release Runtime Group",
|
|
1870
|
+
description: "Apply a runtime image policy to a Thalovant runtime group and return the updated group. Options behave like thalovant_release_hub: everything is optional, omitted fields fall back to the workspace release policy, and passing images switches to custom mode unless mode is also set. Requires the hubs:write scope and a paid plan.",
|
|
1871
|
+
inputSchema: {
|
|
1872
|
+
...controlPlaneSchema,
|
|
1873
|
+
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
1874
|
+
...releaseOptionsSchema,
|
|
1875
|
+
},
|
|
1876
|
+
annotations: {
|
|
1877
|
+
readOnlyHint: false,
|
|
1878
|
+
destructiveHint: false,
|
|
1879
|
+
idempotentHint: false,
|
|
1880
|
+
openWorldHint: true,
|
|
1881
|
+
},
|
|
1882
|
+
}, async ({ runtimeGroupId, channel, mode, version, images, reason, ...auth }) => {
|
|
1883
|
+
const api = await createControlPlane(auth);
|
|
1884
|
+
ensureAuthenticated(api);
|
|
1885
|
+
return jsonContent(redactSecrets(await callControlPlane("write", () => api.releaseRuntimeGroup(runtimeGroupId, releaseOptionsFrom({ channel, mode, version, images, reason })))));
|
|
1886
|
+
});
|
|
1887
|
+
registerThalovantTool(server, "thalovant_install_runtime_group_skill", {
|
|
1888
|
+
title: "Install Runtime Group Skill",
|
|
1889
|
+
description: "Install (or re-install) a skill in a Thalovant runtime group. Installing a skill that is already present updates the existing entry rather than failing. Discover skillId with thalovant_list_marketplace_skills, then confirm installable/purchase_required with thalovant_list_runtime_group_marketplace before calling this. Requires the hubs:write scope and a paid plan (402 'API access requires a paid plan'); a paid marketplace skill ALSO needs marketplace access on the tenant plan, which fails with a second, distinct 402 about paid marketplace access.",
|
|
1890
|
+
inputSchema: {
|
|
1891
|
+
...controlPlaneSchema,
|
|
1892
|
+
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
1893
|
+
skillId: z
|
|
1894
|
+
.string()
|
|
1895
|
+
.min(1)
|
|
1896
|
+
.max(191)
|
|
1897
|
+
.describe("Skill id from the catalog, 1-191 characters. Sent as skill_id in the request BODY on install (it is a path segment only on uninstall). For a catalog install the API persists the resolved catalog id, which may differ from what you send."),
|
|
1898
|
+
marketplaceSkillId: z.string().uuid().optional().describe("Catalog entry UUID, to disambiguate when the skill id alone is ambiguous."),
|
|
1899
|
+
sourceType: z
|
|
1900
|
+
.string()
|
|
1901
|
+
.min(1)
|
|
1902
|
+
.max(32)
|
|
1903
|
+
.optional()
|
|
1904
|
+
.describe('Install source, 1-32 characters, defaulting to "catalog". The API accepts any string here rather than a fixed enum, but only "catalog" (requires the skill to exist in the marketplace catalog) and "git" (requires sourceRef) get special handling.'),
|
|
1905
|
+
sourceRef: z.string().min(1).max(255).optional().describe("Max 255 characters. Required for git installs — a git install without a valid repository URL fails 422."),
|
|
1906
|
+
versionPin: z.string().min(1).max(64).optional().describe("Pin the skill to an exact version. Max 64 characters."),
|
|
1907
|
+
active: z
|
|
1908
|
+
.boolean()
|
|
1909
|
+
.optional()
|
|
1910
|
+
.describe("Whether the skill is enabled after install. Defaults to true. Installing with false also adds the skill to the group's blacklist."),
|
|
1911
|
+
},
|
|
1912
|
+
annotations: {
|
|
1913
|
+
readOnlyHint: false,
|
|
1914
|
+
destructiveHint: false,
|
|
1915
|
+
idempotentHint: true,
|
|
1916
|
+
openWorldHint: true,
|
|
1917
|
+
},
|
|
1918
|
+
}, async ({ runtimeGroupId, skillId, marketplaceSkillId, sourceType, sourceRef, versionPin, active, ...auth }) => {
|
|
1919
|
+
const api = await createControlPlane(auth);
|
|
1920
|
+
ensureAuthenticated(api);
|
|
1921
|
+
const options = { marketplaceSkillId, sourceType, sourceRef, versionPin, active };
|
|
1922
|
+
return jsonContent(redactSecrets(await callControlPlane("skillInstall", () => api.installRuntimeGroupSkill(runtimeGroupId, skillId, options))));
|
|
1923
|
+
});
|
|
1924
|
+
registerThalovantTool(server, "thalovant_uninstall_runtime_group_skill", {
|
|
1925
|
+
title: "Uninstall Runtime Group Skill",
|
|
1926
|
+
description: "Remove one skill from a Thalovant runtime group. This removes only the named skill; the runtime group and its other skills are untouched. Requires the hubs:write scope and a paid plan.",
|
|
1927
|
+
inputSchema: {
|
|
1928
|
+
...controlPlaneSchema,
|
|
1929
|
+
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
1930
|
+
skillId: z.string().min(1).describe("Skill id to remove."),
|
|
1931
|
+
},
|
|
1932
|
+
annotations: {
|
|
1933
|
+
readOnlyHint: false,
|
|
1934
|
+
destructiveHint: true,
|
|
1935
|
+
idempotentHint: true,
|
|
1936
|
+
openWorldHint: true,
|
|
1937
|
+
},
|
|
1938
|
+
}, async ({ runtimeGroupId, skillId, ...auth }) => {
|
|
1939
|
+
const api = await createControlPlane(auth);
|
|
1940
|
+
ensureAuthenticated(api);
|
|
1941
|
+
await callControlPlane("write", () => api.uninstallRuntimeGroupSkill(runtimeGroupId, skillId));
|
|
1942
|
+
return textContent("Runtime group skill uninstalled.");
|
|
1943
|
+
});
|
|
1944
|
+
if (destructiveToolsEnabled()) {
|
|
1945
|
+
registerThalovantTool(server, "thalovant_delete_hub", {
|
|
1946
|
+
title: "Delete Hub",
|
|
1947
|
+
description: "Permanently delete a Thalovant hub along with its dependent clients and ACLs. This cannot be undone. Requires the hub's current etag, read from the `etag` field in the hub resource BODY returned by thalovant_get_hub — the API sends no ETag response header — and sent as If-Match; a missing or stale etag fails 412 and deletes nothing. Requires the hubs:write scope and a paid plan. This tool is disabled unless the operator sets THALOVANT_ENABLE_DESTRUCTIVE_TOOLS.",
|
|
1948
|
+
inputSchema: {
|
|
1949
|
+
...controlPlaneSchema,
|
|
1950
|
+
hubId: z.string().min(1).describe("Hub UUID. This authenticated route rejects slugs."),
|
|
1951
|
+
etag: hubEtagSchema,
|
|
1952
|
+
},
|
|
1953
|
+
annotations: {
|
|
1954
|
+
readOnlyHint: false,
|
|
1955
|
+
destructiveHint: true,
|
|
1956
|
+
idempotentHint: false,
|
|
1957
|
+
openWorldHint: true,
|
|
1958
|
+
},
|
|
1959
|
+
}, async ({ hubId, etag, ...auth }) => {
|
|
1960
|
+
const api = await createControlPlane(auth);
|
|
1961
|
+
ensureAuthenticated(api);
|
|
1962
|
+
await callControlPlane("hubWrite", () => api.deleteHub(hubId, { etag }));
|
|
1963
|
+
return textContent("Hub deleted.");
|
|
1964
|
+
});
|
|
1965
|
+
registerThalovantTool(server, "thalovant_delete_runtime_group", {
|
|
1966
|
+
title: "Delete Runtime Group",
|
|
1967
|
+
description: "Permanently delete a Thalovant runtime group. This cannot be undone. The API answers HTTP 409 for the workspace default group and for a group that still has hubs attached — move or delete those hubs first. This route takes no etag. Requires the hubs:write scope and a paid plan. This tool is disabled unless the operator sets THALOVANT_ENABLE_DESTRUCTIVE_TOOLS.",
|
|
1968
|
+
inputSchema: {
|
|
1969
|
+
...controlPlaneSchema,
|
|
1970
|
+
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
1971
|
+
},
|
|
1972
|
+
annotations: {
|
|
1973
|
+
readOnlyHint: false,
|
|
1974
|
+
destructiveHint: true,
|
|
1975
|
+
idempotentHint: false,
|
|
1976
|
+
openWorldHint: true,
|
|
1977
|
+
},
|
|
1978
|
+
}, async ({ runtimeGroupId, ...auth }) => {
|
|
1979
|
+
const api = await createControlPlane(auth);
|
|
1980
|
+
ensureAuthenticated(api);
|
|
1981
|
+
await callControlPlane("write", () => api.deleteRuntimeGroup(runtimeGroupId));
|
|
1982
|
+
return textContent("Runtime group deleted.");
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1415
1985
|
return server;
|
|
1416
1986
|
}
|
|
1417
1987
|
export async function startHttpServer(config = getHttpConfig()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thalovant/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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.2.
|
|
67
|
+
"@thalovant/sdk": "^0.2.28",
|
|
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.
|
|
10
|
+
"version": "0.1.10",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@thalovant/mcp-server",
|
|
15
|
-
"version": "0.1.
|
|
15
|
+
"version": "0.1.10",
|
|
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.10",
|
|
23
23
|
"transport": {
|
|
24
24
|
"type": "streamable-http",
|
|
25
25
|
"url": "http://127.0.0.1:3000/mcp"
|