agent-trellis 0.14.0 → 0.14.1

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.
@@ -28,8 +28,11 @@ export declare function supportsNativeMcp(version: string | undefined): boolean;
28
28
  * `pi mcp add` writes (observed 2026-10-11): stdio as
29
29
  * `{command, args, env?}`, http as `{url, headers?}`, no `type` field.
30
30
  * `env` names render as `${VAR}` references exactly like the other JSON
31
- * agents; OAuth `auth` metadata is Trellis-side routing information and
32
- * is never rendered (an agent-owned OAuth server becomes a bare `{url}`
33
- * entry that `pi mcp login <name>` authorizes into pi's own token store).
31
+ * agents; OAuth `auth` metadata is Trellis-side routing information and is
32
+ * never rendered — with one exception: a declared `auth.client_name` becomes
33
+ * the entry's `oauth.clientName`, the field `pi mcp add --oauth-client-name`
34
+ * writes, so providers that gate dynamic client registration by client name
35
+ * (Figma) accept `pi mcp login`'s registration and later syncs compare equal
36
+ * instead of stripping the block (oauth-client-name design.md D2).
34
37
  */
35
38
  export declare function piRenderer(def: McpServerDef, _name: string): Record<string, unknown>;
@@ -11,6 +11,7 @@
11
11
  * unchanged. sse is not a pi transport — the adapter turns sse servers
12
12
  * into conflict items rather than writing entries pi cannot connect.
13
13
  */
14
+ import { isOAuthAuth } from "../core/types.js";
14
15
  import { resolvedEnvTextMap } from "../lib/mcpMigrateRead.js";
15
16
  /** The pi release that introduced the native MCP client (`pi mcp`,
16
17
  * `~/.pi/agent/mcp.json`). */
@@ -37,9 +38,12 @@ export function supportsNativeMcp(version) {
37
38
  * `pi mcp add` writes (observed 2026-10-11): stdio as
38
39
  * `{command, args, env?}`, http as `{url, headers?}`, no `type` field.
39
40
  * `env` names render as `${VAR}` references exactly like the other JSON
40
- * agents; OAuth `auth` metadata is Trellis-side routing information and
41
- * is never rendered (an agent-owned OAuth server becomes a bare `{url}`
42
- * entry that `pi mcp login <name>` authorizes into pi's own token store).
41
+ * agents; OAuth `auth` metadata is Trellis-side routing information and is
42
+ * never rendered — with one exception: a declared `auth.client_name` becomes
43
+ * the entry's `oauth.clientName`, the field `pi mcp add --oauth-client-name`
44
+ * writes, so providers that gate dynamic client registration by client name
45
+ * (Figma) accept `pi mcp login`'s registration and later syncs compare equal
46
+ * instead of stripping the block (oauth-client-name design.md D2).
43
47
  */
44
48
  export function piRenderer(def, _name) {
45
49
  if (def.transport === "http" || def.transport === "sse") {
@@ -47,6 +51,9 @@ export function piRenderer(def, _name) {
47
51
  if (def.headers && Object.keys(def.headers).length > 0) {
48
52
  entry.headers = def.headers;
49
53
  }
54
+ if (isOAuthAuth(def.auth) && typeof def.auth === "object" && def.auth.clientName !== undefined) {
55
+ entry.oauth = { clientName: def.auth.clientName };
56
+ }
50
57
  return entry;
51
58
  }
52
59
  const entry = { command: def.command, args: def.args ?? [] };
@@ -10,9 +10,10 @@ import type { BackupSession } from "../lib/backup.js";
10
10
  * The on-disk shape for one server entry — `static_env`/`env_aliases`
11
11
  * (snake_case, like every other multi-word key across `.trellis/*.yaml`)
12
12
  * are translated to `McpServerDef.staticEnv`/`envAliases` (camelCase)
13
- * below, and `auth`'s object form carries `client_id`/`client_secret_env`
14
- * the same way (trellis-mcp-static-env-and-disabled-servers,
15
- * trellis-migrate-env-var-alias, trellis-mcp-oauth-static-client).
13
+ * below, and `auth`'s object form carries `client_id`/`client_secret_env`/
14
+ * `client_name` the same way (trellis-mcp-static-env-and-disabled-servers,
15
+ * trellis-migrate-env-var-alias, trellis-mcp-oauth-static-client,
16
+ * oauth-client-name).
16
17
  */
17
18
  type McpServerDefYaml = Omit<McpServerDef, "staticEnv" | "envAliases" | "auth"> & {
18
19
  static_env?: Record<string, string>;
@@ -28,6 +29,7 @@ type McpAuthYaml = string | {
28
29
  owner?: unknown;
29
30
  client_id?: unknown;
30
31
  client_secret_env?: unknown;
32
+ client_name?: unknown;
31
33
  };
32
34
  /** Inverse of `fromServerDefYaml` (trellis-canonical-cli-crud) — strips
33
35
  * `undefined` fields so the written YAML never gets a literal `null`
@@ -34,12 +34,12 @@ function fromAuthYaml(serverName, auth) {
34
34
  if (auth === "oauth")
35
35
  return "oauth";
36
36
  if (typeof auth !== "object" || auth === null) {
37
- throw new Error(`mcp/servers.yaml: "${serverName}" auth must be the scalar "oauth" or { kind: oauth, client_id?, client_secret_env? }`);
37
+ throw new Error(`mcp/servers.yaml: "${serverName}" auth must be the scalar "oauth" or { kind: oauth, client_id?, client_secret_env?, client_name? }`);
38
38
  }
39
- const { kind, owner, client_id, client_secret_env, ...extra } = auth;
39
+ const { kind, owner, client_id, client_secret_env, client_name, ...extra } = auth;
40
40
  const unknownKey = Object.keys(extra)[0];
41
41
  if (unknownKey !== undefined) {
42
- throw new Error(`mcp/servers.yaml: "${serverName}" auth has unrecognized key "${unknownKey}" — expected kind, owner, client_id, client_secret_env`);
42
+ throw new Error(`mcp/servers.yaml: "${serverName}" auth has unrecognized key "${unknownKey}" — expected kind, owner, client_id, client_secret_env, client_name`);
43
43
  }
44
44
  if (kind !== "oauth") {
45
45
  throw new Error(`mcp/servers.yaml: "${serverName}" auth.kind must be "oauth" — kind is the only recognized discriminator`);
@@ -50,6 +50,9 @@ function fromAuthYaml(serverName, auth) {
50
50
  if (client_secret_env !== undefined && (typeof client_secret_env !== "string" || !isValidSecretVarName(client_secret_env))) {
51
51
  throw new Error(`mcp/servers.yaml: "${serverName}" auth.client_secret_env must be a variable NAME (e.g. FIGMA_CLIENT_SECRET), never a secret value`);
52
52
  }
53
+ if (client_name !== undefined && typeof client_name !== "string") {
54
+ throw new Error(`mcp/servers.yaml: "${serverName}" auth.client_name must be a string`);
55
+ }
53
56
  const out = { kind: "oauth" };
54
57
  if (owner !== undefined && owner !== "agent" && owner !== "trellis")
55
58
  throw new Error(`mcp/servers.yaml: "${serverName}" auth.owner must be agent or trellis`);
@@ -59,6 +62,8 @@ function fromAuthYaml(serverName, auth) {
59
62
  out.clientId = client_id;
60
63
  if (typeof client_secret_env === "string")
61
64
  out.clientSecretEnv = client_secret_env;
65
+ if (typeof client_name === "string")
66
+ out.clientName = client_name;
62
67
  return out;
63
68
  }
64
69
  /** Inverse of `fromServerDefYaml` (trellis-canonical-cli-crud) — strips
@@ -80,6 +85,7 @@ export function toServerDefYaml(def) {
80
85
  ...(auth.owner !== undefined ? { owner: auth.owner } : {}),
81
86
  ...(auth.clientId !== undefined ? { client_id: auth.clientId } : {}),
82
87
  ...(auth.clientSecretEnv !== undefined ? { client_secret_env: auth.clientSecretEnv } : {}),
88
+ ...(auth.clientName !== undefined ? { client_name: auth.clientName } : {}),
83
89
  };
84
90
  }
85
91
  for (const key of Object.keys(out)) {
@@ -19,6 +19,10 @@ export interface McpAuthConfig {
19
19
  owner?: "agent" | "trellis";
20
20
  clientId?: string;
21
21
  clientSecretEnv?: string;
22
+ /** Self-asserted display name an Agent presents at dynamic client
23
+ * registration — for providers that gate DCR by client name (Figma).
24
+ * Public metadata, not a credential (oauth-client-name design.md D1). */
25
+ clientName?: string;
22
26
  }
23
27
  /** True when `auth` classifies the server as OAuth, in either form. Every
24
28
  * former `def.auth === "oauth"` check must go through this now that the
@@ -7948,12 +7948,12 @@ function fromServerDefYaml(serverName, def) {
7948
7948
  function fromAuthYaml(serverName, auth2) {
7949
7949
  if (auth2 === "oauth") return "oauth";
7950
7950
  if (typeof auth2 !== "object" || auth2 === null) {
7951
- throw new Error(`mcp/servers.yaml: "${serverName}" auth must be the scalar "oauth" or { kind: oauth, client_id?, client_secret_env? }`);
7951
+ throw new Error(`mcp/servers.yaml: "${serverName}" auth must be the scalar "oauth" or { kind: oauth, client_id?, client_secret_env?, client_name? }`);
7952
7952
  }
7953
- const { kind, owner, client_id, client_secret_env, ...extra } = auth2;
7953
+ const { kind, owner, client_id, client_secret_env, client_name, ...extra } = auth2;
7954
7954
  const unknownKey = Object.keys(extra)[0];
7955
7955
  if (unknownKey !== void 0) {
7956
- throw new Error(`mcp/servers.yaml: "${serverName}" auth has unrecognized key "${unknownKey}" \u2014 expected kind, owner, client_id, client_secret_env`);
7956
+ throw new Error(`mcp/servers.yaml: "${serverName}" auth has unrecognized key "${unknownKey}" \u2014 expected kind, owner, client_id, client_secret_env, client_name`);
7957
7957
  }
7958
7958
  if (kind !== "oauth") {
7959
7959
  throw new Error(`mcp/servers.yaml: "${serverName}" auth.kind must be "oauth" \u2014 kind is the only recognized discriminator`);
@@ -7964,11 +7964,15 @@ function fromAuthYaml(serverName, auth2) {
7964
7964
  if (client_secret_env !== void 0 && (typeof client_secret_env !== "string" || !isValidSecretVarName(client_secret_env))) {
7965
7965
  throw new Error(`mcp/servers.yaml: "${serverName}" auth.client_secret_env must be a variable NAME (e.g. FIGMA_CLIENT_SECRET), never a secret value`);
7966
7966
  }
7967
+ if (client_name !== void 0 && typeof client_name !== "string") {
7968
+ throw new Error(`mcp/servers.yaml: "${serverName}" auth.client_name must be a string`);
7969
+ }
7967
7970
  const out = { kind: "oauth" };
7968
7971
  if (owner !== void 0 && owner !== "agent" && owner !== "trellis") throw new Error(`mcp/servers.yaml: "${serverName}" auth.owner must be agent or trellis`);
7969
7972
  if (owner !== void 0) out.owner = owner;
7970
7973
  if (typeof client_id === "string") out.clientId = client_id;
7971
7974
  if (typeof client_secret_env === "string") out.clientSecretEnv = client_secret_env;
7975
+ if (typeof client_name === "string") out.clientName = client_name;
7972
7976
  return out;
7973
7977
  }
7974
7978
  function trellisRoot(homeDir) {
@@ -309,6 +309,22 @@ Two pi-specific behaviors to know:
309
309
  run `pi mcp login <name>` once; the grant lives in pi's own token store
310
310
  (`~/.pi/agent/mcp-auth.json`), and `trellis.mcp.status` reports them as
311
311
  `agent-managed`.
312
+ - Some providers (Figma, while in beta) gate OAuth dynamic client
313
+ registration by an allowlisted `client_name`, which rejects pi's default.
314
+ Declare the name canonically and pi registers under it:
315
+
316
+ ```yaml
317
+ figma:
318
+ transport: http
319
+ url: https://mcp.figma.com/mcp
320
+ auth:
321
+ kind: oauth
322
+ client_name: Claude Code
323
+ ```
324
+
325
+ Trellis then writes `oauth.clientName` into the pi entry — the same field
326
+ `pi mcp add --oauth-client-name` writes — so syncs converge instead of
327
+ stripping it.
312
328
 
313
329
  On pi older than 1.1.0 nothing changes: the bridge extension remains the
314
330
  delivery mechanism and `mcp.json` is never written.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-trellis",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "A unified Code Agent Runtime for Skills, MCP, Memory, and shared instructions across Claude Code, Codex, Kiro, pi, Kimi Code, and ZCode.",
5
5
  "license": "MIT",
6
6
  "author": "Paul Leo",