ethagent 3.0.1 → 3.1.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 (73) hide show
  1. package/README.md +6 -1
  2. package/package.json +3 -1
  3. package/src/app/FirstRun.tsx +1 -24
  4. package/src/app/firstRunConfig.ts +26 -0
  5. package/src/auth/openaiOAuth/landingPage.ts +2 -11
  6. package/src/chat/ChatScreen.tsx +32 -117
  7. package/src/chat/MessageList.tsx +18 -260
  8. package/src/chat/chatEnvironment.ts +16 -0
  9. package/src/chat/chatTurnContext.ts +50 -0
  10. package/src/chat/chatTurnOrchestrator.ts +5 -112
  11. package/src/chat/chatTurnRows.ts +64 -0
  12. package/src/chat/commands.ts +3 -178
  13. package/src/chat/continuityEditReview.ts +42 -0
  14. package/src/chat/input/ChatInput.tsx +10 -144
  15. package/src/chat/input/chatInputHelpers.ts +62 -0
  16. package/src/chat/input/inputRendering.tsx +93 -0
  17. package/src/chat/messageMarkdown.ts +220 -0
  18. package/src/chat/messageRows.ts +43 -0
  19. package/src/chat/planImplementation.ts +62 -0
  20. package/src/chat/slashCommandHandlers.ts +165 -0
  21. package/src/chat/slashCommandViews.ts +120 -0
  22. package/src/cli/main.tsx +7 -0
  23. package/src/identity/continuity/challenges.ts +123 -0
  24. package/src/identity/continuity/envelope.ts +49 -1484
  25. package/src/identity/continuity/envelopeCreate.ts +322 -0
  26. package/src/identity/continuity/envelopeCrypto.ts +182 -0
  27. package/src/identity/continuity/envelopeParse.ts +441 -0
  28. package/src/identity/continuity/envelopeTypes.ts +204 -0
  29. package/src/identity/continuity/envelopeVersion.ts +1 -0
  30. package/src/identity/continuity/payloadNormalization.ts +183 -0
  31. package/src/identity/continuity/publicSkills.ts +5 -5
  32. package/src/identity/continuity/skills/loadSkills.ts +12 -69
  33. package/src/identity/continuity/skills/skillPaths.ts +76 -0
  34. package/src/identity/continuity/skillsNormalization.ts +119 -0
  35. package/src/identity/continuity/snapshotToken.ts +28 -0
  36. package/src/identity/hub/continuity/completion.ts +67 -0
  37. package/src/identity/hub/continuity/effects.ts +5 -62
  38. package/src/identity/hub/profile/effects.ts +6 -170
  39. package/src/identity/hub/profile/operatorSave.ts +202 -0
  40. package/src/identity/registry/erc8004/metadata.ts +31 -23
  41. package/src/identity/wallet/browserWallet/html.ts +1 -57
  42. package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
  43. package/src/identity/wallet/page/controller.ts +1 -1
  44. package/src/identity/wallet/page/errorView.ts +122 -0
  45. package/src/identity/wallet/page/view.ts +3 -114
  46. package/src/mcp/manager.ts +8 -66
  47. package/src/mcp/managerHelpers.ts +70 -0
  48. package/src/models/ModelPicker.tsx +69 -889
  49. package/src/models/huggingface.ts +20 -137
  50. package/src/models/huggingfaceStorage.ts +136 -0
  51. package/src/models/llamacpp.ts +37 -303
  52. package/src/models/llamacppCommands.ts +44 -0
  53. package/src/models/llamacppConfig.ts +34 -0
  54. package/src/models/llamacppDiscovery.ts +176 -0
  55. package/src/models/llamacppOutput.ts +65 -0
  56. package/src/models/modelPickerCatalogFlow.ts +56 -0
  57. package/src/models/modelPickerCredentials.ts +166 -0
  58. package/src/models/modelPickerData.ts +41 -0
  59. package/src/models/modelPickerDisplay.tsx +132 -0
  60. package/src/models/modelPickerHfFlow.ts +192 -0
  61. package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
  62. package/src/models/modelPickerTypes.ts +69 -0
  63. package/src/models/modelPickerUninstallFlow.ts +48 -0
  64. package/src/models/modelPickerViewHelpers.ts +174 -0
  65. package/src/providers/openai-chat.ts +5 -124
  66. package/src/providers/openaiChatWire.ts +124 -0
  67. package/src/runtime/providerTurn.ts +38 -0
  68. package/src/runtime/textToolParser.ts +161 -0
  69. package/src/runtime/toolIntent.ts +1 -1
  70. package/src/runtime/turn.ts +43 -499
  71. package/src/runtime/turnNudges.ts +223 -0
  72. package/src/runtime/turnTypes.ts +86 -0
  73. package/src/ui/terminalTitle.ts +30 -0
@@ -0,0 +1,85 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const WALLET_PAGE_ENTRY_FILE = 'page.tsx'
6
+ const WALLET_PAGE_MODULE_FILES = [
7
+ join('page', 'types.ts'),
8
+ join('page', 'html.ts'),
9
+ join('page', 'constants.ts'),
10
+ join('page', 'styles', 'base.ts'),
11
+ join('page', 'styles', 'components.ts'),
12
+ join('page', 'styles', 'responsive.ts'),
13
+ join('page', 'styles', 'index.ts'),
14
+ join('page', 'markup.ts'),
15
+ join('page', 'grainient.ts'),
16
+ join('page', 'state.ts'),
17
+ join('page', 'copy.ts'),
18
+ join('page', 'errorView.ts'),
19
+ join('page', 'walletProvider.ts'),
20
+ join('page', 'view.ts'),
21
+ join('page', 'controller.ts'),
22
+ ] as const
23
+
24
+ export function walletPageSourceFiles(fromUrl = import.meta.url): string[] {
25
+ const sourceRoot = locateWalletPageSourceRoot(fromUrl)
26
+ return [
27
+ ...WALLET_PAGE_MODULE_FILES.map(file => join(sourceRoot, file)),
28
+ join(sourceRoot, WALLET_PAGE_ENTRY_FILE),
29
+ ]
30
+ }
31
+
32
+ export function walletPageSourceFile(relativeFile: string, fromUrl = import.meta.url): string {
33
+ return join(locateWalletPageSourceRoot(fromUrl), relativeFile)
34
+ }
35
+
36
+ export function loadWalletPageRawSource(fromUrl = import.meta.url): string {
37
+ return walletPageSourceFiles(fromUrl).map(file => readFileSync(file, 'utf8')).join('\n')
38
+ }
39
+
40
+ export function loadWalletPageSource(fromUrl = import.meta.url): string {
41
+ return stripWalletModuleSyntax(loadWalletPageRawSource(fromUrl))
42
+ }
43
+
44
+ export function stripWalletModuleSyntax(source: string): string {
45
+ const out: string[] = []
46
+ let skippingImport = false
47
+ for (const line of source.split(/\r?\n/)) {
48
+ const trimmed = line.trim()
49
+ if (skippingImport) {
50
+ if (/\bfrom\s+['"][^'"]+['"]/.test(trimmed) || trimmed.endsWith(';')) skippingImport = false
51
+ continue
52
+ }
53
+ if (trimmed.startsWith('import ')) {
54
+ if (!/\bfrom\s+['"][^'"]+['"]/.test(trimmed) && !trimmed.endsWith(';')) skippingImport = true
55
+ continue
56
+ }
57
+ out.push(line.replace(/^export\s+(?=(async\s+function|const|let|function|interface|type|class)\b)/, ''))
58
+ }
59
+ return out.join('\n')
60
+ }
61
+
62
+ function locateWalletPageSourceRoot(fromUrl: string): string {
63
+ for (const candidate of walletPageSourceRootCandidates(fromUrl)) {
64
+ if (hasWalletPageSourceFiles(candidate)) return candidate
65
+ }
66
+ throw new Error('could not locate browser wallet page source files')
67
+ }
68
+
69
+ function walletPageSourceRootCandidates(fromUrl: string): string[] {
70
+ const start = dirname(fileURLToPath(fromUrl))
71
+ const candidates = [join(start, '..')]
72
+ for (let dir = start; ; dir = dirname(dir)) {
73
+ candidates.push(join(dir, 'src', 'identity', 'wallet'))
74
+ const parent = dirname(dir)
75
+ if (parent === dir) break
76
+ }
77
+ return Array.from(new Set(candidates))
78
+ }
79
+
80
+ function hasWalletPageSourceFiles(sourceRoot: string): boolean {
81
+ return [
82
+ ...WALLET_PAGE_MODULE_FILES,
83
+ WALLET_PAGE_ENTRY_FILE,
84
+ ].every(file => existsSync(join(sourceRoot, file)))
85
+ }
@@ -11,12 +11,12 @@ import {
11
11
  errorSlot,
12
12
  getLastWalletError,
13
13
  initializeViewElements,
14
- serializeWalletError,
15
14
  setState,
16
15
  showPreparedMessage,
17
16
  statusHint,
18
17
  statusText,
19
18
  } from './view.js'
19
+ import { serializeWalletError } from './errorView.js'
20
20
  import {
21
21
  buildTxParams,
22
22
  clearCurrentWalletMethod,
@@ -0,0 +1,122 @@
1
+ import { config } from './state.js'
2
+ import type { WalletErrorPayload } from './types.js'
3
+ import { escapeHtml } from './html.js'
4
+ import {
5
+ chainLabel,
6
+ PURPOSE_COPY,
7
+ type PurposeCopyEntry,
8
+ } from './copy.js'
9
+
10
+ export function serializeWalletError(err: unknown, method: string | null): WalletErrorPayload {
11
+ const e = err as any
12
+ const message = ((e && (e.message ?? String(err))) || 'Something went wrong.').trim()
13
+ const codeRaw = e && (e.code ?? e.errorCode)
14
+ const code = codeRaw === undefined || codeRaw === null ? undefined : String(codeRaw)
15
+ const data = safeStringify(e && e.data, 500)
16
+ const causes: string[] = []
17
+ let cur = e && e.cause
18
+ for (let i = 0; i < 5 && cur; i++) {
19
+ const cm = ((cur && (cur.message ?? String(cur))) || '').trim()
20
+ if (cm) causes.push(cm)
21
+ cur = cur && cur.cause
22
+ }
23
+ const out: WalletErrorPayload = { message }
24
+ if (code) out.code = code
25
+ if (data) out.data = data
26
+ if (causes.length) out.causes = causes
27
+ if (method) out.method = method
28
+ if (config.purpose) out.purpose = config.purpose
29
+ if (config.chainIdHex) out.chainIdHex = config.chainIdHex
30
+ return out
31
+ }
32
+
33
+ export function walletErrorHtml(payload: WalletErrorPayload): string {
34
+ const msg = payload.message || 'Something went wrong.'
35
+ const isNoWallet = /no wallet|window\.ethereum|metamask|rabby|brave|extension/i.test(msg)
36
+ const isUserReject = /user rejected|user denied|cancelled|canceled/i.test(msg)
37
+ const isWrongChain = /chain|network/i.test(msg) && !isNoWallet
38
+ const isExecutionRevert = /execution reverted|revert/i.test(msg)
39
+ const isOwnerWalletRequired = /owner wallet required/i.test(msg)
40
+ const isOperatorWalletRequired = /operator wallet required/i.test(msg)
41
+ let title = 'Wallet Error'
42
+ let body = msg
43
+ let hint = 'Press <code>enter</code> to retry or <code>esc</code> to abort.'
44
+ const codeClass = classifyByCode(payload.code)
45
+ if (codeClass) {
46
+ title = codeClass.title
47
+ hint = codeClass.hint
48
+ } else if (isOwnerWalletRequired) {
49
+ title = 'Owner Wallet Required'
50
+ body = msg.replace(/^owner wallet required:\s*/i, '')
51
+ body = body ? body.charAt(0).toUpperCase() + body.slice(1) : 'Switch to the owner wallet.'
52
+ hint = 'Switch to the owner wallet, then retry.'
53
+ } else if (isOperatorWalletRequired) {
54
+ title = 'Operator Wallet Required'
55
+ body = msg.replace(/^operator wallet required:\s*/i, '')
56
+ body = body ? body.charAt(0).toUpperCase() + body.slice(1) : 'Switch to the operator wallet.'
57
+ hint = 'Switch to the operator wallet, then retry.'
58
+ } else if (isNoWallet) {
59
+ title = 'No Wallet'
60
+ body = 'Install a wallet.'
61
+ hint = 'Install a wallet, then retry.'
62
+ } else if (isUserReject) {
63
+ title = 'Rejected'
64
+ body = 'Request declined in wallet.'
65
+ hint = 'Press <code>enter</code> to retry or <code>esc</code> to abort.'
66
+ } else if (isWrongChain) {
67
+ title = 'Wrong Network'
68
+ hint = `Switch to <code>${escapeHtml(chainLabel(config.chainIdHex))}</code>, then retry.`
69
+ } else if (isExecutionRevert) {
70
+ title = 'Transaction Reverted'
71
+ hint = 'Use the expected wallet and check ENS ownership, then retry.'
72
+ }
73
+ if (body) body = body.charAt(0).toUpperCase() + body.slice(1)
74
+ payload.title = title
75
+ const action = actionContextFor(payload)
76
+ let html = `<p class="error-title">${escapeHtml(title)}</p>`
77
+ + `<p class="error-msg">${escapeHtml(body)}</p>`
78
+ if (action) html += `<p class="error-action">${escapeHtml(action)}</p>`
79
+ if (payload.causes && payload.causes.length) {
80
+ for (const cause of payload.causes) {
81
+ html += `<p class="error-cause">Caused by: ${escapeHtml(cause)}</p>`
82
+ }
83
+ }
84
+ html += `<p class="error-hint">${hint}</p>`
85
+ return html
86
+ }
87
+
88
+ function safeStringify(v: unknown, max: number): string | undefined {
89
+ if (v === undefined || v === null) return undefined
90
+ let s: string
91
+ try { s = typeof v === 'string' ? v : JSON.stringify(v) } catch (_) { s = String(v) }
92
+ if (!s) return undefined
93
+ return s.length > max ? s.slice(0, max) + '...' : s
94
+ }
95
+
96
+ function actionContextFor(payload: WalletErrorPayload): string {
97
+ const purpose = payload.purpose || config.purpose
98
+ if (purpose) {
99
+ const copy = (PURPOSE_COPY as any)[purpose] as PurposeCopyEntry | undefined
100
+ const ctx = copy && (copy as any).errorContext
101
+ if (typeof ctx === 'string' && ctx) return ctx
102
+ }
103
+ if (payload.method) return `during ${payload.method}`
104
+ return ''
105
+ }
106
+
107
+ function classifyByCode(code: string | undefined): { title: string; hint: string } | null {
108
+ if (!code) return null
109
+ switch (code) {
110
+ case '4001': return { title: 'Rejected', hint: 'Press <code>enter</code> to retry or <code>esc</code> to abort.' }
111
+ case '4100': return { title: 'Wallet Not Authorized', hint: 'Connect this site in your wallet, then retry.' }
112
+ case '4200': return { title: 'Method Not Supported by Wallet', hint: 'Use a wallet that supports this transaction type, then retry.' }
113
+ case '4900': return { title: 'Wallet Disconnected', hint: 'Reconnect your wallet, then retry.' }
114
+ case '4901': return { title: 'Wrong Network', hint: `Switch to <code>${escapeHtml(chainLabel(config.chainIdHex))}</code>, then retry.` }
115
+ case '-32603': return { title: 'Internal Wallet RPC Error', hint: "The wallet's connected RPC failed. Try again, or switch RPC in your wallet settings." }
116
+ case '-32602': return { title: 'Invalid Request Parameters', hint: 'Press <code>enter</code> to retry or <code>esc</code> to abort.' }
117
+ case '-32000': case '-32001': case '-32002': case '-32003': case '-32004':
118
+ case '-32005': case '-32006': case '-32007': case '-32008': case '-32009':
119
+ return { title: 'Wallet RPC Error', hint: 'The wallet RPC reported a server error. Try again or switch RPC in your wallet.' }
120
+ default: return null
121
+ }
122
+ }
@@ -1,12 +1,11 @@
1
1
  import { config } from './state.js'
2
2
  import type { WalletErrorPayload } from './types.js'
3
- import { escapeHtml, glyphs } from './html.js'
3
+ import { glyphs } from './html.js'
4
4
  import {
5
5
  accountCopy,
6
6
  chainLabel,
7
7
  FLOW_COPY,
8
8
  isTransactionFlow,
9
- PURPOSE_COPY,
10
9
  purposeCopy,
11
10
  signCopy,
12
11
  STATE_TITLES,
@@ -14,8 +13,8 @@ import {
14
13
  transactionCopy,
15
14
  transactionPurposeTitle,
16
15
  type FlowCopy,
17
- type PurposeCopyEntry,
18
16
  } from './copy.js'
17
+ import { walletErrorHtml } from './errorView.js'
19
18
 
20
19
  let card: HTMLElement;
21
20
  let promptText: HTMLElement;
@@ -416,118 +415,8 @@ export function setState(state: string, payload?: any): void {
416
415
 
417
416
  let lastWalletError: WalletErrorPayload | null = null;
418
417
 
419
- function safeStringify(v: unknown, max: number): string | undefined {
420
- if (v === undefined || v === null) return undefined;
421
- let s: string;
422
- try { s = typeof v === "string" ? v : JSON.stringify(v); } catch (_) { s = String(v); }
423
- if (!s) return undefined;
424
- return s.length > max ? s.slice(0, max) + "..." : s;
425
- }
426
-
427
- export function serializeWalletError(err: unknown, method: string | null): WalletErrorPayload {
428
- const e = err as any;
429
- const message = ((e && (e.message ?? String(err))) || "Something went wrong.").trim();
430
- const codeRaw = e && (e.code ?? e.errorCode);
431
- const code = codeRaw === undefined || codeRaw === null ? undefined : String(codeRaw);
432
- const data = safeStringify(e && e.data, 500);
433
- const causes: string[] = [];
434
- let cur = e && e.cause;
435
- for (let i = 0; i < 5 && cur; i++) {
436
- const cm = ((cur && (cur.message ?? String(cur))) || "").trim();
437
- if (cm) causes.push(cm);
438
- cur = cur && cur.cause;
439
- }
440
- const out: WalletErrorPayload = { message };
441
- if (code) out.code = code;
442
- if (data) out.data = data;
443
- if (causes.length) out.causes = causes;
444
- if (method) out.method = method;
445
- if (config.purpose) out.purpose = config.purpose;
446
- if (config.chainIdHex) out.chainIdHex = config.chainIdHex;
447
- return out;
448
- }
449
-
450
- function actionContextFor(payload: WalletErrorPayload): string {
451
- const purpose = payload.purpose || config.purpose;
452
- if (purpose) {
453
- const copy = (PURPOSE_COPY as any)[purpose] as PurposeCopyEntry | undefined;
454
- const ctx = copy && (copy as any).errorContext;
455
- if (typeof ctx === "string" && ctx) return ctx;
456
- }
457
- if (payload.method) return "during " + payload.method;
458
- return "";
459
- }
460
-
461
- function classifyByCode(code: string | undefined): { title: string; hint: string } | null {
462
- if (!code) return null;
463
- switch (code) {
464
- case "4001": return { title: "Rejected", hint: "Press <code>enter</code> to retry or <code>esc</code> to abort." };
465
- case "4100": return { title: "Wallet Not Authorized", hint: "Connect this site in your wallet, then retry." };
466
- case "4200": return { title: "Method Not Supported by Wallet", hint: "Use a wallet that supports this transaction type, then retry." };
467
- case "4900": return { title: "Wallet Disconnected", hint: "Reconnect your wallet, then retry." };
468
- case "4901": return { title: "Wrong Network", hint: "Switch to <code>" + escapeHtml(chainLabel(config.chainIdHex)) + "</code>, then retry." };
469
- case "-32603": return { title: "Internal Wallet RPC Error", hint: "The wallet's connected RPC failed. Try again, or switch RPC in your wallet settings." };
470
- case "-32602": return { title: "Invalid Request Parameters", hint: "Press <code>enter</code> to retry or <code>esc</code> to abort." };
471
- case "-32000": case "-32001": case "-32002": case "-32003": case "-32004":
472
- case "-32005": case "-32006": case "-32007": case "-32008": case "-32009":
473
- return { title: "Wallet RPC Error", hint: "The wallet RPC reported a server error. Try again or switch RPC in your wallet." };
474
- default: return null;
475
- }
476
- }
477
-
478
418
  function renderError(payload: WalletErrorPayload): void {
479
- const msg = payload.message || "Something went wrong.";
480
- const isNoWallet = /no wallet|window\.ethereum|metamask|rabby|brave|extension/i.test(msg);
481
- const isUserReject = /user rejected|user denied|cancelled|canceled/i.test(msg);
482
- const isWrongChain = /chain|network/i.test(msg) && !isNoWallet;
483
- const isExecutionRevert = /execution reverted|revert/i.test(msg);
484
- const isOwnerWalletRequired = /owner wallet required/i.test(msg);
485
- const isOperatorWalletRequired = /operator wallet required/i.test(msg);
486
- let title = "Wallet Error";
487
- let body = msg;
488
- let hint = "Press <code>enter</code> to retry or <code>esc</code> to abort.";
489
- const codeClass = classifyByCode(payload.code);
490
- if (codeClass) {
491
- title = codeClass.title;
492
- hint = codeClass.hint;
493
- } else if (isOwnerWalletRequired) {
494
- title = "Owner Wallet Required";
495
- body = msg.replace(/^owner wallet required:\s*/i, "");
496
- body = body ? body.charAt(0).toUpperCase() + body.slice(1) : "Switch to the owner wallet.";
497
- hint = "Switch to the owner wallet, then retry.";
498
- } else if (isOperatorWalletRequired) {
499
- title = "Operator Wallet Required";
500
- body = msg.replace(/^operator wallet required:\s*/i, "");
501
- body = body ? body.charAt(0).toUpperCase() + body.slice(1) : "Switch to the operator wallet.";
502
- hint = "Switch to the operator wallet, then retry.";
503
- } else if (isNoWallet) {
504
- title = "No Wallet";
505
- body = "Install a wallet.";
506
- hint = "Install a wallet, then retry.";
507
- } else if (isUserReject) {
508
- title = "Rejected";
509
- body = "Request declined in wallet.";
510
- hint = "Press <code>enter</code> to retry or <code>esc</code> to abort.";
511
- } else if (isWrongChain) {
512
- title = "Wrong Network";
513
- hint = "Switch to <code>" + escapeHtml(chainLabel(config.chainIdHex)) + "</code>, then retry.";
514
- } else if (isExecutionRevert) {
515
- title = "Transaction Reverted";
516
- hint = "Use the expected wallet and check ENS ownership, then retry.";
517
- }
518
- if (body) body = body.charAt(0).toUpperCase() + body.slice(1);
519
- payload.title = title;
520
- const action = actionContextFor(payload);
521
- let html = '<p class="error-title">' + escapeHtml(title) + "</p>"
522
- + '<p class="error-msg">' + escapeHtml(body) + "</p>";
523
- if (action) html += '<p class="error-action">' + escapeHtml(action) + "</p>";
524
- if (payload.causes && payload.causes.length) {
525
- for (const cause of payload.causes) {
526
- html += '<p class="error-cause">Caused by: ' + escapeHtml(cause) + "</p>";
527
- }
528
- }
529
- html += '<p class="error-hint">' + hint + "</p>";
530
- errorSlot.innerHTML = html;
419
+ errorSlot.innerHTML = walletErrorHtml(payload);
531
420
  lastWalletError = payload;
532
421
  }
533
422
 
@@ -2,9 +2,6 @@ import { Ajv } from 'ajv'
2
2
  import { z } from 'zod'
3
3
  import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
4
4
  import { Client } from '@modelcontextprotocol/sdk/client/index.js'
5
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
6
- import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
7
- import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
8
5
  import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'
9
6
  import type { Tool, ToolResult } from '../tools/contracts.js'
10
7
  import {
@@ -28,12 +25,19 @@ import {
28
25
  formatMcpResourceResult,
29
26
  promptMessagesToText,
30
27
  } from './output.js'
28
+ import {
29
+ createTransport,
30
+ findScopedServer,
31
+ findServerSnapshot,
32
+ normalizeInputSchemaJson,
33
+ parsePromptArgs,
34
+ } from './managerHelpers.js'
31
35
 
32
36
  const MCP_CONNECT_TIMEOUT_MS = 10_000
33
37
  const MCP_LIST_TIMEOUT_MS = 10_000
34
38
  const MCP_TOOL_TIMEOUT_MS = 120_000
35
39
 
36
- type ListedMcpTool = {
40
+ export type ListedMcpTool = {
37
41
  name: string
38
42
  description?: string
39
43
  inputSchema: {
@@ -476,65 +480,3 @@ export class McpManager implements McpRuntime {
476
480
  this.tools = []
477
481
  }
478
482
  }
479
-
480
- function createTransport(config: McpServerConfig, cwd: string): Transport {
481
- if (config.type === 'http') {
482
- return new StreamableHTTPClientTransport(new URL(config.url), {
483
- requestInit: config.headers ? { headers: config.headers } : undefined,
484
- })
485
- }
486
- if (config.type === 'sse') {
487
- return new SSEClientTransport(new URL(config.url), {
488
- requestInit: config.headers ? { headers: config.headers } : undefined,
489
- eventSourceInit: config.headers ? { fetch: (url, init) => fetch(url, { ...init, headers: config.headers }) } : undefined,
490
- })
491
- }
492
- return new StdioClientTransport({
493
- command: config.command,
494
- args: config.args ?? [],
495
- env: config.env ? mergeProcessEnv(config.env) : undefined,
496
- cwd: config.cwd ?? cwd,
497
- stderr: 'pipe',
498
- })
499
- }
500
-
501
- function mergeProcessEnv(extra: Record<string, string>): Record<string, string> {
502
- const env: Record<string, string> = {}
503
- for (const [key, value] of Object.entries(process.env)) {
504
- if (value !== undefined) env[key] = value
505
- }
506
- return { ...env, ...extra }
507
- }
508
-
509
- function normalizeInputSchemaJson(schema: ListedMcpTool['inputSchema']): Tool['inputSchemaJson'] {
510
- return {
511
- type: 'object',
512
- properties: schema.properties,
513
- required: schema.required,
514
- oneOf: Array.isArray(schema.oneOf) ? schema.oneOf as Array<Record<string, unknown>> : undefined,
515
- anyOf: Array.isArray(schema.anyOf) ? schema.anyOf as Array<Record<string, unknown>> : undefined,
516
- additionalProperties: schema.additionalProperties as boolean | undefined,
517
- }
518
- }
519
-
520
- function findScopedServer(servers: ScopedMcpServerConfig[], name: string): ScopedMcpServerConfig | undefined {
521
- const normalized = normalizeNameForMcp(name)
522
- return servers.find(server => server.name === name || normalizeNameForMcp(server.name) === normalized)
523
- }
524
-
525
- function findServerSnapshot(servers: McpServerSnapshot[], name: string): McpServerSnapshot | undefined {
526
- const normalized = normalizeNameForMcp(name)
527
- return servers.find(server => server.name === name || server.normalizedName === normalized)
528
- }
529
-
530
- function parsePromptArgs(value: string): Record<string, string> {
531
- const args: Record<string, string> = {}
532
- for (const token of value.trim().split(/\s+/).filter(Boolean)) {
533
- const idx = token.indexOf('=')
534
- if (idx === -1) continue
535
- const key = token.slice(0, idx)
536
- if (!key) continue
537
- args[key] = token.slice(idx + 1)
538
- }
539
- return args
540
- }
@@ -0,0 +1,70 @@
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
+ }