@thalovant/mcp-server 0.6.0 → 0.6.2

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.2 — 2026-10-02
4
+
5
+ - Takes `@thalovant/sdk` ^0.9.3, which the lock resolves. Its `reply.claimed` now recognizes a skill's own confident fallback-tier answer; the earlier range silently swallowed a genuinely successful Home Assistant reply (a `claimed` answer from the home skill's fallback tier read as "nothing answered"). No code change in this package: the logic is the SDK's `replyClaimMetadata`, which `src/reply-output.ts` delegates to.
6
+ - The parity contract acknowledges the reference's final round-two digest, and the vendored `reply-claim-vectors.json` carries the round's cases with each event's name and meta, so the assertion-on-a-non-speak-event-is-ignored case is actually exercised rather than built as a bare `speak`.
7
+ - `package.json`, `server.json`, the OCI label and this file move to 0.6.2 together, so the release inventory's `compatible_node_sdk` can follow `package.json` to `@thalovant/sdk ^0.9.3`: the platform contracts gate compares the two byte for byte, and the dependency moved on 2026-09-30 without a release to record it.
8
+
9
+ ## 0.6.1 — 2026-09-28
10
+
11
+ - **Sign in as a registered app.** `thalovant_begin_device_login` takes an optional `clientId`, passed to the API as `client_id` only when given: `thalovant-home-assistant` makes the approval page show the platform's name for Home Assistant as verified, and approving it again replaces the token it already holds. An id the API does not know is refused with 400 `unknown_client`, and the tool error carries the code and sentence like any other refusal.
12
+ - **An echoed validation input never reaches a tool error.** The `fields:` line of a tool error printed a validation error's `input` whenever it was a bare string or sat under an ordinary key: the secret redaction matches key names only. Each entry of a `detail` or Problem+JSON `errors` list now has its `input` replaced with `"[omitted]"` before redaction, whatever its key or shape.
13
+ - **Where the Noise key lives.** `@thalovant/sdk` 0.9.1 keeps an identity file's Noise key and hub pins in that file's folder, so every program reading the file presents one key, and copies them there once from the shared folder when that key already met the hub. Runtime tools given an `identityFile` follow it.
14
+ - Takes `@thalovant/sdk` ^0.9.1, which carries `clientId`, `describeDeviceLogin()`, the refused-client-key error and the key folder. The lock resolves the published 0.9.1.
15
+ - `fast-uri`, which `ajv` brings in, is overridden to 3.1.8: 3.1.6 now carries two high advisories (GHSA-qw65-cvwx-89v3, GHSA-58mr-gqgx-xq4g), and 3.1.8 still satisfies `ajv`'s `^3.0.1`.
16
+ - The parity contract acknowledges the reference's `3b0acf7c8b2f` and declares its new `link-carriers` capability not applicable: this server's carriers are the Node SDK's, which runs those vectors. The re-vendored `api-error-vectors.json` and `device-login-vectors.json` run through the SDK that resolves here and through the tools over stdio; the `describe` cases through the SDK only, since a model has no approval screen to read. All 78 recorded cases, and each file's digest, equal the reference's.
17
+
3
18
  ## 0.6.0 — 2026-09-28
4
19
 
5
20
  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.
package/README.md CHANGED
@@ -49,7 +49,7 @@ With none of these configured, a person can sign the server in through the brows
49
49
 
50
50
  A tool call cannot wait for a person, so the device flow (RFC 8628) is two tools:
51
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.
52
+ 1. `thalovant_begin_device_login` with optional `scopes` (an empty list, like none, asks for the API's default), `clientName` and `clientId` answers `loginId`, `userCode`, `verificationUri`, `verificationUriComplete`, `interval` and `expiresIn`. Show the person the URL and the code. `clientId` signs in as a registered app, `thalovant-home-assistant` for Home Assistant: the approval page shows the platform's name for it as verified, and approving it again replaces the token it already holds. An id the API does not know is refused (400 `unknown_client`).
53
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
54
 
55
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.
@@ -453,8 +453,9 @@ fields: {"allowed_images":{"bus":[...],"core":[...]},"allowed_repositories":{"co
453
453
  - `fields` is every other member of the error body as compact JSON (the
454
454
  Problem+JSON `type`, `title`, `status` and `instance` are left out), with the
455
455
  same secret redaction as every other tool output. A value a validation error
456
- echoes back from the request is redacted there and never reaches the first
457
- line. A plan refusal carries `resource`, `limit`, `used` and `plan` here.
456
+ echoes back from the request -- each entry's `input` in a `detail` or
457
+ `errors` list, whatever its key or shape -- is replaced with `"[omitted]"`
458
+ there and never reaches the first line. A plan refusal carries `resource`, `limit`, `used` and `plan` here.
458
459
 
459
460
  A hint may follow for statuses that need one. A `platform_image_required` or
460
461
  `plan_limit` 403 gets its own hint rather than the general scope hint.
@@ -530,7 +531,7 @@ Runtime calls sharing the same hub client identity run sequentially within one
530
531
  MCP process. Use a distinct client identity for each independently running MCP
531
532
  server so the hub can keep their sessions separate.
532
533
 
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
534
+ Version 0.6.2 requires `@thalovant/sdk` `^0.9.3` (0.9.3 through versions below 0.10.0). Since 0.5.1 the server enforces
534
535
  secure effective MQTT URLs and carries a single connection deadline through
535
536
  MQTT setup and HTTP failure cleanup. Runtime tools support
536
537
  HiveMind v3 Noise over WSS, HTTPS and MQTT over TLS. `thalovant_healthcheck`
@@ -7,8 +7,10 @@
7
7
  * image each refused key may be instead, and that list was the part cut off.
8
8
  * So the code, the whole sentence (when the message line had to shorten it)
9
9
  * and every other field of the body follow, as one compact JSON object.
10
- * `redact` is the server's own secret redaction, applied to the fields: a
11
- * validation error echoes back what it was sent.
10
+ * `redact` is the server's own secret redaction, applied to the fields. A
11
+ * validation error echoes back what it was sent as each entry's `input`, which
12
+ * is replaced with "[omitted]" first: redaction by key name cannot see a bare
13
+ * string, or a secret under an ordinary key.
12
14
  *
13
15
  * Returns undefined for anything that is not a `ThalovantApiError`.
14
16
  */
@@ -5,6 +5,22 @@ import { ThalovantApiError } from "@thalovant/sdk";
5
5
  * are the handler's own bookkeeping.
6
6
  */
7
7
  const ENVELOPE = new Set(["type", "title", "status", "instance"]);
8
+ /**
9
+ * The lists a validation error arrives in: FastAPI's `detail` and a
10
+ * Problem+JSON `errors`. Each entry's `input` is what the request sent,
11
+ * echoed back -- a generated secret, a name, a whole spec -- under whatever key
12
+ * and in whatever shape the field had, so key-name redaction cannot catch it.
13
+ */
14
+ const VALIDATION_LISTS = new Set(["detail", "errors"]);
15
+ const OMITTED = "[omitted]";
16
+ /** The entries of a validation list, each with its echoed `input` replaced. */
17
+ function withoutEchoedInput(value) {
18
+ if (!Array.isArray(value))
19
+ return value;
20
+ return value.map((entry) => (entry && typeof entry === "object" && !Array.isArray(entry) && "input" in entry
21
+ ? { ...entry, input: OMITTED }
22
+ : entry));
23
+ }
8
24
  /** A detail or a field list longer than this is cut, and says so. */
9
25
  const MAX_SECTION_CHARS = 4_000;
10
26
  function bounded(text) {
@@ -21,8 +37,10 @@ function bounded(text) {
21
37
  * image each refused key may be instead, and that list was the part cut off.
22
38
  * So the code, the whole sentence (when the message line had to shorten it)
23
39
  * and every other field of the body follow, as one compact JSON object.
24
- * `redact` is the server's own secret redaction, applied to the fields: a
25
- * validation error echoes back what it was sent.
40
+ * `redact` is the server's own secret redaction, applied to the fields. A
41
+ * validation error echoes back what it was sent as each entry's `input`, which
42
+ * is replaced with "[omitted]" first: redaction by key name cannot see a bare
43
+ * string, or a secret under an ordinary key.
26
44
  *
27
45
  * Returns undefined for anything that is not a `ThalovantApiError`.
28
46
  */
@@ -45,7 +63,7 @@ export function apiErrorText(error, redact) {
45
63
  // Already on a line above when they were read as strings.
46
64
  if ((key === "code" || key === "detail") && typeof value === "string")
47
65
  continue;
48
- fields[key] = value;
66
+ fields[key] = VALIDATION_LISTS.has(key) ? withoutEchoedInput(value) : value;
49
67
  }
50
68
  if (Object.keys(fields).length)
51
69
  lines.push(`fields: ${bounded(JSON.stringify(redact(fields)))}`);
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
9
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
10
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
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
+ import { CONNECTION_TYPE_HOME_ASSISTANT, DEFAULT_CONTROL_API_URL, HOME_ASSISTANT_CLIENT_ID, HOME_ASSISTANT_SCOPES, ThalovantAdmissionFailedError, ThalovantAdmissionTimeoutError, ThalovantAlreadyLinkedError, ThalovantApiError, ThalovantApiUnreachableError, ThalovantAuthError, ThalovantClient, ThalovantControlPlane, ThalovantDeviceLoginDeniedError, ThalovantDeviceLoginExpiredError, ThalovantDeviceLoginPendingError, ThalovantIdentity, ThalovantPlanError, ThalovantUnsupportedConnectionTypeError, buildClientContext, buildLocation, } from "@thalovant/sdk";
12
12
  import { AsyncLocalStorage } from "node:async_hooks";
13
13
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
14
14
  import { createServer as createHttpServer } from "node:http";
@@ -1464,7 +1464,7 @@ export function createServer() {
1464
1464
  });
1465
1465
  registerThalovantTool(server, "thalovant_begin_device_login", {
1466
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.",
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, and signs in as the registered app thalovant-home-assistant (clientId).",
1468
1468
  inputSchema: {
1469
1469
  apiUrl: controlPlaneSchema.apiUrl,
1470
1470
  scopes: z
@@ -1472,7 +1472,13 @@ export function createServer() {
1472
1472
  .max(32)
1473
1473
  .optional()
1474
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."),
1475
+ clientName: z.string().min(1).max(128).optional().describe("The name the approval page and the dashboard's token list show. With clientId, the device's own label beside the app's verified name."),
1476
+ clientId: z
1477
+ .string()
1478
+ .min(1)
1479
+ .max(128)
1480
+ .optional()
1481
+ .describe(`Sign in as a registered app: ${HOME_ASSISTANT_CLIENT_ID} for Home Assistant. The approval page then shows the platform's name for the app as verified, and approving it again replaces the token it already holds. The app may ask only for its own scopes, and an id the API does not know is refused (400 unknown_client). Left out when omitted.`),
1476
1482
  },
1477
1483
  annotations: {
1478
1484
  readOnlyHint: false,
@@ -1480,9 +1486,9 @@ export function createServer() {
1480
1486
  idempotentHint: false,
1481
1487
  openWorldHint: true,
1482
1488
  },
1483
- }, async ({ apiUrl, scopes, clientName }) => {
1489
+ }, async ({ apiUrl, scopes, clientName, clientId }) => {
1484
1490
  const api = await deviceLoginControlPlane(apiUrl);
1485
- const grant = await api.beginDeviceLogin({ scopes, clientName });
1491
+ const grant = await api.beginDeviceLogin({ scopes, clientName, clientId });
1486
1492
  const loginId = deviceLogins.start(currentPrincipal().id, api, grant);
1487
1493
  return jsonContent({
1488
1494
  loginId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thalovant/mcp-server",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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,12 +64,12 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@modelcontextprotocol/sdk": "^1.30.0",
67
- "@thalovant/sdk": "^0.9.0",
67
+ "@thalovant/sdk": "^0.9.3",
68
68
  "jose": "^6.2.8",
69
69
  "zod": "^3.25.76"
70
70
  },
71
71
  "overrides": {
72
- "fast-uri": "3.1.6"
72
+ "fast-uri": "3.1.8"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@types/node": "^22.18.12",
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.6.0",
10
+ "version": "0.6.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@thalovant/mcp-server",
15
- "version": "0.6.0",
15
+ "version": "0.6.2",
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.6.0",
22
+ "identifier": "ghcr.io/thalovant/thalovant-mcp:0.6.2",
23
23
  "transport": {
24
24
  "type": "streamable-http",
25
25
  "url": "http://127.0.0.1:3000/mcp"