@poncho-ai/cli 0.2.0 → 0.3.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.
- package/.turbo/turbo-build.log +16 -16
- package/CHANGELOG.md +13 -0
- package/dist/chunk-5OOJ5TLB.js +2154 -0
- package/dist/chunk-BDNKTEA4.js +2105 -0
- package/dist/chunk-BVYTTIAO.js +2153 -0
- package/dist/chunk-DBSQYKIE.js +2013 -0
- package/dist/chunk-G5KKCSYK.js +2050 -0
- package/dist/chunk-GOSKILLU.js +1994 -0
- package/dist/chunk-HD7LBEVL.js +2051 -0
- package/dist/chunk-HDKWXGXT.js +2155 -0
- package/dist/chunk-HGZ24QKS.js +1995 -0
- package/dist/chunk-IERP5HOH.js +2064 -0
- package/dist/chunk-KADTBRXA.js +2025 -0
- package/dist/chunk-QSPWSMRC.js +2019 -0
- package/dist/chunk-SMFVGWV6.js +2155 -0
- package/dist/chunk-WWZOZFTC.js +2052 -0
- package/dist/chunk-ZXANINYQ.js +2062 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -1
- package/package.json +2 -2
- package/src/index.ts +364 -5
- package/test/cli.test.ts +15 -1
package/dist/cli.js
CHANGED
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-
|
|
20
|
+
} from "./chunk-HDKWXGXT.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.
|
|
3
|
+
"version": "0.3.0",
|
|
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.
|
|
28
|
+
"@poncho-ai/harness": "0.3.0",
|
|
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,
|
|
@@ -299,17 +301,48 @@ Connect remote MCP servers and expose their tools to the agent:
|
|
|
299
301
|
|
|
300
302
|
\`\`\`bash
|
|
301
303
|
# Add remote MCP server
|
|
302
|
-
poncho mcp add --url
|
|
304
|
+
poncho mcp add --url https://mcp.example.com/github --name github --auth-bearer-env GITHUB_TOKEN
|
|
303
305
|
|
|
304
306
|
# List configured servers
|
|
305
307
|
poncho mcp list
|
|
306
308
|
|
|
309
|
+
# Discover and select MCP tools into config allowlist
|
|
310
|
+
poncho mcp tools list github
|
|
311
|
+
poncho mcp tools select github
|
|
312
|
+
|
|
307
313
|
# Remove a server
|
|
308
314
|
poncho mcp remove github
|
|
309
315
|
\`\`\`
|
|
310
316
|
|
|
311
317
|
Set required secrets in \`.env\` (for example, \`GITHUB_TOKEN=...\`).
|
|
312
318
|
|
|
319
|
+
## Tool Intent in Frontmatter
|
|
320
|
+
|
|
321
|
+
Declare tool intent directly in \`AGENT.md\` and \`SKILL.md\` frontmatter:
|
|
322
|
+
|
|
323
|
+
\`\`\`yaml
|
|
324
|
+
tools:
|
|
325
|
+
mcp:
|
|
326
|
+
- github/list_issues
|
|
327
|
+
- github/*
|
|
328
|
+
scripts:
|
|
329
|
+
- starter/scripts/*
|
|
330
|
+
\`\`\`
|
|
331
|
+
|
|
332
|
+
How it works:
|
|
333
|
+
|
|
334
|
+
- \`AGENT.md\` provides fallback MCP intent when no skill is active.
|
|
335
|
+
- \`SKILL.md\` intent applies when you activate that skill (\`activate_skill\`).
|
|
336
|
+
- Skill scripts are accessible by default from each skill's \`scripts/\` directory.
|
|
337
|
+
- \`AGENT.md\` \`tools.scripts\` can still be used to narrow script access when active skills do not set script intent.
|
|
338
|
+
- Active skills are unioned, then filtered by policy in \`poncho.config.js\`.
|
|
339
|
+
- Deactivating a skill (\`deactivate_skill\`) removes its MCP tools from runtime registration.
|
|
340
|
+
|
|
341
|
+
Pattern format is strict slash-only:
|
|
342
|
+
|
|
343
|
+
- MCP: \`server/tool\`, \`server/*\`
|
|
344
|
+
- Scripts: \`skill/scripts/file.ts\`, \`skill/scripts/*\`
|
|
345
|
+
|
|
313
346
|
## Configuration
|
|
314
347
|
|
|
315
348
|
Core files:
|
|
@@ -335,6 +368,21 @@ export default {
|
|
|
335
368
|
telemetry: {
|
|
336
369
|
enabled: true,
|
|
337
370
|
},
|
|
371
|
+
mcp: [
|
|
372
|
+
{
|
|
373
|
+
name: "github",
|
|
374
|
+
url: "https://mcp.example.com/github",
|
|
375
|
+
auth: { type: "bearer", tokenEnv: "GITHUB_TOKEN" },
|
|
376
|
+
tools: {
|
|
377
|
+
mode: "allowlist",
|
|
378
|
+
include: ["github/list_issues", "github/get_issue"],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
scripts: {
|
|
383
|
+
mode: "allowlist",
|
|
384
|
+
include: ["starter/scripts/*"],
|
|
385
|
+
},
|
|
338
386
|
tools: {
|
|
339
387
|
defaults: {
|
|
340
388
|
list_directory: true,
|
|
@@ -396,7 +444,6 @@ const VERCEL_RUNTIME_DEPENDENCIES: Record<string, string> = {
|
|
|
396
444
|
mustache: "^4.2.0",
|
|
397
445
|
openai: "^6.3.0",
|
|
398
446
|
redis: "^5.10.0",
|
|
399
|
-
ws: "^8.18.0",
|
|
400
447
|
yaml: "^2.8.1",
|
|
401
448
|
};
|
|
402
449
|
const TEST_TEMPLATE = `tests:
|
|
@@ -548,6 +595,52 @@ const writeConfigFile = async (workingDir: string, config: PonchoConfig): Promis
|
|
|
548
595
|
await writeFile(resolve(workingDir, "poncho.config.js"), serialized, "utf8");
|
|
549
596
|
};
|
|
550
597
|
|
|
598
|
+
const ensureEnvPlaceholder = async (filePath: string, key: string): Promise<boolean> => {
|
|
599
|
+
const normalizedKey = key.trim();
|
|
600
|
+
if (!normalizedKey) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
let content = "";
|
|
604
|
+
try {
|
|
605
|
+
content = await readFile(filePath, "utf8");
|
|
606
|
+
} catch {
|
|
607
|
+
await writeFile(filePath, `${normalizedKey}=\n`, "utf8");
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
const present = content
|
|
611
|
+
.split(/\r?\n/)
|
|
612
|
+
.some((line) => line.trimStart().startsWith(`${normalizedKey}=`));
|
|
613
|
+
if (present) {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
const withTrailingNewline = content.length === 0 || content.endsWith("\n")
|
|
617
|
+
? content
|
|
618
|
+
: `${content}\n`;
|
|
619
|
+
await writeFile(filePath, `${withTrailingNewline}${normalizedKey}=\n`, "utf8");
|
|
620
|
+
return true;
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const removeEnvPlaceholder = async (filePath: string, key: string): Promise<boolean> => {
|
|
624
|
+
const normalizedKey = key.trim();
|
|
625
|
+
if (!normalizedKey) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
let content = "";
|
|
629
|
+
try {
|
|
630
|
+
content = await readFile(filePath, "utf8");
|
|
631
|
+
} catch {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
const lines = content.split(/\r?\n/);
|
|
635
|
+
const filtered = lines.filter((line) => !line.trimStart().startsWith(`${normalizedKey}=`));
|
|
636
|
+
if (filtered.length === lines.length) {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
const nextContent = filtered.join("\n").replace(/\n+$/, "");
|
|
640
|
+
await writeFile(filePath, nextContent.length > 0 ? `${nextContent}\n` : "", "utf8");
|
|
641
|
+
return true;
|
|
642
|
+
};
|
|
643
|
+
|
|
551
644
|
const gitInit = (cwd: string): Promise<boolean> =>
|
|
552
645
|
new Promise((resolve) => {
|
|
553
646
|
const child = spawn("git", ["init"], { cwd, stdio: "ignore" });
|
|
@@ -1588,6 +1681,7 @@ export const mcpAdd = async (
|
|
|
1588
1681
|
url?: string;
|
|
1589
1682
|
name?: string;
|
|
1590
1683
|
envVars?: string[];
|
|
1684
|
+
authBearerEnv?: string;
|
|
1591
1685
|
},
|
|
1592
1686
|
): Promise<void> => {
|
|
1593
1687
|
const config = (await loadPonchoConfig(workingDir)) ?? { mcp: [] };
|
|
@@ -1595,14 +1689,56 @@ export const mcpAdd = async (
|
|
|
1595
1689
|
if (!options.url) {
|
|
1596
1690
|
throw new Error("Remote MCP only: provide --url for a remote MCP server.");
|
|
1597
1691
|
}
|
|
1692
|
+
if (options.url.startsWith("ws://") || options.url.startsWith("wss://")) {
|
|
1693
|
+
throw new Error("WebSocket MCP URLs are no longer supported. Use an HTTP MCP endpoint.");
|
|
1694
|
+
}
|
|
1695
|
+
if (!options.url.startsWith("http://") && !options.url.startsWith("https://")) {
|
|
1696
|
+
throw new Error("Invalid MCP URL. Expected http:// or https://.");
|
|
1697
|
+
}
|
|
1698
|
+
const serverName = options.name ?? normalizeMcpName({ url: options.url });
|
|
1598
1699
|
mcp.push({
|
|
1599
|
-
name:
|
|
1700
|
+
name: serverName,
|
|
1600
1701
|
url: options.url,
|
|
1601
1702
|
env: options.envVars ?? [],
|
|
1703
|
+
auth: options.authBearerEnv
|
|
1704
|
+
? {
|
|
1705
|
+
type: "bearer",
|
|
1706
|
+
tokenEnv: options.authBearerEnv,
|
|
1707
|
+
}
|
|
1708
|
+
: undefined,
|
|
1602
1709
|
});
|
|
1603
1710
|
|
|
1604
1711
|
await writeConfigFile(workingDir, { ...config, mcp });
|
|
1605
|
-
|
|
1712
|
+
let envSeedMessage: string | undefined;
|
|
1713
|
+
if (options.authBearerEnv) {
|
|
1714
|
+
const envPath = resolve(workingDir, ".env");
|
|
1715
|
+
const envExamplePath = resolve(workingDir, ".env.example");
|
|
1716
|
+
const addedEnv = await ensureEnvPlaceholder(envPath, options.authBearerEnv);
|
|
1717
|
+
const addedEnvExample = await ensureEnvPlaceholder(envExamplePath, options.authBearerEnv);
|
|
1718
|
+
if (addedEnv || addedEnvExample) {
|
|
1719
|
+
envSeedMessage = `Added ${options.authBearerEnv}= to ${addedEnv ? ".env" : ""}${addedEnv && addedEnvExample ? " and " : ""}${addedEnvExample ? ".env.example" : ""}.`;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
const nextSteps: string[] = [];
|
|
1723
|
+
let step = 1;
|
|
1724
|
+
if (options.authBearerEnv) {
|
|
1725
|
+
nextSteps.push(` ${step}) Set token in .env: ${options.authBearerEnv}=...`);
|
|
1726
|
+
step += 1;
|
|
1727
|
+
}
|
|
1728
|
+
nextSteps.push(` ${step}) Discover tools: poncho mcp tools list ${serverName}`);
|
|
1729
|
+
step += 1;
|
|
1730
|
+
nextSteps.push(` ${step}) Select tools: poncho mcp tools select ${serverName}`);
|
|
1731
|
+
step += 1;
|
|
1732
|
+
nextSteps.push(` ${step}) Verify config: poncho mcp list`);
|
|
1733
|
+
process.stdout.write(
|
|
1734
|
+
[
|
|
1735
|
+
`MCP server added: ${serverName}`,
|
|
1736
|
+
...(envSeedMessage ? [envSeedMessage] : []),
|
|
1737
|
+
"Next steps:",
|
|
1738
|
+
...nextSteps,
|
|
1739
|
+
"",
|
|
1740
|
+
].join("\n"),
|
|
1741
|
+
);
|
|
1606
1742
|
};
|
|
1607
1743
|
|
|
1608
1744
|
export const mcpList = async (workingDir: string): Promise<void> => {
|
|
@@ -1610,20 +1746,204 @@ export const mcpList = async (workingDir: string): Promise<void> => {
|
|
|
1610
1746
|
const mcp = config?.mcp ?? [];
|
|
1611
1747
|
if (mcp.length === 0) {
|
|
1612
1748
|
process.stdout.write("No MCP servers configured.\n");
|
|
1749
|
+
if (config?.scripts) {
|
|
1750
|
+
process.stdout.write(
|
|
1751
|
+
`Script policy: mode=${config.scripts.mode ?? "all"} include=${config.scripts.include?.length ?? 0} exclude=${config.scripts.exclude?.length ?? 0}\n`,
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1613
1754
|
return;
|
|
1614
1755
|
}
|
|
1615
1756
|
process.stdout.write("Configured MCP servers:\n");
|
|
1616
1757
|
for (const entry of mcp) {
|
|
1617
|
-
|
|
1758
|
+
const auth =
|
|
1759
|
+
entry.auth?.type === "bearer" ? `auth=bearer:${entry.auth.tokenEnv}` : "auth=none";
|
|
1760
|
+
const mode = entry.tools?.mode ?? "all";
|
|
1761
|
+
process.stdout.write(
|
|
1762
|
+
`- ${entry.name ?? entry.url} (remote: ${entry.url}, ${auth}, mode=${mode})\n`,
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
if (config?.scripts) {
|
|
1766
|
+
process.stdout.write(
|
|
1767
|
+
`Script policy: mode=${config.scripts.mode ?? "all"} include=${config.scripts.include?.length ?? 0} exclude=${config.scripts.exclude?.length ?? 0}\n`,
|
|
1768
|
+
);
|
|
1618
1769
|
}
|
|
1619
1770
|
};
|
|
1620
1771
|
|
|
1621
1772
|
export const mcpRemove = async (workingDir: string, name: string): Promise<void> => {
|
|
1622
1773
|
const config = (await loadPonchoConfig(workingDir)) ?? { mcp: [] };
|
|
1623
1774
|
const before = config.mcp ?? [];
|
|
1775
|
+
const removed = before.filter((entry) => normalizeMcpName(entry) === name);
|
|
1624
1776
|
const filtered = before.filter((entry) => normalizeMcpName(entry) !== name);
|
|
1625
1777
|
await writeConfigFile(workingDir, { ...config, mcp: filtered });
|
|
1778
|
+
const removedTokenEnvNames = new Set(
|
|
1779
|
+
removed
|
|
1780
|
+
.map((entry) =>
|
|
1781
|
+
entry.auth?.type === "bearer" ? entry.auth.tokenEnv?.trim() ?? "" : "",
|
|
1782
|
+
)
|
|
1783
|
+
.filter((value) => value.length > 0),
|
|
1784
|
+
);
|
|
1785
|
+
const stillUsedTokenEnvNames = new Set(
|
|
1786
|
+
filtered
|
|
1787
|
+
.map((entry) =>
|
|
1788
|
+
entry.auth?.type === "bearer" ? entry.auth.tokenEnv?.trim() ?? "" : "",
|
|
1789
|
+
)
|
|
1790
|
+
.filter((value) => value.length > 0),
|
|
1791
|
+
);
|
|
1792
|
+
const removedFromExample: string[] = [];
|
|
1793
|
+
for (const tokenEnv of removedTokenEnvNames) {
|
|
1794
|
+
if (stillUsedTokenEnvNames.has(tokenEnv)) {
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
const changed = await removeEnvPlaceholder(resolve(workingDir, ".env.example"), tokenEnv);
|
|
1798
|
+
if (changed) {
|
|
1799
|
+
removedFromExample.push(tokenEnv);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1626
1802
|
process.stdout.write(`Removed MCP server: ${name}\n`);
|
|
1803
|
+
if (removedFromExample.length > 0) {
|
|
1804
|
+
process.stdout.write(
|
|
1805
|
+
`Removed unused token placeholder(s) from .env.example: ${removedFromExample.join(", ")}\n`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
|
|
1810
|
+
const resolveMcpEntry = async (
|
|
1811
|
+
workingDir: string,
|
|
1812
|
+
serverName: string,
|
|
1813
|
+
): Promise<{ config: PonchoConfig; index: number }> => {
|
|
1814
|
+
const config = (await loadPonchoConfig(workingDir)) ?? { mcp: [] };
|
|
1815
|
+
const entries = config.mcp ?? [];
|
|
1816
|
+
const index = entries.findIndex((entry) => normalizeMcpName(entry) === serverName);
|
|
1817
|
+
if (index < 0) {
|
|
1818
|
+
throw new Error(`MCP server "${serverName}" is not configured.`);
|
|
1819
|
+
}
|
|
1820
|
+
return { config, index };
|
|
1821
|
+
};
|
|
1822
|
+
|
|
1823
|
+
const discoverMcpTools = async (
|
|
1824
|
+
workingDir: string,
|
|
1825
|
+
serverName: string,
|
|
1826
|
+
): Promise<string[]> => {
|
|
1827
|
+
dotenv.config({ path: resolve(workingDir, ".env") });
|
|
1828
|
+
const { config, index } = await resolveMcpEntry(workingDir, serverName);
|
|
1829
|
+
const entry = (config.mcp ?? [])[index];
|
|
1830
|
+
const bridge = new LocalMcpBridge({ mcp: [entry] });
|
|
1831
|
+
try {
|
|
1832
|
+
await bridge.startLocalServers();
|
|
1833
|
+
await bridge.discoverTools();
|
|
1834
|
+
return bridge.listDiscoveredTools(normalizeMcpName(entry));
|
|
1835
|
+
} finally {
|
|
1836
|
+
await bridge.stopLocalServers();
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1840
|
+
export const mcpToolsList = async (
|
|
1841
|
+
workingDir: string,
|
|
1842
|
+
serverName: string,
|
|
1843
|
+
): Promise<void> => {
|
|
1844
|
+
const discovered = await discoverMcpTools(workingDir, serverName);
|
|
1845
|
+
if (discovered.length === 0) {
|
|
1846
|
+
process.stdout.write(`No tools discovered for MCP server "${serverName}".\n`);
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
process.stdout.write(`Discovered tools for "${serverName}":\n`);
|
|
1850
|
+
for (const tool of discovered) {
|
|
1851
|
+
process.stdout.write(`- ${tool}\n`);
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
export const mcpToolsSelect = async (
|
|
1856
|
+
workingDir: string,
|
|
1857
|
+
serverName: string,
|
|
1858
|
+
options: {
|
|
1859
|
+
all?: boolean;
|
|
1860
|
+
toolsCsv?: string;
|
|
1861
|
+
},
|
|
1862
|
+
): Promise<void> => {
|
|
1863
|
+
const discovered = await discoverMcpTools(workingDir, serverName);
|
|
1864
|
+
if (discovered.length === 0) {
|
|
1865
|
+
process.stdout.write(`No tools discovered for MCP server "${serverName}".\n`);
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
let selected: string[] = [];
|
|
1869
|
+
if (options.all) {
|
|
1870
|
+
selected = [...discovered];
|
|
1871
|
+
} else if (options.toolsCsv && options.toolsCsv.trim().length > 0) {
|
|
1872
|
+
const requested = options.toolsCsv
|
|
1873
|
+
.split(",")
|
|
1874
|
+
.map((part) => part.trim())
|
|
1875
|
+
.filter((part) => part.length > 0);
|
|
1876
|
+
selected = discovered.filter((tool) => requested.includes(tool));
|
|
1877
|
+
} else {
|
|
1878
|
+
process.stdout.write(`Discovered tools for "${serverName}":\n`);
|
|
1879
|
+
discovered.forEach((tool, idx) => {
|
|
1880
|
+
process.stdout.write(`${idx + 1}. ${tool}\n`);
|
|
1881
|
+
});
|
|
1882
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1883
|
+
const answer = await rl.question(
|
|
1884
|
+
"Enter comma-separated tool numbers/names to allow (or * for all): ",
|
|
1885
|
+
);
|
|
1886
|
+
rl.close();
|
|
1887
|
+
const raw = answer.trim();
|
|
1888
|
+
if (raw === "*") {
|
|
1889
|
+
selected = [...discovered];
|
|
1890
|
+
} else {
|
|
1891
|
+
const tokens = raw
|
|
1892
|
+
.split(",")
|
|
1893
|
+
.map((part) => part.trim())
|
|
1894
|
+
.filter((part) => part.length > 0);
|
|
1895
|
+
const fromIndex = tokens
|
|
1896
|
+
.map((token) => Number.parseInt(token, 10))
|
|
1897
|
+
.filter((value) => !Number.isNaN(value))
|
|
1898
|
+
.map((index) => discovered[index - 1])
|
|
1899
|
+
.filter((value): value is string => typeof value === "string");
|
|
1900
|
+
const byName = discovered.filter((tool) => tokens.includes(tool));
|
|
1901
|
+
selected = [...new Set([...fromIndex, ...byName])];
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (selected.length === 0) {
|
|
1905
|
+
throw new Error("No valid tools selected.");
|
|
1906
|
+
}
|
|
1907
|
+
const includePatterns =
|
|
1908
|
+
selected.length === discovered.length
|
|
1909
|
+
? [`${serverName}/*`]
|
|
1910
|
+
: selected.sort();
|
|
1911
|
+
const { config, index } = await resolveMcpEntry(workingDir, serverName);
|
|
1912
|
+
const mcp = [...(config.mcp ?? [])];
|
|
1913
|
+
const existing = mcp[index];
|
|
1914
|
+
mcp[index] = {
|
|
1915
|
+
...existing,
|
|
1916
|
+
tools: {
|
|
1917
|
+
...(existing.tools ?? {}),
|
|
1918
|
+
mode: "allowlist",
|
|
1919
|
+
include: includePatterns,
|
|
1920
|
+
},
|
|
1921
|
+
};
|
|
1922
|
+
await writeConfigFile(workingDir, { ...config, mcp });
|
|
1923
|
+
process.stdout.write(
|
|
1924
|
+
`Updated ${serverName} to allowlist ${includePatterns.join(", ")} in poncho.config.js.\n`,
|
|
1925
|
+
);
|
|
1926
|
+
process.stdout.write(
|
|
1927
|
+
"\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",
|
|
1928
|
+
);
|
|
1929
|
+
process.stdout.write(
|
|
1930
|
+
"\nOption A: AGENT.md (global fallback intent)\n" +
|
|
1931
|
+
"Paste this into AGENT.md frontmatter:\n" +
|
|
1932
|
+
"---\n" +
|
|
1933
|
+
"tools:\n" +
|
|
1934
|
+
" mcp:\n" +
|
|
1935
|
+
includePatterns.map((tool) => ` - ${tool}`).join("\n") +
|
|
1936
|
+
"\n---\n",
|
|
1937
|
+
);
|
|
1938
|
+
process.stdout.write(
|
|
1939
|
+
"\nOption B: SKILL.md (only when that skill is activated)\n" +
|
|
1940
|
+
"Paste this into SKILL.md frontmatter:\n" +
|
|
1941
|
+
"---\n" +
|
|
1942
|
+
"tools:\n" +
|
|
1943
|
+
" mcp:\n" +
|
|
1944
|
+
includePatterns.map((tool) => ` - ${tool}`).join("\n") +
|
|
1945
|
+
"\n---\n",
|
|
1946
|
+
);
|
|
1627
1947
|
};
|
|
1628
1948
|
|
|
1629
1949
|
export const buildCli = (): Command => {
|
|
@@ -1739,6 +2059,10 @@ export const buildCli = (): Command => {
|
|
|
1739
2059
|
.command("add")
|
|
1740
2060
|
.requiredOption("--url <url>", "remote MCP url")
|
|
1741
2061
|
.option("--name <name>", "server name")
|
|
2062
|
+
.option(
|
|
2063
|
+
"--auth-bearer-env <name>",
|
|
2064
|
+
"env var name containing bearer token for this MCP server",
|
|
2065
|
+
)
|
|
1742
2066
|
.option("--env <name>", "env variable (repeatable)", (value, all: string[]) => {
|
|
1743
2067
|
all.push(value);
|
|
1744
2068
|
return all;
|
|
@@ -1748,6 +2072,7 @@ export const buildCli = (): Command => {
|
|
|
1748
2072
|
options: {
|
|
1749
2073
|
url?: string;
|
|
1750
2074
|
name?: string;
|
|
2075
|
+
authBearerEnv?: string;
|
|
1751
2076
|
env: string[];
|
|
1752
2077
|
},
|
|
1753
2078
|
) => {
|
|
@@ -1755,6 +2080,7 @@ export const buildCli = (): Command => {
|
|
|
1755
2080
|
url: options.url,
|
|
1756
2081
|
name: options.name,
|
|
1757
2082
|
envVars: options.env,
|
|
2083
|
+
authBearerEnv: options.authBearerEnv,
|
|
1758
2084
|
});
|
|
1759
2085
|
},
|
|
1760
2086
|
);
|
|
@@ -1774,6 +2100,39 @@ export const buildCli = (): Command => {
|
|
|
1774
2100
|
await mcpRemove(process.cwd(), name);
|
|
1775
2101
|
});
|
|
1776
2102
|
|
|
2103
|
+
const mcpToolsCommand = mcpCommand
|
|
2104
|
+
.command("tools")
|
|
2105
|
+
.description("Discover and curate tools for a configured MCP server");
|
|
2106
|
+
|
|
2107
|
+
mcpToolsCommand
|
|
2108
|
+
.command("list")
|
|
2109
|
+
.argument("<name>", "server name")
|
|
2110
|
+
.description("Discover and list tools from a configured MCP server")
|
|
2111
|
+
.action(async (name: string) => {
|
|
2112
|
+
await mcpToolsList(process.cwd(), name);
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
mcpToolsCommand
|
|
2116
|
+
.command("select")
|
|
2117
|
+
.argument("<name>", "server name")
|
|
2118
|
+
.description("Select MCP tools and store as config allowlist")
|
|
2119
|
+
.option("--all", "select all discovered tools", false)
|
|
2120
|
+
.option("--tools <csv>", "comma-separated discovered tool names")
|
|
2121
|
+
.action(
|
|
2122
|
+
async (
|
|
2123
|
+
name: string,
|
|
2124
|
+
options: {
|
|
2125
|
+
all: boolean;
|
|
2126
|
+
tools?: string;
|
|
2127
|
+
},
|
|
2128
|
+
) => {
|
|
2129
|
+
await mcpToolsSelect(process.cwd(), name, {
|
|
2130
|
+
all: options.all,
|
|
2131
|
+
toolsCsv: options.tools,
|
|
2132
|
+
});
|
|
2133
|
+
},
|
|
2134
|
+
);
|
|
2135
|
+
|
|
1777
2136
|
return program;
|
|
1778
2137
|
};
|
|
1779
2138
|
|
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: "
|
|
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 });
|