@poncho-ai/cli 0.2.0 → 0.3.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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "./chunk-W7J5XM2X.js";
4
+ } from "./chunk-AS3CEZHY.js";
5
5
  import "./chunk-3BEWSRFW.js";
6
6
 
7
7
  // src/cli.ts
package/dist/index.d.ts CHANGED
@@ -37,11 +37,17 @@ declare const mcpAdd: (workingDir: string, options: {
37
37
  url?: string;
38
38
  name?: string;
39
39
  envVars?: string[];
40
+ authBearerEnv?: string;
40
41
  }) => Promise<void>;
41
42
  declare const mcpList: (workingDir: string) => Promise<void>;
42
43
  declare const mcpRemove: (workingDir: string, name: string) => Promise<void>;
44
+ declare const mcpToolsList: (workingDir: string, serverName: string) => Promise<void>;
45
+ declare const mcpToolsSelect: (workingDir: string, serverName: string, options: {
46
+ all?: boolean;
47
+ toolsCsv?: string;
48
+ }) => Promise<void>;
43
49
  declare const buildCli: () => Command;
44
50
  declare const main: (argv?: string[]) => Promise<void>;
45
51
  declare const packageRoot: string;
46
52
 
47
- export { type RequestHandler, addSkill, buildCli, buildTarget, createRequestHandler, initProject, listTools, main, mcpAdd, mcpList, mcpRemove, packageRoot, runInteractive, runOnce, runTests, startDevServer, updateAgentGuidance };
53
+ export { type RequestHandler, addSkill, buildCli, buildTarget, createRequestHandler, initProject, listTools, main, mcpAdd, mcpList, mcpRemove, mcpToolsList, mcpToolsSelect, packageRoot, runInteractive, runOnce, runTests, startDevServer, updateAgentGuidance };
package/dist/index.js CHANGED
@@ -9,13 +9,15 @@ import {
9
9
  mcpAdd,
10
10
  mcpList,
11
11
  mcpRemove,
12
+ mcpToolsList,
13
+ mcpToolsSelect,
12
14
  packageRoot,
13
15
  runInteractive,
14
16
  runOnce,
15
17
  runTests,
16
18
  startDevServer,
17
19
  updateAgentGuidance
18
- } from "./chunk-W7J5XM2X.js";
20
+ } from "./chunk-AS3CEZHY.js";
19
21
  import "./chunk-3BEWSRFW.js";
20
22
  export {
21
23
  addSkill,
@@ -28,6 +30,8 @@ export {
28
30
  mcpAdd,
29
31
  mcpList,
30
32
  mcpRemove,
33
+ mcpToolsList,
34
+ mcpToolsSelect,
31
35
  packageRoot,
32
36
  runInteractive,
33
37
  runOnce,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "CLI for building and deploying AI agents",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  "react": "^19.2.4",
26
26
  "react-devtools-core": "^6.1.5",
27
27
  "yaml": "^2.8.1",
28
- "@poncho-ai/harness": "0.2.0",
28
+ "@poncho-ai/harness": "0.3.1",
29
29
  "@poncho-ai/sdk": "0.2.0"
30
30
  },
31
31
  "devDependencies": {
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import { createRequire } from "node:module";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import {
14
14
  AgentHarness,
15
+ LocalMcpBridge,
15
16
  TelemetryEmitter,
16
17
  createConversationStore,
17
18
  loadPonchoConfig,
@@ -36,6 +37,7 @@ import {
36
37
  setCookie,
37
38
  verifyPassphrase,
38
39
  } from "./web-ui.js";
40
+ import { createInterface } from "node:readline/promises";
39
41
  import {
40
42
  runInitOnboarding,
41
43
  type InitOnboardingOptions,
@@ -172,17 +174,7 @@ Environment: {{runtime.environment}}
172
174
  - Use tools when needed
173
175
  - Explain your reasoning clearly
174
176
  - Ask clarifying questions when requirements are ambiguous
175
- - For setup/configuration/skills/MCP questions, proactively read \`README.md\` with \`read_file\` before answering.
176
- - Prefer concrete commands and examples from \`README.md\` over assumptions.
177
177
  - Never claim a file/tool change unless the corresponding tool call actually succeeded
178
-
179
- ## Default Capabilities in a Fresh Project
180
-
181
- - Built-in tools: \`list_directory\` and \`read_file\`
182
- - \`write_file\` is available in development, and disabled by default in production
183
- - A starter local skill is included (\`starter-echo\`)
184
- - Bash/shell commands are **not** available unless you install and enable a shell tool/skill
185
- - Git operations are only available if a git-capable tool/skill is configured
186
178
  `;
187
179
 
188
180
  /**
@@ -299,17 +291,48 @@ Connect remote MCP servers and expose their tools to the agent:
299
291
 
300
292
  \`\`\`bash
301
293
  # Add remote MCP server
302
- poncho mcp add --url wss://mcp.example.com/github --name github --env GITHUB_TOKEN
294
+ poncho mcp add --url https://mcp.example.com/github --name github --auth-bearer-env GITHUB_TOKEN
303
295
 
304
296
  # List configured servers
305
297
  poncho mcp list
306
298
 
299
+ # Discover and select MCP tools into config allowlist
300
+ poncho mcp tools list github
301
+ poncho mcp tools select github
302
+
307
303
  # Remove a server
308
304
  poncho mcp remove github
309
305
  \`\`\`
310
306
 
311
307
  Set required secrets in \`.env\` (for example, \`GITHUB_TOKEN=...\`).
312
308
 
309
+ ## Tool Intent in Frontmatter
310
+
311
+ Declare tool intent directly in \`AGENT.md\` and \`SKILL.md\` frontmatter:
312
+
313
+ \`\`\`yaml
314
+ tools:
315
+ mcp:
316
+ - github/list_issues
317
+ - github/*
318
+ scripts:
319
+ - starter/scripts/*
320
+ \`\`\`
321
+
322
+ How it works:
323
+
324
+ - \`AGENT.md\` provides fallback MCP intent when no skill is active.
325
+ - \`SKILL.md\` intent applies when you activate that skill (\`activate_skill\`).
326
+ - Skill scripts are accessible by default from each skill's \`scripts/\` directory.
327
+ - \`AGENT.md\` \`tools.scripts\` can still be used to narrow script access when active skills do not set script intent.
328
+ - Active skills are unioned, then filtered by policy in \`poncho.config.js\`.
329
+ - Deactivating a skill (\`deactivate_skill\`) removes its MCP tools from runtime registration.
330
+
331
+ Pattern format is strict slash-only:
332
+
333
+ - MCP: \`server/tool\`, \`server/*\`
334
+ - Scripts: \`skill/scripts/file.ts\`, \`skill/scripts/*\`
335
+
313
336
  ## Configuration
314
337
 
315
338
  Core files:
@@ -335,6 +358,21 @@ export default {
335
358
  telemetry: {
336
359
  enabled: true,
337
360
  },
361
+ mcp: [
362
+ {
363
+ name: "github",
364
+ url: "https://mcp.example.com/github",
365
+ auth: { type: "bearer", tokenEnv: "GITHUB_TOKEN" },
366
+ tools: {
367
+ mode: "allowlist",
368
+ include: ["github/list_issues", "github/get_issue"],
369
+ },
370
+ },
371
+ ],
372
+ scripts: {
373
+ mode: "allowlist",
374
+ include: ["starter/scripts/*"],
375
+ },
338
376
  tools: {
339
377
  defaults: {
340
378
  list_directory: true,
@@ -396,7 +434,6 @@ const VERCEL_RUNTIME_DEPENDENCIES: Record<string, string> = {
396
434
  mustache: "^4.2.0",
397
435
  openai: "^6.3.0",
398
436
  redis: "^5.10.0",
399
- ws: "^8.18.0",
400
437
  yaml: "^2.8.1",
401
438
  };
402
439
  const TEST_TEMPLATE = `tests:
@@ -548,6 +585,52 @@ const writeConfigFile = async (workingDir: string, config: PonchoConfig): Promis
548
585
  await writeFile(resolve(workingDir, "poncho.config.js"), serialized, "utf8");
549
586
  };
550
587
 
588
+ const ensureEnvPlaceholder = async (filePath: string, key: string): Promise<boolean> => {
589
+ const normalizedKey = key.trim();
590
+ if (!normalizedKey) {
591
+ return false;
592
+ }
593
+ let content = "";
594
+ try {
595
+ content = await readFile(filePath, "utf8");
596
+ } catch {
597
+ await writeFile(filePath, `${normalizedKey}=\n`, "utf8");
598
+ return true;
599
+ }
600
+ const present = content
601
+ .split(/\r?\n/)
602
+ .some((line) => line.trimStart().startsWith(`${normalizedKey}=`));
603
+ if (present) {
604
+ return false;
605
+ }
606
+ const withTrailingNewline = content.length === 0 || content.endsWith("\n")
607
+ ? content
608
+ : `${content}\n`;
609
+ await writeFile(filePath, `${withTrailingNewline}${normalizedKey}=\n`, "utf8");
610
+ return true;
611
+ };
612
+
613
+ const removeEnvPlaceholder = async (filePath: string, key: string): Promise<boolean> => {
614
+ const normalizedKey = key.trim();
615
+ if (!normalizedKey) {
616
+ return false;
617
+ }
618
+ let content = "";
619
+ try {
620
+ content = await readFile(filePath, "utf8");
621
+ } catch {
622
+ return false;
623
+ }
624
+ const lines = content.split(/\r?\n/);
625
+ const filtered = lines.filter((line) => !line.trimStart().startsWith(`${normalizedKey}=`));
626
+ if (filtered.length === lines.length) {
627
+ return false;
628
+ }
629
+ const nextContent = filtered.join("\n").replace(/\n+$/, "");
630
+ await writeFile(filePath, nextContent.length > 0 ? `${nextContent}\n` : "", "utf8");
631
+ return true;
632
+ };
633
+
551
634
  const gitInit = (cwd: string): Promise<boolean> =>
552
635
  new Promise((resolve) => {
553
636
  const child = spawn("git", ["init"], { cwd, stdio: "ignore" });
@@ -1588,6 +1671,7 @@ export const mcpAdd = async (
1588
1671
  url?: string;
1589
1672
  name?: string;
1590
1673
  envVars?: string[];
1674
+ authBearerEnv?: string;
1591
1675
  },
1592
1676
  ): Promise<void> => {
1593
1677
  const config = (await loadPonchoConfig(workingDir)) ?? { mcp: [] };
@@ -1595,14 +1679,56 @@ export const mcpAdd = async (
1595
1679
  if (!options.url) {
1596
1680
  throw new Error("Remote MCP only: provide --url for a remote MCP server.");
1597
1681
  }
1682
+ if (options.url.startsWith("ws://") || options.url.startsWith("wss://")) {
1683
+ throw new Error("WebSocket MCP URLs are no longer supported. Use an HTTP MCP endpoint.");
1684
+ }
1685
+ if (!options.url.startsWith("http://") && !options.url.startsWith("https://")) {
1686
+ throw new Error("Invalid MCP URL. Expected http:// or https://.");
1687
+ }
1688
+ const serverName = options.name ?? normalizeMcpName({ url: options.url });
1598
1689
  mcp.push({
1599
- name: options.name ?? normalizeMcpName({ url: options.url }),
1690
+ name: serverName,
1600
1691
  url: options.url,
1601
1692
  env: options.envVars ?? [],
1693
+ auth: options.authBearerEnv
1694
+ ? {
1695
+ type: "bearer",
1696
+ tokenEnv: options.authBearerEnv,
1697
+ }
1698
+ : undefined,
1602
1699
  });
1603
1700
 
1604
1701
  await writeConfigFile(workingDir, { ...config, mcp });
1605
- process.stdout.write("MCP server added.\n");
1702
+ let envSeedMessage: string | undefined;
1703
+ if (options.authBearerEnv) {
1704
+ const envPath = resolve(workingDir, ".env");
1705
+ const envExamplePath = resolve(workingDir, ".env.example");
1706
+ const addedEnv = await ensureEnvPlaceholder(envPath, options.authBearerEnv);
1707
+ const addedEnvExample = await ensureEnvPlaceholder(envExamplePath, options.authBearerEnv);
1708
+ if (addedEnv || addedEnvExample) {
1709
+ envSeedMessage = `Added ${options.authBearerEnv}= to ${addedEnv ? ".env" : ""}${addedEnv && addedEnvExample ? " and " : ""}${addedEnvExample ? ".env.example" : ""}.`;
1710
+ }
1711
+ }
1712
+ const nextSteps: string[] = [];
1713
+ let step = 1;
1714
+ if (options.authBearerEnv) {
1715
+ nextSteps.push(` ${step}) Set token in .env: ${options.authBearerEnv}=...`);
1716
+ step += 1;
1717
+ }
1718
+ nextSteps.push(` ${step}) Discover tools: poncho mcp tools list ${serverName}`);
1719
+ step += 1;
1720
+ nextSteps.push(` ${step}) Select tools: poncho mcp tools select ${serverName}`);
1721
+ step += 1;
1722
+ nextSteps.push(` ${step}) Verify config: poncho mcp list`);
1723
+ process.stdout.write(
1724
+ [
1725
+ `MCP server added: ${serverName}`,
1726
+ ...(envSeedMessage ? [envSeedMessage] : []),
1727
+ "Next steps:",
1728
+ ...nextSteps,
1729
+ "",
1730
+ ].join("\n"),
1731
+ );
1606
1732
  };
1607
1733
 
1608
1734
  export const mcpList = async (workingDir: string): Promise<void> => {
@@ -1610,20 +1736,204 @@ export const mcpList = async (workingDir: string): Promise<void> => {
1610
1736
  const mcp = config?.mcp ?? [];
1611
1737
  if (mcp.length === 0) {
1612
1738
  process.stdout.write("No MCP servers configured.\n");
1739
+ if (config?.scripts) {
1740
+ process.stdout.write(
1741
+ `Script policy: mode=${config.scripts.mode ?? "all"} include=${config.scripts.include?.length ?? 0} exclude=${config.scripts.exclude?.length ?? 0}\n`,
1742
+ );
1743
+ }
1613
1744
  return;
1614
1745
  }
1615
1746
  process.stdout.write("Configured MCP servers:\n");
1616
1747
  for (const entry of mcp) {
1617
- process.stdout.write(`- ${entry.name ?? entry.url} (remote: ${entry.url})\n`);
1748
+ const auth =
1749
+ entry.auth?.type === "bearer" ? `auth=bearer:${entry.auth.tokenEnv}` : "auth=none";
1750
+ const mode = entry.tools?.mode ?? "all";
1751
+ process.stdout.write(
1752
+ `- ${entry.name ?? entry.url} (remote: ${entry.url}, ${auth}, mode=${mode})\n`,
1753
+ );
1754
+ }
1755
+ if (config?.scripts) {
1756
+ process.stdout.write(
1757
+ `Script policy: mode=${config.scripts.mode ?? "all"} include=${config.scripts.include?.length ?? 0} exclude=${config.scripts.exclude?.length ?? 0}\n`,
1758
+ );
1618
1759
  }
1619
1760
  };
1620
1761
 
1621
1762
  export const mcpRemove = async (workingDir: string, name: string): Promise<void> => {
1622
1763
  const config = (await loadPonchoConfig(workingDir)) ?? { mcp: [] };
1623
1764
  const before = config.mcp ?? [];
1765
+ const removed = before.filter((entry) => normalizeMcpName(entry) === name);
1624
1766
  const filtered = before.filter((entry) => normalizeMcpName(entry) !== name);
1625
1767
  await writeConfigFile(workingDir, { ...config, mcp: filtered });
1768
+ const removedTokenEnvNames = new Set(
1769
+ removed
1770
+ .map((entry) =>
1771
+ entry.auth?.type === "bearer" ? entry.auth.tokenEnv?.trim() ?? "" : "",
1772
+ )
1773
+ .filter((value) => value.length > 0),
1774
+ );
1775
+ const stillUsedTokenEnvNames = new Set(
1776
+ filtered
1777
+ .map((entry) =>
1778
+ entry.auth?.type === "bearer" ? entry.auth.tokenEnv?.trim() ?? "" : "",
1779
+ )
1780
+ .filter((value) => value.length > 0),
1781
+ );
1782
+ const removedFromExample: string[] = [];
1783
+ for (const tokenEnv of removedTokenEnvNames) {
1784
+ if (stillUsedTokenEnvNames.has(tokenEnv)) {
1785
+ continue;
1786
+ }
1787
+ const changed = await removeEnvPlaceholder(resolve(workingDir, ".env.example"), tokenEnv);
1788
+ if (changed) {
1789
+ removedFromExample.push(tokenEnv);
1790
+ }
1791
+ }
1626
1792
  process.stdout.write(`Removed MCP server: ${name}\n`);
1793
+ if (removedFromExample.length > 0) {
1794
+ process.stdout.write(
1795
+ `Removed unused token placeholder(s) from .env.example: ${removedFromExample.join(", ")}\n`,
1796
+ );
1797
+ }
1798
+ };
1799
+
1800
+ const resolveMcpEntry = async (
1801
+ workingDir: string,
1802
+ serverName: string,
1803
+ ): Promise<{ config: PonchoConfig; index: number }> => {
1804
+ const config = (await loadPonchoConfig(workingDir)) ?? { mcp: [] };
1805
+ const entries = config.mcp ?? [];
1806
+ const index = entries.findIndex((entry) => normalizeMcpName(entry) === serverName);
1807
+ if (index < 0) {
1808
+ throw new Error(`MCP server "${serverName}" is not configured.`);
1809
+ }
1810
+ return { config, index };
1811
+ };
1812
+
1813
+ const discoverMcpTools = async (
1814
+ workingDir: string,
1815
+ serverName: string,
1816
+ ): Promise<string[]> => {
1817
+ dotenv.config({ path: resolve(workingDir, ".env") });
1818
+ const { config, index } = await resolveMcpEntry(workingDir, serverName);
1819
+ const entry = (config.mcp ?? [])[index];
1820
+ const bridge = new LocalMcpBridge({ mcp: [entry] });
1821
+ try {
1822
+ await bridge.startLocalServers();
1823
+ await bridge.discoverTools();
1824
+ return bridge.listDiscoveredTools(normalizeMcpName(entry));
1825
+ } finally {
1826
+ await bridge.stopLocalServers();
1827
+ }
1828
+ };
1829
+
1830
+ export const mcpToolsList = async (
1831
+ workingDir: string,
1832
+ serverName: string,
1833
+ ): Promise<void> => {
1834
+ const discovered = await discoverMcpTools(workingDir, serverName);
1835
+ if (discovered.length === 0) {
1836
+ process.stdout.write(`No tools discovered for MCP server "${serverName}".\n`);
1837
+ return;
1838
+ }
1839
+ process.stdout.write(`Discovered tools for "${serverName}":\n`);
1840
+ for (const tool of discovered) {
1841
+ process.stdout.write(`- ${tool}\n`);
1842
+ }
1843
+ };
1844
+
1845
+ export const mcpToolsSelect = async (
1846
+ workingDir: string,
1847
+ serverName: string,
1848
+ options: {
1849
+ all?: boolean;
1850
+ toolsCsv?: string;
1851
+ },
1852
+ ): Promise<void> => {
1853
+ const discovered = await discoverMcpTools(workingDir, serverName);
1854
+ if (discovered.length === 0) {
1855
+ process.stdout.write(`No tools discovered for MCP server "${serverName}".\n`);
1856
+ return;
1857
+ }
1858
+ let selected: string[] = [];
1859
+ if (options.all) {
1860
+ selected = [...discovered];
1861
+ } else if (options.toolsCsv && options.toolsCsv.trim().length > 0) {
1862
+ const requested = options.toolsCsv
1863
+ .split(",")
1864
+ .map((part) => part.trim())
1865
+ .filter((part) => part.length > 0);
1866
+ selected = discovered.filter((tool) => requested.includes(tool));
1867
+ } else {
1868
+ process.stdout.write(`Discovered tools for "${serverName}":\n`);
1869
+ discovered.forEach((tool, idx) => {
1870
+ process.stdout.write(`${idx + 1}. ${tool}\n`);
1871
+ });
1872
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1873
+ const answer = await rl.question(
1874
+ "Enter comma-separated tool numbers/names to allow (or * for all): ",
1875
+ );
1876
+ rl.close();
1877
+ const raw = answer.trim();
1878
+ if (raw === "*") {
1879
+ selected = [...discovered];
1880
+ } else {
1881
+ const tokens = raw
1882
+ .split(",")
1883
+ .map((part) => part.trim())
1884
+ .filter((part) => part.length > 0);
1885
+ const fromIndex = tokens
1886
+ .map((token) => Number.parseInt(token, 10))
1887
+ .filter((value) => !Number.isNaN(value))
1888
+ .map((index) => discovered[index - 1])
1889
+ .filter((value): value is string => typeof value === "string");
1890
+ const byName = discovered.filter((tool) => tokens.includes(tool));
1891
+ selected = [...new Set([...fromIndex, ...byName])];
1892
+ }
1893
+ }
1894
+ if (selected.length === 0) {
1895
+ throw new Error("No valid tools selected.");
1896
+ }
1897
+ const includePatterns =
1898
+ selected.length === discovered.length
1899
+ ? [`${serverName}/*`]
1900
+ : selected.sort();
1901
+ const { config, index } = await resolveMcpEntry(workingDir, serverName);
1902
+ const mcp = [...(config.mcp ?? [])];
1903
+ const existing = mcp[index];
1904
+ mcp[index] = {
1905
+ ...existing,
1906
+ tools: {
1907
+ ...(existing.tools ?? {}),
1908
+ mode: "allowlist",
1909
+ include: includePatterns,
1910
+ },
1911
+ };
1912
+ await writeConfigFile(workingDir, { ...config, mcp });
1913
+ process.stdout.write(
1914
+ `Updated ${serverName} to allowlist ${includePatterns.join(", ")} in poncho.config.js.\n`,
1915
+ );
1916
+ process.stdout.write(
1917
+ "\nRequired next step: add MCP intent in AGENT.md or SKILL.md. Without this, these MCP tools will not be registered for the model.\n",
1918
+ );
1919
+ process.stdout.write(
1920
+ "\nOption A: AGENT.md (global fallback intent)\n" +
1921
+ "Paste this into AGENT.md frontmatter:\n" +
1922
+ "---\n" +
1923
+ "tools:\n" +
1924
+ " mcp:\n" +
1925
+ includePatterns.map((tool) => ` - ${tool}`).join("\n") +
1926
+ "\n---\n",
1927
+ );
1928
+ process.stdout.write(
1929
+ "\nOption B: SKILL.md (only when that skill is activated)\n" +
1930
+ "Paste this into SKILL.md frontmatter:\n" +
1931
+ "---\n" +
1932
+ "tools:\n" +
1933
+ " mcp:\n" +
1934
+ includePatterns.map((tool) => ` - ${tool}`).join("\n") +
1935
+ "\n---\n",
1936
+ );
1627
1937
  };
1628
1938
 
1629
1939
  export const buildCli = (): Command => {
@@ -1739,6 +2049,10 @@ export const buildCli = (): Command => {
1739
2049
  .command("add")
1740
2050
  .requiredOption("--url <url>", "remote MCP url")
1741
2051
  .option("--name <name>", "server name")
2052
+ .option(
2053
+ "--auth-bearer-env <name>",
2054
+ "env var name containing bearer token for this MCP server",
2055
+ )
1742
2056
  .option("--env <name>", "env variable (repeatable)", (value, all: string[]) => {
1743
2057
  all.push(value);
1744
2058
  return all;
@@ -1748,6 +2062,7 @@ export const buildCli = (): Command => {
1748
2062
  options: {
1749
2063
  url?: string;
1750
2064
  name?: string;
2065
+ authBearerEnv?: string;
1751
2066
  env: string[];
1752
2067
  },
1753
2068
  ) => {
@@ -1755,6 +2070,7 @@ export const buildCli = (): Command => {
1755
2070
  url: options.url,
1756
2071
  name: options.name,
1757
2072
  envVars: options.env,
2073
+ authBearerEnv: options.authBearerEnv,
1758
2074
  });
1759
2075
  },
1760
2076
  );
@@ -1774,6 +2090,39 @@ export const buildCli = (): Command => {
1774
2090
  await mcpRemove(process.cwd(), name);
1775
2091
  });
1776
2092
 
2093
+ const mcpToolsCommand = mcpCommand
2094
+ .command("tools")
2095
+ .description("Discover and curate tools for a configured MCP server");
2096
+
2097
+ mcpToolsCommand
2098
+ .command("list")
2099
+ .argument("<name>", "server name")
2100
+ .description("Discover and list tools from a configured MCP server")
2101
+ .action(async (name: string) => {
2102
+ await mcpToolsList(process.cwd(), name);
2103
+ });
2104
+
2105
+ mcpToolsCommand
2106
+ .command("select")
2107
+ .argument("<name>", "server name")
2108
+ .description("Select MCP tools and store as config allowlist")
2109
+ .option("--all", "select all discovered tools", false)
2110
+ .option("--tools <csv>", "comma-separated discovered tool names")
2111
+ .action(
2112
+ async (
2113
+ name: string,
2114
+ options: {
2115
+ all: boolean;
2116
+ tools?: string;
2117
+ },
2118
+ ) => {
2119
+ await mcpToolsSelect(process.cwd(), name, {
2120
+ all: options.all,
2121
+ toolsCsv: options.tools,
2122
+ });
2123
+ },
2124
+ );
2125
+
1777
2126
  return program;
1778
2127
  };
1779
2128
 
package/test/cli.test.ts CHANGED
@@ -482,7 +482,7 @@ describe("cli", () => {
482
482
  const projectDir = join(tempDir, "aux-agent");
483
483
 
484
484
  await listTools(projectDir);
485
- await mcpAdd(projectDir, { url: "wss://example.com/mcp", name: "remote-mcp" });
485
+ await mcpAdd(projectDir, { url: "https://example.com/mcp", name: "remote-mcp" });
486
486
  await mcpList(projectDir);
487
487
  await mcpRemove(projectDir, "remote-mcp");
488
488
 
@@ -525,6 +525,20 @@ describe("cli", () => {
525
525
  expect(result.failed).toBe(0);
526
526
  });
527
527
 
528
+ it("seeds bearer token placeholders in env files when adding mcp auth", async () => {
529
+ await initProject("mcp-env-seed-agent", { workingDir: tempDir });
530
+ const projectDir = join(tempDir, "mcp-env-seed-agent");
531
+ await mcpAdd(projectDir, {
532
+ url: "https://example.com/mcp",
533
+ name: "remote-mcp",
534
+ authBearerEnv: "LINEAR_TOKEN",
535
+ });
536
+ const envFile = await readFile(join(projectDir, ".env"), "utf8");
537
+ const envExampleFile = await readFile(join(projectDir, ".env.example"), "utf8");
538
+ expect(envFile).toContain("LINEAR_TOKEN=");
539
+ expect(envExampleFile).toContain("LINEAR_TOKEN=");
540
+ });
541
+
528
542
  it("does not modify AGENT.md when no deprecated embedded guidance exists", async () => {
529
543
  const projectDir = join(tempDir, "legacy-agent");
530
544
  await mkdir(projectDir, { recursive: true });