@bubblebrain-ai/bubble 0.0.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.
Files changed (248) hide show
  1. package/README.md +70 -0
  2. package/dist/agent/evidence-tracker.d.ts +15 -0
  3. package/dist/agent/evidence-tracker.js +93 -0
  4. package/dist/agent/execution-governor.d.ts +30 -0
  5. package/dist/agent/execution-governor.js +169 -0
  6. package/dist/agent/subtask-policy.d.ts +14 -0
  7. package/dist/agent/subtask-policy.js +60 -0
  8. package/dist/agent/task-classifier.d.ts +3 -0
  9. package/dist/agent/task-classifier.js +36 -0
  10. package/dist/agent/tool-arbiter.d.ts +7 -0
  11. package/dist/agent/tool-arbiter.js +33 -0
  12. package/dist/agent/tool-intent.d.ts +20 -0
  13. package/dist/agent/tool-intent.js +176 -0
  14. package/dist/agent.d.ts +95 -0
  15. package/dist/agent.js +672 -0
  16. package/dist/approval/controller.d.ts +48 -0
  17. package/dist/approval/controller.js +78 -0
  18. package/dist/approval/danger.d.ts +13 -0
  19. package/dist/approval/danger.js +55 -0
  20. package/dist/approval/diff-hunks.d.ts +12 -0
  21. package/dist/approval/diff-hunks.js +32 -0
  22. package/dist/approval/session-cache.d.ts +35 -0
  23. package/dist/approval/session-cache.js +68 -0
  24. package/dist/approval/tool-helper.d.ts +14 -0
  25. package/dist/approval/tool-helper.js +32 -0
  26. package/dist/approval/types.d.ts +56 -0
  27. package/dist/approval/types.js +8 -0
  28. package/dist/bubble-home.d.ts +8 -0
  29. package/dist/bubble-home.js +19 -0
  30. package/dist/cli.d.ts +19 -0
  31. package/dist/cli.js +82 -0
  32. package/dist/config.d.ts +41 -0
  33. package/dist/config.js +144 -0
  34. package/dist/context/budget.d.ts +21 -0
  35. package/dist/context/budget.js +72 -0
  36. package/dist/context/compact-llm.d.ts +16 -0
  37. package/dist/context/compact-llm.js +132 -0
  38. package/dist/context/compact.d.ts +15 -0
  39. package/dist/context/compact.js +251 -0
  40. package/dist/context/overflow.d.ts +9 -0
  41. package/dist/context/overflow.js +46 -0
  42. package/dist/context/projector.d.ts +26 -0
  43. package/dist/context/projector.js +150 -0
  44. package/dist/context/prune.d.ts +9 -0
  45. package/dist/context/prune.js +111 -0
  46. package/dist/lsp/config.d.ts +18 -0
  47. package/dist/lsp/config.js +58 -0
  48. package/dist/lsp/diagnostics.d.ts +24 -0
  49. package/dist/lsp/diagnostics.js +103 -0
  50. package/dist/lsp/index.d.ts +3 -0
  51. package/dist/lsp/index.js +3 -0
  52. package/dist/lsp/service.d.ts +85 -0
  53. package/dist/lsp/service.js +695 -0
  54. package/dist/main.d.ts +5 -0
  55. package/dist/main.js +352 -0
  56. package/dist/mcp/client.d.ts +68 -0
  57. package/dist/mcp/client.js +163 -0
  58. package/dist/mcp/config.d.ts +26 -0
  59. package/dist/mcp/config.js +127 -0
  60. package/dist/mcp/manager.d.ts +55 -0
  61. package/dist/mcp/manager.js +296 -0
  62. package/dist/mcp/name.d.ts +26 -0
  63. package/dist/mcp/name.js +40 -0
  64. package/dist/mcp/transports.d.ts +53 -0
  65. package/dist/mcp/transports.js +248 -0
  66. package/dist/mcp/types.d.ts +111 -0
  67. package/dist/mcp/types.js +14 -0
  68. package/dist/memory/db.d.ts +62 -0
  69. package/dist/memory/db.js +313 -0
  70. package/dist/memory/index.d.ts +9 -0
  71. package/dist/memory/index.js +9 -0
  72. package/dist/memory/paths.d.ts +18 -0
  73. package/dist/memory/paths.js +38 -0
  74. package/dist/memory/phase1.d.ts +23 -0
  75. package/dist/memory/phase1.js +172 -0
  76. package/dist/memory/phase2.d.ts +19 -0
  77. package/dist/memory/phase2.js +100 -0
  78. package/dist/memory/prompts.d.ts +19 -0
  79. package/dist/memory/prompts.js +99 -0
  80. package/dist/memory/reset.d.ts +1 -0
  81. package/dist/memory/reset.js +13 -0
  82. package/dist/memory/start.d.ts +24 -0
  83. package/dist/memory/start.js +50 -0
  84. package/dist/memory/storage.d.ts +10 -0
  85. package/dist/memory/storage.js +82 -0
  86. package/dist/memory/store.d.ts +43 -0
  87. package/dist/memory/store.js +193 -0
  88. package/dist/memory/usage.d.ts +1 -0
  89. package/dist/memory/usage.js +38 -0
  90. package/dist/model-catalog.d.ts +20 -0
  91. package/dist/model-catalog.js +99 -0
  92. package/dist/model-config.d.ts +32 -0
  93. package/dist/model-config.js +59 -0
  94. package/dist/model-pricing.d.ts +23 -0
  95. package/dist/model-pricing.js +46 -0
  96. package/dist/oauth/index.d.ts +3 -0
  97. package/dist/oauth/index.js +2 -0
  98. package/dist/oauth/openai-codex.d.ts +9 -0
  99. package/dist/oauth/openai-codex.js +173 -0
  100. package/dist/oauth/storage.d.ts +18 -0
  101. package/dist/oauth/storage.js +60 -0
  102. package/dist/oauth/types.d.ts +15 -0
  103. package/dist/oauth/types.js +1 -0
  104. package/dist/orchestrator/default-hooks.d.ts +2 -0
  105. package/dist/orchestrator/default-hooks.js +96 -0
  106. package/dist/orchestrator/hooks.d.ts +78 -0
  107. package/dist/orchestrator/hooks.js +52 -0
  108. package/dist/orchestrator/workflow.d.ts +10 -0
  109. package/dist/orchestrator/workflow.js +22 -0
  110. package/dist/permission/mode.d.ts +23 -0
  111. package/dist/permission/mode.js +20 -0
  112. package/dist/permissions/rule.d.ts +39 -0
  113. package/dist/permissions/rule.js +234 -0
  114. package/dist/permissions/settings.d.ts +71 -0
  115. package/dist/permissions/settings.js +202 -0
  116. package/dist/permissions/types.d.ts +61 -0
  117. package/dist/permissions/types.js +14 -0
  118. package/dist/prompt/compose.d.ts +12 -0
  119. package/dist/prompt/compose.js +67 -0
  120. package/dist/prompt/environment.d.ts +12 -0
  121. package/dist/prompt/environment.js +38 -0
  122. package/dist/prompt/provider-prompts/anthropic.d.ts +1 -0
  123. package/dist/prompt/provider-prompts/anthropic.js +5 -0
  124. package/dist/prompt/provider-prompts/codex.d.ts +1 -0
  125. package/dist/prompt/provider-prompts/codex.js +5 -0
  126. package/dist/prompt/provider-prompts/default.d.ts +1 -0
  127. package/dist/prompt/provider-prompts/default.js +6 -0
  128. package/dist/prompt/provider-prompts/gemini.d.ts +1 -0
  129. package/dist/prompt/provider-prompts/gemini.js +5 -0
  130. package/dist/prompt/provider-prompts/gpt.d.ts +1 -0
  131. package/dist/prompt/provider-prompts/gpt.js +5 -0
  132. package/dist/prompt/reminders.d.ts +30 -0
  133. package/dist/prompt/reminders.js +164 -0
  134. package/dist/prompt/runtime.d.ts +12 -0
  135. package/dist/prompt/runtime.js +31 -0
  136. package/dist/prompt/skills.d.ts +2 -0
  137. package/dist/prompt/skills.js +4 -0
  138. package/dist/provider-openai-codex.d.ts +14 -0
  139. package/dist/provider-openai-codex.js +409 -0
  140. package/dist/provider-registry.d.ts +56 -0
  141. package/dist/provider-registry.js +244 -0
  142. package/dist/provider-transform.d.ts +10 -0
  143. package/dist/provider-transform.js +69 -0
  144. package/dist/provider.d.ts +31 -0
  145. package/dist/provider.js +269 -0
  146. package/dist/question/controller.d.ts +22 -0
  147. package/dist/question/controller.js +97 -0
  148. package/dist/question/index.d.ts +2 -0
  149. package/dist/question/index.js +2 -0
  150. package/dist/question/types.d.ts +42 -0
  151. package/dist/question/types.js +6 -0
  152. package/dist/session-log.d.ts +16 -0
  153. package/dist/session-log.js +267 -0
  154. package/dist/session-types.d.ts +55 -0
  155. package/dist/session-types.js +1 -0
  156. package/dist/session.d.ts +32 -0
  157. package/dist/session.js +135 -0
  158. package/dist/skills/discovery.d.ts +12 -0
  159. package/dist/skills/discovery.js +148 -0
  160. package/dist/skills/format.d.ts +2 -0
  161. package/dist/skills/format.js +47 -0
  162. package/dist/skills/frontmatter.d.ts +5 -0
  163. package/dist/skills/frontmatter.js +60 -0
  164. package/dist/skills/invocation.d.ts +8 -0
  165. package/dist/skills/invocation.js +51 -0
  166. package/dist/skills/registry.d.ts +17 -0
  167. package/dist/skills/registry.js +42 -0
  168. package/dist/skills/types.d.ts +32 -0
  169. package/dist/skills/types.js +1 -0
  170. package/dist/slash-commands/commands.d.ts +7 -0
  171. package/dist/slash-commands/commands.js +779 -0
  172. package/dist/slash-commands/index.d.ts +4 -0
  173. package/dist/slash-commands/index.js +8 -0
  174. package/dist/slash-commands/registry.d.ts +31 -0
  175. package/dist/slash-commands/registry.js +70 -0
  176. package/dist/slash-commands/types.d.ts +44 -0
  177. package/dist/slash-commands/types.js +1 -0
  178. package/dist/slash-commands/unified.d.ts +38 -0
  179. package/dist/slash-commands/unified.js +38 -0
  180. package/dist/system-prompt.d.ts +34 -0
  181. package/dist/system-prompt.js +7 -0
  182. package/dist/tools/bash.d.ts +6 -0
  183. package/dist/tools/bash.js +135 -0
  184. package/dist/tools/edit.d.ts +16 -0
  185. package/dist/tools/edit.js +95 -0
  186. package/dist/tools/exa-mcp.d.ts +3 -0
  187. package/dist/tools/exa-mcp.js +74 -0
  188. package/dist/tools/exit-plan-mode.d.ts +17 -0
  189. package/dist/tools/exit-plan-mode.js +68 -0
  190. package/dist/tools/glob.d.ts +5 -0
  191. package/dist/tools/glob.js +129 -0
  192. package/dist/tools/grep.d.ts +5 -0
  193. package/dist/tools/grep.js +111 -0
  194. package/dist/tools/index.d.ts +36 -0
  195. package/dist/tools/index.js +59 -0
  196. package/dist/tools/lsp.d.ts +4 -0
  197. package/dist/tools/lsp.js +92 -0
  198. package/dist/tools/memory.d.ts +3 -0
  199. package/dist/tools/memory.js +90 -0
  200. package/dist/tools/question.d.ts +3 -0
  201. package/dist/tools/question.js +174 -0
  202. package/dist/tools/read.d.ts +7 -0
  203. package/dist/tools/read.js +83 -0
  204. package/dist/tools/sensitive-paths.d.ts +3 -0
  205. package/dist/tools/sensitive-paths.js +24 -0
  206. package/dist/tools/skill.d.ts +5 -0
  207. package/dist/tools/skill.js +51 -0
  208. package/dist/tools/task.d.ts +2 -0
  209. package/dist/tools/task.js +57 -0
  210. package/dist/tools/todo.d.ts +12 -0
  211. package/dist/tools/todo.js +151 -0
  212. package/dist/tools/tool-search.d.ts +23 -0
  213. package/dist/tools/tool-search.js +124 -0
  214. package/dist/tools/web-fetch.d.ts +6 -0
  215. package/dist/tools/web-fetch.js +75 -0
  216. package/dist/tools/web-search.d.ts +5 -0
  217. package/dist/tools/web-search.js +49 -0
  218. package/dist/tools/write.d.ts +11 -0
  219. package/dist/tools/write.js +77 -0
  220. package/dist/tui/display-history.d.ts +35 -0
  221. package/dist/tui/display-history.js +243 -0
  222. package/dist/tui/file-mentions.d.ts +29 -0
  223. package/dist/tui/file-mentions.js +174 -0
  224. package/dist/tui/image-paste.d.ts +54 -0
  225. package/dist/tui/image-paste.js +288 -0
  226. package/dist/tui/markdown-theme-rules.d.ts +23 -0
  227. package/dist/tui/markdown-theme-rules.js +164 -0
  228. package/dist/tui/markdown-theme.d.ts +5 -0
  229. package/dist/tui/markdown-theme.js +27 -0
  230. package/dist/tui/opencode-spinner.d.ts +21 -0
  231. package/dist/tui/opencode-spinner.js +216 -0
  232. package/dist/tui/prompt-keybindings.d.ts +41 -0
  233. package/dist/tui/prompt-keybindings.js +28 -0
  234. package/dist/tui/recent-activity.d.ts +8 -0
  235. package/dist/tui/recent-activity.js +71 -0
  236. package/dist/tui/run.d.ts +39 -0
  237. package/dist/tui/run.js +5696 -0
  238. package/dist/tui/sidebar-mcp.d.ts +31 -0
  239. package/dist/tui/sidebar-mcp.js +62 -0
  240. package/dist/tui/sidebar-state.d.ts +12 -0
  241. package/dist/tui/sidebar-state.js +69 -0
  242. package/dist/types.d.ts +219 -0
  243. package/dist/types.js +4 -0
  244. package/dist/variant/thinking-level.d.ts +5 -0
  245. package/dist/variant/thinking-level.js +25 -0
  246. package/dist/variant/variant-resolver.d.ts +4 -0
  247. package/dist/variant/variant-resolver.js +12 -0
  248. package/package.json +47 -0
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Load and validate mcpServers from user / project / local settings files.
3
+ *
4
+ * Precedence: later scope wins (user → project → local). Each server is
5
+ * identified by its key in mcpServers; if the same name appears in multiple
6
+ * scopes, the higher-precedence scope overwrites, and we record a diagnostic.
7
+ *
8
+ * Env expansion: ${VAR} in command, args, env values, url, or header values
9
+ * is replaced with the matching process.env entry. Missing vars become empty
10
+ * strings and yield a diagnostic (non-fatal).
11
+ */
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { getBubbleHome } from "../bubble-home.js";
15
+ export function loadMcpConfig(options) {
16
+ const bubbleHome = options.bubbleHome ?? getBubbleHome();
17
+ const paths = {
18
+ user: join(bubbleHome, "settings.json"),
19
+ project: join(options.cwd, ".bubble", "settings.json"),
20
+ local: join(options.cwd, ".bubble", "settings.local.json"),
21
+ };
22
+ const diagnostics = [];
23
+ const merged = new Map();
24
+ for (const scope of ["user", "project", "local"]) {
25
+ const path = paths[scope];
26
+ if (!existsSync(path))
27
+ continue;
28
+ let raw;
29
+ try {
30
+ raw = JSON.parse(readFileSync(path, "utf-8"));
31
+ }
32
+ catch (err) {
33
+ diagnostics.push({ scope, path, message: `Failed to parse: ${err.message}` });
34
+ continue;
35
+ }
36
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
37
+ continue;
38
+ const mcpServers = raw.mcpServers;
39
+ if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers))
40
+ continue;
41
+ for (const [name, value] of Object.entries(mcpServers)) {
42
+ const validated = validateServerConfig(value, (msg) => {
43
+ diagnostics.push({ scope, path, message: `mcpServers.${name}: ${msg}` });
44
+ });
45
+ if (!validated)
46
+ continue;
47
+ const expanded = expandConfigEnv(validated, (missing) => {
48
+ diagnostics.push({
49
+ scope,
50
+ path,
51
+ message: `mcpServers.${name}: env var "${missing}" is not set; expanded to empty string.`,
52
+ });
53
+ });
54
+ if (merged.has(name)) {
55
+ diagnostics.push({
56
+ scope,
57
+ path,
58
+ message: `mcpServers.${name}: overrides entry from ${merged.get(name).scope} scope.`,
59
+ });
60
+ }
61
+ merged.set(name, { name, scope, config: expanded });
62
+ }
63
+ }
64
+ return { servers: [...merged.values()], diagnostics };
65
+ }
66
+ function validateServerConfig(value, report) {
67
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
68
+ report("expected an object");
69
+ return null;
70
+ }
71
+ const v = value;
72
+ const type = typeof v.type === "string" ? v.type : v.command ? "stdio" : undefined;
73
+ if (type === "stdio") {
74
+ if (typeof v.command !== "string" || !v.command.trim()) {
75
+ report('stdio server requires a non-empty "command"');
76
+ return null;
77
+ }
78
+ const args = Array.isArray(v.args) ? v.args.filter((a) => typeof a === "string") : [];
79
+ const env = v.env && typeof v.env === "object" && !Array.isArray(v.env)
80
+ ? Object.fromEntries(Object.entries(v.env).filter(([, val]) => typeof val === "string"))
81
+ : undefined;
82
+ const cwd = typeof v.cwd === "string" ? v.cwd : undefined;
83
+ return { type: "stdio", command: v.command, args, env, cwd };
84
+ }
85
+ if (type === "http" || type === "sse") {
86
+ if (typeof v.url !== "string" || !v.url.trim()) {
87
+ report(`${type} server requires a non-empty "url"`);
88
+ return null;
89
+ }
90
+ const headers = v.headers && typeof v.headers === "object" && !Array.isArray(v.headers)
91
+ ? Object.fromEntries(Object.entries(v.headers).filter(([, val]) => typeof val === "string"))
92
+ : undefined;
93
+ return { type, url: v.url, headers };
94
+ }
95
+ report(`unsupported transport type "${String(v.type)}" (expected "stdio", "http", or "sse")`);
96
+ return null;
97
+ }
98
+ function expandEnv(input, onMissing) {
99
+ return input.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => {
100
+ const value = process.env[name];
101
+ if (value === undefined) {
102
+ onMissing(name);
103
+ return "";
104
+ }
105
+ return value;
106
+ });
107
+ }
108
+ function expandConfigEnv(config, onMissing) {
109
+ if (config.type === "stdio") {
110
+ return {
111
+ type: "stdio",
112
+ command: expandEnv(config.command, onMissing),
113
+ args: config.args?.map((a) => expandEnv(a, onMissing)),
114
+ env: config.env
115
+ ? Object.fromEntries(Object.entries(config.env).map(([k, v]) => [k, expandEnv(v, onMissing)]))
116
+ : undefined,
117
+ cwd: config.cwd ? expandEnv(config.cwd, onMissing) : undefined,
118
+ };
119
+ }
120
+ return {
121
+ type: config.type,
122
+ url: expandEnv(config.url, onMissing),
123
+ headers: config.headers
124
+ ? Object.fromEntries(Object.entries(config.headers).map(([k, v]) => [k, expandEnv(v, onMissing)]))
125
+ : undefined,
126
+ };
127
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * MCPConnectionManager — brings up all configured servers in parallel,
3
+ * discovers their tools, and exposes ToolRegistryEntry[] for the agent.
4
+ *
5
+ * One failed server never blocks the others. Failures are captured in
6
+ * per-server state so /mcp can surface them to the user without taking the
7
+ * agent down.
8
+ */
9
+ import type { ToolRegistryEntry } from "../types.js";
10
+ import type { UnifiedCommand } from "../slash-commands/unified.js";
11
+ import type { McpServerState, ScopedMcpServerConfig } from "./types.js";
12
+ export interface McpManagerOptions {
13
+ servers: ScopedMcpServerConfig[];
14
+ clientInfo?: {
15
+ name: string;
16
+ version: string;
17
+ };
18
+ /** Log callback for non-fatal issues. Defaults to stderr. */
19
+ onDiagnostic?: (message: string) => void;
20
+ }
21
+ export declare class McpManager {
22
+ private readonly options;
23
+ private connections;
24
+ private readonly clientInfo;
25
+ private readonly onDiagnostic;
26
+ constructor(options: McpManagerOptions);
27
+ /** Connect all configured servers. Never throws — per-server errors are captured in state. */
28
+ start(): Promise<void>;
29
+ private connectOne;
30
+ /** Disconnect and reconnect a single server. Returns the new state. */
31
+ reconnect(name: string): Promise<McpServerState | null>;
32
+ /** Shutdown all connections. Best-effort; errors are ignored. */
33
+ shutdown(): Promise<void>;
34
+ getStates(): McpServerState[];
35
+ /** Produce ToolRegistryEntry[] for every tool from every connected server. */
36
+ getToolEntries(): ToolRegistryEntry[];
37
+ /**
38
+ * Produce UnifiedCommand[] for every prompt from every connected server.
39
+ * Each command, when invoked, calls `prompts/get` with positionally-parsed
40
+ * arguments and returns an `inject` payload that the harness hands back to
41
+ * the agent as the user's next turn.
42
+ *
43
+ * Naming: prompts are exposed as /<promptName> directly (flat namespace,
44
+ * matching opencode's convention). When two servers export the same prompt
45
+ * name, last-registered wins and a diagnostic is emitted so the user can
46
+ * see which server "shadowed" which. Server iteration order follows the
47
+ * Map insertion order (stable: servers are inserted once at constructor
48
+ * time from the config's declared order).
49
+ *
50
+ * Tools, in contrast, keep their `mcp__<server>__<tool>` names so existing
51
+ * Claude Code-style permission rules (e.g. `Bash(git status)` neighbours)
52
+ * continue to match. See src/mcp/name.ts.
53
+ */
54
+ getPromptCommands(): UnifiedCommand[];
55
+ }
@@ -0,0 +1,296 @@
1
+ /**
2
+ * MCPConnectionManager — brings up all configured servers in parallel,
3
+ * discovers their tools, and exposes ToolRegistryEntry[] for the agent.
4
+ *
5
+ * One failed server never blocks the others. Failures are captured in
6
+ * per-server state so /mcp can surface them to the user without taking the
7
+ * agent down.
8
+ */
9
+ import { MCPClient } from "./client.js";
10
+ import { HttpTransport, StdioTransport } from "./transports.js";
11
+ import { buildMcpToolName, normalizeNameForMCP } from "./name.js";
12
+ export class McpManager {
13
+ options;
14
+ connections = new Map();
15
+ clientInfo;
16
+ onDiagnostic;
17
+ constructor(options) {
18
+ this.options = options;
19
+ this.clientInfo = options.clientInfo ?? { name: "bubble", version: "0.1.0" };
20
+ this.onDiagnostic = options.onDiagnostic ?? ((msg) => console.error(msg));
21
+ for (const server of options.servers) {
22
+ this.connections.set(server.name, {
23
+ state: { name: server.name, scope: server.scope, config: server.config, status: { kind: "disabled" } },
24
+ });
25
+ }
26
+ }
27
+ /** Connect all configured servers. Never throws — per-server errors are captured in state. */
28
+ async start() {
29
+ await Promise.all(this.options.servers.map(async (server) => {
30
+ await this.connectOne(server);
31
+ }));
32
+ }
33
+ async connectOne(server) {
34
+ const existing = this.connections.get(server.name);
35
+ try {
36
+ const client = await createClient(server.config, this.clientInfo);
37
+ const tools = await client.listTools();
38
+ const prompts = client.capabilities?.prompts
39
+ ? await client.listPrompts().catch((err) => {
40
+ this.onDiagnostic(`[mcp:${server.name}] prompts/list failed: ${err.message}`);
41
+ return [];
42
+ })
43
+ : [];
44
+ existing.client = client;
45
+ existing.state = {
46
+ name: server.name,
47
+ scope: server.scope,
48
+ config: server.config,
49
+ status: { kind: "connected", tools, prompts, serverInfo: client.serverInfo },
50
+ };
51
+ }
52
+ catch (err) {
53
+ const message = err instanceof Error ? err.message : String(err);
54
+ existing.state = {
55
+ name: server.name,
56
+ scope: server.scope,
57
+ config: server.config,
58
+ status: { kind: "failed", error: message },
59
+ };
60
+ this.onDiagnostic(`[mcp:${server.name}] failed to connect: ${message}`);
61
+ }
62
+ }
63
+ /** Disconnect and reconnect a single server. Returns the new state. */
64
+ async reconnect(name) {
65
+ const conn = this.connections.get(name);
66
+ if (!conn)
67
+ return null;
68
+ if (conn.client) {
69
+ try {
70
+ await conn.client.close();
71
+ }
72
+ catch {
73
+ // ignore
74
+ }
75
+ conn.client = undefined;
76
+ }
77
+ const server = {
78
+ name: conn.state.name,
79
+ scope: conn.state.scope,
80
+ config: conn.state.config,
81
+ };
82
+ await this.connectOne(server);
83
+ return conn.state;
84
+ }
85
+ /** Shutdown all connections. Best-effort; errors are ignored. */
86
+ async shutdown() {
87
+ await Promise.all([...this.connections.values()].map(async (conn) => {
88
+ if (!conn.client)
89
+ return;
90
+ try {
91
+ await conn.client.close();
92
+ }
93
+ catch {
94
+ // ignore
95
+ }
96
+ conn.client = undefined;
97
+ }));
98
+ }
99
+ getStates() {
100
+ return [...this.connections.values()].map((c) => c.state);
101
+ }
102
+ /** Produce ToolRegistryEntry[] for every tool from every connected server. */
103
+ getToolEntries() {
104
+ const entries = [];
105
+ for (const conn of this.connections.values()) {
106
+ if (conn.state.status.kind !== "connected" || !conn.client)
107
+ continue;
108
+ const serverName = conn.state.name;
109
+ for (const tool of conn.state.status.tools) {
110
+ entries.push(buildToolEntry(serverName, tool, () => conn.client));
111
+ }
112
+ }
113
+ return entries;
114
+ }
115
+ /**
116
+ * Produce UnifiedCommand[] for every prompt from every connected server.
117
+ * Each command, when invoked, calls `prompts/get` with positionally-parsed
118
+ * arguments and returns an `inject` payload that the harness hands back to
119
+ * the agent as the user's next turn.
120
+ *
121
+ * Naming: prompts are exposed as /<promptName> directly (flat namespace,
122
+ * matching opencode's convention). When two servers export the same prompt
123
+ * name, last-registered wins and a diagnostic is emitted so the user can
124
+ * see which server "shadowed" which. Server iteration order follows the
125
+ * Map insertion order (stable: servers are inserted once at constructor
126
+ * time from the config's declared order).
127
+ *
128
+ * Tools, in contrast, keep their `mcp__<server>__<tool>` names so existing
129
+ * Claude Code-style permission rules (e.g. `Bash(git status)` neighbours)
130
+ * continue to match. See src/mcp/name.ts.
131
+ */
132
+ getPromptCommands() {
133
+ const byName = new Map();
134
+ for (const conn of this.connections.values()) {
135
+ if (conn.state.status.kind !== "connected" || !conn.client)
136
+ continue;
137
+ const serverName = conn.state.name;
138
+ for (const prompt of conn.state.status.prompts) {
139
+ const displayName = normalizeNameForMCP(prompt.name);
140
+ const cmd = buildPromptCommand(serverName, displayName, prompt, () => conn.client);
141
+ const existing = byName.get(displayName);
142
+ if (existing) {
143
+ this.onDiagnostic(`[mcp] prompt /${displayName} from server "${serverName}" shadows the same prompt from "${existing.sourceLabel ?? "unknown"}"`);
144
+ }
145
+ byName.set(displayName, cmd);
146
+ }
147
+ }
148
+ return [...byName.values()];
149
+ }
150
+ }
151
+ async function createClient(config, clientInfo) {
152
+ const transport = config.type === "stdio" ? new StdioTransport(config) : new HttpTransport(config);
153
+ const client = new MCPClient(transport, clientInfo);
154
+ await client.start();
155
+ return client;
156
+ }
157
+ function buildToolEntry(serverName, tool, getClient) {
158
+ const name = buildMcpToolName(serverName, tool.name);
159
+ const parameters = coerceSchema(tool.inputSchema);
160
+ const descriptionPrefix = `[MCP:${serverName}]`;
161
+ const description = [descriptionPrefix, tool.description?.trim() || tool.name].filter(Boolean).join(" ");
162
+ return {
163
+ name,
164
+ description,
165
+ parameters,
166
+ readOnly: false, // Conservative default; user can allow-list.
167
+ deferred: true, // Load schema on demand via tool_search to keep context small.
168
+ async execute(args) {
169
+ const client = getClient();
170
+ if (!client) {
171
+ return {
172
+ content: `Error: MCP server "${serverName}" is not connected.`,
173
+ isError: true,
174
+ };
175
+ }
176
+ try {
177
+ const result = await client.callTool(tool.name, args);
178
+ const text = formatToolContent(result.content);
179
+ return { content: text || "(no content)", isError: result.isError === true };
180
+ }
181
+ catch (err) {
182
+ return {
183
+ content: `Error calling ${serverName}.${tool.name}: ${err.message || String(err)}`,
184
+ isError: true,
185
+ };
186
+ }
187
+ },
188
+ };
189
+ }
190
+ function coerceSchema(input) {
191
+ if (input && typeof input === "object" && !Array.isArray(input)) {
192
+ const s = input;
193
+ if (s.type === "object" && typeof s.properties === "object") {
194
+ return input;
195
+ }
196
+ }
197
+ // Fallback — some servers omit inputSchema or send something odd.
198
+ return { type: "object", properties: {} };
199
+ }
200
+ function buildPromptCommand(serverName, displayName, prompt, getClient) {
201
+ const argNames = (prompt.arguments ?? []).map((a) => a.name);
202
+ const argSig = argNames.length > 0 ? ` <${argNames.join("> <")}>` : "";
203
+ const description = `[MCP:${serverName}] ${prompt.description?.trim() || prompt.name}${argSig ? ` (args:${argSig})` : ""}`;
204
+ return {
205
+ name: displayName,
206
+ description,
207
+ source: "mcp",
208
+ sourceLabel: serverName,
209
+ async handler(args) {
210
+ const client = getClient();
211
+ if (!client) {
212
+ return `Error: MCP server "${serverName}" is not connected.`;
213
+ }
214
+ const values = parsePositionalArgs(args);
215
+ const mapped = {};
216
+ for (let i = 0; i < argNames.length; i++) {
217
+ mapped[argNames[i]] = values[i] ?? "";
218
+ }
219
+ const required = (prompt.arguments ?? []).filter((a) => a.required).map((a) => a.name);
220
+ const missing = required.filter((n) => !mapped[n]);
221
+ if (missing.length > 0) {
222
+ return `Error: /${displayName} is missing required arg${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`;
223
+ }
224
+ try {
225
+ const result = await client.getPrompt(prompt.name, mapped);
226
+ const text = flattenPromptMessages(result.messages);
227
+ if (!text.trim()) {
228
+ return `Error: /${displayName} returned an empty prompt.`;
229
+ }
230
+ return { inject: text };
231
+ }
232
+ catch (err) {
233
+ return `Error calling ${serverName}.${prompt.name}: ${err.message || String(err)}`;
234
+ }
235
+ },
236
+ };
237
+ }
238
+ /**
239
+ * Split positional args on whitespace, honoring double-quoted phrases so an
240
+ * argument like `"attention is all you need"` stays intact. Backslash escaping
241
+ * is not supported — keeps v1 predictable.
242
+ */
243
+ function parsePositionalArgs(raw) {
244
+ const out = [];
245
+ const re = /"([^"]*)"|(\S+)/g;
246
+ let m;
247
+ while ((m = re.exec(raw)) !== null) {
248
+ out.push(m[1] ?? m[2]);
249
+ }
250
+ return out;
251
+ }
252
+ /**
253
+ * Collapse `prompts/get` messages into a single user-input string for the
254
+ * agent. Text from every message is concatenated; non-text blocks are
255
+ * annotated. Role labels are preserved only when the prompt mixes roles, so
256
+ * simple single-user-message prompts pass through cleanly.
257
+ */
258
+ function flattenPromptMessages(messages) {
259
+ const allUser = messages.every((m) => m.role === "user");
260
+ const parts = [];
261
+ for (const msg of messages) {
262
+ const blocks = Array.isArray(msg.content) ? msg.content : [msg.content];
263
+ const text = blocks.map(promptBlockToText).filter(Boolean).join("\n");
264
+ if (!text)
265
+ continue;
266
+ parts.push(allUser ? text : `[${msg.role}]\n${text}`);
267
+ }
268
+ return parts.join("\n\n");
269
+ }
270
+ function promptBlockToText(block) {
271
+ if (block.type === "text" && typeof block.text === "string")
272
+ return block.text;
273
+ if (block.type === "resource")
274
+ return `[resource: ${block.mimeType ?? "unknown"}]`;
275
+ if (block.type === "image")
276
+ return `[image: ${block.mimeType ?? "unknown"}]`;
277
+ return `[${block.type}]`;
278
+ }
279
+ function formatToolContent(content) {
280
+ const parts = [];
281
+ for (const item of content) {
282
+ if (item.type === "text" && typeof item.text === "string") {
283
+ parts.push(item.text);
284
+ }
285
+ else if (item.type === "image" && item.mimeType) {
286
+ parts.push(`[image: ${item.mimeType}, ${(item.data?.length ?? 0)} bytes base64]`);
287
+ }
288
+ else if (item.type === "resource") {
289
+ parts.push(`[resource: ${item.mimeType ?? "unknown"}]`);
290
+ }
291
+ else {
292
+ parts.push(`[${item.type}]`);
293
+ }
294
+ }
295
+ return parts.join("\n");
296
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * MCP tool / server name normalization.
3
+ *
4
+ * Mirrors Claude Code's convention so that permission rules written for Claude
5
+ * Code-style `mcp__<server>__<tool>` names work the same here.
6
+ *
7
+ * The OpenAI tool name pattern is roughly /^[a-zA-Z0-9_-]{1,64}$/. We replace
8
+ * any character outside that set with an underscore. Server and tool names are
9
+ * normalized independently so a "." or " " in a server name can't collide with
10
+ * the "__" delimiter.
11
+ */
12
+ export declare function normalizeNameForMCP(name: string): string;
13
+ export declare function getMcpPrefix(serverName: string): string;
14
+ export declare function buildMcpToolName(serverName: string, toolName: string): string;
15
+ /**
16
+ * Parse a tool name back to its server + tool components.
17
+ *
18
+ * Known limitation (inherited from Claude Code): if a server name contains
19
+ * "__", the split is ambiguous. In that case we greedily take the first segment
20
+ * as the server. Since server names come from user config and typically don't
21
+ * contain double underscores, this is acceptable in v1.
22
+ */
23
+ export declare function mcpInfoFromString(toolString: string): {
24
+ serverName: string;
25
+ toolName: string;
26
+ } | null;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * MCP tool / server name normalization.
3
+ *
4
+ * Mirrors Claude Code's convention so that permission rules written for Claude
5
+ * Code-style `mcp__<server>__<tool>` names work the same here.
6
+ *
7
+ * The OpenAI tool name pattern is roughly /^[a-zA-Z0-9_-]{1,64}$/. We replace
8
+ * any character outside that set with an underscore. Server and tool names are
9
+ * normalized independently so a "." or " " in a server name can't collide with
10
+ * the "__" delimiter.
11
+ */
12
+ export function normalizeNameForMCP(name) {
13
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_");
14
+ }
15
+ export function getMcpPrefix(serverName) {
16
+ return `mcp__${normalizeNameForMCP(serverName)}__`;
17
+ }
18
+ export function buildMcpToolName(serverName, toolName) {
19
+ return `${getMcpPrefix(serverName)}${normalizeNameForMCP(toolName)}`;
20
+ }
21
+ /**
22
+ * Parse a tool name back to its server + tool components.
23
+ *
24
+ * Known limitation (inherited from Claude Code): if a server name contains
25
+ * "__", the split is ambiguous. In that case we greedily take the first segment
26
+ * as the server. Since server names come from user config and typically don't
27
+ * contain double underscores, this is acceptable in v1.
28
+ */
29
+ export function mcpInfoFromString(toolString) {
30
+ const parts = toolString.split("__");
31
+ if (parts.length < 3 || parts[0] !== "mcp")
32
+ return null;
33
+ const serverName = parts[1];
34
+ if (!serverName)
35
+ return null;
36
+ const toolName = parts.slice(2).join("__");
37
+ if (!toolName)
38
+ return null;
39
+ return { serverName, toolName };
40
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * MCP transports — minimal, dependency-free JSON-RPC clients.
3
+ *
4
+ * Two transports in v1:
5
+ * - StdioTransport: child process, line-delimited JSON on stdin/stdout
6
+ * - HttpTransport: POST JSON-RPC over HTTP (Streamable HTTP), handle both
7
+ * application/json and text/event-stream responses. Also used for `sse`
8
+ * servers — the spec lets a server respond with either content-type.
9
+ *
10
+ * These deliberately do NOT depend on @modelcontextprotocol/sdk. The SDK pulls
11
+ * in zod + schema machinery we don't need. Our surface area here is small:
12
+ * initialize + tools/list + tools/call. Adding more methods is just more
13
+ * JSON-RPC calls on the same pipe.
14
+ */
15
+ import type { HttpServerConfig, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, McpTransport, SseServerConfig, StdioServerConfig } from "./types.js";
16
+ type IncomingHandler = (msg: JsonRpcResponse | JsonRpcNotification | JsonRpcRequest) => void;
17
+ export declare class StdioTransport implements McpTransport {
18
+ private readonly config;
19
+ private child?;
20
+ private buffer;
21
+ private stderrBuffer;
22
+ private messageHandler?;
23
+ private errorHandler?;
24
+ private closeHandler?;
25
+ private closed;
26
+ constructor(config: StdioServerConfig);
27
+ start(): Promise<void>;
28
+ private onStdout;
29
+ send(message: JsonRpcRequest | JsonRpcNotification): Promise<void>;
30
+ onMessage(handler: IncomingHandler): void;
31
+ onError(handler: (err: Error) => void): void;
32
+ onClose(handler: () => void): void;
33
+ close(): Promise<void>;
34
+ getStderr(): string;
35
+ }
36
+ export declare class HttpTransport implements McpTransport {
37
+ private messageHandler?;
38
+ private errorHandler?;
39
+ private closeHandler?;
40
+ private sessionId?;
41
+ private closed;
42
+ private readonly url;
43
+ private readonly baseHeaders;
44
+ constructor(config: HttpServerConfig | SseServerConfig);
45
+ start(): Promise<void>;
46
+ send(message: JsonRpcRequest | JsonRpcNotification): Promise<void>;
47
+ private parseSseBody;
48
+ onMessage(handler: IncomingHandler): void;
49
+ onError(handler: (err: Error) => void): void;
50
+ onClose(handler: () => void): void;
51
+ close(): Promise<void>;
52
+ }
53
+ export {};