@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/.turbo/turbo-build.log +16 -16
- package/CHANGELOG.md +24 -0
- package/dist/chunk-5OOJ5TLB.js +2154 -0
- package/dist/chunk-AS3CEZHY.js +2145 -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 -15
- 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-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.
|
|
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.
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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 });
|