@hue-run/sdk 0.9.0 → 0.11.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.
Files changed (56) hide show
  1. package/CLI.md +240 -49
  2. package/ENVIRONMENTS.md +48 -9
  3. package/EVALUATIONS.md +41 -21
  4. package/README.md +70 -30
  5. package/dist/cli/eval-direct.d.ts +12 -1
  6. package/dist/cli/eval-direct.js +145 -33
  7. package/dist/cli/eval.d.ts +28 -1
  8. package/dist/cli/eval.js +504 -81
  9. package/dist/cli/listen.d.ts +76 -0
  10. package/dist/cli/listen.js +816 -0
  11. package/dist/cli/mcp.d.ts +65 -7
  12. package/dist/cli/mcp.js +271 -73
  13. package/dist/client.d.ts +7 -4
  14. package/dist/client.js +36 -10
  15. package/dist/evals/checkpoint.d.ts +15 -1
  16. package/dist/evals/checkpoint.js +34 -3
  17. package/dist/evals/client.d.ts +24 -4
  18. package/dist/evals/client.js +92 -38
  19. package/dist/evals/environment-target.d.ts +18 -3
  20. package/dist/evals/environment-target.js +22 -10
  21. package/dist/evals/exit-cleanup.d.ts +4 -0
  22. package/dist/evals/exit-cleanup.js +26 -0
  23. package/dist/evals/files.d.ts +69 -3
  24. package/dist/evals/files.js +255 -42
  25. package/dist/evals/json.js +116 -9
  26. package/dist/evals/local-worker.d.ts +21 -3
  27. package/dist/evals/local-worker.js +14 -0
  28. package/dist/evals/runner.d.ts +45 -0
  29. package/dist/evals/runner.js +199 -21
  30. package/dist/evals/scenarios.d.ts +7 -5
  31. package/dist/evals/scenarios.js +71 -14
  32. package/dist/evals/scorer-publication.js +58 -3
  33. package/dist/evals/simulation.d.ts +17 -4
  34. package/dist/evals/simulation.js +8 -0
  35. package/dist/evals/types.d.ts +48 -0
  36. package/dist/evals/verdicts.d.ts +26 -2
  37. package/dist/evals/verdicts.js +72 -9
  38. package/dist/evals.d.ts +4 -3
  39. package/dist/evals.js +2 -2
  40. package/dist/inline-files.d.ts +4 -0
  41. package/dist/inline-files.js +48 -17
  42. package/dist/provider-tools.d.ts +12 -1
  43. package/dist/provider-tools.js +192 -106
  44. package/dist/safety.d.ts +7 -1
  45. package/dist/safety.js +9 -7
  46. package/dist/setup/cli.js +31 -8
  47. package/dist/setup/peers.d.ts +13 -0
  48. package/dist/setup/peers.js +44 -0
  49. package/dist/snapshot.d.ts +2 -2
  50. package/dist/snapshot.js +47 -12
  51. package/dist/tool-definitions.d.ts +18 -0
  52. package/dist/tool-definitions.js +232 -0
  53. package/dist/transport.js +2 -2
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/package.json +1 -1
package/dist/cli/mcp.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  /**
2
- * `hue mcp install`: writes or prints the coding-agent configuration for Hue's MCP server. Every
3
- * shape mirrors the snippets Hue shows in Settings and references the `HUE_MCP_KEY` environment
4
- * variable (or a VS Code password input); a key value is never written.
2
+ * `hue mcp install`: writes or prints the coding-agent configuration for Hue's MCP server. The
3
+ * shapes are those of Hue's published connection guide (https://docs.hue.run/agents/mcp-server),
4
+ * kept as a separate copy here. Key configurations reference the `HUE_MCP_KEY` environment
5
+ * variable (or a VS Code password input); sign-in configurations hold only the URL. A key value is
6
+ * never written.
5
7
  */
6
8
  /** Streams, environment and working directory for {@link runMcpCommand}; tests inject these. */
7
9
  export interface McpCommandIo {
@@ -12,10 +14,27 @@ export interface McpCommandIo {
12
14
  cwd?: string;
13
15
  }
14
16
  export declare const DEFAULT_MCP_URL = "https://mcp.hue.run/mcp";
15
- /** Prompt to paste into the agent after installation; identical to the one Hue shows. */
16
- export declare const MCP_VERIFY_PROMPT = "Use the Hue MCP: call get_project_context, then show my 5 most recent error traces with links.";
17
- export declare const MCP_USAGE = "Usage: hue mcp install --client <claude-code|cursor|codex|vscode|windsurf|gemini>\n [--url URL] [--scope project|user] [--dry-run] [--print]\n\nConfigure a coding agent to use the Hue MCP server. The configuration references the\nHUE_MCP_KEY environment variable; a key value is never written.\n\nOptions:\n --client NAME Coding agent to configure (required)\n --url URL Hue MCP endpoint (default https://mcp.hue.run/mcp)\n --scope SCOPE claude-code only: project writes .mcp.json (default); user runs\n `claude mcp add --scope user`\n --dry-run Print the resulting file content or command without writing or running\n --print Print the configuration snippet only\n -h, --help Show this help\n\nFiles: claude-code .mcp.json, cursor .cursor/mcp.json, vscode .vscode/mcp.json (relative to the\ncurrent directory). codex and gemini use their own CLI when it is on PATH; windsurf prints the\nsnippet for its user configuration file.";
18
- /** Canonical Hue client snippets; the JSON values are also the merge entries for config files. */
17
+ /**
18
+ * Prompt to paste into the agent after installation. `list_projects` answers for every credential:
19
+ * a project key or earlier connection returns its one project, and an organization connection
20
+ * lists them, after which each call takes `project_id`. It reads what needs attention and falls
21
+ * back to recent traces, so a project without errors still proves the read path.
22
+ */
23
+ export declare const MCP_VERIFY_PROMPT = "Use the Hue MCP: call list_projects and confirm which project to inspect. Then call get_project_context and show that project's traces from the last 24 hours that need attention or have errors, with links; if there are none, show its 5 most recent traces. For an organization connection, pass the project's id as project_id on each call after list_projects.";
24
+ /** Toolset names Hue's MCP server accepts: `all`, its catalog groups and the `observe` profile. */
25
+ export declare const MCP_TOOLSETS: readonly ["all", "observe", "project", "traces", "evals", "environments", "intents", "docs"];
26
+ /** The header that selects toolsets for a sign-in connection, whose URL must stay bare. */
27
+ export declare const TOOLSETS_HEADER = "X-Hue-MCP-Toolsets";
28
+ export declare const MCP_USAGE = "Usage: hue mcp install --client <claude-code|codex|conductor|cursor|vscode|windsurf|gemini>\n [--auth key|oauth] [--read-only] [--toolsets NAMES] [--url URL]\n [--scope project|user] [--dry-run] [--print]\n\nConfigure a coding agent to use the Hue MCP server. A key configuration references the\nHUE_MCP_KEY environment variable; a key value is never written. A sign-in configuration holds\nonly the URL: the client opens Hue in a browser, where you approve access to the projects of\none organization.\n\nOptions:\n --client NAME Coding agent to configure (required)\n --auth MODE key: reference HUE_MCP_KEY (the default, except for conductor)\n oauth: URL only, sign in with Hue in the client (claude-code, codex and\n conductor; the default for conductor)\n --read-only key only: add ?read_only=true to the URL so write tools are hidden. A\n sign-in connection has Read and write access; use a Read key for read-only\n --toolsets NAMES\n List only these tools, comma-separated: observe (production reads),\n all, project, traces, evals, environments, intents or docs. A key\n configuration adds ?toolsets= to the URL; a sign-in one sends the\n X-Hue-MCP-Toolsets header. cursor defaults to observe; others to all\n --url URL Hue MCP endpoint (default https://mcp.hue.run/mcp)\n --scope SCOPE claude-code only: project writes .mcp.json (default); user runs\n `claude mcp add --scope user`\n --dry-run Print the resulting file content or commands without writing or running\n --print Print the configuration snippet only\n -h, --help Show this help\n\nFiles: claude-code .mcp.json, cursor .cursor/mcp.json, vscode .vscode/mcp.json (relative to the\ncurrent directory). codex and gemini use their own CLI when it is on PATH; windsurf prints the\nsnippet for its user configuration file. conductor registers the server for its Claude Code\n(user scope) and Codex agents with their CLIs, printing the command for a CLI not on PATH.";
29
+ /**
30
+ * A URL as a POSIX shell word for printed commands: `?` is a zsh glob and `&` ends a command, so a
31
+ * URL with a query (such as `?read_only=true`) is single-quoted. Executed commands pass argv.
32
+ */
33
+ export declare function shellWord(value: string): string;
34
+ /**
35
+ * Canonical Hue client snippets for a key configuration; the JSON values are also the merge
36
+ * entries for config files.
37
+ */
19
38
  export declare function renderMcpSnippets(url: string): {
20
39
  claudeCodeServer: {
21
40
  type: string;
@@ -67,6 +86,45 @@ export declare function renderMcpSnippets(url: string): {
67
86
  display: string;
68
87
  };
69
88
  };
89
+ /**
90
+ * Sign-in snippets: the bare server URL, which the client uses to discover Hue's OAuth metadata,
91
+ * and, when toolsets are selected, the header that carries them.
92
+ */
93
+ export declare function renderMcpSignInSnippets(url: string, toolsets?: string): {
94
+ claudeCodeServer: {
95
+ type: string;
96
+ url: string;
97
+ headers?: {
98
+ "X-Hue-MCP-Toolsets": string;
99
+ } | undefined;
100
+ };
101
+ claudeCodeProjectJson: string;
102
+ claudeCodeCli: {
103
+ args: string[];
104
+ display: string;
105
+ };
106
+ codexCli: {
107
+ args: string[];
108
+ display: string;
109
+ };
110
+ codexToml: string;
111
+ };
112
+ /**
113
+ * Parses a comma-separated `--toolsets` value into Hue's canonical form, or returns an error.
114
+ * Unknown names are refused: Hue ignores them and would list the whole catalog, hiding a typo.
115
+ */
116
+ export declare function parseToolsets(value: string): {
117
+ toolsets: string;
118
+ } | {
119
+ error: string;
120
+ };
121
+ /**
122
+ * `?toolsets=` lists only the selected tools for a key configuration; commas stay readable.
123
+ * Without a selection the parameter is removed, so the URL lists every tool.
124
+ */
125
+ export declare function toolsetsMcpUrl(url: string, toolsets: string | undefined): string;
126
+ /** `?read_only=true` hides and rejects Hue's write tools for any key. */
127
+ export declare function readOnlyMcpUrl(url: string): string;
70
128
  /** Validates the MCP endpoint: HTTPS, or HTTP for loopback test servers; no credentials or hash. */
71
129
  export declare function parseMcpUrl(value: string): string | null;
72
130
  /**
package/dist/cli/mcp.js CHANGED
@@ -10,37 +10,92 @@ const SERVER_NAME = "hue";
10
10
  const ENV_VAR = "HUE_MCP_KEY";
11
11
  const INPUT_ID = `${SERVER_NAME}-mcp-key`;
12
12
  const MAX_CONFIG_BYTES = 1024 * 1024;
13
- /** Prompt to paste into the agent after installation; identical to the one Hue shows. */
14
- export const MCP_VERIFY_PROMPT = "Use the Hue MCP: call get_project_context, then show my 5 most recent error traces with links.";
15
- const CLIENT_IDS = ["claude-code", "cursor", "codex", "vscode", "windsurf", "gemini"];
13
+ /**
14
+ * Prompt to paste into the agent after installation. `list_projects` answers for every credential:
15
+ * a project key or earlier connection returns its one project, and an organization connection
16
+ * lists them, after which each call takes `project_id`. It reads what needs attention and falls
17
+ * back to recent traces, so a project without errors still proves the read path.
18
+ */
19
+ export const MCP_VERIFY_PROMPT = "Use the Hue MCP: call list_projects and confirm which project to inspect. Then call get_project_context and show that project's traces from the last 24 hours that need attention or have errors, with links; if there are none, show its 5 most recent traces. For an organization connection, pass the project's id as project_id on each call after list_projects.";
20
+ const CLIENT_IDS = [
21
+ "claude-code",
22
+ "codex",
23
+ "conductor",
24
+ "cursor",
25
+ "vscode",
26
+ "windsurf",
27
+ "gemini",
28
+ ];
16
29
  const CLIENT_LABELS = {
17
30
  "claude-code": "Claude Code",
18
- cursor: "Cursor",
19
31
  codex: "Codex",
32
+ conductor: "Conductor",
33
+ cursor: "Cursor",
20
34
  vscode: "VS Code",
21
35
  windsurf: "Windsurf",
22
36
  gemini: "Gemini CLI",
23
37
  };
24
- export const MCP_USAGE = `Usage: hue mcp install --client <claude-code|cursor|codex|vscode|windsurf|gemini>
25
- [--url URL] [--scope project|user] [--dry-run] [--print]
38
+ /** Clients whose browser sign-in with Hue works against the deployed server. */
39
+ const OAUTH_CLIENTS = ["claude-code", "codex", "conductor"];
40
+ /** Toolset names Hue's MCP server accepts: `all`, its catalog groups and the `observe` profile. */
41
+ export const MCP_TOOLSETS = [
42
+ "all",
43
+ "observe",
44
+ "project",
45
+ "traces",
46
+ "evals",
47
+ "environments",
48
+ "intents",
49
+ "docs",
50
+ ];
51
+ /** The header that selects toolsets for a sign-in connection, whose URL must stay bare. */
52
+ export const TOOLSETS_HEADER = "X-Hue-MCP-Toolsets";
53
+ /** Clients whose configuration selects a toolset unless `--toolsets` says otherwise. */
54
+ const DEFAULT_TOOLSETS = { cursor: "observe" };
55
+ export const MCP_USAGE = `Usage: hue mcp install --client <claude-code|codex|conductor|cursor|vscode|windsurf|gemini>
56
+ [--auth key|oauth] [--read-only] [--toolsets NAMES] [--url URL]
57
+ [--scope project|user] [--dry-run] [--print]
26
58
 
27
- Configure a coding agent to use the Hue MCP server. The configuration references the
28
- ${ENV_VAR} environment variable; a key value is never written.
59
+ Configure a coding agent to use the Hue MCP server. A key configuration references the
60
+ ${ENV_VAR} environment variable; a key value is never written. A sign-in configuration holds
61
+ only the URL: the client opens Hue in a browser, where you approve access to the projects of
62
+ one organization.
29
63
 
30
64
  Options:
31
65
  --client NAME Coding agent to configure (required)
66
+ --auth MODE key: reference ${ENV_VAR} (the default, except for conductor)
67
+ oauth: URL only, sign in with Hue in the client (claude-code, codex and
68
+ conductor; the default for conductor)
69
+ --read-only key only: add ?read_only=true to the URL so write tools are hidden. A
70
+ sign-in connection has Read and write access; use a Read key for read-only
71
+ --toolsets NAMES
72
+ List only these tools, comma-separated: observe (production reads),
73
+ all, project, traces, evals, environments, intents or docs. A key
74
+ configuration adds ?toolsets= to the URL; a sign-in one sends the
75
+ ${TOOLSETS_HEADER} header. cursor defaults to observe; others to all
32
76
  --url URL Hue MCP endpoint (default ${DEFAULT_MCP_URL})
33
77
  --scope SCOPE claude-code only: project writes .mcp.json (default); user runs
34
78
  \`claude mcp add --scope user\`
35
- --dry-run Print the resulting file content or command without writing or running
79
+ --dry-run Print the resulting file content or commands without writing or running
36
80
  --print Print the configuration snippet only
37
81
  -h, --help Show this help
38
82
 
39
83
  Files: claude-code .mcp.json, cursor .cursor/mcp.json, vscode .vscode/mcp.json (relative to the
40
84
  current directory). codex and gemini use their own CLI when it is on PATH; windsurf prints the
41
- snippet for its user configuration file.`;
85
+ snippet for its user configuration file. conductor registers the server for its Claude Code
86
+ (user scope) and Codex agents with their CLIs, printing the command for a CLI not on PATH.`;
42
87
  const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
43
- /** Canonical Hue client snippets; the JSON values are also the merge entries for config files. */
88
+ /**
89
+ * A URL as a POSIX shell word for printed commands: `?` is a zsh glob and `&` ends a command, so a
90
+ * URL with a query (such as `?read_only=true`) is single-quoted. Executed commands pass argv.
91
+ */
92
+ export function shellWord(value) {
93
+ return /^[\w@%+=:,./-]+$/u.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
94
+ }
95
+ /**
96
+ * Canonical Hue client snippets for a key configuration; the JSON values are also the merge
97
+ * entries for config files.
98
+ */
44
99
  export function renderMcpSnippets(url) {
45
100
  const bearer = (reference) => `Bearer ${reference}`;
46
101
  const claudeCodeServer = {
@@ -57,7 +112,7 @@ export function renderMcpSnippets(url) {
57
112
  const vscodeInput = {
58
113
  type: "promptString",
59
114
  id: INPUT_ID,
60
- description: "Hue coding-agent key",
115
+ description: "Hue Read or Read and write API key",
61
116
  password: true,
62
117
  };
63
118
  const windsurfServer = {
@@ -80,13 +135,13 @@ export function renderMcpSnippets(url) {
80
135
  "--header",
81
136
  `Authorization: Bearer \${${ENV_VAR}}`,
82
137
  ],
83
- display: `claude mcp add --transport http --scope user ${SERVER_NAME} ${url} --header 'Authorization: Bearer \${${ENV_VAR}}'`,
138
+ display: `claude mcp add --transport http --scope user ${SERVER_NAME} ${shellWord(url)} --header 'Authorization: Bearer \${${ENV_VAR}}'`,
84
139
  },
85
140
  cursorServer,
86
141
  cursorJson: json({ mcpServers: { [SERVER_NAME]: cursorServer } }),
87
142
  codexCli: {
88
143
  args: ["mcp", "add", SERVER_NAME, "--url", url, "--bearer-token-env-var", ENV_VAR],
89
- display: `codex mcp add ${SERVER_NAME} --url ${url} --bearer-token-env-var ${ENV_VAR}`,
144
+ display: `codex mcp add ${SERVER_NAME} --url ${shellWord(url)} --bearer-token-env-var ${ENV_VAR}`,
90
145
  },
91
146
  codexToml: `[mcp_servers.${SERVER_NAME}]\nurl = "${url}"\nbearer_token_env_var = "${ENV_VAR}"\n`,
92
147
  vscodeServer,
@@ -98,19 +153,88 @@ export function renderMcpSnippets(url) {
98
153
  args: [
99
154
  "mcp",
100
155
  "add",
156
+ "--scope",
157
+ "user",
101
158
  "--transport",
102
159
  "http",
103
160
  SERVER_NAME,
104
161
  url,
105
- "-H",
106
- `Authorization: Bearer $${ENV_VAR}`,
162
+ "--header",
163
+ `Authorization: Bearer \${${ENV_VAR}}`,
107
164
  ],
108
165
  // Single quotes keep the reference literal when a person runs this in a shell where the
109
- // key is exported; Gemini CLI resolves $HUE_MCP_KEY from its settings at runtime.
110
- display: `gemini mcp add --transport http ${SERVER_NAME} ${url} -H 'Authorization: Bearer $${ENV_VAR}'`,
166
+ // key is exported; Gemini CLI expands ${HUE_MCP_KEY} from its settings at connection time.
167
+ display: `gemini mcp add --scope user --transport http ${SERVER_NAME} ${shellWord(url)} --header 'Authorization: Bearer \${${ENV_VAR}}'`,
168
+ },
169
+ };
170
+ }
171
+ /**
172
+ * Sign-in snippets: the bare server URL, which the client uses to discover Hue's OAuth metadata,
173
+ * and, when toolsets are selected, the header that carries them.
174
+ */
175
+ export function renderMcpSignInSnippets(url, toolsets) {
176
+ const headers = toolsets ? { [TOOLSETS_HEADER]: toolsets } : undefined;
177
+ const claudeCodeServer = { type: "http", url, ...(headers ? { headers } : {}) };
178
+ const headerArgs = toolsets ? ["--header", `${TOOLSETS_HEADER}: ${toolsets}`] : [];
179
+ return {
180
+ claudeCodeServer,
181
+ claudeCodeProjectJson: json({ mcpServers: { [SERVER_NAME]: claudeCodeServer } }),
182
+ claudeCodeCli: {
183
+ args: [
184
+ "mcp",
185
+ "add",
186
+ "--transport",
187
+ "http",
188
+ "--scope",
189
+ "user",
190
+ SERVER_NAME,
191
+ url,
192
+ ...headerArgs,
193
+ ],
194
+ display: `claude mcp add --transport http --scope user ${SERVER_NAME} ${shellWord(url)}${toolsets ? ` --header '${TOOLSETS_HEADER}: ${toolsets}'` : ""}`,
195
+ },
196
+ // `codex mcp add` cannot store a header; the TOML carries it and the next steps say so.
197
+ codexCli: {
198
+ args: ["mcp", "add", SERVER_NAME, "--url", url],
199
+ display: `codex mcp add ${SERVER_NAME} --url ${shellWord(url)}`,
111
200
  },
201
+ codexToml: `[mcp_servers.${SERVER_NAME}]\nurl = "${url}"\n${toolsets ? `http_headers = { "${TOOLSETS_HEADER}" = "${toolsets}" }\n` : ""}`,
112
202
  };
113
203
  }
204
+ /**
205
+ * Parses a comma-separated `--toolsets` value into Hue's canonical form, or returns an error.
206
+ * Unknown names are refused: Hue ignores them and would list the whole catalog, hiding a typo.
207
+ */
208
+ export function parseToolsets(value) {
209
+ const names = [...new Set(value.split(",").map((name) => name.trim()))];
210
+ if (names.some((name) => !name))
211
+ return { error: "--toolsets takes comma-separated names, such as observe or traces,docs." };
212
+ const unknown = names.filter((name) => !MCP_TOOLSETS.includes(name));
213
+ if (unknown.length)
214
+ return {
215
+ error: `Unknown toolset${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}. Choose from ${MCP_TOOLSETS.join(", ")}.`,
216
+ };
217
+ return { toolsets: names.join(",") };
218
+ }
219
+ /**
220
+ * `?toolsets=` lists only the selected tools for a key configuration; commas stay readable.
221
+ * Without a selection the parameter is removed, so the URL lists every tool.
222
+ */
223
+ export function toolsetsMcpUrl(url, toolsets) {
224
+ const parsed = new URL(url);
225
+ parsed.searchParams.delete("toolsets");
226
+ if (!toolsets)
227
+ return parsed.href;
228
+ const query = parsed.search.slice(1);
229
+ parsed.search = `${query ? `${query}&` : ""}toolsets=${toolsets}`;
230
+ return parsed.href;
231
+ }
232
+ /** `?read_only=true` hides and rejects Hue's write tools for any key. */
233
+ export function readOnlyMcpUrl(url) {
234
+ const parsed = new URL(url);
235
+ parsed.searchParams.set("read_only", "true");
236
+ return parsed.href;
237
+ }
114
238
  class ConfigError extends Error {
115
239
  constructor(message) {
116
240
  super(message);
@@ -264,6 +388,9 @@ function parseMcpArguments(argv) {
264
388
  strict: true,
265
389
  options: {
266
390
  client: { type: "string" },
391
+ auth: { type: "string" },
392
+ "read-only": { type: "boolean", default: false },
393
+ toolsets: { type: "string" },
267
394
  url: { type: "string" },
268
395
  scope: { type: "string" },
269
396
  "dry-run": { type: "boolean", default: false },
@@ -276,26 +403,48 @@ function displayPath(cwd, path) {
276
403
  const shown = relative(cwd, path);
277
404
  return shown && !shown.startsWith("..") && !isAbsolute(shown) ? shown : path;
278
405
  }
279
- function planFor(client, scope, url) {
406
+ const notOnPath = (executable, label) => `${executable} is not on PATH. Run this where ${label} is installed:`;
407
+ /** `auth: "oauth"` is only planned for {@link OAUTH_CLIENTS}; the caller refuses the others. */
408
+ function planFor(client, auth, scope, url, toolsets) {
280
409
  const snippets = renderMcpSnippets(url);
410
+ // Claude Code and Codex take the same plan shape with or without a key.
411
+ const shared = auth === "oauth" ? renderMcpSignInSnippets(url, toolsets) : snippets;
412
+ const claudeUser = {
413
+ executable: "claude",
414
+ label: "Claude Code",
415
+ ...shared.claudeCodeCli,
416
+ fallback: [notOnPath("claude", "Claude Code")],
417
+ };
418
+ const codex = {
419
+ executable: "codex",
420
+ label: "Codex",
421
+ ...shared.codexCli,
422
+ fallback: [
423
+ "codex is not on PATH. Add this to ~/.codex/config.toml (or run the command where Codex is installed):",
424
+ shared.codexToml.trimEnd(),
425
+ ],
426
+ };
281
427
  switch (client) {
282
428
  case "claude-code":
283
429
  return scope === "user"
284
- ? {
285
- kind: "cli",
286
- executable: "claude",
287
- args: snippets.claudeCodeCli.args,
288
- display: snippets.claudeCodeCli.display,
289
- snippet: `${snippets.claudeCodeCli.display}\n`,
290
- fallback: ["claude is not on PATH. Run this where Claude Code is installed:"],
291
- }
430
+ ? { kind: "cli", commands: [claudeUser], snippet: `${claudeUser.display}\n` }
292
431
  : {
293
432
  kind: "file",
294
433
  file: ".mcp.json",
295
434
  key: "mcpServers",
296
- entry: snippets.claudeCodeServer,
297
- snippet: snippets.claudeCodeProjectJson,
435
+ entry: shared.claudeCodeServer,
436
+ snippet: shared.claudeCodeProjectJson,
298
437
  };
438
+ case "codex":
439
+ return { kind: "cli", commands: [codex], snippet: shared.codexToml };
440
+ case "conductor":
441
+ // Conductor has no MCP configuration of its own: its Claude Code and Codex agents read their
442
+ // user configuration in every workspace.
443
+ return {
444
+ kind: "cli",
445
+ commands: [claudeUser, codex],
446
+ snippet: `${claudeUser.display}\n${codex.display}\n`,
447
+ };
299
448
  case "cursor":
300
449
  return {
301
450
  kind: "file",
@@ -304,18 +453,6 @@ function planFor(client, scope, url) {
304
453
  entry: snippets.cursorServer,
305
454
  snippet: snippets.cursorJson,
306
455
  };
307
- case "codex":
308
- return {
309
- kind: "cli",
310
- executable: "codex",
311
- args: snippets.codexCli.args,
312
- display: snippets.codexCli.display,
313
- snippet: snippets.codexToml,
314
- fallback: [
315
- "codex is not on PATH. Add this to ~/.codex/config.toml (or run the command where Codex is installed):",
316
- snippets.codexToml.trimEnd(),
317
- ],
318
- };
319
456
  case "vscode":
320
457
  return {
321
458
  kind: "file",
@@ -331,27 +468,51 @@ function planFor(client, scope, url) {
331
468
  snippet: snippets.windsurfJson,
332
469
  hint: "Merge this into ~/.codeium/windsurf/mcp_config.json (Windsurf > Settings > MCP); the command does not write to your home directory.",
333
470
  };
334
- case "gemini":
335
- return {
336
- kind: "cli",
471
+ case "gemini": {
472
+ const gemini = {
337
473
  executable: "gemini",
338
- args: snippets.geminiCli.args,
339
- display: snippets.geminiCli.display,
340
- snippet: `${snippets.geminiCli.display}\n`,
341
- fallback: ["gemini is not on PATH. Run this where Gemini CLI is installed:"],
474
+ label: "Gemini CLI",
475
+ ...snippets.geminiCli,
476
+ fallback: [notOnPath("gemini", "Gemini CLI")],
342
477
  };
478
+ return { kind: "cli", commands: [gemini], snippet: `${gemini.display}\n` };
479
+ }
343
480
  }
344
481
  }
345
- function nextSteps(client) {
482
+ function nextSteps(client, auth, toolsets) {
346
483
  const label = CLIENT_LABELS[client];
347
- const lines = client === "vscode"
348
- ? [
484
+ const approve = "Sign in to Hue and approve the connection. It can read and write every active project in the organization you choose, within your role; for read-only access, use a Read project key with --auth key.";
485
+ let lines;
486
+ if (auth === "oauth")
487
+ lines =
488
+ client === "claude-code"
489
+ ? [`In Claude Code, run /mcp, select ${SERVER_NAME} and choose Authenticate. ${approve}`]
490
+ : client === "codex"
491
+ ? [
492
+ `If Codex did not open Hue in your browser, run: codex mcp login ${SERVER_NAME}`,
493
+ approve,
494
+ ]
495
+ : [
496
+ `In Conductor, open MCP status from the plug icon or /mcp-status, refresh, and use ${SERVER_NAME}'s authentication action. ${approve}`,
497
+ "Start a new agent session if the Hue tools do not appear. Hue has not yet verified signed-in Conductor end to end; if the tools still do not load, rerun with --auth key.",
498
+ ];
499
+ else if (client === "vscode")
500
+ lines = [
349
501
  `${label} prompts for the key (input ${INPUT_ID}) when the server starts; paste the ${ENV_VAR} value that hue login stored in .env.hue.`,
350
- ]
351
- : [
502
+ ];
503
+ else if (client === "conductor")
504
+ lines = [
505
+ `Conductor agents read ${ENV_VAR} from the login-shell environment Conductor captures, so export it there, not only in a terminal; hue login stores it in .env.hue. Sign-in (--auth oauth) needs no variable.`,
506
+ ];
507
+ else
508
+ lines = [
352
509
  `Export ${ENV_VAR} in the shell that starts ${label}; hue login stores it in .env.hue:`,
353
510
  " set -a; . ./.env.hue; set +a",
511
+ `An app started from the Dock or a launcher does not see that shell's variables; start ${label} from the shell${OAUTH_CLIENTS.includes(client) ? ", or sign in instead with --auth oauth" : ""}.`,
354
512
  ];
513
+ // `codex mcp add` stores no headers, so a sign-in toolset selection is added by hand.
514
+ if (auth === "oauth" && toolsets && (client === "codex" || client === "conductor"))
515
+ lines.push(`To list only the ${toolsets} tools in Codex, add this line under [mcp_servers.${SERVER_NAME}] in ~/.codex/config.toml:`, ` http_headers = { "${TOOLSETS_HEADER}" = "${toolsets}" }`);
355
516
  return [...lines, "Then ask your agent:", ` ${MCP_VERIFY_PROMPT}`];
356
517
  }
357
518
  /**
@@ -387,20 +548,52 @@ export async function runMcpCommand(argv, io = {}) {
387
548
  const client = parsed.values.client;
388
549
  if (!client || !CLIENT_IDS.includes(client))
389
550
  return fail(`${client ? `Unknown client: ${client}.` : "--client is required."} Choose one of ${CLIENT_IDS.join(", ")}.\n\n${MCP_USAGE}`, 2);
551
+ const clientId = client;
552
+ const auth = parsed.values.auth ?? (clientId === "conductor" ? "oauth" : "key");
553
+ if (auth !== "key" && auth !== "oauth")
554
+ return fail(`--auth must be key or oauth.\n\n${MCP_USAGE}`, 2);
555
+ if (auth === "oauth" && !OAUTH_CLIENTS.includes(clientId))
556
+ return fail(clientId === "cursor"
557
+ ? "--auth oauth is not available for cursor: Hue does not yet accept Cursor's sign-in callback. Use a key (the default), or sign in from claude-code or codex."
558
+ : `--auth oauth is available for ${OAUTH_CLIENTS.join(", ")}. Use a key (the default) for ${clientId}.`, 2);
390
559
  const scope = parsed.values.scope ?? "project";
391
560
  if (scope !== "project" && scope !== "user")
392
561
  return fail(`--scope must be project or user.\n\n${MCP_USAGE}`, 2);
393
- if (scope === "user" && client !== "claude-code")
562
+ if (scope === "user" && clientId !== "claude-code")
394
563
  return fail("--scope user is only available with --client claude-code.", 2);
395
- const url = parseMcpUrl(parsed.values.url ?? DEFAULT_MCP_URL);
564
+ let url = parseMcpUrl(parsed.values.url ?? DEFAULT_MCP_URL);
396
565
  if (!url)
397
566
  return fail("--url must be an HTTPS URL such as https://mcp.hue.run/mcp (plain HTTP is accepted for loopback test servers only).", 2);
398
- const plan = planFor(client, scope, url);
567
+ // A sign-in connection's access is chosen when it is approved; read_only is not part of it.
568
+ if (auth === "oauth" &&
569
+ (parsed.values["read-only"] || new URL(url).searchParams.has("read_only")))
570
+ return fail("A sign-in connection has Read and write access and cannot be made read-only by its URL. For read-only access, use a Read project key (--auth key), or add --read-only to a key configuration.", 2);
571
+ if (parsed.values["read-only"])
572
+ url = readOnlyMcpUrl(url);
573
+ // Toolsets are not part of the protected resource either: a sign-in URL stays bare.
574
+ if (auth === "oauth" && new URL(url).searchParams.has("toolsets"))
575
+ return fail("Leave toolsets off a sign-in URL; --toolsets sends them in a header instead.", 2);
576
+ // --toolsets wins over a selection already in --url, which wins over the client's default.
577
+ // Both sources are validated: Hue ignores an unknown name and would list every tool.
578
+ const requested = parsed.values.toolsets ?? new URL(url).searchParams.get("toolsets");
579
+ let toolsets = DEFAULT_TOOLSETS[clientId];
580
+ if (requested !== null) {
581
+ const selected = parseToolsets(requested);
582
+ if ("error" in selected)
583
+ return fail(`${parsed.values.toolsets === undefined ? "In --url: " : ""}${selected.error}\n\n${MCP_USAGE}`, 2);
584
+ toolsets = selected.toolsets;
585
+ }
586
+ // `all` is Hue's default, so it needs no selection.
587
+ if (toolsets === "all")
588
+ toolsets = undefined;
589
+ if (auth === "key")
590
+ url = toolsetsMcpUrl(url, toolsets);
591
+ const plan = planFor(clientId, auth, scope, url, auth === "oauth" ? toolsets : undefined);
399
592
  if (parsed.values.print) {
400
593
  stdout.write(plan.snippet);
401
594
  return 0;
402
595
  }
403
- const steps = nextSteps(client);
596
+ const steps = nextSteps(clientId, auth, toolsets);
404
597
  if (plan.kind === "manual") {
405
598
  out(plan.hint);
406
599
  stdout.write(plan.snippet);
@@ -410,25 +603,30 @@ export async function runMcpCommand(argv, io = {}) {
410
603
  }
411
604
  if (plan.kind === "cli") {
412
605
  if (parsed.values["dry-run"]) {
413
- out(`Would run: ${plan.display}`);
606
+ for (const command of plan.commands)
607
+ out(`Would run: ${command.display}`);
414
608
  return 0;
415
609
  }
416
- const executable = await findExecutable(plan.executable, env);
417
- if (!executable) {
418
- for (const line of plan.fallback)
419
- out(line);
420
- out(plan.display);
421
- for (const line of steps)
422
- out(line);
423
- return 0;
610
+ let failed = false;
611
+ for (const command of plan.commands) {
612
+ const executable = await findExecutable(command.executable, env);
613
+ if (!executable) {
614
+ for (const line of command.fallback)
615
+ out(line);
616
+ out(command.display);
617
+ continue;
618
+ }
619
+ out(`Running: ${command.display}`);
620
+ const code = await runClientCli(executable, command.args, { cwd, env, stdout, stderr });
621
+ if (code === 0) {
622
+ out(`Registered the "${SERVER_NAME}" MCP server (${url}) with ${command.label}.`);
623
+ continue;
624
+ }
625
+ failed = true;
626
+ stderr.write(`${command.executable} ${code === null ? "could not be started" : `exited with code ${code}`}. Run this command yourself:\n${command.display}\n`);
424
627
  }
425
- out(`Running: ${plan.display}`);
426
- const code = await runClientCli(executable, plan.args, { cwd, env, stdout, stderr });
427
- if (code === null)
428
- return fail(`${plan.executable} could not be started. Run this command yourself:\n${plan.display}`);
429
- if (code !== 0)
430
- return fail(`${plan.executable} exited with code ${code}. Run this command yourself:\n${plan.display}`);
431
- out(`Registered the "${SERVER_NAME}" MCP server (${url}) with ${CLIENT_LABELS[client]}.`);
628
+ if (failed)
629
+ return 1;
432
630
  for (const line of steps)
433
631
  out(line);
434
632
  return 0;
package/dist/client.d.ts CHANGED
@@ -129,11 +129,14 @@ export declare class HueClient {
129
129
  * `hue.tool()` call saw: OpenAI Responses `mcp_call`, `web_search_call`, `file_search_call` and
130
130
  * `code_interpreter_call` items, and Anthropic Messages `mcp_tool_use` / `server_tool_use`
131
131
  * blocks with their result blocks. Each becomes an `execute_tool {name}` child span of the active
132
- * (or given) context with `gen_ai.tool.type` `extension` and `gen_ai.tool.call.id`; MCP calls add
132
+ * (or given) context with `gen_ai.tool.type` `extension`, `gen_ai.tool.call.id` and
133
+ * `hue.tool.call.position` (the item's 0-based position in the response); MCP calls add
133
134
  * `mcp.server.name` (the provider's label, or the `servers` entry for it). Arguments and results
134
- * follow `captureContent`; a failed call carries `error.type` and ERROR status. An OpenAI
135
- * `mcp_list_tools` item becomes a `tools/list` child span carrying that server's tools as
136
- * `gen_ai.tool.definitions`. Call it inside `hue.model()` so the spans nest under the model call
135
+ * follow `captureContent`; a failed call carries `error.type` and ERROR status, and with content
136
+ * capture a failed OpenAI MCP call's status description is the provider's scrubbed, bounded error
137
+ * text. An OpenAI `mcp_list_tools` item becomes a `tools/list` child span carrying that server's
138
+ * tools as `gen_ai.tool.definitions`, or without content capture only `hue.tool.names` and
139
+ * `hue.tool.definitions.sha256`. Call it inside `hue.model()` so the spans nest under the model call
137
140
  * and `provider` defaults to its provider; pass `request` to record each server's host as
138
141
  * `server.address`. The spans have no duration of their own: the provider ran the tools inside
139
142
  * the model request. Unreadable items are skipped and counted; nothing is thrown.