@fitchmultz/pi-mcp-adapter 5.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 (176) hide show
  1. package/CHANGELOG.md +750 -0
  2. package/LICENSE +21 -0
  3. package/OAUTH.md +462 -0
  4. package/README.md +820 -0
  5. package/abort.ts +34 -0
  6. package/agent-dir.ts +24 -0
  7. package/app-bridge.bundle.js +67 -0
  8. package/banner.png +0 -0
  9. package/checkpoint.ts +55 -0
  10. package/cli.js +222 -0
  11. package/commands.ts +619 -0
  12. package/config.ts +1204 -0
  13. package/consent-manager.ts +25 -0
  14. package/direct-tools.ts +408 -0
  15. package/dist/abort.d.ts +2 -0
  16. package/dist/abort.js +35 -0
  17. package/dist/agent-dir.d.ts +3 -0
  18. package/dist/agent-dir.js +21 -0
  19. package/dist/app-bridge.bundle.js +67 -0
  20. package/dist/checkpoint.d.ts +18 -0
  21. package/dist/checkpoint.js +53 -0
  22. package/dist/commands.d.ts +20 -0
  23. package/dist/commands.js +521 -0
  24. package/dist/config.d.ts +109 -0
  25. package/dist/config.js +1013 -0
  26. package/dist/consent-manager.d.ts +8 -0
  27. package/dist/consent-manager.js +21 -0
  28. package/dist/direct-tools.d.ts +10 -0
  29. package/dist/direct-tools.js +350 -0
  30. package/dist/docs/client-metadata.json +14 -0
  31. package/dist/elicitation-handler.d.ts +17 -0
  32. package/dist/elicitation-handler.js +308 -0
  33. package/dist/error-signal.d.ts +15 -0
  34. package/dist/error-signal.js +21 -0
  35. package/dist/glimpse-ui.d.ts +9 -0
  36. package/dist/glimpse-ui.js +75 -0
  37. package/dist/host-html-template.d.ts +15 -0
  38. package/dist/host-html-template.js +429 -0
  39. package/dist/index.d.ts +7 -0
  40. package/dist/index.js +840 -0
  41. package/dist/init.d.ts +25 -0
  42. package/dist/init.js +562 -0
  43. package/dist/legacy-migration.d.ts +18 -0
  44. package/dist/legacy-migration.js +45 -0
  45. package/dist/lifecycle.d.ts +40 -0
  46. package/dist/lifecycle.js +167 -0
  47. package/dist/logger.d.ts +38 -0
  48. package/dist/logger.js +102 -0
  49. package/dist/mcp-auth-flow.d.ts +113 -0
  50. package/dist/mcp-auth-flow.js +834 -0
  51. package/dist/mcp-auth.d.ts +127 -0
  52. package/dist/mcp-auth.js +658 -0
  53. package/dist/mcp-callback-server.d.ts +56 -0
  54. package/dist/mcp-callback-server.js +371 -0
  55. package/dist/mcp-code.d.ts +51 -0
  56. package/dist/mcp-code.js +366 -0
  57. package/dist/mcp-keyring-helper.cjs +89 -0
  58. package/dist/mcp-oauth-provider.d.ts +154 -0
  59. package/dist/mcp-oauth-provider.js +672 -0
  60. package/dist/mcp-output-guard.d.ts +59 -0
  61. package/dist/mcp-output-guard.js +332 -0
  62. package/dist/mcp-panel.d.ts +66 -0
  63. package/dist/mcp-panel.js +889 -0
  64. package/dist/mcp-script-worker.mjs +128 -0
  65. package/dist/mcp-setup-panel.d.ts +84 -0
  66. package/dist/mcp-setup-panel.js +561 -0
  67. package/dist/mcp-status.d.ts +11 -0
  68. package/dist/mcp-status.js +96 -0
  69. package/dist/mcp-trace.d.ts +95 -0
  70. package/dist/mcp-trace.js +233 -0
  71. package/dist/metadata-cache.d.ts +18 -0
  72. package/dist/metadata-cache.js +277 -0
  73. package/dist/npx-resolver.d.ts +6 -0
  74. package/dist/npx-resolver.js +448 -0
  75. package/dist/onboarding-state.d.ts +11 -0
  76. package/dist/onboarding-state.js +47 -0
  77. package/dist/panel-keys.d.ts +16 -0
  78. package/dist/panel-keys.js +15 -0
  79. package/dist/prompts.d.ts +56 -0
  80. package/dist/prompts.js +284 -0
  81. package/dist/proxy-modes.d.ts +38 -0
  82. package/dist/proxy-modes.js +1235 -0
  83. package/dist/resource-tools.d.ts +1 -0
  84. package/dist/resource-tools.js +14 -0
  85. package/dist/runtime-owner.d.ts +18 -0
  86. package/dist/runtime-owner.js +117 -0
  87. package/dist/sampling-handler.d.ts +15 -0
  88. package/dist/sampling-handler.js +211 -0
  89. package/dist/search-ranking.d.ts +20 -0
  90. package/dist/search-ranking.js +180 -0
  91. package/dist/server-manager.d.ts +114 -0
  92. package/dist/server-manager.js +1175 -0
  93. package/dist/session-recovery.d.ts +31 -0
  94. package/dist/session-recovery.js +310 -0
  95. package/dist/state.d.ts +63 -0
  96. package/dist/state.js +1 -0
  97. package/dist/tool-approval.d.ts +10 -0
  98. package/dist/tool-approval.js +42 -0
  99. package/dist/tool-metadata.d.ts +9 -0
  100. package/dist/tool-metadata.js +193 -0
  101. package/dist/tool-registrar.d.ts +10 -0
  102. package/dist/tool-registrar.js +64 -0
  103. package/dist/tool-result-renderer.d.ts +45 -0
  104. package/dist/tool-result-renderer.js +224 -0
  105. package/dist/ts-shape.d.ts +2 -0
  106. package/dist/ts-shape.js +147 -0
  107. package/dist/types.d.ts +516 -0
  108. package/dist/types.js +168 -0
  109. package/dist/ui-resource-handler.d.ts +17 -0
  110. package/dist/ui-resource-handler.js +209 -0
  111. package/dist/ui-server.d.ts +51 -0
  112. package/dist/ui-server.js +669 -0
  113. package/dist/ui-session.d.ts +39 -0
  114. package/dist/ui-session.js +463 -0
  115. package/dist/ui-stream-types.d.ts +104 -0
  116. package/dist/ui-stream-types.js +58 -0
  117. package/dist/ui-tool-visibility.d.ts +4 -0
  118. package/dist/ui-tool-visibility.js +26 -0
  119. package/dist/unix-socket-transport.d.ts +15 -0
  120. package/dist/unix-socket-transport.js +84 -0
  121. package/dist/utils.d.ts +30 -0
  122. package/dist/utils.js +276 -0
  123. package/docs/client-metadata.json +14 -0
  124. package/elicitation-handler.ts +349 -0
  125. package/error-signal.ts +21 -0
  126. package/glimpse-ui.ts +80 -0
  127. package/host-html-template.ts +456 -0
  128. package/index.ts +900 -0
  129. package/init.ts +627 -0
  130. package/legacy-migration.ts +47 -0
  131. package/lifecycle.ts +165 -0
  132. package/logger.ts +127 -0
  133. package/mcp-auth-flow.ts +975 -0
  134. package/mcp-auth.ts +793 -0
  135. package/mcp-callback-server.ts +450 -0
  136. package/mcp-code.ts +386 -0
  137. package/mcp-keyring-helper.cjs +89 -0
  138. package/mcp-oauth-provider.ts +774 -0
  139. package/mcp-output-guard.ts +412 -0
  140. package/mcp-panel.ts +978 -0
  141. package/mcp-script-worker.mjs +128 -0
  142. package/mcp-setup-panel.ts +666 -0
  143. package/mcp-status.ts +109 -0
  144. package/mcp-trace.ts +295 -0
  145. package/metadata-cache.ts +333 -0
  146. package/npx-resolver.ts +488 -0
  147. package/onboarding-state.ts +58 -0
  148. package/package.json +182 -0
  149. package/panel-keys.ts +37 -0
  150. package/prompts.ts +337 -0
  151. package/proxy-modes.ts +1361 -0
  152. package/resource-tools.ts +17 -0
  153. package/runtime-owner.ts +123 -0
  154. package/sampling-handler.ts +273 -0
  155. package/scripts/build.mjs +131 -0
  156. package/scripts/prepare.mjs +102 -0
  157. package/search-ranking.ts +196 -0
  158. package/server-manager.ts +1320 -0
  159. package/session-recovery.ts +316 -0
  160. package/skills/mcp-scripting/SKILL.md +38 -0
  161. package/state.ts +64 -0
  162. package/tool-approval.ts +78 -0
  163. package/tool-metadata.ts +235 -0
  164. package/tool-registrar.ts +69 -0
  165. package/tool-result-renderer.ts +286 -0
  166. package/ts-shape.ts +145 -0
  167. package/tsconfig.build.json +13 -0
  168. package/tsconfig.json +16 -0
  169. package/types.ts +810 -0
  170. package/ui-resource-handler.ts +249 -0
  171. package/ui-server.ts +808 -0
  172. package/ui-session.ts +568 -0
  173. package/ui-stream-types.ts +89 -0
  174. package/ui-tool-visibility.ts +25 -0
  175. package/unix-socket-transport.ts +85 -0
  176. package/utils.ts +309 -0
@@ -0,0 +1,889 @@
1
+ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
+ import { copyToClipboard } from "@earendil-works/pi-coding-agent";
3
+ import { createPanelKeys } from "./panel-keys.js";
4
+ import { isServerDisabled, isToolAllowed } from "./types.js";
5
+ import { resourceNameToToolName } from "./resource-tools.js";
6
+ import { sanitizeTerminalText, stripOscSequences } from "./utils.js";
7
+ const DEFAULT_THEME = {
8
+ border: "2",
9
+ title: "2",
10
+ selected: "36",
11
+ direct: "32",
12
+ needsAuth: "33",
13
+ placeholder: "2;3",
14
+ description: "2",
15
+ hint: "2",
16
+ confirm: "32",
17
+ cancel: "31",
18
+ };
19
+ function fg(code, text) {
20
+ if (!code)
21
+ return text;
22
+ return `\x1b[${code}m${text}\x1b[0m`;
23
+ }
24
+ const RAINBOW_COLORS = [
25
+ "38;2;178;129;214",
26
+ "38;2;215;135;175",
27
+ "38;2;254;188;56",
28
+ "38;2;228;192;15",
29
+ "38;2;137;210;129",
30
+ "38;2;0;175;175",
31
+ "38;2;23;143;185",
32
+ ];
33
+ function rainbowProgress(filled, total) {
34
+ const dots = [];
35
+ for (let i = 0; i < total; i++) {
36
+ const color = RAINBOW_COLORS[i % RAINBOW_COLORS.length];
37
+ if (!color)
38
+ continue;
39
+ dots.push(fg(color, i < filled ? "●" : "○"));
40
+ }
41
+ return dots.join(" ");
42
+ }
43
+ function fuzzyScore(query, text) {
44
+ const lq = query.toLowerCase();
45
+ const lt = text.toLowerCase();
46
+ if (lt.includes(lq))
47
+ return 100 + (lq.length / lt.length) * 50;
48
+ let score = 0;
49
+ let qi = 0;
50
+ let consecutive = 0;
51
+ for (let i = 0; i < lt.length && qi < lq.length; i++) {
52
+ if (lt[i] === lq[qi]) {
53
+ score += 10 + consecutive;
54
+ consecutive += 5;
55
+ qi++;
56
+ }
57
+ else {
58
+ consecutive = 0;
59
+ }
60
+ }
61
+ return qi === lq.length ? score : 0;
62
+ }
63
+ function sanitizeDisplayText(text) {
64
+ return sanitizeTerminalText(text ?? "");
65
+ }
66
+ function sanitizeRowContent(content) {
67
+ const withoutOsc = stripOscSequences(content);
68
+ let result = "";
69
+ let pendingSpace = false;
70
+ for (let i = 0; i < withoutOsc.length; i++) {
71
+ const rest = withoutOsc.slice(i);
72
+ const ansi = rest.match(/^(?:\x1b\[[0-?]*[ -/]*[@-~]|\x1b[@-Z\\-_])/);
73
+ if (ansi) {
74
+ result += ansi[0];
75
+ i += ansi[0].length - 1;
76
+ continue;
77
+ }
78
+ const code = withoutOsc.charCodeAt(i);
79
+ if (code <= 0x1f || code === 0x7f || (code >= 0x80 && code <= 0x9f)) {
80
+ pendingSpace = true;
81
+ continue;
82
+ }
83
+ if (pendingSpace && result && !result.endsWith(" ")) {
84
+ result += " ";
85
+ }
86
+ pendingSpace = false;
87
+ result += withoutOsc[i];
88
+ }
89
+ return result;
90
+ }
91
+ function estimateTokens(tool) {
92
+ const schemaLen = JSON.stringify(tool.inputSchema ?? {}).length;
93
+ const descLen = tool.description?.length ?? 0;
94
+ return Math.ceil((tool.name.length + descLen + schemaLen) / 4) + 10;
95
+ }
96
+ class McpPanel {
97
+ callbacks;
98
+ done;
99
+ noticeLines;
100
+ prefix;
101
+ servers = [];
102
+ cursorIndex = 0;
103
+ nameQuery = "";
104
+ descSearchActive = false;
105
+ descQuery = "";
106
+ dirty = false;
107
+ confirmingDiscard = false;
108
+ discardSelected = 1;
109
+ importNotice = null;
110
+ authNotice = null;
111
+ authInFlight = null;
112
+ inactivityTimeout = null;
113
+ visibleItems = [];
114
+ tui;
115
+ t = DEFAULT_THEME;
116
+ authOnly;
117
+ keys;
118
+ static MAX_VISIBLE = 12;
119
+ static INACTIVITY_MS = 60_000;
120
+ constructor(config, cache, provenance, callbacks, tui, done, options = {}) {
121
+ this.callbacks = callbacks;
122
+ this.done = done;
123
+ this.tui = tui;
124
+ this.noticeLines = options.noticeLines ?? [];
125
+ this.authOnly = options.authOnly === true;
126
+ this.keys = createPanelKeys(options.keybindings);
127
+ this.prefix = config.settings?.toolPrefix ?? "server";
128
+ for (const [serverName, definition] of Object.entries(config.mcpServers)) {
129
+ if (this.authOnly && !callbacks.canAuthenticate(serverName))
130
+ continue;
131
+ const prov = provenance.get(serverName);
132
+ const serverCache = cache?.servers?.[serverName];
133
+ const globalDirect = config.settings?.directTools;
134
+ let toolFilter = false;
135
+ if (definition.directTools !== undefined) {
136
+ toolFilter = definition.directTools;
137
+ }
138
+ else if (globalDirect) {
139
+ toolFilter = globalDirect;
140
+ }
141
+ const tools = [];
142
+ if (serverCache && !this.authOnly && !isServerDisabled(definition)) {
143
+ for (const tool of serverCache.tools ?? []) {
144
+ if (!isToolAllowed(tool.name, serverName, this.prefix, definition.includeTools, definition.excludeTools)) {
145
+ continue;
146
+ }
147
+ const isDirect = toolFilter === true || (Array.isArray(toolFilter) && toolFilter.includes(tool.name));
148
+ tools.push({
149
+ name: tool.name,
150
+ description: tool.description ?? "",
151
+ isDirect,
152
+ wasDirect: isDirect,
153
+ estimatedTokens: estimateTokens(tool),
154
+ });
155
+ }
156
+ if (definition.exposeResources !== false) {
157
+ for (const resource of serverCache.resources ?? []) {
158
+ const baseName = `read_${resourceNameToToolName(resource.name)}`;
159
+ if (!isToolAllowed(baseName, serverName, this.prefix, definition.includeTools, definition.excludeTools)) {
160
+ continue;
161
+ }
162
+ const isDirect = toolFilter === true || (Array.isArray(toolFilter) && toolFilter.includes(baseName));
163
+ const ct = {
164
+ name: baseName,
165
+ ...(resource.description !== undefined ? { description: resource.description } : {}),
166
+ };
167
+ tools.push({
168
+ name: baseName,
169
+ description: resource.description ?? `Read resource: ${resource.uri}`,
170
+ isDirect,
171
+ wasDirect: isDirect,
172
+ estimatedTokens: estimateTokens(ct),
173
+ });
174
+ }
175
+ }
176
+ }
177
+ const status = callbacks.getConnectionStatus(serverName);
178
+ const failureMessage = callbacks.getFailureMessage?.(serverName) ?? null;
179
+ this.servers.push({
180
+ name: serverName,
181
+ expanded: false,
182
+ source: prov?.kind ?? "user",
183
+ ...(prov?.importKind !== undefined ? { importKind: prov.importKind } : {}),
184
+ ...(definition.includeTools !== undefined ? { includeTools: definition.includeTools } : {}),
185
+ ...(definition.excludeTools !== undefined ? { excludeTools: definition.excludeTools } : {}),
186
+ exposeResources: definition.exposeResources !== false,
187
+ connectionStatus: status,
188
+ failureMessage,
189
+ tools,
190
+ hasCachedData: !!serverCache,
191
+ });
192
+ }
193
+ this.rebuildVisibleItems();
194
+ this.resetInactivityTimeout();
195
+ }
196
+ resetInactivityTimeout() {
197
+ if (this.inactivityTimeout)
198
+ clearTimeout(this.inactivityTimeout);
199
+ this.inactivityTimeout = setTimeout(() => {
200
+ this.cleanup();
201
+ this.done({ cancelled: true, changes: new Map() });
202
+ }, McpPanel.INACTIVITY_MS);
203
+ }
204
+ cleanup() {
205
+ if (this.inactivityTimeout) {
206
+ clearTimeout(this.inactivityTimeout);
207
+ this.inactivityTimeout = null;
208
+ }
209
+ }
210
+ rebuildVisibleItems() {
211
+ const query = this.descSearchActive ? this.descQuery : this.nameQuery;
212
+ const mode = this.descSearchActive ? "desc" : "name";
213
+ this.visibleItems = [];
214
+ for (let si = 0; si < this.servers.length; si++) {
215
+ const server = this.servers[si];
216
+ if (!server)
217
+ continue;
218
+ if (query && this.authOnly) {
219
+ const score = mode === "name" ? fuzzyScore(query, server.name) : 0;
220
+ if (score > 0) {
221
+ this.visibleItems.push({ type: "server", serverIndex: si });
222
+ }
223
+ continue;
224
+ }
225
+ this.visibleItems.push({ type: "server", serverIndex: si });
226
+ if (server.expanded || query) {
227
+ for (let ti = 0; ti < server.tools.length; ti++) {
228
+ const tool = server.tools[ti];
229
+ if (!tool)
230
+ continue;
231
+ if (query) {
232
+ const score = mode === "name"
233
+ ? Math.max(fuzzyScore(query, tool.name), fuzzyScore(query, server.name) * 0.6)
234
+ : fuzzyScore(query, tool.description);
235
+ if (score === 0)
236
+ continue;
237
+ }
238
+ this.visibleItems.push({ type: "tool", serverIndex: si, toolIndex: ti });
239
+ }
240
+ }
241
+ }
242
+ if (query && !this.authOnly) {
243
+ this.visibleItems = this.visibleItems.filter((item) => {
244
+ if (item.type === "server") {
245
+ return this.visibleItems.some((other) => other.type === "tool" && other.serverIndex === item.serverIndex);
246
+ }
247
+ return true;
248
+ });
249
+ }
250
+ }
251
+ updateDirty() {
252
+ this.dirty = this.servers.some((s) => s.tools.some((t) => t.isDirect !== t.wasDirect));
253
+ }
254
+ buildResult() {
255
+ const changes = new Map();
256
+ for (const server of this.servers) {
257
+ const changed = server.tools.some((t) => t.isDirect !== t.wasDirect);
258
+ if (!changed)
259
+ continue;
260
+ const directTools = server.tools.filter((t) => t.isDirect);
261
+ if (directTools.length === server.tools.length && server.tools.length > 0) {
262
+ changes.set(server.name, true);
263
+ }
264
+ else if (directTools.length === 0) {
265
+ changes.set(server.name, false);
266
+ }
267
+ else {
268
+ changes.set(server.name, directTools.map((t) => t.name));
269
+ }
270
+ }
271
+ return { changes, cancelled: false };
272
+ }
273
+ handleInput(data) {
274
+ this.resetInactivityTimeout();
275
+ this.importNotice = null;
276
+ if (!this.authInFlight)
277
+ this.authNotice = null;
278
+ if (this.confirmingDiscard) {
279
+ this.handleDiscardInput(data);
280
+ return;
281
+ }
282
+ // Global shortcuts — always work, even during desc search
283
+ if (matchesKey(data, "ctrl+c")) {
284
+ this.cleanup();
285
+ this.done({ cancelled: true, changes: new Map() });
286
+ return;
287
+ }
288
+ if (matchesKey(data, "ctrl+s")) {
289
+ this.cleanup();
290
+ this.done(this.buildResult());
291
+ return;
292
+ }
293
+ // Modal description search mode
294
+ if (this.descSearchActive) {
295
+ if (matchesKey(data, "escape") || this.keys.selectConfirm(data)) {
296
+ this.descSearchActive = false;
297
+ this.descQuery = "";
298
+ this.rebuildVisibleItems();
299
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
300
+ return;
301
+ }
302
+ if (matchesKey(data, "backspace")) {
303
+ if (this.descQuery.length > 0) {
304
+ this.descQuery = this.descQuery.slice(0, -1);
305
+ this.rebuildVisibleItems();
306
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
307
+ }
308
+ return;
309
+ }
310
+ if (this.keys.selectUp(data)) {
311
+ this.moveCursor(-1);
312
+ return;
313
+ }
314
+ if (this.keys.selectDown(data)) {
315
+ this.moveCursor(1);
316
+ return;
317
+ }
318
+ if (matchesKey(data, "space")) {
319
+ // Toggle even while in desc search
320
+ const item = this.visibleItems[this.cursorIndex];
321
+ if (item)
322
+ this.toggleItem(item);
323
+ return;
324
+ }
325
+ if (data.length === 1 && data.charCodeAt(0) >= 32) {
326
+ this.descQuery += data;
327
+ this.rebuildVisibleItems();
328
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
329
+ return;
330
+ }
331
+ return;
332
+ }
333
+ if (matchesKey(data, "escape")) {
334
+ if (this.nameQuery) {
335
+ this.nameQuery = "";
336
+ this.rebuildVisibleItems();
337
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
338
+ return;
339
+ }
340
+ if (this.dirty) {
341
+ this.confirmingDiscard = true;
342
+ this.discardSelected = 1;
343
+ return;
344
+ }
345
+ this.cleanup();
346
+ this.done({ cancelled: true, changes: new Map() });
347
+ return;
348
+ }
349
+ if (this.keys.selectUp(data)) {
350
+ this.moveCursor(-1);
351
+ return;
352
+ }
353
+ if (this.keys.selectDown(data)) {
354
+ this.moveCursor(1);
355
+ return;
356
+ }
357
+ if (matchesKey(data, "space")) {
358
+ const item = this.visibleItems[this.cursorIndex];
359
+ if (item && !this.authOnly)
360
+ this.toggleItem(item);
361
+ return;
362
+ }
363
+ if (this.keys.selectConfirm(data)) {
364
+ const item = this.visibleItems[this.cursorIndex];
365
+ if (!item)
366
+ return;
367
+ const server = this.servers[item.serverIndex];
368
+ if (!server)
369
+ return;
370
+ if (item.type === "server") {
371
+ if (server.connectionStatus === "disabled")
372
+ return;
373
+ if (this.authOnly || server.connectionStatus === "needs-auth") {
374
+ this.authenticateServer(server);
375
+ return;
376
+ }
377
+ server.expanded = !server.expanded;
378
+ this.rebuildVisibleItems();
379
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
380
+ }
381
+ else if (item.toolIndex !== undefined) {
382
+ const tool = server.tools[item.toolIndex];
383
+ if (!tool)
384
+ return;
385
+ tool.isDirect = !tool.isDirect;
386
+ if (tool.isDirect && server.source === "import") {
387
+ this.importNotice = "Only tool selections save to Pi config.";
388
+ }
389
+ this.updateDirty();
390
+ }
391
+ return;
392
+ }
393
+ if (matchesKey(data, "ctrl+a")) {
394
+ const item = this.visibleItems[this.cursorIndex];
395
+ if (item)
396
+ this.authenticateSelectedServer(item);
397
+ return;
398
+ }
399
+ if (matchesKey(data, "ctrl+r")) {
400
+ const item = this.visibleItems[this.cursorIndex];
401
+ if (!item)
402
+ return;
403
+ const server = this.servers[item.serverIndex];
404
+ if (server)
405
+ this.reconnectServer(server);
406
+ return;
407
+ }
408
+ if (matchesKey(data, "ctrl+y")) {
409
+ const item = this.visibleItems[this.cursorIndex];
410
+ if (!item)
411
+ return;
412
+ const server = this.servers[item.serverIndex];
413
+ if (!server || server.connectionStatus !== "failed" || !server.failureMessage)
414
+ return;
415
+ const serverName = sanitizeDisplayText(server.name);
416
+ const failureMessage = sanitizeDisplayText(server.failureMessage);
417
+ copyToClipboard(failureMessage).then(() => {
418
+ this.authNotice = `Copied error for ${serverName} to clipboard`;
419
+ this.tui.requestRender();
420
+ }).catch((error) => {
421
+ const message = sanitizeDisplayText(error instanceof Error ? error.message : String(error));
422
+ this.authNotice = `Failed to copy error for ${serverName}: ${message}`;
423
+ this.tui.requestRender();
424
+ });
425
+ return;
426
+ }
427
+ if (data === "?") {
428
+ if (this.authOnly)
429
+ return;
430
+ this.descSearchActive = true;
431
+ this.descQuery = "";
432
+ this.rebuildVisibleItems();
433
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
434
+ return;
435
+ }
436
+ // Backspace removes from name query
437
+ if (matchesKey(data, "backspace")) {
438
+ if (this.nameQuery.length > 0) {
439
+ this.nameQuery = this.nameQuery.slice(0, -1);
440
+ this.rebuildVisibleItems();
441
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
442
+ }
443
+ return;
444
+ }
445
+ // All other printable chars → always-on name search
446
+ if (data.length === 1 && data.charCodeAt(0) >= 32) {
447
+ this.nameQuery += data;
448
+ this.rebuildVisibleItems();
449
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
450
+ return;
451
+ }
452
+ }
453
+ authenticateSelectedServer(item) {
454
+ const server = this.servers[item.serverIndex];
455
+ if (server)
456
+ this.authenticateServer(server);
457
+ }
458
+ authenticateServer(server) {
459
+ if (this.authInFlight)
460
+ return;
461
+ if (server.connectionStatus === "connecting" || server.connectionStatus === "disabled")
462
+ return;
463
+ const serverName = sanitizeDisplayText(server.name);
464
+ if (!this.callbacks.canAuthenticate(server.name)) {
465
+ this.authNotice = `${serverName} does not use OAuth authentication.`;
466
+ return;
467
+ }
468
+ this.authInFlight = server.name;
469
+ this.authNotice = `Authenticating ${serverName}...`;
470
+ this.tui.requestRender();
471
+ this.callbacks.authenticate(server.name).then((result) => {
472
+ server.connectionStatus = this.callbacks.getConnectionStatus(server.name);
473
+ if (result.ok) {
474
+ this.authNotice = `OAuth finished for ${serverName}. Reconnecting...`;
475
+ this.authInFlight = null;
476
+ this.tui.requestRender();
477
+ this.reconnectServer(server, { afterAuth: true });
478
+ return;
479
+ }
480
+ const message = sanitizeDisplayText(result.message);
481
+ this.authNotice = `OAuth failed for ${serverName}${message ? `: ${message}` : ". Check the notification for details."}`;
482
+ this.authInFlight = null;
483
+ this.tui.requestRender();
484
+ }).catch((error) => {
485
+ const message = sanitizeDisplayText(error instanceof Error ? error.message : String(error));
486
+ server.connectionStatus = this.callbacks.getConnectionStatus(server.name);
487
+ this.authNotice = `OAuth failed for ${serverName}: ${message}`;
488
+ this.authInFlight = null;
489
+ this.tui.requestRender();
490
+ });
491
+ }
492
+ reconnectServer(server, options = {}) {
493
+ if (server.connectionStatus === "connecting" || server.connectionStatus === "disabled")
494
+ return;
495
+ const serverName = sanitizeDisplayText(server.name);
496
+ server.connectionStatus = "connecting";
497
+ this.tui.requestRender();
498
+ this.callbacks.reconnect(server.name, options.afterAuth).then((connected) => {
499
+ server.connectionStatus = this.callbacks.getConnectionStatus(server.name);
500
+ server.failureMessage = this.callbacks.getFailureMessage?.(server.name) ?? null;
501
+ if (server.connectionStatus === "connected") {
502
+ const entry = this.callbacks.refreshCacheAfterReconnect(server.name);
503
+ if (entry) {
504
+ this.rebuildServerTools(server, entry);
505
+ }
506
+ server.hasCachedData = true;
507
+ }
508
+ if (options.afterAuth) {
509
+ this.authNotice = connected && server.connectionStatus === "connected"
510
+ ? `OAuth finished for ${serverName}. Reconnected.`
511
+ : `OAuth finished for ${serverName}, but reconnect did not complete. Press ctrl+r to retry.`;
512
+ }
513
+ this.tui.requestRender();
514
+ }).catch((error) => {
515
+ server.connectionStatus = "failed";
516
+ const message = sanitizeDisplayText(error instanceof Error ? error.message : String(error));
517
+ this.authNotice = `Reconnect failed for ${serverName}: ${message}`;
518
+ this.tui.requestRender();
519
+ });
520
+ }
521
+ toggleItem(item) {
522
+ if (this.authOnly)
523
+ return;
524
+ const server = this.servers[item.serverIndex];
525
+ if (!server)
526
+ return;
527
+ if (item.type === "server") {
528
+ const newState = !server.tools.every((t) => t.isDirect);
529
+ if (server.source === "import" && newState) {
530
+ this.importNotice = "Only tool selections save to Pi config.";
531
+ }
532
+ for (const t of server.tools)
533
+ t.isDirect = newState;
534
+ }
535
+ else if (item.toolIndex !== undefined) {
536
+ const tool = server.tools[item.toolIndex];
537
+ if (!tool)
538
+ return;
539
+ tool.isDirect = !tool.isDirect;
540
+ if (tool.isDirect && server.source === "import") {
541
+ this.importNotice = "Only tool selections save to Pi config.";
542
+ }
543
+ }
544
+ this.updateDirty();
545
+ }
546
+ handleDiscardInput(data) {
547
+ if (matchesKey(data, "ctrl+c")) {
548
+ this.cleanup();
549
+ this.done({ cancelled: true, changes: new Map() });
550
+ return;
551
+ }
552
+ if (matchesKey(data, "escape") || data === "n" || data === "N") {
553
+ this.confirmingDiscard = false;
554
+ return;
555
+ }
556
+ if (this.keys.selectConfirm(data)) {
557
+ this.cleanup();
558
+ if (this.discardSelected === 0) {
559
+ this.done({ cancelled: true, changes: new Map() });
560
+ }
561
+ else {
562
+ this.done(this.buildResult());
563
+ }
564
+ return;
565
+ }
566
+ if (data === "y" || data === "Y") {
567
+ this.cleanup();
568
+ this.done({ cancelled: true, changes: new Map() });
569
+ return;
570
+ }
571
+ if (matchesKey(data, "left") || matchesKey(data, "right") || matchesKey(data, "tab")) {
572
+ this.discardSelected = this.discardSelected === 0 ? 1 : 0;
573
+ }
574
+ }
575
+ moveCursor(delta) {
576
+ if (this.visibleItems.length === 0)
577
+ return;
578
+ this.cursorIndex = Math.max(0, Math.min(this.visibleItems.length - 1, this.cursorIndex + delta));
579
+ }
580
+ rebuildServerTools(server, entry) {
581
+ const existingState = new Map();
582
+ for (const t of server.tools)
583
+ existingState.set(t.name, t.isDirect);
584
+ const newTools = [];
585
+ for (const tool of entry.tools ?? []) {
586
+ if (!isToolAllowed(tool.name, server.name, this.prefix, server.includeTools, server.excludeTools)) {
587
+ continue;
588
+ }
589
+ const prev = existingState.get(tool.name);
590
+ const isDirect = prev !== undefined ? prev : false;
591
+ newTools.push({
592
+ name: tool.name,
593
+ description: tool.description ?? "",
594
+ isDirect,
595
+ wasDirect: prev !== undefined ? server.tools.find((t) => t.name === tool.name)?.wasDirect ?? false : false,
596
+ estimatedTokens: estimateTokens(tool),
597
+ });
598
+ }
599
+ if (server.exposeResources) {
600
+ for (const resource of entry.resources ?? []) {
601
+ const baseName = `read_${resourceNameToToolName(resource.name)}`;
602
+ if (!isToolAllowed(baseName, server.name, this.prefix, server.includeTools, server.excludeTools)) {
603
+ continue;
604
+ }
605
+ const prev = existingState.get(baseName);
606
+ const isDirect = prev !== undefined ? prev : false;
607
+ const ct = {
608
+ name: baseName,
609
+ ...(resource.description !== undefined ? { description: resource.description } : {}),
610
+ };
611
+ newTools.push({
612
+ name: baseName,
613
+ description: resource.description ?? `Read resource: ${resource.uri}`,
614
+ isDirect,
615
+ wasDirect: prev !== undefined ? server.tools.find((t) => t.name === baseName)?.wasDirect ?? false : false,
616
+ estimatedTokens: estimateTokens(ct),
617
+ });
618
+ }
619
+ }
620
+ server.tools = newTools;
621
+ this.rebuildVisibleItems();
622
+ this.updateDirty();
623
+ }
624
+ render(width) {
625
+ const innerW = width - 2;
626
+ const lines = [];
627
+ const t = this.t;
628
+ const bold = (s) => `\x1b[1m${s}\x1b[22m`;
629
+ const italic = (s) => `\x1b[3m${s}\x1b[23m`;
630
+ const inverse = (s) => `\x1b[7m${s}\x1b[27m`;
631
+ const row = (content) => fg(t.border, "│") + truncateToWidth(" " + sanitizeRowContent(content), innerW, "…", true) + fg(t.border, "│");
632
+ const emptyRow = () => fg(t.border, "│") + " ".repeat(innerW) + fg(t.border, "│");
633
+ const divider = () => fg(t.border, "├" + "─".repeat(innerW) + "┤");
634
+ const titleText = this.authOnly ? " MCP OAuth " : " MCP Servers ";
635
+ const borderLen = innerW - visibleWidth(titleText);
636
+ const leftB = Math.floor(borderLen / 2);
637
+ const rightB = borderLen - leftB;
638
+ lines.push(fg(t.border, "╭" + "─".repeat(leftB)) + fg(t.title, titleText) + fg(t.border, "─".repeat(rightB) + "╮"));
639
+ lines.push(emptyRow());
640
+ const cursor = fg(t.selected, "│");
641
+ const searchIcon = fg(t.border, "◎");
642
+ if (this.descSearchActive) {
643
+ lines.push(row(`${searchIcon} ${fg(t.needsAuth, "desc:")} ${this.descQuery}${cursor}`));
644
+ }
645
+ else if (this.nameQuery) {
646
+ lines.push(row(`${searchIcon} ${this.nameQuery}${cursor}`));
647
+ }
648
+ else {
649
+ lines.push(row(`${searchIcon} ${fg(t.placeholder, italic("search..."))}`));
650
+ }
651
+ lines.push(emptyRow());
652
+ if (this.noticeLines.length > 0) {
653
+ for (const notice of this.noticeLines) {
654
+ lines.push(row(fg(t.hint, italic(sanitizeDisplayText(notice)))));
655
+ }
656
+ lines.push(emptyRow());
657
+ }
658
+ lines.push(divider());
659
+ if (this.servers.length === 0) {
660
+ lines.push(emptyRow());
661
+ lines.push(row(fg(t.hint, italic(this.authOnly ? "No OAuth-capable MCP servers configured." : "No MCP servers configured."))));
662
+ lines.push(emptyRow());
663
+ }
664
+ else {
665
+ const maxVis = McpPanel.MAX_VISIBLE;
666
+ const total = this.visibleItems.length;
667
+ const startIdx = Math.max(0, Math.min(this.cursorIndex - Math.floor(maxVis / 2), total - maxVis));
668
+ const endIdx = Math.min(startIdx + maxVis, total);
669
+ lines.push(emptyRow());
670
+ for (let i = startIdx; i < endIdx; i++) {
671
+ const item = this.visibleItems[i];
672
+ if (!item)
673
+ continue;
674
+ const isCursor = i === this.cursorIndex;
675
+ const server = this.servers[item.serverIndex];
676
+ if (!server)
677
+ continue;
678
+ if (item.type === "server") {
679
+ lines.push(row(this.renderServerRow(server, isCursor)));
680
+ if (isCursor && server.connectionStatus === "failed" && server.failureMessage) {
681
+ for (const line of this.wrapText(sanitizeDisplayText(server.failureMessage), innerW - 6)) {
682
+ lines.push(row(` ${fg(t.cancel, line)}`));
683
+ }
684
+ }
685
+ }
686
+ else if (item.toolIndex !== undefined) {
687
+ const tool = server.tools[item.toolIndex];
688
+ if (tool)
689
+ lines.push(row(this.renderToolRow(tool, isCursor, innerW)));
690
+ }
691
+ }
692
+ lines.push(emptyRow());
693
+ if (total > maxVis) {
694
+ const prog = Math.round(((this.cursorIndex + 1) / total) * 10);
695
+ lines.push(row(`${rainbowProgress(prog, 10)} ${fg(t.hint, `${this.cursorIndex + 1}/${total}`)}`));
696
+ lines.push(emptyRow());
697
+ }
698
+ if (this.importNotice) {
699
+ lines.push(row(fg(t.needsAuth, italic(sanitizeDisplayText(this.importNotice)))));
700
+ lines.push(emptyRow());
701
+ }
702
+ if (this.authNotice) {
703
+ lines.push(row(fg(t.needsAuth, italic(sanitizeDisplayText(this.authNotice)))));
704
+ lines.push(emptyRow());
705
+ }
706
+ }
707
+ lines.push(divider());
708
+ lines.push(emptyRow());
709
+ if (this.confirmingDiscard) {
710
+ const discardBtn = this.discardSelected === 0
711
+ ? inverse(bold(fg(t.cancel, " Discard ")))
712
+ : fg(t.hint, " Discard ");
713
+ const keepBtn = this.discardSelected === 1
714
+ ? inverse(bold(fg(t.confirm, " Keep & Close ")))
715
+ : fg(t.hint, " Keep & Close ");
716
+ lines.push(row(`Discard unsaved changes? ${discardBtn} ${keepBtn}`));
717
+ }
718
+ else {
719
+ if (this.authOnly) {
720
+ lines.push(row(fg(t.description, "select a server to authenticate")));
721
+ }
722
+ else {
723
+ const directCount = this.servers.reduce((sum, s) => sum + s.tools.filter((t) => t.isDirect).length, 0);
724
+ const totalTokens = this.servers.reduce((sum, s) => sum + s.tools.filter((t) => t.isDirect).reduce((ts, t) => ts + t.estimatedTokens, 0), 0);
725
+ const stats = directCount > 0 ? `${directCount} direct ~${totalTokens.toLocaleString()} tokens` : "no direct tools";
726
+ lines.push(row(fg(t.description, stats + (this.dirty ? fg(t.needsAuth, " (unsaved)") : ""))));
727
+ }
728
+ }
729
+ lines.push(emptyRow());
730
+ const hints = this.authOnly
731
+ ? [
732
+ italic("↑↓") + " navigate",
733
+ italic("⏎") + " auth",
734
+ italic("ctrl+a") + " auth",
735
+ italic("esc") + " clear/close",
736
+ italic("ctrl+c") + " quit",
737
+ ]
738
+ : [
739
+ italic("↑↓") + " navigate",
740
+ italic("space") + " toggle",
741
+ italic("⏎") + " expand/auth",
742
+ italic("ctrl+a") + " auth",
743
+ italic("ctrl+r") + " reconnect",
744
+ ...(this.selectedServerHasFailureMessage() ? [italic("ctrl+y") + " copy error"] : []),
745
+ italic("?") + " desc search",
746
+ italic("ctrl+s") + " save",
747
+ italic("esc") + " clear/close",
748
+ italic("ctrl+c") + " quit",
749
+ ];
750
+ const gap = " ";
751
+ const gapW = 2;
752
+ const maxW = innerW - 2;
753
+ let curLine = "";
754
+ let curW = 0;
755
+ for (const hint of hints) {
756
+ const hw = visibleWidth(hint);
757
+ const needed = curW === 0 ? hw : gapW + hw;
758
+ if (curW > 0 && curW + needed > maxW) {
759
+ lines.push(row(fg(t.hint, curLine)));
760
+ curLine = hint;
761
+ curW = hw;
762
+ }
763
+ else {
764
+ curLine += (curW > 0 ? gap : "") + hint;
765
+ curW += needed;
766
+ }
767
+ }
768
+ if (curLine)
769
+ lines.push(row(fg(t.hint, curLine)));
770
+ lines.push(fg(t.border, "╰" + "─".repeat(innerW) + "╯"));
771
+ return lines;
772
+ }
773
+ renderServerRow(server, isCursor) {
774
+ const t = this.t;
775
+ const bold = (s) => `\x1b[1m${s}\x1b[22m`;
776
+ const expandIcon = server.expanded ? "▾" : "▸";
777
+ const prefix = isCursor ? fg(t.selected, expandIcon) : fg(t.border, server.expanded ? expandIcon : "·");
778
+ const serverName = sanitizeDisplayText(server.name);
779
+ const importKind = sanitizeDisplayText(server.importKind ?? "import");
780
+ const nameStr = isCursor ? bold(fg(t.selected, serverName)) : serverName;
781
+ const importLabel = server.source === "import" ? fg(t.description, ` (${importKind})`) : "";
782
+ const statusLabel = this.renderConnectionStatus(server);
783
+ if (!server.hasCachedData && !this.authOnly) {
784
+ return `${prefix} ${nameStr}${importLabel} ${fg(t.description, "(not cached)")}${statusLabel}`;
785
+ }
786
+ const directCount = server.tools.filter((t) => t.isDirect).length;
787
+ const totalCount = server.tools.length;
788
+ let toggleIcon = fg(t.description, "○");
789
+ if (directCount === totalCount && totalCount > 0) {
790
+ toggleIcon = fg(t.direct, "●");
791
+ }
792
+ else if (directCount > 0) {
793
+ toggleIcon = fg(t.needsAuth, "◐");
794
+ }
795
+ let toolInfo = "";
796
+ if (totalCount > 0) {
797
+ toolInfo = `${directCount}/${totalCount}`;
798
+ if (directCount > 0) {
799
+ const tokens = server.tools.filter((t) => t.isDirect).reduce((s, t) => s + t.estimatedTokens, 0);
800
+ toolInfo += ` ~${tokens.toLocaleString()}`;
801
+ }
802
+ toolInfo = fg(t.description, toolInfo);
803
+ }
804
+ return `${prefix} ${toggleIcon} ${nameStr}${importLabel} ${toolInfo}${statusLabel}`;
805
+ }
806
+ selectedServerHasFailureMessage() {
807
+ const item = this.visibleItems[this.cursorIndex];
808
+ if (!item)
809
+ return false;
810
+ const server = this.servers[item.serverIndex];
811
+ return server?.connectionStatus === "failed" && !!server.failureMessage;
812
+ }
813
+ wrapText(text, width) {
814
+ const max = Math.max(8, width);
815
+ const words = text.split(/\s+/).filter(Boolean);
816
+ const lines = [];
817
+ let current = "";
818
+ const splitLongWord = (word) => {
819
+ let rest = word;
820
+ while (visibleWidth(rest) > max) {
821
+ let take = "";
822
+ let index = 0;
823
+ while (index < rest.length && visibleWidth(take + rest.charAt(index)) <= max) {
824
+ take += rest.charAt(index);
825
+ index++;
826
+ }
827
+ if (!take)
828
+ take = rest.charAt(0);
829
+ lines.push(take);
830
+ rest = rest.slice(take.length);
831
+ }
832
+ return rest;
833
+ };
834
+ for (const word of words) {
835
+ const candidate = current ? `${current} ${word}` : word;
836
+ if (visibleWidth(candidate) <= max) {
837
+ current = candidate;
838
+ }
839
+ else {
840
+ if (current)
841
+ lines.push(current);
842
+ current = splitLongWord(word);
843
+ }
844
+ }
845
+ if (current)
846
+ lines.push(current);
847
+ return lines.length > 0 ? lines : [text];
848
+ }
849
+ renderConnectionStatus(server) {
850
+ const t = this.t;
851
+ if (this.authInFlight === server.name)
852
+ return ` ${fg(t.needsAuth, "authenticating")}`;
853
+ if (server.connectionStatus === "disabled")
854
+ return ` ${fg(t.description, "disabled")}`;
855
+ if (server.connectionStatus === "needs-auth")
856
+ return ` ${fg(t.needsAuth, "needs auth")}`;
857
+ if (server.connectionStatus === "connecting")
858
+ return ` ${fg(t.needsAuth, "connecting")}`;
859
+ if (server.connectionStatus === "failed")
860
+ return ` ${fg(t.cancel, "failed")}`;
861
+ if (this.authOnly && server.connectionStatus === "connected")
862
+ return ` ${fg(t.direct, "connected")}`;
863
+ if (this.authOnly)
864
+ return ` ${fg(t.description, "idle")}`;
865
+ return "";
866
+ }
867
+ renderToolRow(tool, isCursor, innerW) {
868
+ const t = this.t;
869
+ const bold = (s) => `\x1b[1m${s}\x1b[22m`;
870
+ const toggleIcon = tool.isDirect ? fg(t.direct, "●") : fg(t.description, "○");
871
+ const cursor = isCursor ? fg(t.selected, "▸") : " ";
872
+ const toolName = sanitizeDisplayText(tool.name);
873
+ const description = sanitizeDisplayText(tool.description);
874
+ const nameStr = isCursor ? bold(fg(t.selected, toolName)) : toolName;
875
+ const prefixLen = 7 + visibleWidth(toolName);
876
+ const maxDescLen = Math.max(0, innerW - prefixLen - 8);
877
+ const descStr = maxDescLen > 5 && description
878
+ ? fg(t.description, "— " + truncateToWidth(description, maxDescLen, "…"))
879
+ : "";
880
+ return ` ${cursor} ${toggleIcon} ${nameStr} ${descStr}`;
881
+ }
882
+ invalidate() { }
883
+ dispose() {
884
+ this.cleanup();
885
+ }
886
+ }
887
+ export function createMcpPanel(config, cache, provenance, callbacks, tui, done, options) {
888
+ return new McpPanel(config, cache, provenance, callbacks, tui, done, options ?? {});
889
+ }