bincode-cli 1.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 (300) hide show
  1. package/AGENTS.md +27 -0
  2. package/README.md +15 -0
  3. package/bin/bincode +98 -0
  4. package/bunfig.toml +4 -0
  5. package/package.json +124 -0
  6. package/parsers-config.ts +239 -0
  7. package/script/build.ts +167 -0
  8. package/script/postinstall.mjs +206 -0
  9. package/script/publish.ts +99 -0
  10. package/script/schema.ts +47 -0
  11. package/src/acp/README.md +164 -0
  12. package/src/acp/agent.ts +1051 -0
  13. package/src/acp/session.ts +101 -0
  14. package/src/acp/types.ts +22 -0
  15. package/src/agent/agent.ts +398 -0
  16. package/src/agent/generate.txt +75 -0
  17. package/src/agent/prompt/compaction.txt +12 -0
  18. package/src/agent/prompt/explore.txt +18 -0
  19. package/src/agent/prompt/summary.txt +10 -0
  20. package/src/agent/prompt/title.txt +36 -0
  21. package/src/auth/bineric-login.ts +506 -0
  22. package/src/auth/index.ts +70 -0
  23. package/src/bun/index.ts +114 -0
  24. package/src/bus/bus-event.ts +43 -0
  25. package/src/bus/global.ts +10 -0
  26. package/src/bus/index.ts +105 -0
  27. package/src/cli/auth-check.ts +61 -0
  28. package/src/cli/bootstrap.ts +21 -0
  29. package/src/cli/cmd/acp.ts +88 -0
  30. package/src/cli/cmd/agent.ts +256 -0
  31. package/src/cli/cmd/auth.ts +436 -0
  32. package/src/cli/cmd/cmd.ts +7 -0
  33. package/src/cli/cmd/debug/config.ts +15 -0
  34. package/src/cli/cmd/debug/file.ts +91 -0
  35. package/src/cli/cmd/debug/index.ts +43 -0
  36. package/src/cli/cmd/debug/lsp.ts +48 -0
  37. package/src/cli/cmd/debug/ripgrep.ts +83 -0
  38. package/src/cli/cmd/debug/scrap.ts +15 -0
  39. package/src/cli/cmd/debug/skill.ts +15 -0
  40. package/src/cli/cmd/debug/snapshot.ts +48 -0
  41. package/src/cli/cmd/export.ts +88 -0
  42. package/src/cli/cmd/generate.ts +38 -0
  43. package/src/cli/cmd/github.ts +1399 -0
  44. package/src/cli/cmd/import.ts +98 -0
  45. package/src/cli/cmd/login.ts +112 -0
  46. package/src/cli/cmd/logout.ts +38 -0
  47. package/src/cli/cmd/mcp.ts +654 -0
  48. package/src/cli/cmd/models.ts +77 -0
  49. package/src/cli/cmd/pr.ts +112 -0
  50. package/src/cli/cmd/run.ts +368 -0
  51. package/src/cli/cmd/serve.ts +31 -0
  52. package/src/cli/cmd/session.ts +106 -0
  53. package/src/cli/cmd/stats.ts +298 -0
  54. package/src/cli/cmd/tui/app.tsx +669 -0
  55. package/src/cli/cmd/tui/attach.ts +30 -0
  56. package/src/cli/cmd/tui/component/border.tsx +21 -0
  57. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  58. package/src/cli/cmd/tui/component/dialog-command.tsx +123 -0
  59. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  60. package/src/cli/cmd/tui/component/dialog-model.tsx +223 -0
  61. package/src/cli/cmd/tui/component/dialog-provider.tsx +224 -0
  62. package/src/cli/cmd/tui/component/dialog-session-list.tsx +102 -0
  63. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  64. package/src/cli/cmd/tui/component/dialog-status.tsx +162 -0
  65. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  66. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  67. package/src/cli/cmd/tui/component/logo.tsx +32 -0
  68. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +560 -0
  69. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  70. package/src/cli/cmd/tui/component/prompt/index.tsx +1052 -0
  71. package/src/cli/cmd/tui/context/args.tsx +14 -0
  72. package/src/cli/cmd/tui/context/directory.ts +13 -0
  73. package/src/cli/cmd/tui/context/exit.tsx +23 -0
  74. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  75. package/src/cli/cmd/tui/context/keybind.tsx +101 -0
  76. package/src/cli/cmd/tui/context/kv.tsx +49 -0
  77. package/src/cli/cmd/tui/context/local.tsx +339 -0
  78. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  79. package/src/cli/cmd/tui/context/route.tsx +46 -0
  80. package/src/cli/cmd/tui/context/sdk.tsx +74 -0
  81. package/src/cli/cmd/tui/context/sync.tsx +372 -0
  82. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  83. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  84. package/src/cli/cmd/tui/context/theme/bincode.json +245 -0
  85. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  86. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  87. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  88. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  89. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  90. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  91. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  92. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  93. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  94. package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
  95. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  96. package/src/cli/cmd/tui/context/theme/lucent-orng.json +227 -0
  97. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  98. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  99. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  100. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  101. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  102. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  103. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  104. package/src/cli/cmd/tui/context/theme/orng.json +245 -0
  105. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  106. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  107. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  108. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  109. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  110. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  111. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  112. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  113. package/src/cli/cmd/tui/context/theme.tsx +1109 -0
  114. package/src/cli/cmd/tui/event.ts +40 -0
  115. package/src/cli/cmd/tui/routes/home.tsx +105 -0
  116. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
  117. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
  118. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  119. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  120. package/src/cli/cmd/tui/routes/session/footer.tsx +88 -0
  121. package/src/cli/cmd/tui/routes/session/header.tsx +141 -0
  122. package/src/cli/cmd/tui/routes/session/index.tsx +1888 -0
  123. package/src/cli/cmd/tui/routes/session/sidebar.tsx +321 -0
  124. package/src/cli/cmd/tui/spawn.ts +60 -0
  125. package/src/cli/cmd/tui/thread.ts +120 -0
  126. package/src/cli/cmd/tui/ui/dialog-alert.tsx +57 -0
  127. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
  128. package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
  129. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +77 -0
  130. package/src/cli/cmd/tui/ui/dialog-select.tsx +330 -0
  131. package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
  132. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  133. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  134. package/src/cli/cmd/tui/util/clipboard.ts +127 -0
  135. package/src/cli/cmd/tui/util/editor.ts +32 -0
  136. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  137. package/src/cli/cmd/tui/worker.ts +63 -0
  138. package/src/cli/cmd/uninstall.ts +344 -0
  139. package/src/cli/cmd/upgrade.ts +67 -0
  140. package/src/cli/cmd/web.ts +84 -0
  141. package/src/cli/error.ts +55 -0
  142. package/src/cli/ui.ts +84 -0
  143. package/src/cli/upgrade.ts +25 -0
  144. package/src/command/index.ts +80 -0
  145. package/src/command/template/initialize.txt +10 -0
  146. package/src/command/template/review.txt +97 -0
  147. package/src/config/config.ts +995 -0
  148. package/src/config/markdown.ts +41 -0
  149. package/src/env/index.ts +26 -0
  150. package/src/file/ignore.ts +83 -0
  151. package/src/file/index.ts +328 -0
  152. package/src/file/ripgrep.ts +393 -0
  153. package/src/file/time.ts +64 -0
  154. package/src/file/watcher.ts +103 -0
  155. package/src/flag/flag.ts +46 -0
  156. package/src/format/formatter.ts +315 -0
  157. package/src/format/index.ts +137 -0
  158. package/src/global/index.ts +52 -0
  159. package/src/id/id.ts +73 -0
  160. package/src/ide/index.ts +76 -0
  161. package/src/index.ts +217 -0
  162. package/src/installation/index.ts +196 -0
  163. package/src/lsp/client.ts +229 -0
  164. package/src/lsp/index.ts +485 -0
  165. package/src/lsp/language.ts +116 -0
  166. package/src/lsp/server.ts +1895 -0
  167. package/src/mcp/auth.ts +135 -0
  168. package/src/mcp/index.ts +654 -0
  169. package/src/mcp/oauth-callback.ts +200 -0
  170. package/src/mcp/oauth-provider.ts +154 -0
  171. package/src/patch/index.ts +622 -0
  172. package/src/permission/index.ts +199 -0
  173. package/src/plugin/index.ts +101 -0
  174. package/src/project/bootstrap.ts +31 -0
  175. package/src/project/instance.ts +78 -0
  176. package/src/project/project.ts +221 -0
  177. package/src/project/state.ts +65 -0
  178. package/src/project/vcs.ts +76 -0
  179. package/src/provider/auth.ts +143 -0
  180. package/src/provider/models-macro.ts +11 -0
  181. package/src/provider/models.ts +106 -0
  182. package/src/provider/provider.ts +1071 -0
  183. package/src/provider/sdk/openai-compatible/src/README.md +5 -0
  184. package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
  185. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +101 -0
  186. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
  187. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +22 -0
  188. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
  189. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
  190. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
  191. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1713 -0
  192. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
  193. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
  194. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
  195. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
  196. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
  197. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
  198. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
  199. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
  200. package/src/provider/transform.ts +455 -0
  201. package/src/pty/index.ts +231 -0
  202. package/src/server/error.ts +36 -0
  203. package/src/server/project.ts +79 -0
  204. package/src/server/server.ts +2642 -0
  205. package/src/server/tui.ts +71 -0
  206. package/src/session/compaction.ts +223 -0
  207. package/src/session/index.ts +458 -0
  208. package/src/session/llm.ts +201 -0
  209. package/src/session/message-v2.ts +659 -0
  210. package/src/session/message.ts +189 -0
  211. package/src/session/processor.ts +409 -0
  212. package/src/session/prompt/anthropic-20250930.txt +166 -0
  213. package/src/session/prompt/anthropic.txt +104 -0
  214. package/src/session/prompt/anthropic_spoof.txt +1 -0
  215. package/src/session/prompt/beast.txt +147 -0
  216. package/src/session/prompt/build-switch.txt +5 -0
  217. package/src/session/prompt/codex.txt +318 -0
  218. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  219. package/src/session/prompt/gemini.txt +155 -0
  220. package/src/session/prompt/max-steps.txt +16 -0
  221. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  222. package/src/session/prompt/plan.txt +26 -0
  223. package/src/session/prompt/polaris.txt +106 -0
  224. package/src/session/prompt/qwen.txt +109 -0
  225. package/src/session/prompt.ts +1446 -0
  226. package/src/session/retry.ts +86 -0
  227. package/src/session/revert.ts +108 -0
  228. package/src/session/status.ts +76 -0
  229. package/src/session/summary.ts +194 -0
  230. package/src/session/system.ts +120 -0
  231. package/src/session/todo.ts +37 -0
  232. package/src/share/share-next.ts +194 -0
  233. package/src/share/share.ts +87 -0
  234. package/src/shell/shell.ts +67 -0
  235. package/src/skill/index.ts +1 -0
  236. package/src/skill/skill.ts +83 -0
  237. package/src/snapshot/index.ts +197 -0
  238. package/src/storage/storage.ts +226 -0
  239. package/src/tool/bash.ts +306 -0
  240. package/src/tool/bash.txt +158 -0
  241. package/src/tool/batch.ts +175 -0
  242. package/src/tool/batch.txt +24 -0
  243. package/src/tool/codesearch.ts +138 -0
  244. package/src/tool/codesearch.txt +12 -0
  245. package/src/tool/edit.ts +675 -0
  246. package/src/tool/edit.txt +10 -0
  247. package/src/tool/glob.ts +65 -0
  248. package/src/tool/glob.txt +6 -0
  249. package/src/tool/grep.ts +121 -0
  250. package/src/tool/grep.txt +8 -0
  251. package/src/tool/invalid.ts +17 -0
  252. package/src/tool/ls.ts +110 -0
  253. package/src/tool/ls.txt +1 -0
  254. package/src/tool/lsp-diagnostics.ts +26 -0
  255. package/src/tool/lsp-diagnostics.txt +1 -0
  256. package/src/tool/lsp-hover.ts +31 -0
  257. package/src/tool/lsp-hover.txt +1 -0
  258. package/src/tool/lsp.ts +87 -0
  259. package/src/tool/lsp.txt +19 -0
  260. package/src/tool/multiedit.ts +46 -0
  261. package/src/tool/multiedit.txt +41 -0
  262. package/src/tool/patch.ts +233 -0
  263. package/src/tool/patch.txt +1 -0
  264. package/src/tool/read.ts +219 -0
  265. package/src/tool/read.txt +12 -0
  266. package/src/tool/registry.ts +162 -0
  267. package/src/tool/skill.ts +100 -0
  268. package/src/tool/task.ts +136 -0
  269. package/src/tool/task.txt +60 -0
  270. package/src/tool/todo.ts +39 -0
  271. package/src/tool/todoread.txt +14 -0
  272. package/src/tool/todowrite.txt +167 -0
  273. package/src/tool/tool.ts +71 -0
  274. package/src/tool/webfetch.ts +187 -0
  275. package/src/tool/webfetch.txt +13 -0
  276. package/src/tool/websearch.ts +150 -0
  277. package/src/tool/websearch.txt +11 -0
  278. package/src/tool/write.ts +110 -0
  279. package/src/tool/write.txt +8 -0
  280. package/src/util/archive.ts +16 -0
  281. package/src/util/color.ts +19 -0
  282. package/src/util/context.ts +25 -0
  283. package/src/util/defer.ts +12 -0
  284. package/src/util/eventloop.ts +20 -0
  285. package/src/util/filesystem.ts +83 -0
  286. package/src/util/fn.ts +11 -0
  287. package/src/util/iife.ts +3 -0
  288. package/src/util/keybind.ts +102 -0
  289. package/src/util/lazy.ts +11 -0
  290. package/src/util/locale.ts +81 -0
  291. package/src/util/lock.ts +98 -0
  292. package/src/util/log.ts +180 -0
  293. package/src/util/queue.ts +32 -0
  294. package/src/util/rpc.ts +42 -0
  295. package/src/util/scrap.ts +10 -0
  296. package/src/util/signal.ts +12 -0
  297. package/src/util/timeout.ts +14 -0
  298. package/src/util/token.ts +7 -0
  299. package/src/util/wildcard.ts +54 -0
  300. package/tsconfig.json +16 -0
@@ -0,0 +1,654 @@
1
+ import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai"
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js"
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
4
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
5
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
6
+ import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
7
+ import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js"
8
+ import { Config } from "../config/config"
9
+ import { Log } from "../util/log"
10
+ import { NamedError } from "@bincode-ai/util/error"
11
+ import z from "zod/v4"
12
+ import { Instance } from "../project/instance"
13
+ import { Installation } from "../installation"
14
+ import { withTimeout } from "@/util/timeout"
15
+ import { McpOAuthProvider } from "./oauth-provider"
16
+ import { McpOAuthCallback } from "./oauth-callback"
17
+ import { McpAuth } from "./auth"
18
+ import { Bus } from "@/bus"
19
+ import { TuiEvent } from "@/cli/cmd/tui/event"
20
+ import open from "open"
21
+
22
+ export namespace MCP {
23
+ const log = Log.create({ service: "mcp" })
24
+
25
+ export const Failed = NamedError.create(
26
+ "MCPFailed",
27
+ z.object({
28
+ name: z.string(),
29
+ }),
30
+ )
31
+
32
+ type MCPClient = Client
33
+
34
+ export const Status = z
35
+ .discriminatedUnion("status", [
36
+ z
37
+ .object({
38
+ status: z.literal("connected"),
39
+ })
40
+ .meta({
41
+ ref: "MCPStatusConnected",
42
+ }),
43
+ z
44
+ .object({
45
+ status: z.literal("disabled"),
46
+ })
47
+ .meta({
48
+ ref: "MCPStatusDisabled",
49
+ }),
50
+ z
51
+ .object({
52
+ status: z.literal("failed"),
53
+ error: z.string(),
54
+ })
55
+ .meta({
56
+ ref: "MCPStatusFailed",
57
+ }),
58
+ z
59
+ .object({
60
+ status: z.literal("needs_auth"),
61
+ })
62
+ .meta({
63
+ ref: "MCPStatusNeedsAuth",
64
+ }),
65
+ z
66
+ .object({
67
+ status: z.literal("needs_client_registration"),
68
+ error: z.string(),
69
+ })
70
+ .meta({
71
+ ref: "MCPStatusNeedsClientRegistration",
72
+ }),
73
+ ])
74
+ .meta({
75
+ ref: "MCPStatus",
76
+ })
77
+ export type Status = z.infer<typeof Status>
78
+
79
+ // Convert MCP tool definition to AI SDK Tool type
80
+ function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient): Tool {
81
+ const inputSchema = mcpTool.inputSchema
82
+
83
+ // Spread first, then override type to ensure it's always "object"
84
+ const schema: JSONSchema7 = {
85
+ ...(inputSchema as JSONSchema7),
86
+ type: "object",
87
+ properties: (inputSchema.properties ?? {}) as JSONSchema7["properties"],
88
+ additionalProperties: false,
89
+ }
90
+
91
+ return dynamicTool({
92
+ description: mcpTool.description ?? "",
93
+ inputSchema: jsonSchema(schema),
94
+ execute: async (args: unknown) => {
95
+ return client.callTool({
96
+ name: mcpTool.name,
97
+ arguments: args as Record<string, unknown>,
98
+ })
99
+ },
100
+ })
101
+ }
102
+
103
+ // Store transports for OAuth servers to allow finishing auth
104
+ type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport
105
+ const pendingOAuthTransports = new Map<string, TransportWithAuth>()
106
+
107
+ const state = Instance.state(
108
+ async () => {
109
+ const cfg = await Config.get()
110
+ const config = cfg.mcp ?? {}
111
+ const clients: Record<string, MCPClient> = {}
112
+ const status: Record<string, Status> = {}
113
+
114
+ await Promise.all(
115
+ Object.entries(config).map(async ([key, mcp]) => {
116
+ // If disabled by config, mark as disabled without trying to connect
117
+ if (mcp.enabled === false) {
118
+ status[key] = { status: "disabled" }
119
+ return
120
+ }
121
+
122
+ const result = await create(key, mcp).catch(() => undefined)
123
+ if (!result) return
124
+
125
+ status[key] = result.status
126
+
127
+ if (result.mcpClient) {
128
+ clients[key] = result.mcpClient
129
+ }
130
+ }),
131
+ )
132
+ return {
133
+ status,
134
+ clients,
135
+ }
136
+ },
137
+ async (state) => {
138
+ await Promise.all(
139
+ Object.values(state.clients).map((client) =>
140
+ client.close().catch((error) => {
141
+ log.error("Failed to close MCP client", {
142
+ error,
143
+ })
144
+ }),
145
+ ),
146
+ )
147
+ pendingOAuthTransports.clear()
148
+ },
149
+ )
150
+
151
+ export async function add(name: string, mcp: Config.Mcp) {
152
+ const s = await state()
153
+ const result = await create(name, mcp)
154
+ if (!result) {
155
+ const status = {
156
+ status: "failed" as const,
157
+ error: "unknown error",
158
+ }
159
+ s.status[name] = status
160
+ return {
161
+ status,
162
+ }
163
+ }
164
+ if (!result.mcpClient) {
165
+ s.status[name] = result.status
166
+ return {
167
+ status: s.status,
168
+ }
169
+ }
170
+ s.clients[name] = result.mcpClient
171
+ s.status[name] = result.status
172
+
173
+ return {
174
+ status: s.status,
175
+ }
176
+ }
177
+
178
+ async function create(key: string, mcp: Config.Mcp) {
179
+ if (mcp.enabled === false) {
180
+ log.info("mcp server disabled", { key })
181
+ return {
182
+ mcpClient: undefined,
183
+ status: { status: "disabled" as const },
184
+ }
185
+ }
186
+ log.info("found", { key, type: mcp.type })
187
+ let mcpClient: MCPClient | undefined
188
+ let status: Status | undefined = undefined
189
+
190
+ if (mcp.type === "remote") {
191
+ // OAuth is enabled by default for remote servers unless explicitly disabled with oauth: false
192
+ const oauthDisabled = mcp.oauth === false
193
+ const oauthConfig = typeof mcp.oauth === "object" ? mcp.oauth : undefined
194
+ let authProvider: McpOAuthProvider | undefined
195
+
196
+ if (!oauthDisabled) {
197
+ authProvider = new McpOAuthProvider(
198
+ key,
199
+ mcp.url,
200
+ {
201
+ clientId: oauthConfig?.clientId,
202
+ clientSecret: oauthConfig?.clientSecret,
203
+ scope: oauthConfig?.scope,
204
+ },
205
+ {
206
+ onRedirect: async (url) => {
207
+ log.info("oauth redirect requested", { key, url: url.toString() })
208
+ // Store the URL - actual browser opening is handled by startAuth
209
+ },
210
+ },
211
+ )
212
+ }
213
+
214
+ const transports: Array<{ name: string; transport: TransportWithAuth }> = [
215
+ {
216
+ name: "StreamableHTTP",
217
+ transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
218
+ authProvider,
219
+ requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
220
+ }),
221
+ },
222
+ {
223
+ name: "SSE",
224
+ transport: new SSEClientTransport(new URL(mcp.url), {
225
+ authProvider,
226
+ requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
227
+ }),
228
+ },
229
+ ]
230
+
231
+ let lastError: Error | undefined
232
+ for (const { name, transport } of transports) {
233
+ try {
234
+ const client = new Client({
235
+ name: "bincode",
236
+ version: Installation.VERSION,
237
+ })
238
+ await client.connect(transport)
239
+ mcpClient = client
240
+ log.info("connected", { key, transport: name })
241
+ status = { status: "connected" }
242
+ break
243
+ } catch (error) {
244
+ lastError = error instanceof Error ? error : new Error(String(error))
245
+
246
+ // Handle OAuth-specific errors
247
+ if (error instanceof UnauthorizedError) {
248
+ log.info("mcp server requires authentication", { key, transport: name })
249
+
250
+ // Check if this is a "needs registration" error
251
+ if (lastError.message.includes("registration") || lastError.message.includes("client_id")) {
252
+ status = {
253
+ status: "needs_client_registration" as const,
254
+ error: "Server does not support dynamic client registration. Please provide clientId in config.",
255
+ }
256
+ // Show toast for needs_client_registration
257
+ Bus.publish(TuiEvent.ToastShow, {
258
+ title: "MCP Authentication Required",
259
+ message: `Server "${key}" requires a pre-registered client ID. Add clientId to your config.`,
260
+ variant: "warning",
261
+ duration: 8000,
262
+ }).catch((e) => log.debug("failed to show toast", { error: e }))
263
+ } else {
264
+ // Store transport for later finishAuth call
265
+ pendingOAuthTransports.set(key, transport)
266
+ status = { status: "needs_auth" as const }
267
+ // Show toast for needs_auth
268
+ Bus.publish(TuiEvent.ToastShow, {
269
+ title: "MCP Authentication Required",
270
+ message: `Server "${key}" requires authentication. Run: bincode mcp auth ${key}`,
271
+ variant: "warning",
272
+ duration: 8000,
273
+ }).catch((e) => log.debug("failed to show toast", { error: e }))
274
+ }
275
+ break
276
+ }
277
+
278
+ log.debug("transport connection failed", {
279
+ key,
280
+ transport: name,
281
+ url: mcp.url,
282
+ error: lastError.message,
283
+ })
284
+ status = {
285
+ status: "failed" as const,
286
+ error: lastError.message,
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ if (mcp.type === "local") {
293
+ const [cmd, ...args] = mcp.command
294
+ const transport = new StdioClientTransport({
295
+ stderr: "ignore",
296
+ command: cmd,
297
+ args,
298
+ env: {
299
+ ...process.env,
300
+ ...(cmd === "bincode" ? { BUN_BE_BUN: "1" } : {}),
301
+ ...mcp.environment,
302
+ },
303
+ })
304
+
305
+ try {
306
+ const client = new Client({
307
+ name: "bincode",
308
+ version: Installation.VERSION,
309
+ })
310
+ await client.connect(transport)
311
+ mcpClient = client
312
+ status = {
313
+ status: "connected",
314
+ }
315
+ } catch (error) {
316
+ log.error("local mcp startup failed", {
317
+ key,
318
+ command: mcp.command,
319
+ error: error instanceof Error ? error.message : String(error),
320
+ })
321
+ status = {
322
+ status: "failed" as const,
323
+ error: error instanceof Error ? error.message : String(error),
324
+ }
325
+ }
326
+ }
327
+
328
+ if (!status) {
329
+ status = {
330
+ status: "failed" as const,
331
+ error: "Unknown error",
332
+ }
333
+ }
334
+
335
+ if (!mcpClient) {
336
+ return {
337
+ mcpClient: undefined,
338
+ status,
339
+ }
340
+ }
341
+
342
+ const result = await withTimeout(mcpClient.listTools(), mcp.timeout ?? 5000).catch((err) => {
343
+ log.error("failed to get tools from client", { key, error: err })
344
+ return undefined
345
+ })
346
+ if (!result) {
347
+ await mcpClient.close().catch((error) => {
348
+ log.error("Failed to close MCP client", {
349
+ error,
350
+ })
351
+ })
352
+ status = {
353
+ status: "failed",
354
+ error: "Failed to get tools",
355
+ }
356
+ return {
357
+ mcpClient: undefined,
358
+ status: {
359
+ status: "failed" as const,
360
+ error: "Failed to get tools",
361
+ },
362
+ }
363
+ }
364
+
365
+ log.info("create() successfully created client", { key, toolCount: result.tools.length })
366
+ return {
367
+ mcpClient,
368
+ status,
369
+ }
370
+ }
371
+
372
+ export async function status() {
373
+ const s = await state()
374
+ const cfg = await Config.get()
375
+ const config = cfg.mcp ?? {}
376
+ const result: Record<string, Status> = {}
377
+
378
+ // Include all MCPs from config, not just connected ones
379
+ for (const key of Object.keys(config)) {
380
+ result[key] = s.status[key] ?? { status: "disabled" }
381
+ }
382
+
383
+ return result
384
+ }
385
+
386
+ export async function clients() {
387
+ return state().then((state) => state.clients)
388
+ }
389
+
390
+ export async function connect(name: string) {
391
+ const cfg = await Config.get()
392
+ const config = cfg.mcp ?? {}
393
+ const mcp = config[name]
394
+ if (!mcp) {
395
+ log.error("MCP config not found", { name })
396
+ return
397
+ }
398
+
399
+ const result = await create(name, { ...mcp, enabled: true })
400
+
401
+ if (!result) {
402
+ const s = await state()
403
+ s.status[name] = {
404
+ status: "failed",
405
+ error: "Unknown error during connection",
406
+ }
407
+ return
408
+ }
409
+
410
+ const s = await state()
411
+ s.status[name] = result.status
412
+ if (result.mcpClient) {
413
+ s.clients[name] = result.mcpClient
414
+ }
415
+ }
416
+
417
+ export async function disconnect(name: string) {
418
+ const s = await state()
419
+ const client = s.clients[name]
420
+ if (client) {
421
+ await client.close().catch((error) => {
422
+ log.error("Failed to close MCP client", { name, error })
423
+ })
424
+ delete s.clients[name]
425
+ }
426
+ s.status[name] = { status: "disabled" }
427
+ }
428
+
429
+ export async function tools() {
430
+ const result: Record<string, Tool> = {}
431
+ const s = await state()
432
+ const clientsSnapshot = await clients()
433
+
434
+ for (const [clientName, client] of Object.entries(clientsSnapshot)) {
435
+ // Only include tools from connected MCPs (skip disabled ones)
436
+ if (s.status[clientName]?.status !== "connected") {
437
+ continue
438
+ }
439
+
440
+ const toolsResult = await client.listTools().catch((e) => {
441
+ log.error("failed to get tools", { clientName, error: e.message })
442
+ const failedStatus = {
443
+ status: "failed" as const,
444
+ error: e instanceof Error ? e.message : String(e),
445
+ }
446
+ s.status[clientName] = failedStatus
447
+ delete s.clients[clientName]
448
+ return undefined
449
+ })
450
+ if (!toolsResult) {
451
+ continue
452
+ }
453
+ for (const mcpTool of toolsResult.tools) {
454
+ const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_")
455
+ const sanitizedToolName = mcpTool.name.replace(/[^a-zA-Z0-9_-]/g, "_")
456
+ result[sanitizedClientName + "_" + sanitizedToolName] = convertMcpTool(mcpTool, client)
457
+ }
458
+ }
459
+ return result
460
+ }
461
+
462
+ /**
463
+ * Start OAuth authentication flow for an MCP server.
464
+ * Returns the authorization URL that should be opened in a browser.
465
+ */
466
+ export async function startAuth(mcpName: string): Promise<{ authorizationUrl: string }> {
467
+ const cfg = await Config.get()
468
+ const mcpConfig = cfg.mcp?.[mcpName]
469
+
470
+ if (!mcpConfig) {
471
+ throw new Error(`MCP server not found: ${mcpName}`)
472
+ }
473
+
474
+ if (mcpConfig.type !== "remote") {
475
+ throw new Error(`MCP server ${mcpName} is not a remote server`)
476
+ }
477
+
478
+ if (mcpConfig.oauth === false) {
479
+ throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`)
480
+ }
481
+
482
+ // Start the callback server
483
+ await McpOAuthCallback.ensureRunning()
484
+
485
+ // Generate and store a cryptographically secure state parameter BEFORE creating the provider
486
+ // The SDK will call provider.state() to read this value
487
+ const oauthState = Array.from(crypto.getRandomValues(new Uint8Array(32)))
488
+ .map((b) => b.toString(16).padStart(2, "0"))
489
+ .join("")
490
+ await McpAuth.updateOAuthState(mcpName, oauthState)
491
+
492
+ // Create a new auth provider for this flow
493
+ // OAuth config is optional - if not provided, we'll use auto-discovery
494
+ const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined
495
+ let capturedUrl: URL | undefined
496
+ const authProvider = new McpOAuthProvider(
497
+ mcpName,
498
+ mcpConfig.url,
499
+ {
500
+ clientId: oauthConfig?.clientId,
501
+ clientSecret: oauthConfig?.clientSecret,
502
+ scope: oauthConfig?.scope,
503
+ },
504
+ {
505
+ onRedirect: async (url) => {
506
+ capturedUrl = url
507
+ },
508
+ },
509
+ )
510
+
511
+ // Create transport with auth provider
512
+ const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), {
513
+ authProvider,
514
+ })
515
+
516
+ // Try to connect - this will trigger the OAuth flow
517
+ try {
518
+ const client = new Client({
519
+ name: "bincode",
520
+ version: Installation.VERSION,
521
+ })
522
+ await client.connect(transport)
523
+ // If we get here, we're already authenticated
524
+ return { authorizationUrl: "" }
525
+ } catch (error) {
526
+ if (error instanceof UnauthorizedError && capturedUrl) {
527
+ // Store transport for finishAuth
528
+ pendingOAuthTransports.set(mcpName, transport)
529
+ return { authorizationUrl: capturedUrl.toString() }
530
+ }
531
+ throw error
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Complete OAuth authentication after user authorizes in browser.
537
+ * Opens the browser and waits for callback.
538
+ */
539
+ export async function authenticate(mcpName: string): Promise<Status> {
540
+ const { authorizationUrl } = await startAuth(mcpName)
541
+
542
+ if (!authorizationUrl) {
543
+ // Already authenticated
544
+ const s = await state()
545
+ return s.status[mcpName] ?? { status: "connected" }
546
+ }
547
+
548
+ // Get the state that was already generated and stored in startAuth()
549
+ const oauthState = await McpAuth.getOAuthState(mcpName)
550
+ if (!oauthState) {
551
+ throw new Error("OAuth state not found - this should not happen")
552
+ }
553
+
554
+ // The SDK has already added the state parameter to the authorization URL
555
+ // We just need to open the browser
556
+ log.info("opening browser for oauth", { mcpName, url: authorizationUrl, state: oauthState })
557
+ await open(authorizationUrl)
558
+
559
+ // Wait for callback using the OAuth state parameter
560
+ const code = await McpOAuthCallback.waitForCallback(oauthState)
561
+
562
+ // Validate and clear the state
563
+ const storedState = await McpAuth.getOAuthState(mcpName)
564
+ if (storedState !== oauthState) {
565
+ await McpAuth.clearOAuthState(mcpName)
566
+ throw new Error("OAuth state mismatch - potential CSRF attack")
567
+ }
568
+
569
+ await McpAuth.clearOAuthState(mcpName)
570
+
571
+ // Finish auth
572
+ return finishAuth(mcpName, code)
573
+ }
574
+
575
+ /**
576
+ * Complete OAuth authentication with the authorization code.
577
+ */
578
+ export async function finishAuth(mcpName: string, authorizationCode: string): Promise<Status> {
579
+ const transport = pendingOAuthTransports.get(mcpName)
580
+
581
+ if (!transport) {
582
+ throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`)
583
+ }
584
+
585
+ try {
586
+ // Call finishAuth on the transport
587
+ await transport.finishAuth(authorizationCode)
588
+
589
+ // Clear the code verifier after successful auth
590
+ await McpAuth.clearCodeVerifier(mcpName)
591
+
592
+ // Now try to reconnect
593
+ const cfg = await Config.get()
594
+ const mcpConfig = cfg.mcp?.[mcpName]
595
+
596
+ if (!mcpConfig) {
597
+ throw new Error(`MCP server not found: ${mcpName}`)
598
+ }
599
+
600
+ // Re-add the MCP server to establish connection
601
+ pendingOAuthTransports.delete(mcpName)
602
+ const result = await add(mcpName, mcpConfig)
603
+
604
+ const statusRecord = result.status as Record<string, Status>
605
+ return statusRecord[mcpName] ?? { status: "failed", error: "Unknown error after auth" }
606
+ } catch (error) {
607
+ log.error("failed to finish oauth", { mcpName, error })
608
+ return {
609
+ status: "failed",
610
+ error: error instanceof Error ? error.message : String(error),
611
+ }
612
+ }
613
+ }
614
+
615
+ /**
616
+ * Remove OAuth credentials for an MCP server.
617
+ */
618
+ export async function removeAuth(mcpName: string): Promise<void> {
619
+ await McpAuth.remove(mcpName)
620
+ McpOAuthCallback.cancelPending(mcpName)
621
+ pendingOAuthTransports.delete(mcpName)
622
+ await McpAuth.clearOAuthState(mcpName)
623
+ log.info("removed oauth credentials", { mcpName })
624
+ }
625
+
626
+ /**
627
+ * Check if an MCP server supports OAuth (remote servers support OAuth by default unless explicitly disabled).
628
+ */
629
+ export async function supportsOAuth(mcpName: string): Promise<boolean> {
630
+ const cfg = await Config.get()
631
+ const mcpConfig = cfg.mcp?.[mcpName]
632
+ return mcpConfig?.type === "remote" && mcpConfig.oauth !== false
633
+ }
634
+
635
+ /**
636
+ * Check if an MCP server has stored OAuth tokens.
637
+ */
638
+ export async function hasStoredTokens(mcpName: string): Promise<boolean> {
639
+ const entry = await McpAuth.get(mcpName)
640
+ return !!entry?.tokens
641
+ }
642
+
643
+ export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
644
+
645
+ /**
646
+ * Get the authentication status for an MCP server.
647
+ */
648
+ export async function getAuthStatus(mcpName: string): Promise<AuthStatus> {
649
+ const hasTokens = await hasStoredTokens(mcpName)
650
+ if (!hasTokens) return "not_authenticated"
651
+ const expired = await McpAuth.isTokenExpired(mcpName)
652
+ return expired ? "expired" : "authenticated"
653
+ }
654
+ }