rird 1.0.200

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