@yagni-app/code 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +89 -27
  2. package/dist/claudeCompat.d.ts +59 -0
  3. package/dist/claudeCompat.js +109 -2
  4. package/dist/claudePlugins.d.ts +45 -3
  5. package/dist/claudePlugins.js +129 -19
  6. package/dist/cli.js +28 -4
  7. package/dist/doctor.d.ts +42 -4
  8. package/dist/doctor.js +150 -15
  9. package/dist/extension/condensedTools.d.ts +12 -1
  10. package/dist/extension/condensedTools.js +17 -9
  11. package/dist/extension/index.d.ts +18 -5
  12. package/dist/extension/index.js +319 -72
  13. package/dist/extension/mcp/approval.d.ts +45 -0
  14. package/dist/extension/mcp/approval.js +164 -0
  15. package/dist/extension/mcp/auth.d.ts +124 -0
  16. package/dist/extension/mcp/auth.js +560 -0
  17. package/dist/extension/mcp/authStore.d.ts +61 -0
  18. package/dist/extension/mcp/authStore.js +105 -0
  19. package/dist/extension/mcp/callbackPage.d.ts +31 -0
  20. package/dist/extension/mcp/callbackPage.js +222 -0
  21. package/dist/extension/mcp/cliConfig.d.ts +12 -0
  22. package/dist/extension/mcp/cliConfig.js +12 -0
  23. package/dist/extension/mcp/config.d.ts +153 -0
  24. package/dist/extension/mcp/config.js +381 -0
  25. package/dist/extension/mcp/log.d.ts +28 -0
  26. package/dist/extension/mcp/log.js +82 -0
  27. package/dist/extension/mcp/manager.d.ts +100 -0
  28. package/dist/extension/mcp/manager.js +273 -0
  29. package/dist/extension/mcp/names.d.ts +25 -0
  30. package/dist/extension/mcp/names.js +40 -0
  31. package/dist/extension/mcp/panel.d.ts +33 -0
  32. package/dist/extension/mcp/panel.js +268 -0
  33. package/dist/extension/mcp/prompts.d.ts +23 -0
  34. package/dist/extension/mcp/prompts.js +93 -0
  35. package/dist/extension/mcp/startup.d.ts +55 -0
  36. package/dist/extension/mcp/startup.js +152 -0
  37. package/dist/extension/mcp/tools.d.ts +31 -0
  38. package/dist/extension/mcp/tools.js +117 -0
  39. package/dist/extension/mcp/transports.d.ts +17 -0
  40. package/dist/extension/mcp/transports.js +44 -0
  41. package/dist/extension/permission/gate.d.ts +104 -2
  42. package/dist/extension/permission/gate.js +387 -31
  43. package/dist/extension/permission/guardian.d.ts +24 -5
  44. package/dist/extension/permission/guardian.js +162 -24
  45. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  46. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  47. package/dist/extension/permissionRules/engine.d.ts +50 -0
  48. package/dist/extension/permissionRules/engine.js +238 -0
  49. package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
  50. package/dist/extension/permissionRules/loadConfig.js +90 -0
  51. package/dist/extension/permissionRules/parser.d.ts +38 -0
  52. package/dist/extension/permissionRules/parser.js +136 -0
  53. package/dist/extension/permissionRules/pathRules.d.ts +58 -0
  54. package/dist/extension/permissionRules/pathRules.js +120 -0
  55. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  56. package/dist/extension/permissionRules/shellRules.js +221 -0
  57. package/dist/extension/pipeline/invocation.d.ts +3 -6
  58. package/dist/extension/pipeline/invocation.js +3 -6
  59. package/dist/extension/pipeline/personas.js +5 -0
  60. package/dist/extension/pipeline/runner.d.ts +0 -1
  61. package/dist/extension/pipeline/runner.js +6 -14
  62. package/dist/extension/plugins/inventory.d.ts +88 -0
  63. package/dist/extension/plugins/inventory.js +144 -0
  64. package/dist/extension/plugins/panel.d.ts +45 -0
  65. package/dist/extension/plugins/panel.js +293 -0
  66. package/dist/extension/sandbox/bash.d.ts +99 -0
  67. package/dist/extension/sandbox/bash.js +190 -0
  68. package/dist/extension/sandbox/config.d.ts +114 -0
  69. package/dist/extension/sandbox/config.js +366 -0
  70. package/dist/extension/sandbox/manager.d.ts +98 -0
  71. package/dist/extension/sandbox/manager.js +216 -0
  72. package/dist/extension/sandbox/panel.d.ts +111 -0
  73. package/dist/extension/sandbox/panel.js +342 -0
  74. package/dist/extension/sandbox/session.d.ts +85 -0
  75. package/dist/extension/sandbox/session.js +775 -0
  76. package/dist/extension/telemetry/attrs.d.ts +96 -0
  77. package/dist/extension/telemetry/attrs.js +149 -0
  78. package/dist/extension/telemetry/config.d.ts +99 -0
  79. package/dist/extension/telemetry/config.js +193 -0
  80. package/dist/extension/telemetry/index.d.ts +7 -0
  81. package/dist/extension/telemetry/index.js +7 -0
  82. package/dist/extension/telemetry/probe.d.ts +29 -0
  83. package/dist/extension/telemetry/probe.js +122 -0
  84. package/dist/extension/telemetry/register.d.ts +40 -0
  85. package/dist/extension/telemetry/register.js +192 -0
  86. package/dist/extension/telemetry/sdk.d.ts +63 -0
  87. package/dist/extension/telemetry/sdk.js +207 -0
  88. package/dist/extension/telemetry/tracker.d.ts +131 -0
  89. package/dist/extension/telemetry/tracker.js +551 -0
  90. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  91. package/dist/extension/vendor/ignore.d.ts +86 -0
  92. package/dist/extension/vendor/ignore.js +788 -0
  93. package/dist/goHeadless.d.ts +1 -1
  94. package/dist/goHeadless.js +2 -2
  95. package/dist/launch.d.ts +4 -3
  96. package/dist/launch.js +7 -4
  97. package/dist/mcpCommand.d.ts +122 -0
  98. package/dist/mcpCommand.js +787 -0
  99. package/dist/otel.d.ts +69 -63
  100. package/dist/otel.js +154 -119
  101. package/dist/paths.d.ts +13 -0
  102. package/dist/paths.js +18 -0
  103. package/dist/pluginCommand.d.ts +43 -0
  104. package/dist/pluginCommand.js +499 -0
  105. package/dist/pluginStore.d.ts +170 -0
  106. package/dist/pluginStore.js +554 -0
  107. package/dist/upgrade.d.ts +11 -2
  108. package/dist/upgrade.js +48 -8
  109. package/package.json +20 -3
  110. package/dist/extension/mcpTools.d.ts +0 -57
  111. package/dist/extension/mcpTools.js +0 -132
@@ -0,0 +1,93 @@
1
+ /**
2
+ * MCP prompts as dynamically-registered pi slash commands: every prompt a
3
+ * server exposes becomes `/mcp__<server>__<prompt> [args]` whose handler
4
+ * fetches the rendered messages and injects them as the next user turn via
5
+ * pi.sendUserMessage. pi prompt templates are file-only, so runtime
6
+ * registration is the only path — commands are resolved live on each
7
+ * getRegisteredCommands() call.
8
+ */
9
+ import { buildMcpPromptName } from "./names.js";
10
+ export async function registerServerPrompts(pi, manager, serverName) {
11
+ const server = manager.get(serverName);
12
+ const result = { commands: [], warnings: [] };
13
+ if (!server?.client)
14
+ return result;
15
+ let promptList;
16
+ try {
17
+ promptList = await server.client.listPrompts();
18
+ }
19
+ catch (err) {
20
+ result.warnings.push(`${serverName}: listPrompts failed — ${err instanceof Error ? err.message : String(err)}`);
21
+ return result;
22
+ }
23
+ for (const prompt of promptList.prompts ?? []) {
24
+ const commandName = buildMcpPromptName(serverName, prompt.name);
25
+ if (!commandName)
26
+ continue;
27
+ const argSpec = (prompt.arguments ?? [])
28
+ .map((a) => (a.required ? `<${a.name}>` : `[${a.name}]`))
29
+ .join(" ");
30
+ pi.registerCommand(commandName, {
31
+ description: prompt.description
32
+ ? `${prompt.description} (MCP prompt from "${serverName}")`
33
+ : `MCP prompt "${prompt.name}" from server "${serverName}"`,
34
+ handler: async (args, ctx) => {
35
+ const active = manager.get(serverName);
36
+ if (!active?.client) {
37
+ ctx.ui.notify(`MCP server "${serverName}" is not connected (try /mcp reconnect).`);
38
+ return;
39
+ }
40
+ const parsedArgs = parsePromptArgs(args);
41
+ const missing = (prompt.arguments ?? [])
42
+ .filter((a) => a.required && parsedArgs[a.name] === undefined)
43
+ .map((a) => a.name);
44
+ if (missing.length > 0) {
45
+ ctx.ui.notify(`Missing required argument(s): ${missing.join(", ")}`);
46
+ return;
47
+ }
48
+ try {
49
+ const rendered = await active.client.getPrompt({ name: prompt.name, arguments: parsedArgs });
50
+ const text = (rendered.messages ?? [])
51
+ .map((m) => {
52
+ if (!("content" in m))
53
+ return "";
54
+ const content = Array.isArray(m.content) ? m.content : [m.content];
55
+ return content
56
+ .filter((c) => c && typeof c === "object" && "type" in c && c.type === "text")
57
+ .map((c) => c.text)
58
+ .join("\n");
59
+ })
60
+ .filter(Boolean)
61
+ .join("\n\n");
62
+ if (!text) {
63
+ ctx.ui.notify(`Prompt "${prompt.name}" returned no text content.`);
64
+ return;
65
+ }
66
+ pi.sendUserMessage(text);
67
+ }
68
+ catch (err) {
69
+ ctx.ui.notify(`Prompt failed: ${err instanceof Error ? err.message : String(err)}`);
70
+ }
71
+ },
72
+ ...(argSpec ? { getArgumentCompletions: () => null } : {}),
73
+ });
74
+ result.commands.push({
75
+ command: commandName,
76
+ serverName,
77
+ promptName: prompt.name,
78
+ description: prompt.description ?? `MCP prompt "${prompt.name}"`,
79
+ });
80
+ }
81
+ return result;
82
+ }
83
+ /** "a=1 b=two" → { a: "1", b: "two" }; quotes group spaces. */
84
+ export function parsePromptArgs(args) {
85
+ const parsed = {};
86
+ const re = /(\w+)=("([^"]*)"|'([^']*)'|(\S+))/g;
87
+ let match;
88
+ while ((match = re.exec(args)) !== null) {
89
+ parsed[match[1]] = match[3] ?? match[4] ?? match[5] ?? "";
90
+ }
91
+ return parsed;
92
+ }
93
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Session wiring for MCP: load config, apply the approval gate and the
3
+ * kill-switch, connect servers, register tools/prompts/panel, and tear it
4
+ * all down on session_shutdown. Everything is fail-soft — a broken server
5
+ * or a broken config file never blocks the session.
6
+ *
7
+ * Startup notify (one line when a configured server failed to connect) is a
8
+ * deliberate deviation from Claude Code, which stays silent on failures.
9
+ *
10
+ * `YAGNI_CODE_MCP_DISABLED=1` skips the whole subsystem (kill-switch for
11
+ * incidents / suspicious servers), mirroring CC's MCP_AUTO_CONNECT killer
12
+ * but namespaced to this CLI.
13
+ */
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import { McpManager } from "./manager.js";
16
+ export { _setMcpLogHomeForTest, appendMcpLogLine } from "./log.js";
17
+ export declare function isMcpDisabled(env?: NodeJS.ProcessEnv): boolean;
18
+ export interface StartupOutcome {
19
+ manager: McpManager;
20
+ connectedServers: string[];
21
+ failedServers: {
22
+ name: string;
23
+ error: string;
24
+ }[];
25
+ /** OAuth servers whose connect threw `UnauthorizedError` (interactive auth pending). */
26
+ needsAuthServers: string[];
27
+ skippedApproval: string[];
28
+ configErrors: string[];
29
+ /** Full wire names of registered tools that look mutating (gate input). */
30
+ mutatingToolNames: string[];
31
+ }
32
+ /**
33
+ * The full session-start sequence. Pure with respect to config homes (tests
34
+ * point them at tmpdirs via _setMcpHomeForTest / _setMcpLogHomeForTest).
35
+ */
36
+ export declare function startMcp(pi: ExtensionAPI, opts: {
37
+ cwd: string;
38
+ env?: NodeJS.ProcessEnv;
39
+ hasUI: boolean;
40
+ notify?: (msg: string) => void;
41
+ }): Promise<StartupOutcome>;
42
+ /**
43
+ * Derive the connectivity startup notices from the manager's LIVE state.
44
+ *
45
+ * These must NOT be frozen at `startMcp()` time: the index.ts wiring defers
46
+ * them to the first provider response, and by then the user may have already
47
+ * healed a server via `/mcp` (e.g. authenticated a `needs_auth` server). A
48
+ * server that has since reached `connected` is no longer "needs authentication"
49
+ * or "failed to connect", so recomputing against `manager.list()` at flush
50
+ * time is what keeps the banner honest.
51
+ */
52
+ export declare function deriveStartupConnectivityNotices(manager: McpManager): string[];
53
+ /** Wire shutdown cleanup: closes every transport on session_shutdown (any reason). */
54
+ export declare function wireShutdown(pi: ExtensionAPI, manager: McpManager): void;
55
+ //# sourceMappingURL=startup.d.ts.map
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Session wiring for MCP: load config, apply the approval gate and the
3
+ * kill-switch, connect servers, register tools/prompts/panel, and tear it
4
+ * all down on session_shutdown. Everything is fail-soft — a broken server
5
+ * or a broken config file never blocks the session.
6
+ *
7
+ * Startup notify (one line when a configured server failed to connect) is a
8
+ * deliberate deviation from Claude Code, which stays silent on failures.
9
+ *
10
+ * `YAGNI_CODE_MCP_DISABLED=1` skips the whole subsystem (kill-switch for
11
+ * incidents / suspicious servers), mirroring CC's MCP_AUTO_CONNECT killer
12
+ * but namespaced to this CLI.
13
+ */
14
+ import { loadMcpServers, resolveProjectRoot } from "./config.js";
15
+ import { McpManager } from "./manager.js";
16
+ import { readProjectApproval, undecidedProjectServers } from "./approval.js";
17
+ import { registerMcpPanel } from "./panel.js";
18
+ import { registerServerPrompts } from "./prompts.js";
19
+ import { registerServerTools } from "./tools.js";
20
+ // Re-export the log helpers from the shared module so callers (auth.ts) can
21
+ // import logging without taking a dependency on the session wiring that was
22
+ // their previous home.
23
+ export { _setMcpLogHomeForTest, appendMcpLogLine } from "./log.js";
24
+ import { appendMcpLogLine } from "./log.js";
25
+ export function isMcpDisabled(env = process.env) {
26
+ return env.YAGNI_CODE_MCP_DISABLED === "1";
27
+ }
28
+ /**
29
+ * The full session-start sequence. Pure with respect to config homes (tests
30
+ * point them at tmpdirs via _setMcpHomeForTest / _setMcpLogHomeForTest).
31
+ */
32
+ export async function startMcp(pi, opts) {
33
+ const env = opts.env ?? process.env;
34
+ const outcome = {
35
+ manager: new McpManager({
36
+ events: { onLog: appendMcpLogLine },
37
+ env,
38
+ }),
39
+ connectedServers: [],
40
+ failedServers: [],
41
+ needsAuthServers: [],
42
+ skippedApproval: [],
43
+ configErrors: [],
44
+ mutatingToolNames: [],
45
+ };
46
+ if (isMcpDisabled(env)) {
47
+ appendMcpLogLine(JSON.stringify({ ts: new Date().toISOString(), event: "disabled_by_env" }));
48
+ return outcome;
49
+ }
50
+ const repoRoot = resolveProjectRoot(opts.cwd);
51
+ const { servers, errors } = loadMcpServers(opts.cwd, env);
52
+ outcome.configErrors = errors.map((e) => (e.serverName ? `${e.serverName}: ${e.message}` : e.message));
53
+ registerMcpPanel(pi, outcome.manager, () => outcome.configErrors);
54
+ if (servers.length === 0 && errors.length === 0)
55
+ return outcome;
56
+ // Approval gate for project-scope servers and repo-sourced plugin servers
57
+ // (fail-closed: undecided = skip).
58
+ const { state } = readProjectApproval(repoRoot);
59
+ const needsApproval = (s) => s.scope === "project" || s.gated === true;
60
+ const projectNames = servers.filter(needsApproval).map((s) => s.name);
61
+ const undecided = undecidedProjectServers(state, projectNames);
62
+ outcome.skippedApproval = undecided;
63
+ const gate = (server) => {
64
+ if (!needsApproval(server))
65
+ return true;
66
+ return !undecided.includes(server.name);
67
+ };
68
+ // Ask once, interactively, about undecided project servers (CC parity).
69
+ if (undecided.length > 0 && opts.hasUI && opts.notify) {
70
+ opts.notify(`This repo's config (.mcp.json or a repo plugin) defines MCP server(s) ${undecided.join(", ")} — not yet approved. ` +
71
+ `Run /mcp to approve them individually, or \`yagni mcp reset-project-choices\` to clear old choices.`);
72
+ }
73
+ const connectable = [];
74
+ for (const server of servers) {
75
+ if (!gate(server)) {
76
+ outcome.manager.register(server.name, server.scope, server.config, "disabled", server.gated === true);
77
+ continue;
78
+ }
79
+ connectable.push(server);
80
+ }
81
+ // Connect in parallel; each server fails soft.
82
+ const results = await Promise.all(connectable.map(async (server) => {
83
+ const managed = outcome.manager.register(server.name, server.scope, server.config, "connecting", server.gated === true);
84
+ const connected = await outcome.manager.connect(server.name);
85
+ if (connected?.status === "connected") {
86
+ const tools = await registerServerTools(pi, outcome.manager, server.name, env);
87
+ const prompts = await registerServerPrompts(pi, outcome.manager, server.name);
88
+ outcome.mutatingToolNames.push(...tools.mutatingToolNames);
89
+ for (const w of [...tools.warnings, ...prompts.warnings])
90
+ appendMcpLogLine(JSON.stringify({ ts: new Date().toISOString(), server: server.name, event: "registration_warning", warning: w }));
91
+ return { name: server.name, ok: true, toolCount: tools.tools.length, promptCount: prompts.commands.length };
92
+ }
93
+ return {
94
+ name: server.name,
95
+ ok: false,
96
+ needsAuth: connected?.status === "needs_auth",
97
+ error: connected?.error ?? "connect failed",
98
+ toolCount: 0,
99
+ promptCount: 0,
100
+ };
101
+ }));
102
+ for (const r of results) {
103
+ if (r.ok) {
104
+ outcome.connectedServers.push(r.name);
105
+ appendMcpLogLine(JSON.stringify({ ts: new Date().toISOString(), server: r.name, event: "connected", tools: r.toolCount, prompts: r.promptCount }));
106
+ }
107
+ else if ("needsAuth" in r && r.needsAuth) {
108
+ outcome.needsAuthServers.push(r.name);
109
+ appendMcpLogLine(JSON.stringify({ ts: new Date().toISOString(), server: r.name, event: "needs_auth" }));
110
+ }
111
+ else {
112
+ outcome.failedServers.push({ name: r.name, error: r.error });
113
+ }
114
+ }
115
+ return outcome;
116
+ }
117
+ /**
118
+ * Derive the connectivity startup notices from the manager's LIVE state.
119
+ *
120
+ * These must NOT be frozen at `startMcp()` time: the index.ts wiring defers
121
+ * them to the first provider response, and by then the user may have already
122
+ * healed a server via `/mcp` (e.g. authenticated a `needs_auth` server). A
123
+ * server that has since reached `connected` is no longer "needs authentication"
124
+ * or "failed to connect", so recomputing against `manager.list()` at flush
125
+ * time is what keeps the banner honest.
126
+ */
127
+ export function deriveStartupConnectivityNotices(manager) {
128
+ const failed = [];
129
+ const needsAuth = [];
130
+ for (const server of manager.list()) {
131
+ if (server.status === "failed")
132
+ failed.push(server.name);
133
+ else if (server.status === "needs_auth")
134
+ needsAuth.push(server.name);
135
+ }
136
+ const notices = [];
137
+ if (failed.length > 0) {
138
+ notices.push(`MCP server${failed.length === 1 ? "" : "s"} ${failed.join(", ")} failed to connect — run /mcp to inspect or reconnect.`);
139
+ }
140
+ if (needsAuth.length > 0) {
141
+ notices.push(`MCP server${needsAuth.length === 1 ? "" : "s"} ${needsAuth.join(", ")} needs authentication — run /mcp to authenticate.`);
142
+ }
143
+ return notices;
144
+ }
145
+ /** Wire shutdown cleanup: closes every transport on session_shutdown (any reason). */
146
+ export function wireShutdown(pi, manager) {
147
+ pi.on("session_shutdown", async () => {
148
+ appendMcpLogLine(JSON.stringify({ ts: new Date().toISOString(), event: "session_shutdown" }));
149
+ await manager.closeAll();
150
+ });
151
+ }
152
+ //# sourceMappingURL=startup.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Tool registration for connected MCP servers: one pi tool per MCP tool,
3
+ * named mcp__<server>__<tool> (Claude Code-compatible, so muscle memory and
4
+ * prompts transfer). Descriptions are capped at 2048 chars like Claude Code's
5
+ * client; MCP inputSchemas are plain JSON Schema, which is what TypeBox
6
+ * schemas are at runtime, so they pass through untouched.
7
+ *
8
+ * Known v1 limitation (shared with early Claude Code): tools are registered
9
+ * eagerly per server — a server exposing 100 tools puts 100 tools into every
10
+ * request. No tool-list deferral / ToolSearch in v1.
11
+ */
12
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import type { McpManager } from "./manager.js";
14
+ export declare const MAX_MCP_DESCRIPTION_LENGTH = 2048;
15
+ export interface RegisteredToolInfo {
16
+ toolName: string;
17
+ serverName: string;
18
+ originalName: string;
19
+ description: string;
20
+ }
21
+ export interface ToolRegistrationResult {
22
+ tools: RegisteredToolInfo[];
23
+ /** Full registered tool names that look mutating (gate input). */
24
+ mutatingToolNames: string[];
25
+ warnings: string[];
26
+ }
27
+ export declare function registerServerTools(pi: ExtensionAPI, manager: McpManager, serverName: string, env?: NodeJS.ProcessEnv): Promise<ToolRegistrationResult>;
28
+ export declare function capDescription(description: string): string;
29
+ /** Cheap heuristic in the spirit of Claude Code's input-hint check; per-tool annotations arrive via listTools only in newer servers. */
30
+ export declare function looksMutating(toolName: string, description?: string): boolean;
31
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Tool registration for connected MCP servers: one pi tool per MCP tool,
3
+ * named mcp__<server>__<tool> (Claude Code-compatible, so muscle memory and
4
+ * prompts transfer). Descriptions are capped at 2048 chars like Claude Code's
5
+ * client; MCP inputSchemas are plain JSON Schema, which is what TypeBox
6
+ * schemas are at runtime, so they pass through untouched.
7
+ *
8
+ * Known v1 limitation (shared with early Claude Code): tools are registered
9
+ * eagerly per server — a server exposing 100 tools puts 100 tools into every
10
+ * request. No tool-list deferral / ToolSearch in v1.
11
+ */
12
+ import { toolTimeoutFromEnv } from "./manager.js";
13
+ import { buildMcpToolName } from "./names.js";
14
+ export const MAX_MCP_DESCRIPTION_LENGTH = 2048;
15
+ export async function registerServerTools(pi, manager, serverName, env = process.env) {
16
+ const server = manager.get(serverName);
17
+ const result = { tools: [], mutatingToolNames: [], warnings: [] };
18
+ if (!server?.client)
19
+ return result;
20
+ let toolList;
21
+ try {
22
+ toolList = await server.client.listTools();
23
+ }
24
+ catch (err) {
25
+ result.warnings.push(`${serverName}: listTools failed — ${err instanceof Error ? err.message : String(err)}`);
26
+ return result;
27
+ }
28
+ const toolTimeout = toolTimeoutFromEnv(env);
29
+ for (const tool of toolList.tools ?? []) {
30
+ const fullToolName = buildMcpToolName(serverName, tool.name);
31
+ if (!fullToolName) {
32
+ result.warnings.push(`${serverName}: tool "${tool.name}" produced an empty wire name; skipped`);
33
+ continue;
34
+ }
35
+ const description = capDescription(`${tool.description ?? `MCP tool ${tool.name}`} (MCP server "${serverName}")`);
36
+ const mutating = looksMutating(tool.name, tool.description);
37
+ const definition = {
38
+ name: fullToolName,
39
+ label: `${serverName}: ${tool.name}`,
40
+ description,
41
+ parameters: schemaFor(tool.inputSchema),
42
+ async execute(_toolCallId, params) {
43
+ const active = manager.get(serverName);
44
+ if (!active?.client) {
45
+ throw new Error(`MCP server "${serverName}" is not connected (try /mcp reconnect).`);
46
+ }
47
+ const args = (params && typeof params === "object" ? params : {});
48
+ const call = active.client.callTool({ name: tool.name, arguments: args });
49
+ const settled = toolTimeout
50
+ ? await withTimeout(call, toolTimeout, `MCP tool call timed out after ${toolTimeout}ms`)
51
+ : await call;
52
+ return renderCallResult(settled, serverName, tool.name);
53
+ },
54
+ };
55
+ pi.registerTool(definition);
56
+ result.tools.push({ toolName: fullToolName, serverName, originalName: tool.name, description });
57
+ if (mutating)
58
+ result.mutatingToolNames.push(fullToolName);
59
+ }
60
+ return result;
61
+ }
62
+ export function capDescription(description) {
63
+ return description.length > MAX_MCP_DESCRIPTION_LENGTH
64
+ ? description.slice(0, MAX_MCP_DESCRIPTION_LENGTH - 1) + "…"
65
+ : description;
66
+ }
67
+ /** Cheap heuristic in the spirit of Claude Code's input-hint check; per-tool annotations arrive via listTools only in newer servers. */
68
+ export function looksMutating(toolName, description) {
69
+ const name = toolName.toLowerCase();
70
+ const writeHints = /^(create|add|update|edit|delete|remove|set|write|send|post|put|patch|deploy|publish|close|merge|assign|move|archive|trash|restore)/;
71
+ if (writeHints.test(name))
72
+ return true;
73
+ if (description) {
74
+ const d = description.toLowerCase();
75
+ if (/\b(create|delete|update|modify|write|send|publish|deploy|remove)s?\b/.test(d.slice(0, 400)))
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+ function schemaFor(inputSchema) {
81
+ if (inputSchema !== null &&
82
+ typeof inputSchema === "object" &&
83
+ !Array.isArray(inputSchema) &&
84
+ Object.keys(inputSchema).length > 0) {
85
+ // MCP inputSchemas are plain JSON Schema objects, which is exactly what
86
+ // TypeBox schemas are at runtime — pass the server's schema through so
87
+ // the model sees real parameter shapes.
88
+ return inputSchema;
89
+ }
90
+ return { type: "object", properties: {} };
91
+ }
92
+ function renderCallResult(settled, serverName, toolName) {
93
+ const parts = [];
94
+ for (const item of settled.content ?? []) {
95
+ if (item && typeof item === "object" && item.type === "text") {
96
+ parts.push(String(item.text ?? ""));
97
+ }
98
+ }
99
+ const text = parts.join("\n") || "(no text content)";
100
+ if (settled.isError === true) {
101
+ throw new Error(`MCP tool error from "${serverName}.${toolName}": ${text}`);
102
+ }
103
+ return { content: [{ type: "text", text }], details: { server: serverName, tool: toolName } };
104
+ }
105
+ function withTimeout(promise, ms, message) {
106
+ return new Promise((resolve, reject) => {
107
+ const timer = setTimeout(() => reject(new Error(message)), ms);
108
+ promise.then((value) => {
109
+ clearTimeout(timer);
110
+ resolve(value);
111
+ }, (err) => {
112
+ clearTimeout(timer);
113
+ reject(err);
114
+ });
115
+ });
116
+ }
117
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Transport construction: config shape → SDK transport instance. Kept as a
3
+ * separate module so tests can construct transports without a live process,
4
+ * and so the manager stays free of SDK import-path churn.
5
+ */
6
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
8
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
9
+ import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
10
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
11
+ import type { McpHttpServerConfig, McpServerConfig, McpStdioServerConfig } from "./config.js";
12
+ export declare const DEFAULT_CONNECT_TIMEOUT_MS = 30000;
13
+ export declare function stdioTransport(config: McpStdioServerConfig): StdioClientTransport;
14
+ export declare function httpTransport(config: McpHttpServerConfig, authProvider?: OAuthClientProvider, fetchImpl?: typeof fetch): StreamableHTTPClientTransport;
15
+ export declare function sseTransport(config: McpHttpServerConfig, authProvider?: OAuthClientProvider, fetchImpl?: typeof fetch): SSEClientTransport;
16
+ export declare function transportFor(config: McpServerConfig, authProvider?: OAuthClientProvider, fetchImpl?: typeof fetch): Transport;
17
+ //# sourceMappingURL=transports.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Transport construction: config shape → SDK transport instance. Kept as a
3
+ * separate module so tests can construct transports without a live process,
4
+ * and so the manager stays free of SDK import-path churn.
5
+ */
6
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
8
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
9
+ export const DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
10
+ export function stdioTransport(config) {
11
+ return new StdioClientTransport({
12
+ command: config.command,
13
+ args: config.args ?? [],
14
+ env: config.env,
15
+ stderr: "pipe",
16
+ });
17
+ }
18
+ export function httpTransport(config, authProvider, fetchImpl) {
19
+ const url = new URL(config.url);
20
+ const headers = { ...config.headers };
21
+ return new StreamableHTTPClientTransport(url, {
22
+ requestInit: { headers },
23
+ ...(authProvider ? { authProvider } : {}),
24
+ ...(fetchImpl ? { fetch: fetchImpl } : {}),
25
+ });
26
+ }
27
+ export function sseTransport(config, authProvider, fetchImpl) {
28
+ const headers = { ...config.headers };
29
+ return new SSEClientTransport(new URL(config.url), {
30
+ eventSourceInit: {},
31
+ requestInit: { headers },
32
+ ...(authProvider ? { authProvider } : {}),
33
+ ...(fetchImpl ? { fetch: fetchImpl } : {}),
34
+ });
35
+ }
36
+ export function transportFor(config, authProvider, fetchImpl) {
37
+ if (config.type === "http")
38
+ return httpTransport(config, authProvider, fetchImpl);
39
+ if (config.type === "sse")
40
+ return sseTransport(config, authProvider, fetchImpl);
41
+ // type "stdio" or absent (CC-style bare command entries)
42
+ return stdioTransport(config);
43
+ }
44
+ //# sourceMappingURL=transports.js.map