ethagent 3.3.3 → 4.0.0

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 (322) hide show
  1. package/.claude-plugin/marketplace.json +11 -0
  2. package/.claude-plugin/plugin.json +35 -0
  3. package/LICENSE +1 -1
  4. package/README.md +64 -104
  5. package/commands/ethagent.md +40 -0
  6. package/package.json +16 -16
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -6
  8. package/src/app/keybindings/types.ts +1 -6
  9. package/src/cli/ResetConfirmView.tsx +54 -53
  10. package/src/cli/demo.ts +86 -0
  11. package/src/cli/hookIo.ts +45 -0
  12. package/src/cli/main.tsx +94 -123
  13. package/src/cli/memoryGuard.ts +49 -0
  14. package/src/cli/reset.ts +28 -70
  15. package/src/cli/sessionStart.ts +33 -0
  16. package/src/cli/status.ts +46 -0
  17. package/src/cli/sync.ts +167 -0
  18. package/src/cli/syncAdapters/claude-code.ts +86 -0
  19. package/src/cli/syncAdapters/codex.ts +66 -0
  20. package/src/cli/syncAdapters/index.ts +45 -0
  21. package/src/cli/syncAdapters/managedBlock.ts +175 -0
  22. package/src/cli/syncAdapters/shared.ts +63 -0
  23. package/src/identity/continuity/envelopeParse.ts +20 -1
  24. package/src/identity/continuity/publicSkills.ts +3 -1
  25. package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
  26. package/src/identity/continuity/skills/scaffold.ts +5 -2
  27. package/src/identity/continuity/snapshots.ts +12 -5
  28. package/src/identity/continuity/storage/defaults.ts +20 -19
  29. package/src/identity/continuity/storage/status.ts +1 -1
  30. package/src/identity/ens/ensLookup/constants.ts +1 -1
  31. package/src/identity/manager/IdentityManager.tsx +33 -0
  32. package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
  33. package/src/identity/{hub → manager}/Routes.tsx +48 -34
  34. package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
  35. package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
  36. package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
  37. package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
  38. package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
  39. package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
  40. package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
  41. package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
  42. package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
  43. package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
  44. package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
  45. package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
  46. package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
  47. package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
  48. package/src/identity/{hub → manager}/create/effects.ts +19 -10
  49. package/src/identity/manager/create/importScan.ts +122 -0
  50. package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
  51. package/src/identity/{hub → manager}/custody/actions.ts +1 -15
  52. package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
  53. package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
  54. package/src/identity/{hub → manager}/custody/types.ts +1 -2
  55. package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
  56. package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
  57. package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
  58. package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
  59. package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
  60. package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
  61. package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
  62. package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
  63. package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
  64. package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
  65. package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
  66. package/src/identity/{hub → manager}/profile/effects.ts +1 -3
  67. package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
  68. package/src/identity/{hub → manager}/profile/state.ts +1 -1
  69. package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
  70. package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
  71. package/src/identity/{hub → manager}/restore/apply.ts +1 -1
  72. package/src/identity/{hub → manager}/restore/auth.ts +1 -1
  73. package/src/identity/{hub → manager}/restore/discover.ts +1 -1
  74. package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
  75. package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
  76. package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
  77. package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
  78. package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
  79. package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
  80. package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
  81. package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
  82. package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
  83. package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
  84. package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
  85. package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
  86. package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
  87. package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
  88. package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
  89. package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
  90. package/src/identity/manager/shared/effects/types.ts +30 -0
  91. package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
  92. package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
  93. package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
  94. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
  95. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
  96. package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
  97. package/src/identity/{hub → manager}/shared/utils.ts +6 -10
  98. package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
  99. package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
  100. package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
  101. package/src/identity/{hub → manager}/types.ts +5 -6
  102. package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
  103. package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
  104. package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
  105. package/src/identity/registry/erc8004/discovery.ts +3 -17
  106. package/src/identity/registry/erc8004/utils.ts +1 -1
  107. package/src/identity/storage/ipfs.ts +21 -1
  108. package/src/identity/wallet/browserWallet/html.ts +10 -2
  109. package/src/identity/wallet/browserWallet/http.ts +18 -0
  110. package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
  111. package/src/identity/wallet/browserWallet/requests.ts +10 -28
  112. package/src/identity/wallet/browserWallet/session.ts +26 -33
  113. package/src/identity/wallet/browserWallet/validation.ts +14 -0
  114. package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
  115. package/src/identity/wallet/page/boot.ts +43 -0
  116. package/src/identity/wallet/page/config.ts +59 -0
  117. package/src/identity/wallet/page/constants.ts +12 -0
  118. package/src/identity/wallet/page/copy.ts +47 -68
  119. package/src/identity/wallet/page/css.ts +638 -0
  120. package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
  121. package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
  122. package/src/identity/wallet/page/markup.ts +44 -34
  123. package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
  124. package/src/identity/wallet/page/resize.ts +95 -0
  125. package/src/identity/wallet/page/state.ts +135 -8
  126. package/src/identity/wallet/page/timeline.ts +161 -0
  127. package/src/identity/wallet/page/view.ts +22 -302
  128. package/src/storage/config.ts +30 -80
  129. package/src/storage/reset.ts +31 -0
  130. package/src/storage/secrets.ts +1 -16
  131. package/src/ui/Select.tsx +27 -5
  132. package/src/ui/Spinner.tsx +16 -15
  133. package/src/ui/Surface.tsx +21 -17
  134. package/src/ui/TextArea.tsx +173 -0
  135. package/src/ui/TextInput.tsx +31 -133
  136. package/src/ui/theme.ts +22 -13
  137. package/src/utils/clipboard.ts +0 -140
  138. package/src/app/FirstRun.tsx +0 -577
  139. package/src/app/FirstRunTimeline.tsx +0 -51
  140. package/src/app/firstRunConfig.ts +0 -26
  141. package/src/app/hooks/useCancelRequest.ts +0 -22
  142. package/src/app/hooks/useDoublePress.ts +0 -46
  143. package/src/app/hooks/useExitOnCtrlC.ts +0 -36
  144. package/src/auth/openaiOAuth/credentials.ts +0 -47
  145. package/src/auth/openaiOAuth/crypto.ts +0 -23
  146. package/src/auth/openaiOAuth/index.ts +0 -238
  147. package/src/auth/openaiOAuth/landingPage.ts +0 -116
  148. package/src/auth/openaiOAuth/listener.ts +0 -151
  149. package/src/auth/openaiOAuth/refresh.ts +0 -70
  150. package/src/auth/openaiOAuth/shared.ts +0 -115
  151. package/src/chat/ChatBottomPane.tsx +0 -296
  152. package/src/chat/ChatScreen.tsx +0 -1685
  153. package/src/chat/ConversationStack.tsx +0 -56
  154. package/src/chat/MessageList.tsx +0 -638
  155. package/src/chat/SessionStatus.tsx +0 -53
  156. package/src/chat/chatEnvironment.ts +0 -16
  157. package/src/chat/chatScreenUtils.ts +0 -194
  158. package/src/chat/chatSessionState.ts +0 -146
  159. package/src/chat/chatTurnContext.ts +0 -50
  160. package/src/chat/chatTurnOrchestrator.ts +0 -603
  161. package/src/chat/chatTurnRows.ts +0 -64
  162. package/src/chat/commands.ts +0 -494
  163. package/src/chat/continuityEditReview.ts +0 -42
  164. package/src/chat/display/DiffView.tsx +0 -193
  165. package/src/chat/display/SyntaxText.tsx +0 -192
  166. package/src/chat/display/toolCallDisplay.ts +0 -103
  167. package/src/chat/display/toolResultDisplay.ts +0 -19
  168. package/src/chat/input/ChatInput.tsx +0 -625
  169. package/src/chat/input/chatInputHelpers.ts +0 -62
  170. package/src/chat/input/chatInputState.ts +0 -247
  171. package/src/chat/input/chatPaste.ts +0 -49
  172. package/src/chat/input/imageRefs.ts +0 -30
  173. package/src/chat/input/inputRendering.tsx +0 -93
  174. package/src/chat/input/textCursor.ts +0 -212
  175. package/src/chat/messageMarkdown.ts +0 -220
  176. package/src/chat/messageRows.ts +0 -43
  177. package/src/chat/planImplementation.ts +0 -62
  178. package/src/chat/slashCommandHandlers.ts +0 -122
  179. package/src/chat/slashCommandViews.ts +0 -120
  180. package/src/chat/transcript/TranscriptView.tsx +0 -184
  181. package/src/chat/transcript/transcriptViewport.ts +0 -295
  182. package/src/chat/views/ContextLimitView.tsx +0 -95
  183. package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
  184. package/src/chat/views/CopyPicker.tsx +0 -50
  185. package/src/chat/views/PermissionPrompt.tsx +0 -156
  186. package/src/chat/views/PermissionsView.tsx +0 -165
  187. package/src/chat/views/PlanApprovalView.tsx +0 -91
  188. package/src/chat/views/ResumeView.tsx +0 -273
  189. package/src/chat/views/RewindView.tsx +0 -412
  190. package/src/cli/preview.tsx +0 -14
  191. package/src/cli/updateNotice.ts +0 -54
  192. package/src/identity/continuity/privateEdit/apply.ts +0 -170
  193. package/src/identity/continuity/privateEdit/diff.ts +0 -6
  194. package/src/identity/continuity/privateEdit/files.ts +0 -23
  195. package/src/identity/continuity/privateEdit/types.ts +0 -28
  196. package/src/identity/continuity/privateEdit.ts +0 -46
  197. package/src/identity/hub/IdentityHub.tsx +0 -14
  198. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
  199. package/src/identity/hub/ens/effects.ts +0 -218
  200. package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
  201. package/src/identity/hub/shared/effects/types.ts +0 -53
  202. package/src/identity/hub/shared/reconciliation/index.ts +0 -14
  203. package/src/identity/wallet/page/grainient.ts +0 -278
  204. package/src/identity/wallet/page/html.ts +0 -28
  205. package/src/identity/wallet/page/styles/base.ts +0 -259
  206. package/src/identity/wallet/page/styles/components.ts +0 -262
  207. package/src/identity/wallet/page/styles/index.ts +0 -5
  208. package/src/identity/wallet/page/styles/responsive.ts +0 -247
  209. package/src/identity/wallet/page.tsx +0 -38
  210. package/src/mcp/approvals.ts +0 -113
  211. package/src/mcp/config.ts +0 -235
  212. package/src/mcp/manager.ts +0 -482
  213. package/src/mcp/managerHelpers.ts +0 -70
  214. package/src/mcp/names.ts +0 -19
  215. package/src/mcp/output.ts +0 -96
  216. package/src/models/ModelPicker.tsx +0 -1009
  217. package/src/models/catalog.ts +0 -327
  218. package/src/models/huggingface.ts +0 -712
  219. package/src/models/huggingfaceStorage.ts +0 -136
  220. package/src/models/llamacpp.ts +0 -848
  221. package/src/models/llamacppCommands.ts +0 -44
  222. package/src/models/llamacppConfig.ts +0 -34
  223. package/src/models/llamacppDiscovery.ts +0 -176
  224. package/src/models/llamacppOutput.ts +0 -65
  225. package/src/models/llamacppPreflight.ts +0 -158
  226. package/src/models/modelDisplay.ts +0 -180
  227. package/src/models/modelPickerCatalogFlow.ts +0 -56
  228. package/src/models/modelPickerCredentials.ts +0 -166
  229. package/src/models/modelPickerData.ts +0 -41
  230. package/src/models/modelPickerDisplay.tsx +0 -132
  231. package/src/models/modelPickerHfFlow.ts +0 -192
  232. package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
  233. package/src/models/modelPickerOptions.ts +0 -457
  234. package/src/models/modelPickerTypes.ts +0 -69
  235. package/src/models/modelPickerUninstallFlow.ts +0 -48
  236. package/src/models/modelPickerViewHelpers.ts +0 -174
  237. package/src/models/modelRecommendation.ts +0 -139
  238. package/src/models/providerDisplay.ts +0 -16
  239. package/src/models/runtimeDetection.ts +0 -81
  240. package/src/models/uncensoredCatalog.ts +0 -86
  241. package/src/providers/anthropic.ts +0 -290
  242. package/src/providers/contracts.ts +0 -71
  243. package/src/providers/errors.ts +0 -80
  244. package/src/providers/gemini.ts +0 -391
  245. package/src/providers/openai-chat.ts +0 -474
  246. package/src/providers/openai-responses-format.ts +0 -177
  247. package/src/providers/openai-responses.ts +0 -306
  248. package/src/providers/openaiChatWire.ts +0 -124
  249. package/src/providers/registry.ts +0 -120
  250. package/src/providers/retry.ts +0 -58
  251. package/src/providers/sse.ts +0 -93
  252. package/src/runtime/compaction.ts +0 -395
  253. package/src/runtime/cwd.ts +0 -43
  254. package/src/runtime/providerTurn.ts +0 -38
  255. package/src/runtime/sessionMode.ts +0 -55
  256. package/src/runtime/systemPrompt.ts +0 -213
  257. package/src/runtime/textToolParser.ts +0 -161
  258. package/src/runtime/toolClaimGuards.ts +0 -143
  259. package/src/runtime/toolExecution.ts +0 -304
  260. package/src/runtime/toolIntent.ts +0 -143
  261. package/src/runtime/turn.ts +0 -369
  262. package/src/runtime/turnNudges.ts +0 -223
  263. package/src/runtime/turnTypes.ts +0 -86
  264. package/src/storage/factoryReset.ts +0 -127
  265. package/src/storage/history.ts +0 -58
  266. package/src/storage/permissions.ts +0 -76
  267. package/src/storage/rewind.ts +0 -266
  268. package/src/storage/sessionExport.ts +0 -49
  269. package/src/storage/sessions.ts +0 -495
  270. package/src/tools/bashSafety.ts +0 -186
  271. package/src/tools/bashTool.ts +0 -140
  272. package/src/tools/changeDirectoryTool.ts +0 -213
  273. package/src/tools/contracts.ts +0 -192
  274. package/src/tools/deleteFileTool.ts +0 -116
  275. package/src/tools/editTool.ts +0 -165
  276. package/src/tools/editUtils.ts +0 -170
  277. package/src/tools/fileDiff.ts +0 -261
  278. package/src/tools/listDirectoryTool.ts +0 -55
  279. package/src/tools/listSkillFilesTool.ts +0 -77
  280. package/src/tools/listSkillsTool.ts +0 -68
  281. package/src/tools/mcpResourceTools.ts +0 -95
  282. package/src/tools/permissionRules.ts +0 -85
  283. package/src/tools/privateContinuityEditTool.ts +0 -187
  284. package/src/tools/privateContinuityReadTool.ts +0 -106
  285. package/src/tools/readSkillTool.ts +0 -107
  286. package/src/tools/readTool.ts +0 -85
  287. package/src/tools/registry.ts +0 -103
  288. package/src/tools/writeFileTool.ts +0 -167
  289. package/src/ui/BrandSplash.tsx +0 -133
  290. package/src/ui/terminalTitle.ts +0 -30
  291. package/src/utils/images.ts +0 -140
  292. package/src/utils/markdownSegments.ts +0 -51
  293. package/src/utils/messages.ts +0 -37
  294. package/src/utils/withRetry.ts +0 -324
  295. /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
  296. /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
  297. /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
  298. /package/src/identity/{hub → manager}/custody/state.ts +0 -0
  299. /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
  300. /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
  301. /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
  302. /package/src/identity/{hub → manager}/ens/state.ts +0 -0
  303. /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
  304. /package/src/identity/{hub → manager}/ens/types.ts +0 -0
  305. /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
  306. /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
  307. /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
  308. /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
  309. /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
  310. /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
  311. /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
  312. /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
  313. /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
  314. /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
  315. /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
  316. /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
  317. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
  318. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
  319. /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
  320. /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
  321. /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
  322. /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
@@ -1,482 +0,0 @@
1
- import { Ajv } from 'ajv'
2
- import { z } from 'zod'
3
- import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
4
- import { Client } from '@modelcontextprotocol/sdk/client/index.js'
5
- import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'
6
- import type { Tool, ToolResult } from '../tools/contracts.js'
7
- import {
8
- addMcpServerConfig,
9
- loadMcpConfigs,
10
- mcpServerTransport,
11
- parseMcpServerConfigJson,
12
- type McpConfigIssue,
13
- type McpServerConfig,
14
- type ScopedMcpServerConfig,
15
- } from './config.js'
16
- import {
17
- getProjectMcpDecision,
18
- isMcpServerDisabled,
19
- setMcpServerEnabled,
20
- setProjectMcpDecision,
21
- } from './approvals.js'
22
- import { buildMcpToolName, normalizeNameForMcp, parseMcpToolName } from './names.js'
23
- import {
24
- formatMcpCallResult,
25
- formatMcpResourceResult,
26
- promptMessagesToText,
27
- } from './output.js'
28
- import {
29
- createTransport,
30
- findScopedServer,
31
- findServerSnapshot,
32
- normalizeInputSchemaJson,
33
- parsePromptArgs,
34
- } from './managerHelpers.js'
35
-
36
- const MCP_CONNECT_TIMEOUT_MS = 10_000
37
- const MCP_LIST_TIMEOUT_MS = 10_000
38
- const MCP_TOOL_TIMEOUT_MS = 120_000
39
-
40
- export type ListedMcpTool = {
41
- name: string
42
- description?: string
43
- inputSchema: {
44
- type: 'object'
45
- properties?: Record<string, unknown>
46
- required?: string[]
47
- [key: string]: unknown
48
- }
49
- annotations?: {
50
- readOnlyHint?: boolean
51
- destructiveHint?: boolean
52
- openWorldHint?: boolean
53
- }
54
- }
55
-
56
- export type McpResourceInfo = {
57
- server: string
58
- uri: string
59
- name: string
60
- description?: string
61
- mimeType?: string
62
- }
63
-
64
- export type McpPromptInfo = {
65
- server: string
66
- promptName: string
67
- slashName: string
68
- description?: string
69
- arguments?: Array<{ name: string; required?: boolean; description?: string }>
70
- }
71
-
72
- export type McpServerSnapshot = {
73
- name: string
74
- normalizedName: string
75
- scope: 'user' | 'project'
76
- transport: 'stdio' | 'http' | 'sse'
77
- status: 'pending' | 'connected' | 'failed' | 'disabled' | 'rejected'
78
- tools: number
79
- resources: number
80
- prompts: number
81
- message?: string
82
- configHash: string
83
- }
84
-
85
- export type McpSnapshot = {
86
- servers: McpServerSnapshot[]
87
- issues: McpConfigIssue[]
88
- prompts: McpPromptInfo[]
89
- }
90
-
91
- export const EMPTY_MCP_SNAPSHOT: McpSnapshot = { servers: [], issues: [], prompts: [] }
92
-
93
- export type McpRuntime = {
94
- callTool(name: string, input: Record<string, unknown>, signal?: AbortSignal): Promise<ToolResult>
95
- listResources(serverName?: string): Promise<string>
96
- readResource(serverName: string, uri: string, signal?: AbortSignal): Promise<string>
97
- }
98
-
99
- type ConnectedMcpServer = {
100
- name: string
101
- normalizedName: string
102
- config: ScopedMcpServerConfig
103
- client: Client
104
- transport: Transport
105
- tools: ListedMcpTool[]
106
- resources: McpResourceInfo[]
107
- prompts: McpPromptInfo[]
108
- }
109
-
110
- type ToolIndexEntry = {
111
- connection: ConnectedMcpServer
112
- tool: ListedMcpTool
113
- }
114
-
115
- const mcpInputSchema = z.object({}).passthrough()
116
- const ajv = new Ajv({ strict: false })
117
-
118
- export class McpManager implements McpRuntime {
119
- private cwd: string
120
- private closed = false
121
- private snapshot: McpSnapshot = EMPTY_MCP_SNAPSHOT
122
- private tools: Tool[] = []
123
- private connections = new Map<string, ConnectedMcpServer>()
124
- private toolIndex = new Map<string, ToolIndexEntry>()
125
-
126
- constructor(
127
- cwd: string,
128
- private readonly onChange: (snapshot: McpSnapshot) => void,
129
- ) {
130
- this.cwd = cwd
131
- }
132
-
133
- currentSnapshot(): McpSnapshot {
134
- return this.snapshot
135
- }
136
-
137
- getTools(): Tool[] {
138
- return this.tools
139
- }
140
-
141
- getPromptSuggestions(): Array<{ name: string; summary: string; completion: string; executeOnEnter: boolean }> {
142
- return this.snapshot.prompts.map(prompt => ({
143
- name: prompt.slashName.slice(1),
144
- summary: prompt.description ?? `MCP prompt from ${prompt.server}`,
145
- completion: `${prompt.slashName} `,
146
- executeOnEnter: false,
147
- }))
148
- }
149
-
150
- async refresh(cwd = this.cwd): Promise<void> {
151
- if (this.closed) return
152
- this.cwd = cwd
153
- await this.closeConnections()
154
- if (this.closed) return
155
-
156
- const loaded = await loadMcpConfigs(this.cwd)
157
- const statuses: McpServerSnapshot[] = []
158
- const promptInfos: McpPromptInfo[] = []
159
- const nextTools: Tool[] = []
160
- const seenNormalized = new Set<string>()
161
-
162
- for (const server of loaded.servers) {
163
- const normalizedName = normalizeNameForMcp(server.name)
164
- const base: Omit<McpServerSnapshot, 'status' | 'tools' | 'resources' | 'prompts'> = {
165
- name: server.name,
166
- normalizedName,
167
- scope: server.scope,
168
- transport: mcpServerTransport(server.config),
169
- configHash: server.configHash,
170
- }
171
-
172
- if (seenNormalized.has(normalizedName)) {
173
- statuses.push({ ...base, status: 'failed', tools: 0, resources: 0, prompts: 0, message: 'normalized server name collides with another MCP server' })
174
- continue
175
- }
176
- seenNormalized.add(normalizedName)
177
-
178
- if (await isMcpServerDisabled(this.cwd, server.name)) {
179
- statuses.push({ ...base, status: 'disabled', tools: 0, resources: 0, prompts: 0 })
180
- continue
181
- }
182
-
183
- if (server.scope === 'project') {
184
- const decision = await getProjectMcpDecision({
185
- workspaceRoot: this.cwd,
186
- serverName: server.name,
187
- configHash: server.configHash,
188
- })
189
- if (decision === 'rejected') {
190
- statuses.push({ ...base, status: 'rejected', tools: 0, resources: 0, prompts: 0, message: 'project server rejected' })
191
- continue
192
- }
193
- if (decision !== 'approved') {
194
- statuses.push({ ...base, status: 'pending', tools: 0, resources: 0, prompts: 0, message: 'project server needs approval' })
195
- continue
196
- }
197
- }
198
-
199
- const connected = await this.connectServer(server, normalizedName)
200
- if (!connected.ok) {
201
- statuses.push({ ...base, status: 'failed', tools: 0, resources: 0, prompts: 0, message: connected.error })
202
- continue
203
- }
204
-
205
- this.connections.set(normalizedName, connected.server)
206
- for (const remoteTool of connected.server.tools) {
207
- const wrappedTool = this.wrapTool(connected.server, remoteTool)
208
- this.toolIndex.set(wrappedTool.name, { connection: connected.server, tool: remoteTool })
209
- nextTools.push(wrappedTool)
210
- }
211
- promptInfos.push(...connected.server.prompts)
212
- statuses.push({
213
- ...base,
214
- status: 'connected',
215
- tools: connected.server.tools.length,
216
- resources: connected.server.resources.length,
217
- prompts: connected.server.prompts.length,
218
- })
219
- }
220
-
221
- this.tools = nextTools
222
- this.snapshot = { servers: statuses, issues: loaded.issues, prompts: promptInfos }
223
- if (!this.closed) this.onChange(this.snapshot)
224
- }
225
-
226
- async approveServer(serverName: string): Promise<string> {
227
- const loaded = await loadMcpConfigs(this.cwd)
228
- const server = findScopedServer(loaded.servers, serverName)
229
- if (!server) return `MCP server "${serverName}" was not found.`
230
- if (server.scope !== 'project') return `MCP server "${server.name}" is user-scoped and does not need project approval.`
231
- await setProjectMcpDecision({
232
- workspaceRoot: this.cwd,
233
- serverName: server.name,
234
- configHash: server.configHash,
235
- decision: 'approved',
236
- })
237
- await this.refresh()
238
- return `approved MCP project server "${server.name}".`
239
- }
240
-
241
- async rejectServer(serverName: string): Promise<string> {
242
- const loaded = await loadMcpConfigs(this.cwd)
243
- const server = findScopedServer(loaded.servers, serverName)
244
- if (!server) return `MCP server "${serverName}" was not found.`
245
- if (server.scope !== 'project') return `MCP server "${server.name}" is user-scoped; disable it instead.`
246
- await setProjectMcpDecision({
247
- workspaceRoot: this.cwd,
248
- serverName: server.name,
249
- configHash: server.configHash,
250
- decision: 'rejected',
251
- })
252
- await this.refresh()
253
- return `rejected MCP project server "${server.name}".`
254
- }
255
-
256
- async setEnabled(serverName: string, enabled: boolean): Promise<string> {
257
- const loaded = await loadMcpConfigs(this.cwd)
258
- const server = findScopedServer(loaded.servers, serverName)
259
- if (!server) return `MCP server "${serverName}" was not found.`
260
- await setMcpServerEnabled({ workspaceRoot: this.cwd, serverName: server.name, enabled })
261
- await this.refresh()
262
- return `${enabled ? 'enabled' : 'disabled'} MCP server "${server.name}".`
263
- }
264
-
265
- async reconnect(serverName?: string): Promise<string> {
266
- await this.refresh()
267
- if (!serverName || serverName === 'all') return 'reconnected MCP servers.'
268
- const server = findServerSnapshot(this.snapshot.servers, serverName)
269
- return server ? `reconnected MCP server "${server.name}".` : `MCP server "${serverName}" was not found.`
270
- }
271
-
272
- async addJson(name: string, json: string, scope: 'user' | 'project'): Promise<string> {
273
- const config = parseMcpServerConfigJson(json)
274
- const filePath = await addMcpServerConfig({ cwd: this.cwd, scope, name, config })
275
- await this.refresh()
276
- return `added MCP server "${name}" to ${scope} config: ${filePath}`
277
- }
278
-
279
- renderStatus(): string {
280
- const lines: string[] = ['mcp servers:']
281
- if (this.snapshot.servers.length === 0) {
282
- lines.push(' none configured. use /mcp add-json <name> <json>')
283
- } else {
284
- for (const server of this.snapshot.servers) {
285
- const counts = server.status === 'connected'
286
- ? ` · ${server.tools} tools, ${server.resources} resources, ${server.prompts} prompts`
287
- : server.message ? ` · ${server.message}` : ''
288
- lines.push(` ${server.name} ${server.status} ${server.scope}/${server.transport}${counts}`)
289
- }
290
- }
291
- if (this.snapshot.issues.length > 0) {
292
- lines.push('', 'mcp config notes:')
293
- for (const issue of this.snapshot.issues) {
294
- const server = issue.serverName ? ` ${issue.serverName}` : ''
295
- lines.push(` ${issue.severity}${server}: ${issue.message}`)
296
- }
297
- }
298
- return lines.join('\n')
299
- }
300
-
301
- async runPromptSlash(name: string, argsText: string, signal?: AbortSignal): Promise<string | null> {
302
- const parsed = parseMcpToolName(name)
303
- if (!parsed) return null
304
- const connection = this.connections.get(parsed.serverName)
305
- if (!connection) return null
306
- const prompt = connection.prompts.find(entry => normalizeNameForMcp(entry.promptName) === parsed.toolName)
307
- if (!prompt) return null
308
- const args = parsePromptArgs(argsText)
309
- const result = await connection.client.getPrompt(
310
- { name: prompt.promptName, arguments: Object.keys(args).length > 0 ? args : undefined },
311
- { signal, timeout: MCP_TOOL_TIMEOUT_MS },
312
- )
313
- return promptMessagesToText(result)
314
- }
315
-
316
- async callTool(name: string, input: Record<string, unknown>, signal?: AbortSignal): Promise<ToolResult> {
317
- const entry = this.toolIndex.get(name)
318
- if (!entry) {
319
- return { ok: false, summary: `${name} not connected`, content: `MCP tool "${name}" is not connected.` }
320
- }
321
- try {
322
- const result = await entry.connection.client.callTool(
323
- { name: entry.tool.name, arguments: input },
324
- CallToolResultSchema,
325
- { signal, timeout: MCP_TOOL_TIMEOUT_MS },
326
- )
327
- const formatted = formatMcpCallResult(result)
328
- return {
329
- ok: formatted.ok,
330
- summary: `${entry.connection.name}/${entry.tool.name}`,
331
- content: formatted.content,
332
- }
333
- } catch (err: unknown) {
334
- return {
335
- ok: false,
336
- summary: `${entry.connection.name}/${entry.tool.name} failed`,
337
- content: (err as Error).message || 'MCP tool failed',
338
- }
339
- }
340
- }
341
-
342
- async listResources(serverName?: string): Promise<string> {
343
- const connections = serverName
344
- ? [this.getConnection(serverName)].filter((conn): conn is ConnectedMcpServer => Boolean(conn))
345
- : [...this.connections.values()]
346
- if (connections.length === 0) return serverName ? `MCP server "${serverName}" is not connected.` : 'No connected MCP servers.'
347
- const lines: string[] = []
348
- for (const connection of connections) {
349
- lines.push(`${connection.name}:`)
350
- if (connection.resources.length === 0) {
351
- lines.push(' no resources')
352
- } else {
353
- for (const resource of connection.resources) {
354
- const mime = resource.mimeType ? ` ${resource.mimeType}` : ''
355
- const desc = resource.description ? ` · ${resource.description}` : ''
356
- lines.push(` ${resource.uri}${mime}${desc}`)
357
- }
358
- }
359
- }
360
- return lines.join('\n')
361
- }
362
-
363
- async readResource(serverName: string, uri: string, signal?: AbortSignal): Promise<string> {
364
- const connection = this.getConnection(serverName)
365
- if (!connection) return `MCP server "${serverName}" is not connected.`
366
- const result = await connection.client.readResource({ uri }, { signal, timeout: MCP_TOOL_TIMEOUT_MS })
367
- return formatMcpResourceResult(result)
368
- }
369
-
370
- async close(): Promise<void> {
371
- this.closed = true
372
- await this.closeConnections()
373
- }
374
-
375
- private async connectServer(
376
- config: ScopedMcpServerConfig,
377
- normalizedName: string,
378
- ): Promise<{ ok: true; server: ConnectedMcpServer } | { ok: false; error: string }> {
379
- const client = new Client(
380
- { name: 'ethagent', version: '1.0.0' },
381
- { capabilities: {} },
382
- )
383
- const transport = createTransport(config.config, this.cwd)
384
- try {
385
- await client.connect(transport, { timeout: MCP_CONNECT_TIMEOUT_MS })
386
- const capabilities = client.getServerCapabilities()
387
- const tools = capabilities?.tools
388
- ? (await client.listTools(undefined, { timeout: MCP_LIST_TIMEOUT_MS })).tools as ListedMcpTool[]
389
- : []
390
- const resources = capabilities?.resources
391
- ? (await client.listResources(undefined, { timeout: MCP_LIST_TIMEOUT_MS })).resources.map(resource => ({
392
- server: config.name,
393
- uri: resource.uri,
394
- name: resource.name,
395
- description: resource.description,
396
- mimeType: resource.mimeType,
397
- }))
398
- : []
399
- const prompts = capabilities?.prompts
400
- ? (await client.listPrompts(undefined, { timeout: MCP_LIST_TIMEOUT_MS })).prompts.map(prompt => ({
401
- server: config.name,
402
- promptName: prompt.name,
403
- slashName: `/${buildMcpToolName(config.name, prompt.name)}`,
404
- description: prompt.description,
405
- arguments: prompt.arguments,
406
- }))
407
- : []
408
- return {
409
- ok: true,
410
- server: {
411
- name: config.name,
412
- normalizedName,
413
- config,
414
- client,
415
- transport,
416
- tools,
417
- resources,
418
- prompts,
419
- },
420
- }
421
- } catch (err: unknown) {
422
- await transport.close().catch(() => {})
423
- return { ok: false, error: (err as Error).message || 'MCP connection failed' }
424
- }
425
- }
426
-
427
- private wrapTool(connection: ConnectedMcpServer, tool: ListedMcpTool): Tool<typeof mcpInputSchema> {
428
- const toolName = buildMcpToolName(connection.name, tool.name)
429
- const validate = ajv.compile(tool.inputSchema)
430
- const readOnly = tool.annotations?.readOnlyHint === true
431
- return {
432
- name: toolName,
433
- kind: 'mcp',
434
- readOnly,
435
- description: tool.description ?? `MCP tool ${tool.name} from ${connection.name}`,
436
- inputSchema: mcpInputSchema,
437
- inputSchemaJson: normalizeInputSchemaJson(tool.inputSchema),
438
- parse(input) {
439
- const parsed = mcpInputSchema.parse(input)
440
- if (!validate(parsed)) {
441
- throw new Error(`MCP tool input failed schema validation: ${ajv.errorsText(validate.errors)}`)
442
- }
443
- return parsed
444
- },
445
- async buildPermissionRequest() {
446
- return {
447
- kind: 'mcp',
448
- title: 'Allow MCP tool?',
449
- subtitle: `${connection.name} / ${tool.name}`,
450
- serverName: connection.name,
451
- normalizedServerName: connection.normalizedName,
452
- toolName: tool.name,
453
- toolKey: toolName,
454
- readOnly,
455
- destructive: tool.annotations?.destructiveHint === true,
456
- openWorld: tool.annotations?.openWorldHint === true,
457
- canPersistServer: true,
458
- }
459
- },
460
- async execute(input, context) {
461
- if (!context.mcp) {
462
- return { ok: false, summary: `${toolName} unavailable`, content: 'MCP runtime is not available.' }
463
- }
464
- return context.mcp.callTool(toolName, input, context.abortSignal)
465
- },
466
- }
467
- }
468
-
469
- private getConnection(serverName: string): ConnectedMcpServer | undefined {
470
- const normalized = normalizeNameForMcp(serverName)
471
- return this.connections.get(normalized) ?? [...this.connections.values()].find(conn => conn.name === serverName)
472
- }
473
-
474
- private async closeConnections(): Promise<void> {
475
- for (const connection of this.connections.values()) {
476
- await connection.transport.close().catch(() => {})
477
- }
478
- this.connections.clear()
479
- this.toolIndex.clear()
480
- this.tools = []
481
- }
482
- }
@@ -1,70 +0,0 @@
1
- import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
2
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
3
- import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
4
- import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
5
- import type { Tool } from '../tools/contracts.js'
6
- import type { McpServerConfig, ScopedMcpServerConfig } from './config.js'
7
- import { normalizeNameForMcp } from './names.js'
8
- import type { ListedMcpTool, McpServerSnapshot } from './manager.js'
9
-
10
- export function createTransport(config: McpServerConfig, cwd: string): Transport {
11
- if (config.type === 'http') {
12
- return new StreamableHTTPClientTransport(new URL(config.url), {
13
- requestInit: config.headers ? { headers: config.headers } : undefined,
14
- })
15
- }
16
- if (config.type === 'sse') {
17
- return new SSEClientTransport(new URL(config.url), {
18
- requestInit: config.headers ? { headers: config.headers } : undefined,
19
- eventSourceInit: config.headers ? { fetch: (url, init) => fetch(url, { ...init, headers: config.headers }) } : undefined,
20
- })
21
- }
22
- return new StdioClientTransport({
23
- command: config.command,
24
- args: config.args ?? [],
25
- env: config.env ? mergeProcessEnv(config.env) : undefined,
26
- cwd: config.cwd ?? cwd,
27
- stderr: 'pipe',
28
- })
29
- }
30
-
31
- export function normalizeInputSchemaJson(schema: ListedMcpTool['inputSchema']): Tool['inputSchemaJson'] {
32
- return {
33
- type: 'object',
34
- properties: schema.properties,
35
- required: schema.required,
36
- oneOf: Array.isArray(schema.oneOf) ? schema.oneOf as Array<Record<string, unknown>> : undefined,
37
- anyOf: Array.isArray(schema.anyOf) ? schema.anyOf as Array<Record<string, unknown>> : undefined,
38
- additionalProperties: schema.additionalProperties as boolean | undefined,
39
- }
40
- }
41
-
42
- export function findScopedServer(servers: ScopedMcpServerConfig[], name: string): ScopedMcpServerConfig | undefined {
43
- const normalized = normalizeNameForMcp(name)
44
- return servers.find(server => server.name === name || normalizeNameForMcp(server.name) === normalized)
45
- }
46
-
47
- export function findServerSnapshot(servers: McpServerSnapshot[], name: string): McpServerSnapshot | undefined {
48
- const normalized = normalizeNameForMcp(name)
49
- return servers.find(server => server.name === name || server.normalizedName === normalized)
50
- }
51
-
52
- export function parsePromptArgs(value: string): Record<string, string> {
53
- const args: Record<string, string> = {}
54
- for (const token of value.trim().split(/\s+/).filter(Boolean)) {
55
- const idx = token.indexOf('=')
56
- if (idx === -1) continue
57
- const key = token.slice(0, idx)
58
- if (!key) continue
59
- args[key] = token.slice(idx + 1)
60
- }
61
- return args
62
- }
63
-
64
- function mergeProcessEnv(extra: Record<string, string>): Record<string, string> {
65
- const env: Record<string, string> = {}
66
- for (const [key, value] of Object.entries(process.env)) {
67
- if (value !== undefined) env[key] = value
68
- }
69
- return { ...env, ...extra }
70
- }
package/src/mcp/names.ts DELETED
@@ -1,19 +0,0 @@
1
- export function normalizeNameForMcp(name: string): string {
2
- const normalized = name.trim().replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '')
3
- return normalized || 'unnamed'
4
- }
5
-
6
- export function mcpToolPrefix(serverName: string): string {
7
- return `mcp__${normalizeNameForMcp(serverName)}__`
8
- }
9
-
10
- export function buildMcpToolName(serverName: string, toolName: string): string {
11
- return `${mcpToolPrefix(serverName)}${normalizeNameForMcp(toolName)}`
12
- }
13
-
14
- export function parseMcpToolName(value: string): { serverName: string; toolName: string } | null {
15
- const parts = value.split('__')
16
- const [prefix, serverName, ...toolNameParts] = parts
17
- if (prefix !== 'mcp' || !serverName || toolNameParts.length === 0) return null
18
- return { serverName, toolName: toolNameParts.join('__') }
19
- }
package/src/mcp/output.ts DELETED
@@ -1,96 +0,0 @@
1
- export const MAX_MCP_OUTPUT_CHARS = 100_000
2
-
3
- export function formatMcpCallResult(result: unknown): { ok: boolean; content: string } {
4
- if (isRecord(result) && 'toolResult' in result) {
5
- return { ok: true, content: truncateMcpOutput(formatUnknown(result.toolResult)) }
6
- }
7
-
8
- const isError = isRecord(result) && result.isError === true
9
- const parts: string[] = []
10
- if (isRecord(result) && Array.isArray(result.content)) {
11
- for (const block of result.content) parts.push(formatContentBlock(block))
12
- }
13
- if (isRecord(result) && isRecord(result.structuredContent)) {
14
- parts.push(`structuredContent:\n${JSON.stringify(result.structuredContent, null, 2)}`)
15
- }
16
- if (parts.length === 0) parts.push(formatUnknown(result))
17
- const content = parts.filter(Boolean).join('\n\n')
18
- return { ok: !isError, content: truncateMcpOutput(isError ? annotateMcpError(content) : content) }
19
- }
20
-
21
- export function formatMcpResourceResult(result: unknown): string {
22
- if (!isRecord(result) || !Array.isArray(result.contents)) return truncateMcpOutput(formatUnknown(result))
23
- return truncateMcpOutput(result.contents.map(content => {
24
- if (!isRecord(content)) return formatUnknown(content)
25
- const uri = typeof content.uri === 'string' ? content.uri : 'resource'
26
- const mime = typeof content.mimeType === 'string' ? ` (${content.mimeType})` : ''
27
- if (typeof content.text === 'string') return `${uri}${mime}\n${content.text}`
28
- if (typeof content.blob === 'string') return `${uri}${mime}\n[binary blob ${content.blob.length} base64 chars]`
29
- return `${uri}${mime}\n${formatUnknown(content)}`
30
- }).join('\n\n'))
31
- }
32
-
33
- export function truncateMcpOutput(value: string): string {
34
- if (value.length <= MAX_MCP_OUTPUT_CHARS) return value
35
- return `${value.slice(0, MAX_MCP_OUTPUT_CHARS)}\n\n[OUTPUT TRUNCATED · exceeded ${MAX_MCP_OUTPUT_CHARS.toLocaleString()} characters. If this MCP server supports pagination or filters, call it again for a narrower result.]`
36
- }
37
-
38
- export function promptMessagesToText(result: unknown): string {
39
- if (!isRecord(result) || !Array.isArray(result.messages)) return formatUnknown(result)
40
- return result.messages.map(message => {
41
- if (!isRecord(message)) return formatUnknown(message)
42
- const role = typeof message.role === 'string' ? message.role : 'user'
43
- const content = formatContentBlock(message.content)
44
- return `${role}:\n${content}`
45
- }).join('\n\n')
46
- }
47
-
48
- function formatContentBlock(block: unknown): string {
49
- if (!isRecord(block)) return formatUnknown(block)
50
- if (block.type === 'text' && typeof block.text === 'string') return block.text
51
- if (block.type === 'image') {
52
- const mime = typeof block.mimeType === 'string' ? block.mimeType : 'image'
53
- const data = typeof block.data === 'string' ? ` ${block.data.length} base64 chars` : ''
54
- return `[image ${mime}${data}]`
55
- }
56
- if (block.type === 'audio') {
57
- const mime = typeof block.mimeType === 'string' ? block.mimeType : 'audio'
58
- const data = typeof block.data === 'string' ? ` ${block.data.length} base64 chars` : ''
59
- return `[audio ${mime}${data}]`
60
- }
61
- if (block.type === 'resource' && isRecord(block.resource)) {
62
- const resource = block.resource
63
- const uri = typeof resource.uri === 'string' ? resource.uri : 'resource'
64
- if (typeof resource.text === 'string') return `${uri}\n${resource.text}`
65
- if (typeof resource.blob === 'string') return `${uri}\n[binary blob ${resource.blob.length} base64 chars]`
66
- }
67
- if (block.type === 'resource_link') {
68
- const name = typeof block.name === 'string' ? block.name : 'resource'
69
- const uri = typeof block.uri === 'string' ? block.uri : ''
70
- return `[resource link ${name}${uri ? ` ${uri}` : ''}]`
71
- }
72
- return formatUnknown(block)
73
- }
74
-
75
- function formatUnknown(value: unknown): string {
76
- if (typeof value === 'string') return value
77
- return JSON.stringify(value, null, 2) ?? String(value)
78
- }
79
-
80
- function annotateMcpError(content: string): string {
81
- const lower = content.toLowerCase()
82
- const looksRateLimited = lower.includes('rate limit') ||
83
- lower.includes('too quickly') ||
84
- lower.includes('429') ||
85
- lower.includes('ddg detected an anomaly')
86
-
87
- if (!looksRateLimited) return content
88
- return [
89
- content,
90
- '[MCP server returned an upstream rate-limit or anti-abuse error; the MCP transport is still connected. Wait before retrying or use an API-key-backed search server for frequent searches.]',
91
- ].join('\n\n')
92
- }
93
-
94
- function isRecord(value: unknown): value is Record<string, unknown> {
95
- return typeof value === 'object' && value !== null && !Array.isArray(value)
96
- }