@hue-run/sdk 0.10.0 → 0.11.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.
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 no credential. A key value
6
+ * is 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,30 @@ 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
+ /**
27
+ * The header form of `?toolsets=`. Only the sign-in Codex TOML uses it, as Hue's guide does, to
28
+ * keep that URL bare; every other configuration carries the selection in its URL.
29
+ */
30
+ export declare const TOOLSETS_HEADER = "X-Hue-MCP-Toolsets";
31
+ 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 Tools to list, comma-separated: all, observe (production reads),\n project, traces, evals, environments, intents or docs, added to the\n URL as ?toolsets=. Defaults: all for claude-code, codex and conductor,\n which search their own tools; observe for cursor. Without a selection\n Hue lists the production reads and search_hue_tools, which reaches\n every other tool\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.";
32
+ /**
33
+ * A URL as a POSIX shell word for printed commands: `?` is a zsh glob and `&` ends a command, so a
34
+ * URL with a query (such as `?read_only=true`) is single-quoted. Executed commands pass argv.
35
+ */
36
+ export declare function shellWord(value: string): string;
37
+ /**
38
+ * Canonical Hue client snippets for a key configuration; the JSON values are also the merge
39
+ * entries for config files.
40
+ */
19
41
  export declare function renderMcpSnippets(url: string): {
20
42
  claudeCodeServer: {
21
43
  type: string;
@@ -67,6 +89,43 @@ export declare function renderMcpSnippets(url: string): {
67
89
  display: string;
68
90
  };
69
91
  };
92
+ /**
93
+ * Sign-in snippets: the server URL and nothing secret. The client discovers Hue's OAuth metadata
94
+ * from it and sends it, query included, on every request; the token's resource ignores the query,
95
+ * so `?toolsets=` selects tools here as it does for a key.
96
+ */
97
+ export declare function renderMcpSignInSnippets(url: string): {
98
+ claudeCodeServer: {
99
+ type: string;
100
+ url: string;
101
+ };
102
+ claudeCodeProjectJson: string;
103
+ claudeCodeCli: {
104
+ args: string[];
105
+ display: string;
106
+ };
107
+ codexCli: {
108
+ args: string[];
109
+ display: string;
110
+ };
111
+ codexToml: string;
112
+ };
113
+ /**
114
+ * Parses a comma-separated `--toolsets` value into Hue's canonical form, or returns an error.
115
+ * Unknown names are refused: Hue ignores them and would list its default, hiding a typo.
116
+ */
117
+ export declare function parseToolsets(value: string): {
118
+ toolsets: string;
119
+ } | {
120
+ error: string;
121
+ };
122
+ /**
123
+ * `?toolsets=` lists only the selected tools; commas stay readable. Without a selection the
124
+ * parameter is removed, so the connection lists Hue's default.
125
+ */
126
+ export declare function toolsetsMcpUrl(url: string, toolsets: string | undefined): string;
127
+ /** `?read_only=true` hides and rejects Hue's write tools for any key. */
128
+ export declare function readOnlyMcpUrl(url: string): string;
70
129
  /** Validates the MCP endpoint: HTTPS, or HTTP for loopback test servers; no credentials or hash. */
71
130
  export declare function parseMcpUrl(value: string): string | null;
72
131
  /**
package/dist/cli/mcp.js CHANGED
@@ -10,37 +10,110 @@ 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
+ /**
52
+ * The header form of `?toolsets=`. Only the sign-in Codex TOML uses it, as Hue's guide does, to
53
+ * keep that URL bare; every other configuration carries the selection in its URL.
54
+ */
55
+ export const TOOLSETS_HEADER = "X-Hue-MCP-Toolsets";
56
+ /**
57
+ * Toolsets a client's configuration selects unless `--toolsets` says otherwise. Claude Code,
58
+ * Codex and Conductor's agents defer MCP tools behind their own tool search, so listing every tool
59
+ * costs them little. Cursor caps the tools it loads, so it names the production reads. The other
60
+ * clients list Hue's default: the production reads, and `search_hue_tools` with its executors,
61
+ * which reach every other tool.
62
+ */
63
+ const DEFAULT_TOOLSETS = {
64
+ "claude-code": "all",
65
+ codex: "all",
66
+ conductor: "all",
67
+ cursor: "observe",
68
+ };
69
+ /** Codex runs a server's tool calls one at a time without it; each Hue call is independent. */
70
+ const CODEX_PARALLEL = "supports_parallel_tool_calls = true";
71
+ export const MCP_USAGE = `Usage: hue mcp install --client <claude-code|codex|conductor|cursor|vscode|windsurf|gemini>
72
+ [--auth key|oauth] [--read-only] [--toolsets NAMES] [--url URL]
73
+ [--scope project|user] [--dry-run] [--print]
26
74
 
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.
75
+ Configure a coding agent to use the Hue MCP server. A key configuration references the
76
+ ${ENV_VAR} environment variable; a key value is never written. A sign-in configuration holds
77
+ only the URL: the client opens Hue in a browser, where you approve access to the projects of
78
+ one organization.
29
79
 
30
80
  Options:
31
81
  --client NAME Coding agent to configure (required)
82
+ --auth MODE key: reference ${ENV_VAR} (the default, except for conductor)
83
+ oauth: URL only, sign in with Hue in the client (claude-code, codex and
84
+ conductor; the default for conductor)
85
+ --read-only key only: add ?read_only=true to the URL so write tools are hidden. A
86
+ sign-in connection has Read and write access; use a Read key for read-only
87
+ --toolsets NAMES
88
+ Tools to list, comma-separated: all, observe (production reads),
89
+ project, traces, evals, environments, intents or docs, added to the
90
+ URL as ?toolsets=. Defaults: all for claude-code, codex and conductor,
91
+ which search their own tools; observe for cursor. Without a selection
92
+ Hue lists the production reads and search_hue_tools, which reaches
93
+ every other tool
32
94
  --url URL Hue MCP endpoint (default ${DEFAULT_MCP_URL})
33
95
  --scope SCOPE claude-code only: project writes .mcp.json (default); user runs
34
96
  \`claude mcp add --scope user\`
35
- --dry-run Print the resulting file content or command without writing or running
97
+ --dry-run Print the resulting file content or commands without writing or running
36
98
  --print Print the configuration snippet only
37
99
  -h, --help Show this help
38
100
 
39
101
  Files: claude-code .mcp.json, cursor .cursor/mcp.json, vscode .vscode/mcp.json (relative to the
40
102
  current directory). codex and gemini use their own CLI when it is on PATH; windsurf prints the
41
- snippet for its user configuration file.`;
103
+ snippet for its user configuration file. conductor registers the server for its Claude Code
104
+ (user scope) and Codex agents with their CLIs, printing the command for a CLI not on PATH.`;
42
105
  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. */
106
+ /**
107
+ * A URL as a POSIX shell word for printed commands: `?` is a zsh glob and `&` ends a command, so a
108
+ * URL with a query (such as `?read_only=true`) is single-quoted. Executed commands pass argv.
109
+ */
110
+ export function shellWord(value) {
111
+ return /^[\w@%+=:,./-]+$/u.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
112
+ }
113
+ /**
114
+ * Canonical Hue client snippets for a key configuration; the JSON values are also the merge
115
+ * entries for config files.
116
+ */
44
117
  export function renderMcpSnippets(url) {
45
118
  const bearer = (reference) => `Bearer ${reference}`;
46
119
  const claudeCodeServer = {
@@ -57,7 +130,7 @@ export function renderMcpSnippets(url) {
57
130
  const vscodeInput = {
58
131
  type: "promptString",
59
132
  id: INPUT_ID,
60
- description: "Hue coding-agent key",
133
+ description: "Hue Read or Read and write API key",
61
134
  password: true,
62
135
  };
63
136
  const windsurfServer = {
@@ -80,15 +153,15 @@ export function renderMcpSnippets(url) {
80
153
  "--header",
81
154
  `Authorization: Bearer \${${ENV_VAR}}`,
82
155
  ],
83
- display: `claude mcp add --transport http --scope user ${SERVER_NAME} ${url} --header 'Authorization: Bearer \${${ENV_VAR}}'`,
156
+ display: `claude mcp add --transport http --scope user ${SERVER_NAME} ${shellWord(url)} --header 'Authorization: Bearer \${${ENV_VAR}}'`,
84
157
  },
85
158
  cursorServer,
86
159
  cursorJson: json({ mcpServers: { [SERVER_NAME]: cursorServer } }),
87
160
  codexCli: {
88
161
  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}`,
162
+ display: `codex mcp add ${SERVER_NAME} --url ${shellWord(url)} --bearer-token-env-var ${ENV_VAR}`,
90
163
  },
91
- codexToml: `[mcp_servers.${SERVER_NAME}]\nurl = "${url}"\nbearer_token_env_var = "${ENV_VAR}"\n`,
164
+ codexToml: `[mcp_servers.${SERVER_NAME}]\nurl = "${url}"\nbearer_token_env_var = "${ENV_VAR}"\n${CODEX_PARALLEL}\n`,
92
165
  vscodeServer,
93
166
  vscodeInput,
94
167
  vscodeJson: json({ servers: { [SERVER_NAME]: vscodeServer }, inputs: [vscodeInput] }),
@@ -98,19 +171,78 @@ export function renderMcpSnippets(url) {
98
171
  args: [
99
172
  "mcp",
100
173
  "add",
174
+ "--scope",
175
+ "user",
101
176
  "--transport",
102
177
  "http",
103
178
  SERVER_NAME,
104
179
  url,
105
- "-H",
106
- `Authorization: Bearer $${ENV_VAR}`,
180
+ "--header",
181
+ `Authorization: Bearer \${${ENV_VAR}}`,
107
182
  ],
108
183
  // 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}'`,
184
+ // key is exported; Gemini CLI expands ${HUE_MCP_KEY} from its settings at connection time.
185
+ display: `gemini mcp add --scope user --transport http ${SERVER_NAME} ${shellWord(url)} --header 'Authorization: Bearer \${${ENV_VAR}}'`,
186
+ },
187
+ };
188
+ }
189
+ /**
190
+ * Sign-in snippets: the server URL and nothing secret. The client discovers Hue's OAuth metadata
191
+ * from it and sends it, query included, on every request; the token's resource ignores the query,
192
+ * so `?toolsets=` selects tools here as it does for a key.
193
+ */
194
+ export function renderMcpSignInSnippets(url) {
195
+ const toolsets = new URL(url).searchParams.get("toolsets");
196
+ const claudeCodeServer = { type: "http", url };
197
+ return {
198
+ claudeCodeServer,
199
+ claudeCodeProjectJson: json({ mcpServers: { [SERVER_NAME]: claudeCodeServer } }),
200
+ claudeCodeCli: {
201
+ args: ["mcp", "add", "--transport", "http", "--scope", "user", SERVER_NAME, url],
202
+ display: `claude mcp add --transport http --scope user ${SERVER_NAME} ${shellWord(url)}`,
203
+ },
204
+ codexCli: {
205
+ args: ["mcp", "add", SERVER_NAME, "--url", url],
206
+ display: `codex mcp add ${SERVER_NAME} --url ${shellWord(url)}`,
111
207
  },
208
+ // The header form of the same selection, for a TOML entry that keeps its URL bare.
209
+ codexToml: `[mcp_servers.${SERVER_NAME}]\nurl = "${toolsetsMcpUrl(url, undefined)}"\n${toolsets ? `http_headers = { "${TOOLSETS_HEADER}" = "${toolsets}" }\n` : ""}${CODEX_PARALLEL}\n`,
112
210
  };
113
211
  }
212
+ /**
213
+ * Parses a comma-separated `--toolsets` value into Hue's canonical form, or returns an error.
214
+ * Unknown names are refused: Hue ignores them and would list its default, hiding a typo.
215
+ */
216
+ export function parseToolsets(value) {
217
+ const names = [...new Set(value.split(",").map((name) => name.trim()))];
218
+ if (names.some((name) => !name))
219
+ return { error: "--toolsets takes comma-separated names, such as observe or traces,docs." };
220
+ const unknown = names.filter((name) => !MCP_TOOLSETS.includes(name));
221
+ if (unknown.length)
222
+ return {
223
+ error: `Unknown toolset${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}. Choose from ${MCP_TOOLSETS.join(", ")}.`,
224
+ };
225
+ return { toolsets: names.join(",") };
226
+ }
227
+ /**
228
+ * `?toolsets=` lists only the selected tools; commas stay readable. Without a selection the
229
+ * parameter is removed, so the connection lists Hue's default.
230
+ */
231
+ export function toolsetsMcpUrl(url, toolsets) {
232
+ const parsed = new URL(url);
233
+ parsed.searchParams.delete("toolsets");
234
+ if (!toolsets)
235
+ return parsed.href;
236
+ const query = parsed.search.slice(1);
237
+ parsed.search = `${query ? `${query}&` : ""}toolsets=${toolsets}`;
238
+ return parsed.href;
239
+ }
240
+ /** `?read_only=true` hides and rejects Hue's write tools for any key. */
241
+ export function readOnlyMcpUrl(url) {
242
+ const parsed = new URL(url);
243
+ parsed.searchParams.set("read_only", "true");
244
+ return parsed.href;
245
+ }
114
246
  class ConfigError extends Error {
115
247
  constructor(message) {
116
248
  super(message);
@@ -264,6 +396,9 @@ function parseMcpArguments(argv) {
264
396
  strict: true,
265
397
  options: {
266
398
  client: { type: "string" },
399
+ auth: { type: "string" },
400
+ "read-only": { type: "boolean", default: false },
401
+ toolsets: { type: "string" },
267
402
  url: { type: "string" },
268
403
  scope: { type: "string" },
269
404
  "dry-run": { type: "boolean", default: false },
@@ -276,26 +411,48 @@ function displayPath(cwd, path) {
276
411
  const shown = relative(cwd, path);
277
412
  return shown && !shown.startsWith("..") && !isAbsolute(shown) ? shown : path;
278
413
  }
279
- function planFor(client, scope, url) {
414
+ const notOnPath = (executable, label) => `${executable} is not on PATH. Run this where ${label} is installed:`;
415
+ /** `auth: "oauth"` is only planned for {@link OAUTH_CLIENTS}; the caller refuses the others. */
416
+ function planFor(client, auth, scope, url) {
280
417
  const snippets = renderMcpSnippets(url);
418
+ // Claude Code and Codex take the same plan shape with or without a key.
419
+ const shared = auth === "oauth" ? renderMcpSignInSnippets(url) : snippets;
420
+ const claudeUser = {
421
+ executable: "claude",
422
+ label: "Claude Code",
423
+ ...shared.claudeCodeCli,
424
+ fallback: [notOnPath("claude", "Claude Code")],
425
+ };
426
+ const codex = {
427
+ executable: "codex",
428
+ label: "Codex",
429
+ ...shared.codexCli,
430
+ fallback: [
431
+ "codex is not on PATH. Add this to ~/.codex/config.toml (or run the command where Codex is installed):",
432
+ shared.codexToml.trimEnd(),
433
+ ],
434
+ };
281
435
  switch (client) {
282
436
  case "claude-code":
283
437
  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
- }
438
+ ? { kind: "cli", commands: [claudeUser], snippet: `${claudeUser.display}\n` }
292
439
  : {
293
440
  kind: "file",
294
441
  file: ".mcp.json",
295
442
  key: "mcpServers",
296
- entry: snippets.claudeCodeServer,
297
- snippet: snippets.claudeCodeProjectJson,
443
+ entry: shared.claudeCodeServer,
444
+ snippet: shared.claudeCodeProjectJson,
298
445
  };
446
+ case "codex":
447
+ return { kind: "cli", commands: [codex], snippet: shared.codexToml };
448
+ case "conductor":
449
+ // Conductor has no MCP configuration of its own: its Claude Code and Codex agents read their
450
+ // user configuration in every workspace.
451
+ return {
452
+ kind: "cli",
453
+ commands: [claudeUser, codex],
454
+ snippet: `${claudeUser.display}\n${codex.display}\n`,
455
+ };
299
456
  case "cursor":
300
457
  return {
301
458
  kind: "file",
@@ -304,18 +461,6 @@ function planFor(client, scope, url) {
304
461
  entry: snippets.cursorServer,
305
462
  snippet: snippets.cursorJson,
306
463
  };
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
464
  case "vscode":
320
465
  return {
321
466
  kind: "file",
@@ -331,27 +476,51 @@ function planFor(client, scope, url) {
331
476
  snippet: snippets.windsurfJson,
332
477
  hint: "Merge this into ~/.codeium/windsurf/mcp_config.json (Windsurf > Settings > MCP); the command does not write to your home directory.",
333
478
  };
334
- case "gemini":
335
- return {
336
- kind: "cli",
479
+ case "gemini": {
480
+ const gemini = {
337
481
  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:"],
482
+ label: "Gemini CLI",
483
+ ...snippets.geminiCli,
484
+ fallback: [notOnPath("gemini", "Gemini CLI")],
342
485
  };
486
+ return { kind: "cli", commands: [gemini], snippet: `${gemini.display}\n` };
487
+ }
343
488
  }
344
489
  }
345
- function nextSteps(client) {
490
+ function nextSteps(client, auth) {
346
491
  const label = CLIENT_LABELS[client];
347
- const lines = client === "vscode"
348
- ? [
492
+ 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.";
493
+ let lines;
494
+ if (auth === "oauth")
495
+ lines =
496
+ client === "claude-code"
497
+ ? [`In Claude Code, run /mcp, select ${SERVER_NAME} and choose Authenticate. ${approve}`]
498
+ : client === "codex"
499
+ ? [
500
+ `If Codex did not open Hue in your browser, run: codex mcp login ${SERVER_NAME}`,
501
+ approve,
502
+ ]
503
+ : [
504
+ `In Conductor, open MCP status from the plug icon or /mcp-status, refresh, and use ${SERVER_NAME}'s authentication action. ${approve}`,
505
+ "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.",
506
+ ];
507
+ else if (client === "vscode")
508
+ lines = [
349
509
  `${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
- : [
510
+ ];
511
+ else if (client === "conductor")
512
+ lines = [
513
+ `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.`,
514
+ ];
515
+ else
516
+ lines = [
352
517
  `Export ${ENV_VAR} in the shell that starts ${label}; hue login stores it in .env.hue:`,
353
518
  " set -a; . ./.env.hue; set +a",
519
+ `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
520
  ];
521
+ // `codex mcp add` has no option for it, so a registered server gets the line by hand.
522
+ if (client === "codex" || client === "conductor")
523
+ lines.push(`If codex mcp add registered ${SERVER_NAME}, add this line under [mcp_servers.${SERVER_NAME}] in ~/.codex/config.toml so Codex runs Hue's tool calls in parallel:`, ` ${CODEX_PARALLEL}`);
355
524
  return [...lines, "Then ask your agent:", ` ${MCP_VERIFY_PROMPT}`];
356
525
  }
357
526
  /**
@@ -387,20 +556,45 @@ export async function runMcpCommand(argv, io = {}) {
387
556
  const client = parsed.values.client;
388
557
  if (!client || !CLIENT_IDS.includes(client))
389
558
  return fail(`${client ? `Unknown client: ${client}.` : "--client is required."} Choose one of ${CLIENT_IDS.join(", ")}.\n\n${MCP_USAGE}`, 2);
559
+ const clientId = client;
560
+ const auth = parsed.values.auth ?? (clientId === "conductor" ? "oauth" : "key");
561
+ if (auth !== "key" && auth !== "oauth")
562
+ return fail(`--auth must be key or oauth.\n\n${MCP_USAGE}`, 2);
563
+ if (auth === "oauth" && !OAUTH_CLIENTS.includes(clientId))
564
+ return fail(clientId === "cursor"
565
+ ? "--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."
566
+ : `--auth oauth is available for ${OAUTH_CLIENTS.join(", ")}. Use a key (the default) for ${clientId}.`, 2);
390
567
  const scope = parsed.values.scope ?? "project";
391
568
  if (scope !== "project" && scope !== "user")
392
569
  return fail(`--scope must be project or user.\n\n${MCP_USAGE}`, 2);
393
- if (scope === "user" && client !== "claude-code")
570
+ if (scope === "user" && clientId !== "claude-code")
394
571
  return fail("--scope user is only available with --client claude-code.", 2);
395
- const url = parseMcpUrl(parsed.values.url ?? DEFAULT_MCP_URL);
572
+ let url = parseMcpUrl(parsed.values.url ?? DEFAULT_MCP_URL);
396
573
  if (!url)
397
574
  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);
575
+ // A sign-in connection's access is chosen when it is approved; read_only is not part of it.
576
+ if (auth === "oauth" &&
577
+ (parsed.values["read-only"] || new URL(url).searchParams.has("read_only")))
578
+ 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);
579
+ if (parsed.values["read-only"])
580
+ url = readOnlyMcpUrl(url);
581
+ // --toolsets wins over a selection already in --url, which wins over the client's default.
582
+ // Both sources are validated: Hue ignores an unknown name and would list its default.
583
+ const requested = parsed.values.toolsets ?? new URL(url).searchParams.get("toolsets");
584
+ let toolsets = DEFAULT_TOOLSETS[clientId];
585
+ if (requested !== null) {
586
+ const selected = parseToolsets(requested);
587
+ if ("error" in selected)
588
+ return fail(`${parsed.values.toolsets === undefined ? "In --url: " : ""}${selected.error}\n\n${MCP_USAGE}`, 2);
589
+ toolsets = selected.toolsets;
590
+ }
591
+ url = toolsetsMcpUrl(url, toolsets);
592
+ const plan = planFor(clientId, auth, scope, url);
399
593
  if (parsed.values.print) {
400
594
  stdout.write(plan.snippet);
401
595
  return 0;
402
596
  }
403
- const steps = nextSteps(client);
597
+ const steps = nextSteps(clientId, auth);
404
598
  if (plan.kind === "manual") {
405
599
  out(plan.hint);
406
600
  stdout.write(plan.snippet);
@@ -410,25 +604,30 @@ export async function runMcpCommand(argv, io = {}) {
410
604
  }
411
605
  if (plan.kind === "cli") {
412
606
  if (parsed.values["dry-run"]) {
413
- out(`Would run: ${plan.display}`);
607
+ for (const command of plan.commands)
608
+ out(`Would run: ${command.display}`);
414
609
  return 0;
415
610
  }
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;
611
+ let failed = false;
612
+ for (const command of plan.commands) {
613
+ const executable = await findExecutable(command.executable, env);
614
+ if (!executable) {
615
+ for (const line of command.fallback)
616
+ out(line);
617
+ out(command.display);
618
+ continue;
619
+ }
620
+ out(`Running: ${command.display}`);
621
+ const code = await runClientCli(executable, command.args, { cwd, env, stdout, stderr });
622
+ if (code === 0) {
623
+ out(`Registered the "${SERVER_NAME}" MCP server (${url}) with ${command.label}.`);
624
+ continue;
625
+ }
626
+ failed = true;
627
+ stderr.write(`${command.executable} ${code === null ? "could not be started" : `exited with code ${code}`}. Run this command yourself:\n${command.display}\n`);
424
628
  }
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]}.`);
629
+ if (failed)
630
+ return 1;
432
631
  for (const line of steps)
433
632
  out(line);
434
633
  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.