@thalovant/mcp-server 0.5.3 → 0.6.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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 — 2026-09-28
4
+
5
+ Link Home Assistant, or any home controller, to a hub from an agent: sign in on the device, create a connection of the `home_assistant` kind, and wait for the hub to admit it. Existing tool schemas are unchanged; every addition is optional.
6
+
7
+ - **Device sign-in as two tools.** `thalovant_begin_device_login` answers the code and URL to show a person with an opaque `loginId` (an empty `scopes` list is accepted and left out, so the API's default applies); `thalovant_poll_device_login` asks once and answers `pending` (with the interval, lengthened for good after a `slow_down`), `approved`, `expired` or `denied`. The device code and the minted token stay on the server, bound to the principal that began the sign-in, and never reach the model. With no credential configured, every control-plane tool for that principal then uses the token. `thalovant_revoke_device_login` signs out; a token already revoked counts as revoked, and signing out again answers `alreadyRevoked` without asking the API. A sign-in reaches only the configured API origin, because its verification URL is shown to a person.
8
+ - **Connections of a kind.** `thalovant_create_client_identity` takes `connectionType`; the API must answer with the same kind, and a connection it made without it is deleted again before the call fails. The result carries `clientId`, `connectionType` and `operationId`. A refusal adds guidance by kind after the API's own text: a kind the API does not know yet, a plan that does not allow it, a hub already linked (naming the connection that holds the link), or a token to sign in again.
9
+ - **Admission.** `thalovant_wait_for_admission` (read-only) follows the create's operation for the ninety seconds a hub takes and answers `admitted`; `failed`, with the operation's `errorCode` for a failure on the platform or the API's `status`, `code` and `detail` when it refused the wait itself; or `timeout` as an outcome to wait on again. A 429 is ridden out for the time the API names (its body, else `Retry-After`, else `RateLimit-Reset`), and one asking for longer than is left is a timeout at once. A token the API refuses and an API out of reach are tool errors of their own, each with a hint. It honours MCP cancellation and never follows a link to another origin than the API's.
10
+ - **`thalovant_delete_client`**, opt-in with the other destructive tools: `If-Match`, reading the etag first when none is given, one retry on `412`, and a connection already gone counts as deleted.
11
+ - The catalog has 50 default tools (24 in read-only mode, 53 with destructive tools enabled). `thalovant_config_status` reports `deviceLoginSignedIn`.
12
+ - Takes `@thalovant/sdk` ^0.9.0, which carries the device sign-in, the connection kinds and admission; the lock resolves 0.9.0.
13
+ - The parity contract declares `device-login`, `connection-kinds` and `connection-admission` as required and `home-link` as not applicable: a tool call holds a hub connection only for its own length, and nothing could answer a hub's request. The three vector files run through the SDK that resolves here, recorded before asserting, and through the tools over stdio, recorded under the same names; all 46 cases digest as the reference's (reference `2460eb90bd56`). Each worker of the recorder now writes only a shard of its own, and a Vitest global setup (`vitest.config.ts`) merges the shards once after every worker has finished: merging inside the workers let two of them stage the same file and rename each other's away, and a merge that scanned early could land after a complete one.
14
+ - The Dockerfile's version label had stayed at 0.4.0; it now says 0.6.0 like the rest.
15
+
3
16
  ## 0.5.3 — 2026-09-26
4
17
 
5
18
  - A tool error says what the API said, not only the SDK's one-line message. That line is cut at 160 characters, and a `platform_image_required` refusal names every image each refused key may be instead -- longer than that -- so the list a model needed to retry was the part cut off, and `refused_images`, `allowed_images` and `allowed_repositories` never reached it at all. The error text now goes on with `code:`, `detail:` (the whole sentence, when the first line had to shorten it) and `fields:`, every other member of the body as compact, redacted JSON. A value a validation error echoes back from the request is redacted there and never reaches the first line.
package/README.md CHANGED
@@ -43,6 +43,17 @@ The server selects control-plane auth in this order:
43
43
 
44
44
  When a token is set, the server never calls the login endpoint. `thalovant_config_status` reports the active mode as `controlPlaneAuthMode` without revealing token values.
45
45
 
46
+ With none of these configured, a person can sign the server in through the browser instead: see [Device Sign-In](#device-sign-in). The token that sign-in mints is used only when nothing above is configured.
47
+
48
+ ### Device Sign-In
49
+
50
+ A tool call cannot wait for a person, so the device flow (RFC 8628) is two tools:
51
+
52
+ 1. `thalovant_begin_device_login` with optional `scopes` (an empty list, like none, asks for the API's default) and `clientName` answers `loginId`, `userCode`, `verificationUri`, `verificationUriComplete`, `interval` and `expiresIn`. Show the person the URL and the code.
53
+ 2. `thalovant_poll_device_login` with that `loginId` asks once. `outcome` is `pending` (poll again after `interval` seconds, already five seconds longer for good if the API asked to slow down), `approved` (with `tokenId`, `scopes` and `expiresAt`), `expired` or `denied`.
54
+
55
+ The device code and the token never reach the model: the server keeps both, bound to the principal that began the sign-in, for the life of the process. Once approved, every control-plane tool for that principal uses the token when no credential is configured. `thalovant_revoke_device_login` signs out: it revokes that token (a token may always revoke itself, and one already revoked counts as revoked) and forgets it; signing out again answers `alreadyRevoked` without a request, and configured tokens are never touched. A sign-in reaches only the configured API origin (`THALOVANT_API_URL`, or the default), since the verification URL it answers with is shown to a person. A Home Assistant link asks for `hubs:read`, `clients:read` and `clients:write`, which is also all a Free plan can approve.
56
+
46
57
  ### API Tokens (Recommended For AI Agents And CI)
47
58
 
48
59
  Scoped API tokens are the right credential for AI and automation use: they are minted from the Thalovant dashboard (or through the device flow), carry only the scopes you grant, can be revoked individually, and never involve your account password or MFA. Tokens start with `tvpat_`.
@@ -61,7 +72,7 @@ Minimum scopes for the full control-plane tool surface:
61
72
  | `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` |
62
73
  | `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` |
63
74
  | `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 |
64
- | `clients:write` | `thalovant_create_client_identity` (`POST /v1/clients`) |
75
+ | `clients:write` | `thalovant_create_client_identity` (`POST /v1/clients`) and the opt-in `thalovant_delete_client` |
65
76
  | `memory:read` | `thalovant_list_memory_items`, `thalovant_get_memory_summary`, `thalovant_get_memory_item` |
66
77
  | `memory:write` | `thalovant_create_memory_item`, `thalovant_update_memory_item`, `thalovant_delete_memory_item` |
67
78
 
@@ -112,6 +123,18 @@ URL spellings and paths on the same origin are allowed; custom origins must be
112
123
  configured alongside their credentials. Anonymous public discovery may still
113
124
  select a custom API URL.
114
125
 
126
+ ## Link Home Assistant
127
+
128
+ A home controller such as Home Assistant links to a hub with a connection of its own kind:
129
+
130
+ 1. Sign in, when no token is configured: [Device Sign-In](#device-sign-in).
131
+ 2. `thalovant_create_client_identity` with `connectionType: "home_assistant"`. The API must answer with the same kind; one that made an ordinary connection instead has it deleted again and the call fails. The result carries `clientId`, `connectionType` and `operationId`.
132
+ 3. `thalovant_wait_for_admission` with that `operationId`. A hub admits a new connection in about ninety seconds and refuses it until then. `outcome` is `admitted`, `failed` or `timeout`. A failure on the platform carries the operation's `errorCode` (and `status` null); the API refusing the wait itself carries its `status`, `code` and `detail`. A `timeout` means the connection may still be admitted: call the tool again rather than creating another connection. A 429 is waited out for the time the API names (in its body, else `Retry-After`, else `RateLimit-Reset`); one asking for longer than is left is a `timeout` at once. A token the API refuses (401, 403) and an API out of reach are tool errors of their own, each with a hint, never a failed admission. It reads only, honours cancellation, and never follows a `links.self` to another origin than the API's.
133
+
134
+ Each refusal of step 2 names what to do next after the API's own text (see [Tool Errors](#tool-errors)): a kind the API does not know yet, a plan that does not allow the connection, a hub that already holds its one Home Assistant link (with the `client_id` holding it), or a token the API refused (sign in again). `thalovant_delete_client` removes a connection (`If-Match`, reading the etag first when none is given, retrying once on `412`, and counting a connection already gone as deleted); it is a destructive tool the operator enables.
135
+
136
+ Answering the hub's requests afterwards is the integration's own long-lived job, on the data plane; an MCP tool call holds a hub connection only for its own length, so that part of the link is not a tool.
137
+
115
138
  ## Local Stdio
116
139
 
117
140
  The server speaks MCP over stdio and does not write logs to stdout.
@@ -325,6 +348,7 @@ Read-only:
325
348
  - `thalovant_list_hubs`
326
349
  - `thalovant_get_hub`
327
350
  - `thalovant_get_operation`
351
+ - `thalovant_wait_for_admission`
328
352
  - `thalovant_identity_status`
329
353
  - `thalovant_healthcheck`
330
354
  - `thalovant_intent_inventory`
@@ -344,6 +368,12 @@ Skill and runtime-group discovery (read-only):
344
368
  - `thalovant_get_runtime_group_config`
345
369
  - `thalovant_get_hub_runtime_capabilities`
346
370
 
371
+ Sign-in (see [Device Sign-In](#device-sign-in)):
372
+
373
+ - `thalovant_begin_device_login`
374
+ - `thalovant_poll_device_login`
375
+ - `thalovant_revoke_device_login`
376
+
347
377
  Writes or hub events:
348
378
 
349
379
  - `thalovant_create_client_identity`
@@ -382,6 +412,7 @@ Destructive, **not registered unless explicitly enabled** (see [Destructive Tool
382
412
 
383
413
  - `thalovant_delete_hub`
384
414
  - `thalovant_delete_runtime_group`
415
+ - `thalovant_delete_client`
385
416
 
386
417
  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`.
387
418
 
@@ -457,9 +488,9 @@ or use an SDK that preserves large JSON integers.
457
488
 
458
489
  ## Destructive Tools
459
490
 
460
- `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.
491
+ `thalovant_delete_hub`, `thalovant_delete_runtime_group` and `thalovant_delete_client` 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.
461
492
 
462
- 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:
493
+ 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 are opt-in:
463
494
 
464
495
  ```bash
465
496
  export THALOVANT_ENABLE_DESTRUCTIVE_TOOLS="true"
@@ -477,7 +508,7 @@ export MCP_TOOL_DENYLIST="thalovant_delete_hub,thalovant_delete_runtime_group"
477
508
 
478
509
  The global deny applies to every principal and cannot be overridden by a principal's `allowedTools`. To restrict only selected principals, leave these tools out of the global denylist and use those principals' `deniedTools` instead.
479
510
 
480
- 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.
511
+ 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. Deleting a client cuts off the device or link using it; its etag is read first when none is given.
481
512
 
482
513
  ## Non-Catalog Skill Sources
483
514
 
@@ -491,7 +522,7 @@ With the flag unset, a call with any `sourceType` other than `catalog` fails bef
491
522
 
492
523
  ## Read-Only Mode
493
524
 
494
- Set `THALOVANT_MCP_READONLY=1` to register only tools annotated `readOnlyHint: true`. Write and destructive tools are then never registered and never appear in `tools/list`, so an operator can run an observe-only agent without hand-writing a denylist. Like the other registration-time gates it is read when a server instance is created; `thalovant_config_status` reports the state as `readOnly`.
525
+ Set `THALOVANT_MCP_READONLY=1` to register only tools annotated `readOnlyHint: true` (the sign-in tools are not among them; `thalovant_wait_for_admission` is). Write and destructive tools are then never registered and never appear in `tools/list`, so an operator can run an observe-only agent without hand-writing a denylist. Like the other registration-time gates it is read when a server instance is created; `thalovant_config_status` reports the state as `readOnly`.
495
526
 
496
527
  ## HiveMind Runtime Compatibility
497
528
 
@@ -499,7 +530,7 @@ Runtime calls sharing the same hub client identity run sequentially within one
499
530
  MCP process. Use a distinct client identity for each independently running MCP
500
531
  server so the hub can keep their sessions separate.
501
532
 
502
- Version 0.5.1 requires `@thalovant/sdk` `^0.7.1` (0.7.1 through versions below 0.8.0). This release enforces
533
+ Version 0.6.0 requires `@thalovant/sdk` `^0.9.0` (0.9.0 through versions below 0.10.0). Since 0.5.1 the server enforces
503
534
  secure effective MQTT URLs and carries a single connection deadline through
504
535
  MQTT setup and HTTP failure cleanup. Runtime tools support
505
536
  HiveMind v3 Noise over WSS, HTTPS and MQTT over TLS. `thalovant_healthcheck`
@@ -577,7 +608,7 @@ one tool for every SDK conversation or event-listener method.
577
608
 
578
609
  ### Shared-runtime skill management
579
610
 
580
- The catalog has 46 default tools, 23 in read-only mode, and 48 with destructive tools enabled.
611
+ The catalog has 50 default tools, 24 in read-only mode, and 53 with destructive tools enabled.
581
612
 
582
613
  Hub-addressed skill methods select the runtime group attached to the hub UUID.
583
614
  Every hub sharing that group sees the same skill changes and history. The API
@@ -0,0 +1,57 @@
1
+ import type { DeviceAuthorization, ThalovantControlPlane } from "@thalovant/sdk";
2
+ /**
3
+ * Device sign-ins, held by the server between the tool calls that run them.
4
+ *
5
+ * A tool call cannot wait for a person to approve a sign-in in a browser, so
6
+ * the device flow is two tools: one starts it and one polls it. What sits
7
+ * between them stays here, in this process, and never reaches the model: the
8
+ * device code (the secret half of the grant) behind an opaque login id, and
9
+ * the token an approved sign-in minted. Both belong to the principal that
10
+ * started the sign-in; another principal can neither poll it nor use its token.
11
+ */
12
+ /** A sign-in started and not finished. */
13
+ interface PendingLogin {
14
+ readonly principal: string;
15
+ readonly api: ThalovantControlPlane;
16
+ readonly authorization: DeviceAuthorization;
17
+ readonly expiresAt: number;
18
+ }
19
+ /** The token an approved sign-in minted, on the control plane that holds it. */
20
+ export interface SignedIn {
21
+ readonly api: ThalovantControlPlane;
22
+ readonly origin: string;
23
+ }
24
+ export declare class DeviceLogins {
25
+ private readonly now;
26
+ private readonly pending;
27
+ private readonly signedIn;
28
+ /** The id of the token each principal last revoked, until it signs in again. */
29
+ private readonly revoked;
30
+ constructor(now?: () => number);
31
+ /**
32
+ * Keep a started sign-in for `principal`; returns the id its polls name it by.
33
+ *
34
+ * A principal holds at most a few at once, and starting another drops only
35
+ * its own oldest. Throws when the server as a whole holds as many as it will.
36
+ */
37
+ start(principal: string, api: ThalovantControlPlane, authorization: DeviceAuthorization): string;
38
+ /** The pending sign-in `loginId` names, if `principal` started it and it has not expired. */
39
+ find(loginId: string, principal: string): PendingLogin | undefined;
40
+ /** Forget a sign-in that ended: approved, expired or denied. */
41
+ finish(loginId: string): void;
42
+ /** Keep the token an approved sign-in minted, as `principal`'s. */
43
+ approve(principal: string, api: ThalovantControlPlane): void;
44
+ /** The token `principal` signed in with on `origin`, if there is one. */
45
+ signedInOn(principal: string, origin: string): SignedIn | undefined;
46
+ /** The token `principal` signed in with, wherever. */
47
+ signedInAs(principal: string): SignedIn | undefined;
48
+ /** Forget `principal`'s token, remembering its id as revoked. */
49
+ signOut(principal: string, tokenId?: string | null): void;
50
+ /**
51
+ * The id of the token `principal` signed out of, when it has signed out and
52
+ * not signed in since; undefined when it never signed out.
53
+ */
54
+ revokedOf(principal: string): string | null | undefined;
55
+ private prune;
56
+ }
57
+ export {};
@@ -0,0 +1,87 @@
1
+ import { randomUUID } from "node:crypto";
2
+ /** Sign-ins one principal may have pending at once; its own oldest is dropped first. */
3
+ const MAX_PENDING_PER_PRINCIPAL = 4;
4
+ /**
5
+ * Sign-ins pending at once across every principal. Past it a new sign-in is
6
+ * refused rather than evicting somebody else's: dropping another principal's
7
+ * sign-in would let one caller keep everyone else from ever signing in.
8
+ */
9
+ const MAX_PENDING_LOGINS = 256;
10
+ export class DeviceLogins {
11
+ now;
12
+ pending = new Map();
13
+ signedIn = new Map();
14
+ /** The id of the token each principal last revoked, until it signs in again. */
15
+ revoked = new Map();
16
+ constructor(now = () => Date.now()) {
17
+ this.now = now;
18
+ }
19
+ /**
20
+ * Keep a started sign-in for `principal`; returns the id its polls name it by.
21
+ *
22
+ * A principal holds at most a few at once, and starting another drops only
23
+ * its own oldest. Throws when the server as a whole holds as many as it will.
24
+ */
25
+ start(principal, api, authorization) {
26
+ this.prune();
27
+ const own = [...this.pending].filter(([, login]) => login.principal === principal);
28
+ for (const [loginId] of own.slice(0, Math.max(0, own.length - MAX_PENDING_PER_PRINCIPAL + 1))) {
29
+ this.pending.delete(loginId);
30
+ }
31
+ if (this.pending.size >= MAX_PENDING_LOGINS) {
32
+ throw new Error("Too many device sign-ins are in progress on this server. Try again once some have finished or expired.");
33
+ }
34
+ const loginId = randomUUID();
35
+ this.pending.set(loginId, {
36
+ principal,
37
+ api,
38
+ authorization,
39
+ expiresAt: this.now() + authorization.expiresIn * 1000,
40
+ });
41
+ return loginId;
42
+ }
43
+ /** The pending sign-in `loginId` names, if `principal` started it and it has not expired. */
44
+ find(loginId, principal) {
45
+ this.prune();
46
+ const login = this.pending.get(loginId);
47
+ return login && login.principal === principal ? login : undefined;
48
+ }
49
+ /** Forget a sign-in that ended: approved, expired or denied. */
50
+ finish(loginId) {
51
+ this.pending.delete(loginId);
52
+ }
53
+ /** Keep the token an approved sign-in minted, as `principal`'s. */
54
+ approve(principal, api) {
55
+ this.signedIn.set(principal, { api, origin: new URL(api.apiUrl).origin });
56
+ this.revoked.delete(principal);
57
+ }
58
+ /** The token `principal` signed in with on `origin`, if there is one. */
59
+ signedInOn(principal, origin) {
60
+ const signedIn = this.signedIn.get(principal);
61
+ return signedIn && signedIn.origin === origin && signedIn.api.accessToken ? signedIn : undefined;
62
+ }
63
+ /** The token `principal` signed in with, wherever. */
64
+ signedInAs(principal) {
65
+ const signedIn = this.signedIn.get(principal);
66
+ return signedIn?.api.accessToken ? signedIn : undefined;
67
+ }
68
+ /** Forget `principal`'s token, remembering its id as revoked. */
69
+ signOut(principal, tokenId = null) {
70
+ this.signedIn.delete(principal);
71
+ this.revoked.set(principal, tokenId);
72
+ }
73
+ /**
74
+ * The id of the token `principal` signed out of, when it has signed out and
75
+ * not signed in since; undefined when it never signed out.
76
+ */
77
+ revokedOf(principal) {
78
+ return this.revoked.has(principal) ? this.revoked.get(principal) : undefined;
79
+ }
80
+ prune() {
81
+ const now = this.now();
82
+ for (const [loginId, login] of this.pending) {
83
+ if (login.expiresAt <= now)
84
+ this.pending.delete(loginId);
85
+ }
86
+ }
87
+ }
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { apiErrorText } from "./api-error-output.js";
3
+ import { DeviceLogins } from "./device-login.js";
3
4
  import { inventoryPresentation } from "./inventory-output.js";
4
5
  import { runtimeReplyContent } from "./reply-output.js";
5
6
  import { throwIfRuntimeCancelled, withRuntimeLease } from "./runtime-lease.js";
@@ -7,7 +8,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
9
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
10
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
10
- import { DEFAULT_CONTROL_API_URL, ThalovantApiError, ThalovantClient, ThalovantControlPlane, ThalovantIdentity, buildClientContext, buildLocation, } from "@thalovant/sdk";
11
+ import { CONNECTION_TYPE_HOME_ASSISTANT, DEFAULT_CONTROL_API_URL, HOME_ASSISTANT_SCOPES, ThalovantAdmissionFailedError, ThalovantAdmissionTimeoutError, ThalovantAlreadyLinkedError, ThalovantApiError, ThalovantApiUnreachableError, ThalovantAuthError, ThalovantClient, ThalovantControlPlane, ThalovantDeviceLoginDeniedError, ThalovantDeviceLoginExpiredError, ThalovantDeviceLoginPendingError, ThalovantIdentity, ThalovantPlanError, ThalovantUnsupportedConnectionTypeError, buildClientContext, buildLocation, } from "@thalovant/sdk";
11
12
  import { AsyncLocalStorage } from "node:async_hooks";
12
13
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
13
14
  import { createServer as createHttpServer } from "node:http";
@@ -61,6 +62,10 @@ const SECRET_KEYS = [
61
62
  "secret",
62
63
  ];
63
64
  const protocolSchema = z.enum(["wss", "https", "mqtt"]);
65
+ const connectionTypeSchema = z
66
+ .string()
67
+ .regex(/^[a-z][a-z0-9_]{0,63}$/)
68
+ .describe(`The kind of connection, sent as spec.connection_type: voice_satellite, web_chat, developer, embedded, or ${CONNECTION_TYPE_HOME_ASSISTANT} for a Home Assistant link (one per hub). The kind decides what the connection may send and receive.`);
64
69
  const limitSchema = z.number().int().min(1).max(MAX_LIMIT).default(25).describe("Page size, 1-100.");
65
70
  const publicHubsLimitSchema = z
66
71
  .number()
@@ -124,7 +129,14 @@ const runtimeAuthSchema = {
124
129
  * they are absent from tools/list entirely rather than merely failing when
125
130
  * called. See destructiveToolsEnabled().
126
131
  */
127
- const DESTRUCTIVE_TOOLS = ["thalovant_delete_hub", "thalovant_delete_runtime_group"];
132
+ const DESTRUCTIVE_TOOLS = ["thalovant_delete_hub", "thalovant_delete_runtime_group", "thalovant_delete_client"];
133
+ /**
134
+ * Device sign-ins in progress, and the tokens approved ones minted, per
135
+ * principal, for the life of this process. See device-login.ts.
136
+ */
137
+ const deviceLogins = new DeviceLogins();
138
+ /** How long thalovant_wait_for_admission waits by default: a hub admits a new connection in about ninety seconds. */
139
+ const ADMISSION_DEFAULT_TIMEOUT_MS = 120_000;
128
140
  /**
129
141
  * Destructive control-plane tools are opt-in.
130
142
  *
@@ -247,6 +259,40 @@ function controlPlaneErrorHint(profile, status, body, code) {
247
259
  return undefined;
248
260
  }
249
261
  }
262
+ /**
263
+ * Guidance for a refused connection create or delete, by the SDK's class for
264
+ * the refusal. Undefined when the class says nothing a hint could add to.
265
+ */
266
+ function connectionErrorHint(error, connectionType) {
267
+ if (error instanceof ThalovantUnsupportedConnectionTypeError) {
268
+ return error.statusCode === undefined
269
+ ? `The API made an ordinary connection instead of a ${JSON.stringify(connectionType)} one, so the server deleted it again (the message says if that failed). This API does not support the kind yet; retrying will not change that.`
270
+ : `The API does not know the connection type ${JSON.stringify(connectionType)} yet (HTTP 422). Retrying will not change that; omit connectionType for an ordinary connection.`;
271
+ }
272
+ if (error instanceof ThalovantAlreadyLinkedError) {
273
+ return `A hub takes one Home Assistant connection, and this one already has it${error.clientId ? `: client ${error.clientId}` : ""}. Use that connection, or delete it with thalovant_delete_client (a destructive tool the operator enables) and create the link again.`;
274
+ }
275
+ if (error instanceof ThalovantPlanError) {
276
+ return error.code === "plan_limit"
277
+ ? "The plan's connections are used up (see resource, used and limit in the fields above). Delete one with thalovant_delete_client, or move the workspace to a larger plan; granting a scope will not help."
278
+ : "The plan does not allow this connection (HTTP 402). Move the workspace to a paid plan; granting a scope will not help.";
279
+ }
280
+ if (error instanceof ThalovantAuthError) {
281
+ return "The API refused the token itself: it is unknown, expired or revoked (401), the account is locked (423), or it lacks clients:write (403 Insufficient scopes). Sign in again with thalovant_begin_device_login, or configure a token that carries clients:read and clients:write.";
282
+ }
283
+ if (error instanceof ThalovantApiUnreachableError) {
284
+ return "The Thalovant API could not be reached, which says nothing about the connection: it may be admitted, or created, all the same. Try again once the API answers; do not create another connection before checking.";
285
+ }
286
+ return undefined;
287
+ }
288
+ /** Rethrow a refused connection call with its guidance, when it has any. */
289
+ function withConnectionHint(error, connectionType) {
290
+ const hint = connectionErrorHint(error, connectionType);
291
+ if (!hint)
292
+ throw error;
293
+ const said = apiErrorText(error, redactSecrets) ?? (error instanceof Error ? error.message : String(error));
294
+ throw new Error(`${said}\n\n${hint}`, { cause: error });
295
+ }
250
296
  /**
251
297
  * Run a control-plane call and, on failure, append actionable guidance.
252
298
  *
@@ -627,17 +673,38 @@ function redactSecrets(value) {
627
673
  }
628
674
  return output;
629
675
  }
676
+ /** The control-plane URL this principal's credentials belong to. */
677
+ async function configuredControlApiUrl(principal = currentPrincipal()) {
678
+ const credential = await credentialForPrincipal(principal);
679
+ const canUseShared = allowSharedThalovantCredentials(principal);
680
+ return credential?.control?.apiUrl ?? (canUseShared ? process.env.THALOVANT_API_URL : undefined) ?? DEFAULT_CONTROL_API_URL;
681
+ }
682
+ function originOf(url) {
683
+ try {
684
+ return new URL(url).origin;
685
+ }
686
+ catch {
687
+ return undefined;
688
+ }
689
+ }
630
690
  /** Keep server-managed credentials on their configured control-plane origin. */
631
691
  async function createControlPlane(options) {
632
692
  const principal = currentPrincipal();
633
693
  const credential = await credentialForPrincipal(principal);
634
694
  const canUseShared = allowSharedThalovantCredentials(principal);
635
- const configuredApiUrl = credential?.control?.apiUrl ?? (canUseShared ? process.env.THALOVANT_API_URL : undefined) ?? DEFAULT_CONTROL_API_URL;
695
+ const configuredApiUrl = await configuredControlApiUrl(principal);
636
696
  const apiUrl = options.apiUrl ?? configuredApiUrl;
637
- const accessToken = credential?.control?.accessToken ??
638
- (canUseShared ? process.env.THALOVANT_API_TOKEN ?? process.env.THALOVANT_ACCESS_TOKEN : undefined);
639
697
  const email = canUseShared ? process.env.THALOVANT_EMAIL : undefined;
640
698
  const password = canUseShared ? process.env.THALOVANT_PASSWORD : undefined;
699
+ const configuredToken = credential?.control?.accessToken ??
700
+ (canUseShared ? process.env.THALOVANT_API_TOKEN ?? process.env.THALOVANT_ACCESS_TOKEN : undefined);
701
+ // With nothing configured, the token this principal signed in with through
702
+ // thalovant_begin_device_login / thalovant_poll_device_login, which exists
703
+ // only for the configured origin.
704
+ const signedIn = configuredToken || (email && password)
705
+ ? undefined
706
+ : deviceLogins.signedInOn(principal.id, originOf(configuredApiUrl) ?? "");
707
+ const accessToken = configuredToken ?? signedIn?.api.accessToken;
641
708
  if (accessToken || (email && password)) {
642
709
  let endpoint;
643
710
  let configuredEndpoint;
@@ -672,6 +739,24 @@ function ensureAuthenticated(api) {
672
739
  throw new Error("This tool requires Thalovant API auth. Configure THALOVANT_API_TOKEN (recommended: a scoped, revocable API token), THALOVANT_ACCESS_TOKEN, THALOVANT_EMAIL/THALOVANT_PASSWORD, or per-principal Thalovant credentials for this MCP principal.");
673
740
  }
674
741
  }
742
+ /**
743
+ * A control plane for a device sign-in, on the configured API origin only.
744
+ *
745
+ * It carries no credential: the sign-in is what mints one. The origin is fixed
746
+ * because the verification URL the API answers with is shown to a person, and
747
+ * the token it mints is kept here for later calls; a tool argument must not
748
+ * send either somewhere else.
749
+ */
750
+ async function deviceLoginControlPlane(apiUrl) {
751
+ const configuredApiUrl = await configuredControlApiUrl();
752
+ const configured = originOf(configuredApiUrl);
753
+ if (!configured)
754
+ throw new Error("The configured control-plane API URL is invalid.");
755
+ if (apiUrl !== undefined && originOf(apiUrl) !== configured) {
756
+ throw new Error("apiUrl must use the configured Thalovant API origin for a device sign-in.");
757
+ }
758
+ return new ThalovantControlPlane(apiUrl ?? configuredApiUrl, { userAgent: `thalovant-mcp/${VERSION}` });
759
+ }
675
760
  function controlPlaneAuthModeFromEnv() {
676
761
  if (process.env.THALOVANT_API_TOKEN)
677
762
  return "api-token";
@@ -1172,6 +1257,7 @@ export function createServer() {
1172
1257
  }, async () => jsonContent({
1173
1258
  apiUrl: process.env.THALOVANT_API_URL ?? DEFAULT_CONTROL_API_URL,
1174
1259
  controlPlaneAuthMode: controlPlaneAuthModeFromEnv(),
1260
+ deviceLoginSignedIn: Boolean(deviceLogins.signedInAs(currentPrincipal().id)),
1175
1261
  hasApiToken: Boolean(process.env.THALOVANT_API_TOKEN),
1176
1262
  hasAccessToken: Boolean(process.env.THALOVANT_ACCESS_TOKEN),
1177
1263
  hasEmailPassword: Boolean(process.env.THALOVANT_EMAIL && process.env.THALOVANT_PASSWORD),
@@ -1265,7 +1351,7 @@ export function createServer() {
1265
1351
  });
1266
1352
  registerThalovantTool(server, "thalovant_create_client_identity", {
1267
1353
  title: "Create Client Identity",
1268
- description: "Create a Thalovant client identity for a hub. The identity is secret; output is redacted unless savePath is used.",
1354
+ description: "Create a Thalovant client identity (a hub connection) for a hub. The identity is secret; output is redacted unless savePath is used. Pass connectionType (for example home_assistant) for a connection of that kind: the API must answer with the same kind, or the connection it made instead is deleted and the call fails. A new connection is admitted by its hub in about ninety seconds; follow operationId with thalovant_wait_for_admission before connecting.",
1269
1355
  inputSchema: {
1270
1356
  ...controlPlaneSchema,
1271
1357
  hubId: z
@@ -1279,6 +1365,7 @@ export function createServer() {
1279
1365
  preferredProtocols: z.array(protocolSchema).min(1).default(["wss", "https"]),
1280
1366
  idempotencyKey: z.string().min(1).optional(),
1281
1367
  spec: jsonRecordSchema.optional(),
1368
+ connectionType: connectionTypeSchema.optional(),
1282
1369
  savePath: z
1283
1370
  .string()
1284
1371
  .min(1)
@@ -1291,7 +1378,7 @@ export function createServer() {
1291
1378
  idempotentHint: false,
1292
1379
  openWorldHint: true,
1293
1380
  },
1294
- }, async ({ hubId, name, siteId, ownerId, active, preferredProtocols, idempotencyKey, spec, savePath, ...auth }) => {
1381
+ }, async ({ hubId, name, siteId, ownerId, active, preferredProtocols, idempotencyKey, spec, connectionType, savePath, ...auth }) => {
1295
1382
  // Validate the destination BEFORE creating a cloud identity, so a path
1296
1383
  // that escapes the identity directory is refused without leaving an
1297
1384
  // orphaned credential behind in the control plane.
@@ -1306,19 +1393,180 @@ export function createServer() {
1306
1393
  preferredProtocols,
1307
1394
  idempotencyKey,
1308
1395
  spec,
1309
- });
1396
+ connectionType,
1397
+ }).catch((error) => withConnectionHint(error, connectionType));
1310
1398
  const selectedEndpoint = api.requireRuntimeProtocol(result, preferredProtocols[0]);
1311
1399
  const savedIdentityPath = targetIdentityPath ? await writeIdentityFile(targetIdentityPath, result.identity) : undefined;
1312
1400
  return jsonContent({
1313
1401
  result: redactSecrets(result.asObject({ includeSecrets: false })),
1402
+ clientId: result.clientId ?? null,
1403
+ connectionType: result.connectionType ?? null,
1404
+ operationId: result.operation?.id ?? null,
1314
1405
  selectedEndpoint,
1315
1406
  enabledProtocols: result.identity.enabledProtocols(),
1316
1407
  savedIdentityPath,
1317
1408
  secretNotice: savedIdentityPath
1318
1409
  ? "Full identity was written to the requested local path with mode 0600."
1319
1410
  : "Secret identity fields were not returned. Pass savePath to write a protected local identity file.",
1411
+ next: result.operation
1412
+ ? "The hub admits a new connection in about ninety seconds and refuses it until then. Call thalovant_wait_for_admission with this operationId before connecting."
1413
+ : "The API returned no admission operation, so there is nothing to wait for.",
1414
+ });
1415
+ });
1416
+ registerThalovantTool(server, "thalovant_wait_for_admission", {
1417
+ title: "Wait For Connection Admission",
1418
+ description: "Wait until a hub admits a connection made with thalovant_create_client_identity: about ninety seconds after the create. Follows the operation the create returned (operationId), reading it every pollIntervalMs; a 429 is waited out for the time the API names. outcome is admitted (ready, or an operation the API no longer tracks), failed -- the platform could not admit it (errorCode says why, status is null) or the API refused the wait itself (status, code and detail say what it answered) -- or timeout: timeoutMs passed first and the connection may still be admitted, so call this tool again rather than creating another connection. A token the API refuses (401, 403) or an API out of reach is a tool error of its own, not a failed admission. Omit operationId when the create returned none. Reads only.",
1419
+ inputSchema: {
1420
+ ...controlPlaneSchema,
1421
+ operationId: z
1422
+ .string()
1423
+ .min(1)
1424
+ .max(500)
1425
+ .optional()
1426
+ .describe("operationId from thalovant_create_client_identity: the operation's id or its links.self path. An absolute URL must be on the API's own origin."),
1427
+ timeoutMs: z.number().int().min(1_000).max(MAX_TIMEOUT_MS).default(ADMISSION_DEFAULT_TIMEOUT_MS),
1428
+ pollIntervalMs: z.number().int().min(500).max(10_000).default(2_000).describe("Milliseconds between two reads of the operation."),
1429
+ },
1430
+ annotations: {
1431
+ readOnlyHint: true,
1432
+ openWorldHint: true,
1433
+ },
1434
+ }, async ({ operationId, timeoutMs, pollIntervalMs, ...auth }) => {
1435
+ const api = await createControlPlane(auth);
1436
+ ensureAuthenticated(api);
1437
+ const signal = requestContext.getStore()?.signal;
1438
+ throwIfRuntimeCancelled(signal);
1439
+ try {
1440
+ await api.waitForAdmission(operationId ?? null, { timeoutMs: clampTimeout(timeoutMs), pollIntervalMs, signal });
1441
+ return jsonContent({ outcome: "admitted" });
1442
+ }
1443
+ catch (error) {
1444
+ if (error instanceof ThalovantAdmissionTimeoutError) {
1445
+ return jsonContent({
1446
+ outcome: "timeout",
1447
+ next: "The hub has not admitted the connection yet and may still. Call thalovant_wait_for_admission again with the same operationId; do not create another connection.",
1448
+ });
1449
+ }
1450
+ if (error instanceof ThalovantAdmissionFailedError) {
1451
+ // The API's own words when it refused the wait; null for a failure on the platform.
1452
+ return jsonContent(redactSecrets({
1453
+ outcome: "failed",
1454
+ errorCode: error.errorCode ?? null,
1455
+ status: error.statusCode ?? null,
1456
+ code: error.code ?? null,
1457
+ detail: error.detail ?? null,
1458
+ message: error.message,
1459
+ }));
1460
+ }
1461
+ throwIfRuntimeCancelled(signal);
1462
+ withConnectionHint(error);
1463
+ }
1464
+ });
1465
+ registerThalovantTool(server, "thalovant_begin_device_login", {
1466
+ title: "Begin Device Sign-In",
1467
+ description: "Start a device sign-in (RFC 8628) when no API token is configured: a person approves it in a browser. Show the person verificationUri and userCode (or verificationUriComplete, which carries the code), then call thalovant_poll_device_login with loginId every `interval` seconds. The device code stays on this server. Once approved, control-plane tools for this principal use the token it minted. A Home Assistant link asks for hubs:read, clients:read and clients:write, which is also all a Free plan can approve.",
1468
+ inputSchema: {
1469
+ apiUrl: controlPlaneSchema.apiUrl,
1470
+ scopes: z
1471
+ .array(z.string().min(1).max(64))
1472
+ .max(32)
1473
+ .optional()
1474
+ .describe(`Scopes the token will carry. The API applies its default when omitted or empty (an empty list is left out of the request). A Home Assistant link asks for ${HOME_ASSISTANT_SCOPES.join(", ")}.`),
1475
+ clientName: z.string().min(1).max(128).optional().describe("The name the approval page and the dashboard's token list show."),
1476
+ },
1477
+ annotations: {
1478
+ readOnlyHint: false,
1479
+ destructiveHint: false,
1480
+ idempotentHint: false,
1481
+ openWorldHint: true,
1482
+ },
1483
+ }, async ({ apiUrl, scopes, clientName }) => {
1484
+ const api = await deviceLoginControlPlane(apiUrl);
1485
+ const grant = await api.beginDeviceLogin({ scopes, clientName });
1486
+ const loginId = deviceLogins.start(currentPrincipal().id, api, grant);
1487
+ return jsonContent({
1488
+ loginId,
1489
+ userCode: grant.userCode,
1490
+ verificationUri: grant.verificationUri,
1491
+ verificationUriComplete: grant.verificationUriComplete,
1492
+ interval: grant.interval,
1493
+ expiresIn: grant.expiresIn,
1494
+ next: `Ask the person to open ${grant.verificationUri} and enter ${grant.userCode}, then call thalovant_poll_device_login with this loginId every ${grant.interval} seconds until the outcome is no longer pending. The code expires in ${grant.expiresIn} seconds.`,
1320
1495
  });
1321
1496
  });
1497
+ registerThalovantTool(server, "thalovant_poll_device_login", {
1498
+ title: "Poll Device Sign-In",
1499
+ description: "Ask once whether the person approved a device sign-in started with thalovant_begin_device_login. outcome is pending (poll again after `interval` seconds, already lengthened if the API asked to slow down), approved (control-plane tools for this principal now use the token; tokenId, scopes and expiresAt describe it), expired or denied (begin a new sign-in). The token itself is never returned.",
1500
+ inputSchema: {
1501
+ loginId: z.string().min(1).max(64).describe("loginId from thalovant_begin_device_login."),
1502
+ },
1503
+ annotations: {
1504
+ readOnlyHint: false,
1505
+ destructiveHint: false,
1506
+ idempotentHint: false,
1507
+ openWorldHint: true,
1508
+ },
1509
+ }, async ({ loginId }) => {
1510
+ const principal = currentPrincipal().id;
1511
+ const login = deviceLogins.find(loginId, principal);
1512
+ if (!login) {
1513
+ throw new Error("No device sign-in in progress has this loginId: it expired, already ended, or was started by another principal. Begin a new one with thalovant_begin_device_login.");
1514
+ }
1515
+ try {
1516
+ const token = await login.api.pollDeviceLogin(login.authorization);
1517
+ deviceLogins.finish(loginId);
1518
+ deviceLogins.approve(principal, login.api);
1519
+ return jsonContent({
1520
+ outcome: "approved",
1521
+ tokenType: token.tokenType,
1522
+ scopes: [...token.scopes],
1523
+ expiresAt: token.expiresAt,
1524
+ tokenId: token.tokenId,
1525
+ next: "Signed in. Control-plane tools for this principal now use this token until thalovant_revoke_device_login or a server restart.",
1526
+ });
1527
+ }
1528
+ catch (error) {
1529
+ if (error instanceof ThalovantDeviceLoginPendingError) {
1530
+ return jsonContent({ outcome: "pending", interval: error.interval });
1531
+ }
1532
+ if (error instanceof ThalovantDeviceLoginExpiredError) {
1533
+ deviceLogins.finish(loginId);
1534
+ return jsonContent({ outcome: "expired", status: error.statusCode ?? null, next: "The code expired before anybody approved it. Begin a new sign-in." });
1535
+ }
1536
+ if (error instanceof ThalovantDeviceLoginDeniedError) {
1537
+ deviceLogins.finish(loginId);
1538
+ return jsonContent({ outcome: "denied", status: error.statusCode ?? null, next: "The person declined the sign-in in the browser." });
1539
+ }
1540
+ throw error;
1541
+ }
1542
+ });
1543
+ registerThalovantTool(server, "thalovant_revoke_device_login", {
1544
+ title: "Revoke Device Sign-In",
1545
+ description: "Sign out: revoke the API token this principal signed in with through thalovant_poll_device_login, and stop using it. A token may always revoke itself, and one already revoked counts as revoked; signing out again answers alreadyRevoked without a request. Tokens configured on the server are never touched.",
1546
+ inputSchema: {},
1547
+ annotations: {
1548
+ readOnlyHint: false,
1549
+ destructiveHint: true,
1550
+ idempotentHint: true,
1551
+ openWorldHint: true,
1552
+ },
1553
+ }, async () => {
1554
+ const principal = currentPrincipal().id;
1555
+ const signedIn = deviceLogins.signedInAs(principal);
1556
+ if (!signedIn) {
1557
+ // Signing out twice is signing out once: the token is already gone.
1558
+ const earlier = deviceLogins.revokedOf(principal);
1559
+ if (earlier !== undefined)
1560
+ return jsonContent({ revoked: true, tokenId: earlier, alreadyRevoked: true });
1561
+ throw new Error("This principal has not signed in with a device sign-in, so there is no token of its own to revoke.");
1562
+ }
1563
+ const tokenId = signedIn.api.tokenId ?? null;
1564
+ // A token the API no longer accepts (401 on its own revoke) is already
1565
+ // revoked; the SDK counts that as done.
1566
+ await signedIn.api.revokeApiToken();
1567
+ deviceLogins.signOut(principal, tokenId);
1568
+ return jsonContent({ revoked: true, tokenId });
1569
+ });
1322
1570
  registerThalovantTool(server, "thalovant_identity_status", {
1323
1571
  title: "Identity Status",
1324
1572
  description: "Load a Thalovant identity and return redacted protocol and endpoint status.",
@@ -2294,6 +2542,26 @@ export function createServer() {
2294
2542
  await callControlPlane("hubWrite", () => api.deleteHub(hubId, { etag }));
2295
2543
  return textContent("Hub deleted.");
2296
2544
  });
2545
+ registerThalovantTool(server, "thalovant_delete_client", {
2546
+ title: "Delete Client",
2547
+ description: "Permanently delete a client (a hub connection): the device or link using it loses access to the hub. This cannot be undone. etag is the client's current etag, sent as If-Match; when omitted the server reads it first. A client changed underneath (412) is read again and the delete retried once, and a client already gone counts as deleted. Requires the clients:write scope. This tool is disabled unless the operator sets THALOVANT_ENABLE_DESTRUCTIVE_TOOLS.",
2548
+ inputSchema: {
2549
+ ...controlPlaneSchema,
2550
+ clientId: z.string().min(1).max(128).describe("Client UUID, for example the clientId a create returned or the client_id of an already-linked refusal."),
2551
+ etag: z.string().min(1).optional().describe("The client's current etag. Read first when omitted."),
2552
+ },
2553
+ annotations: {
2554
+ readOnlyHint: false,
2555
+ destructiveHint: true,
2556
+ idempotentHint: true,
2557
+ openWorldHint: true,
2558
+ },
2559
+ }, async ({ clientId, etag, ...auth }) => {
2560
+ const api = await createControlPlane(auth);
2561
+ ensureAuthenticated(api);
2562
+ await api.deleteClient(clientId, { etag }).catch((error) => withConnectionHint(error));
2563
+ return jsonContent({ deleted: true, clientId });
2564
+ });
2297
2565
  registerThalovantTool(server, "thalovant_delete_runtime_group", {
2298
2566
  title: "Delete Runtime Group",
2299
2567
  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.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thalovant/mcp-server",
3
- "version": "0.5.3",
3
+ "version": "0.6.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.8.7",
67
+ "@thalovant/sdk": "^0.9.0",
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.5.3",
10
+ "version": "0.6.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@thalovant/mcp-server",
15
- "version": "0.5.3",
15
+ "version": "0.6.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.5.3",
22
+ "identifier": "ghcr.io/thalovant/thalovant-mcp:0.6.0",
23
23
  "transport": {
24
24
  "type": "streamable-http",
25
25
  "url": "http://127.0.0.1:3000/mcp"