@oh-my-pi/pi-coding-agent 11.8.2 → 11.9.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.
- package/CHANGELOG.md +42 -0
- package/docs/tui.md +9 -9
- package/package.json +7 -7
- package/src/capability/mcp.ts +9 -0
- package/src/cli/file-processor.ts +8 -13
- package/src/cli/oclif-help.ts +1 -1
- package/src/cli.ts +14 -0
- package/src/commit/git/index.ts +16 -16
- package/src/config/file-lock.ts +1 -1
- package/src/config/keybindings.ts +11 -11
- package/src/config/model-registry.ts +31 -66
- package/src/config/settings.ts +88 -95
- package/src/config.ts +2 -2
- package/src/cursor.ts +4 -4
- package/src/debug/index.ts +28 -28
- package/src/discovery/builtin.ts +48 -0
- package/src/discovery/codex.ts +5 -13
- package/src/discovery/cursor.ts +2 -7
- package/src/discovery/mcp-json.ts +33 -0
- package/src/exa/mcp-client.ts +2 -2
- package/src/exa/websets.ts +2 -2
- package/src/export/html/index.ts +3 -3
- package/src/export/ttsr.ts +27 -27
- package/src/extensibility/custom-tools/loader.ts +9 -9
- package/src/extensibility/extensions/runner.ts +64 -64
- package/src/extensibility/hooks/runner.ts +46 -46
- package/src/extensibility/plugins/manager.ts +49 -49
- package/src/extensibility/slash-commands.ts +1 -0
- package/src/index.ts +0 -3
- package/src/internal-urls/router.ts +5 -5
- package/src/ipy/kernel.ts +61 -57
- package/src/lsp/client.ts +1 -1
- package/src/lsp/clients/biome-client.ts +2 -2
- package/src/lsp/clients/lsp-linter-client.ts +7 -7
- package/src/lsp/index.ts +9 -9
- package/src/mcp/config-writer.ts +194 -0
- package/src/mcp/config.ts +20 -6
- package/src/mcp/index.ts +4 -0
- package/src/mcp/loader.ts +6 -0
- package/src/mcp/manager.ts +139 -50
- package/src/mcp/oauth-discovery.ts +274 -0
- package/src/mcp/oauth-flow.ts +229 -0
- package/src/mcp/tool-bridge.ts +20 -20
- package/src/mcp/transports/http.ts +107 -66
- package/src/mcp/transports/stdio.ts +74 -59
- package/src/mcp/types.ts +15 -1
- package/src/modes/components/assistant-message.ts +25 -25
- package/src/modes/components/bash-execution.ts +51 -51
- package/src/modes/components/bordered-loader.ts +7 -7
- package/src/modes/components/branch-summary-message.ts +7 -7
- package/src/modes/components/compaction-summary-message.ts +7 -7
- package/src/modes/components/countdown-timer.ts +15 -15
- package/src/modes/components/custom-editor.ts +22 -22
- package/src/modes/components/custom-message.ts +21 -21
- package/src/modes/components/dynamic-border.ts +3 -3
- package/src/modes/components/extensions/extension-dashboard.ts +72 -72
- package/src/modes/components/extensions/extension-list.ts +99 -97
- package/src/modes/components/extensions/inspector-panel.ts +26 -26
- package/src/modes/components/footer.ts +36 -36
- package/src/modes/components/history-search.ts +52 -52
- package/src/modes/components/hook-editor.ts +20 -20
- package/src/modes/components/hook-input.ts +20 -20
- package/src/modes/components/hook-message.ts +22 -22
- package/src/modes/components/hook-selector.ts +52 -52
- package/src/modes/components/index.ts +0 -1
- package/src/modes/components/login-dialog.ts +57 -57
- package/src/modes/components/mcp-add-wizard.ts +1286 -0
- package/src/modes/components/model-selector.ts +173 -173
- package/src/modes/components/oauth-selector.ts +45 -45
- package/src/modes/components/plugin-settings.ts +52 -52
- package/src/modes/components/python-execution.ts +53 -53
- package/src/modes/components/queue-mode-selector.ts +7 -7
- package/src/modes/components/read-tool-group.ts +23 -23
- package/src/modes/components/session-selector.ts +40 -37
- package/src/modes/components/settings-selector.ts +80 -80
- package/src/modes/components/show-images-selector.ts +7 -7
- package/src/modes/components/skill-message.ts +27 -27
- package/src/modes/components/status-line-segment-editor.ts +81 -81
- package/src/modes/components/status-line.ts +73 -73
- package/src/modes/components/theme-selector.ts +11 -11
- package/src/modes/components/thinking-selector.ts +7 -7
- package/src/modes/components/todo-display.ts +19 -19
- package/src/modes/components/todo-reminder.ts +9 -9
- package/src/modes/components/tool-execution.ts +212 -216
- package/src/modes/components/tree-selector.ts +144 -144
- package/src/modes/components/ttsr-notification.ts +17 -17
- package/src/modes/components/user-message-selector.ts +18 -18
- package/src/modes/components/welcome.ts +10 -10
- package/src/modes/controllers/command-controller.ts +0 -7
- package/src/modes/controllers/event-controller.ts +23 -23
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +12 -9
- package/src/modes/controllers/mcp-command-controller.ts +1223 -0
- package/src/modes/interactive-mode.ts +240 -241
- package/src/modes/rpc/rpc-client.ts +77 -77
- package/src/modes/rpc/rpc-mode.ts +5 -5
- package/src/modes/theme/theme.ts +113 -113
- package/src/modes/types.ts +1 -1
- package/src/patch/index.ts +45 -45
- package/src/prompts/tools/task.md +22 -2
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +512 -476
- package/src/session/agent-storage.ts +72 -75
- package/src/session/auth-storage.ts +186 -252
- package/src/session/history-storage.ts +36 -38
- package/src/session/session-manager.ts +300 -299
- package/src/session/session-storage.ts +65 -90
- package/src/ssh/connection-manager.ts +9 -9
- package/src/system-prompt.ts +2 -3
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +28 -40
- package/src/task/index.ts +13 -12
- package/src/task/subprocess-tool-registry.ts +5 -5
- package/src/task/worktree.ts +8 -5
- package/src/tools/ask.ts +7 -7
- package/src/tools/bash.ts +15 -10
- package/src/tools/browser.ts +130 -127
- package/src/tools/calculator.ts +46 -46
- package/src/tools/context.ts +9 -9
- package/src/tools/exit-plan-mode.ts +5 -5
- package/src/tools/fetch.ts +5 -5
- package/src/tools/find.ts +16 -16
- package/src/tools/grep.ts +12 -24
- package/src/tools/index.ts +1 -1
- package/src/tools/notebook.ts +6 -6
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/python.ts +12 -11
- package/src/tools/read.ts +17 -17
- package/src/tools/ssh.ts +9 -9
- package/src/tools/submit-result.ts +13 -13
- package/src/tools/todo-write.ts +6 -6
- package/src/tools/write.ts +10 -10
- package/src/tui/output-block.ts +6 -6
- package/src/tui/utils.ts +9 -9
- package/src/utils/event-bus.ts +13 -11
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/ignore-files.ts +1 -1
- package/src/web/search/index.ts +5 -5
- package/src/web/search/providers/anthropic.ts +7 -2
- package/examples/hooks/snake.ts +0 -342
- package/src/modes/components/armin.ts +0 -379
|
@@ -0,0 +1,1286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Add Wizard Component
|
|
3
|
+
*
|
|
4
|
+
* Interactive multi-step wizard for adding MCP servers.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
Container,
|
|
8
|
+
Input,
|
|
9
|
+
matchesKey,
|
|
10
|
+
replaceTabs,
|
|
11
|
+
Spacer,
|
|
12
|
+
Text,
|
|
13
|
+
TruncatedText,
|
|
14
|
+
truncateToWidth,
|
|
15
|
+
} from "@oh-my-pi/pi-tui";
|
|
16
|
+
import { validateServerName } from "../../mcp/config-writer";
|
|
17
|
+
import { analyzeAuthError, discoverOAuthEndpoints } from "../../mcp/oauth-discovery";
|
|
18
|
+
import type { MCPHttpServerConfig, MCPServerConfig, MCPSseServerConfig, MCPStdioServerConfig } from "../../mcp/types";
|
|
19
|
+
import { theme } from "../theme/theme";
|
|
20
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
21
|
+
|
|
22
|
+
type TransportType = "stdio" | "http" | "sse";
|
|
23
|
+
type AuthMethod = "none" | "oauth" | "manual";
|
|
24
|
+
type AuthLocation = "env" | "header";
|
|
25
|
+
type Scope = "user" | "project";
|
|
26
|
+
|
|
27
|
+
type WizardStep =
|
|
28
|
+
| "name"
|
|
29
|
+
| "transport"
|
|
30
|
+
| "command"
|
|
31
|
+
| "args"
|
|
32
|
+
| "url"
|
|
33
|
+
| "auth-method"
|
|
34
|
+
| "oauth-error"
|
|
35
|
+
| "oauth-auth-url"
|
|
36
|
+
| "oauth-token-url"
|
|
37
|
+
| "oauth-client-id"
|
|
38
|
+
| "oauth-client-secret"
|
|
39
|
+
| "oauth-scopes"
|
|
40
|
+
| "apikey"
|
|
41
|
+
| "auth-location"
|
|
42
|
+
| "env-var-name"
|
|
43
|
+
| "header-name"
|
|
44
|
+
| "scope"
|
|
45
|
+
| "confirm";
|
|
46
|
+
|
|
47
|
+
interface WizardState {
|
|
48
|
+
name: string;
|
|
49
|
+
transport: TransportType | null;
|
|
50
|
+
command: string;
|
|
51
|
+
args: string;
|
|
52
|
+
url: string;
|
|
53
|
+
authMethod: AuthMethod;
|
|
54
|
+
oauthAuthUrl: string;
|
|
55
|
+
oauthTokenUrl: string;
|
|
56
|
+
oauthClientId: string;
|
|
57
|
+
oauthClientSecret: string;
|
|
58
|
+
oauthScopes: string;
|
|
59
|
+
oauthCredentialId: string | null;
|
|
60
|
+
apiKey: string;
|
|
61
|
+
authLocation: AuthLocation | null;
|
|
62
|
+
envVarName: string;
|
|
63
|
+
headerName: string;
|
|
64
|
+
scope: Scope | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Max display width for sanitized error/URL text in wizard TUI */
|
|
68
|
+
const MAX_DISPLAY_WIDTH = 120;
|
|
69
|
+
|
|
70
|
+
/** Sanitize a string for TUI display: replace tabs and truncate */
|
|
71
|
+
function sanitize(text: string): string {
|
|
72
|
+
return truncateToWidth(replaceTabs(text), MAX_DISPLAY_WIDTH);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class MCPAddWizard extends Container {
|
|
76
|
+
#currentStep: WizardStep = "name";
|
|
77
|
+
#state: WizardState = {
|
|
78
|
+
name: "",
|
|
79
|
+
transport: null,
|
|
80
|
+
command: "",
|
|
81
|
+
args: "",
|
|
82
|
+
url: "",
|
|
83
|
+
authMethod: "none",
|
|
84
|
+
oauthAuthUrl: "",
|
|
85
|
+
oauthTokenUrl: "",
|
|
86
|
+
oauthClientId: "",
|
|
87
|
+
oauthClientSecret: "",
|
|
88
|
+
oauthScopes: "",
|
|
89
|
+
oauthCredentialId: null,
|
|
90
|
+
apiKey: "",
|
|
91
|
+
authLocation: null,
|
|
92
|
+
envVarName: "API_KEY",
|
|
93
|
+
headerName: "Authorization",
|
|
94
|
+
scope: null,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
#contentContainer: Container;
|
|
98
|
+
#inputField: Input | null = null;
|
|
99
|
+
#selectedIndex = 0;
|
|
100
|
+
#validationError: string | null = null;
|
|
101
|
+
#onCompleteCallback: (name: string, config: MCPServerConfig, scope: Scope) => void;
|
|
102
|
+
#onCancelCallback: () => void;
|
|
103
|
+
#onOAuthCallback:
|
|
104
|
+
| ((authUrl: string, tokenUrl: string, clientId: string, clientSecret: string, scopes: string) => Promise<string>)
|
|
105
|
+
| null = null;
|
|
106
|
+
#onTestConnectionCallback: ((config: MCPServerConfig) => Promise<void>) | null = null;
|
|
107
|
+
#onRenderCallback: (() => void) | null = null;
|
|
108
|
+
|
|
109
|
+
constructor(
|
|
110
|
+
onComplete: (name: string, config: MCPServerConfig, scope: Scope) => void,
|
|
111
|
+
onCancel: () => void,
|
|
112
|
+
onOAuth?: (
|
|
113
|
+
authUrl: string,
|
|
114
|
+
tokenUrl: string,
|
|
115
|
+
clientId: string,
|
|
116
|
+
clientSecret: string,
|
|
117
|
+
scopes: string,
|
|
118
|
+
) => Promise<string>,
|
|
119
|
+
onTestConnection?: (config: MCPServerConfig) => Promise<void>,
|
|
120
|
+
onRender?: () => void,
|
|
121
|
+
initialName?: string,
|
|
122
|
+
) {
|
|
123
|
+
super();
|
|
124
|
+
this.#onCompleteCallback = onComplete;
|
|
125
|
+
this.#onCancelCallback = onCancel;
|
|
126
|
+
this.#onOAuthCallback = onOAuth ?? null;
|
|
127
|
+
this.#onTestConnectionCallback = onTestConnection ?? null;
|
|
128
|
+
this.#onRenderCallback = onRender ?? null;
|
|
129
|
+
if (initialName && initialName.trim().length > 0) {
|
|
130
|
+
this.#state.name = initialName.trim();
|
|
131
|
+
this.#currentStep = "transport";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add border
|
|
135
|
+
this.addChild(new DynamicBorder());
|
|
136
|
+
this.addChild(new Spacer(1));
|
|
137
|
+
|
|
138
|
+
// Add title
|
|
139
|
+
this.addChild(new TruncatedText(theme.bold("Add MCP Server")));
|
|
140
|
+
this.addChild(new Spacer(1));
|
|
141
|
+
|
|
142
|
+
// Content container for step-specific content
|
|
143
|
+
this.#contentContainer = new Container();
|
|
144
|
+
this.addChild(this.#contentContainer);
|
|
145
|
+
|
|
146
|
+
this.addChild(new Spacer(1));
|
|
147
|
+
|
|
148
|
+
// Add bottom border
|
|
149
|
+
this.addChild(new DynamicBorder());
|
|
150
|
+
|
|
151
|
+
// Render first step
|
|
152
|
+
this.#renderStep();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#requestRender(): void {
|
|
156
|
+
this.#onRenderCallback?.();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#renderStep(): void {
|
|
160
|
+
this.#contentContainer.clear();
|
|
161
|
+
this.#inputField = null; // Reset input field
|
|
162
|
+
|
|
163
|
+
switch (this.#currentStep) {
|
|
164
|
+
case "name":
|
|
165
|
+
this.#renderNameStep();
|
|
166
|
+
break;
|
|
167
|
+
case "transport":
|
|
168
|
+
this.#renderTransportStep();
|
|
169
|
+
break;
|
|
170
|
+
case "command":
|
|
171
|
+
this.#renderCommandStep();
|
|
172
|
+
break;
|
|
173
|
+
case "args":
|
|
174
|
+
this.#renderArgsStep();
|
|
175
|
+
break;
|
|
176
|
+
case "url":
|
|
177
|
+
this.#renderUrlStep();
|
|
178
|
+
break;
|
|
179
|
+
case "auth-method":
|
|
180
|
+
this.#renderAuthMethodStep();
|
|
181
|
+
break;
|
|
182
|
+
case "oauth-error":
|
|
183
|
+
this.#renderOAuthErrorStep();
|
|
184
|
+
break;
|
|
185
|
+
case "oauth-auth-url":
|
|
186
|
+
this.#renderOAuthAuthUrlStep();
|
|
187
|
+
break;
|
|
188
|
+
case "oauth-token-url":
|
|
189
|
+
this.#renderOAuthTokenUrlStep();
|
|
190
|
+
break;
|
|
191
|
+
case "oauth-client-id":
|
|
192
|
+
this.#renderOAuthClientIdStep();
|
|
193
|
+
break;
|
|
194
|
+
case "oauth-client-secret":
|
|
195
|
+
this.#renderOAuthClientSecretStep();
|
|
196
|
+
break;
|
|
197
|
+
case "oauth-scopes":
|
|
198
|
+
this.#renderOAuthScopesStep();
|
|
199
|
+
break;
|
|
200
|
+
case "apikey":
|
|
201
|
+
this.#renderApiKeyStep();
|
|
202
|
+
break;
|
|
203
|
+
case "auth-location":
|
|
204
|
+
this.#renderAuthLocationStep();
|
|
205
|
+
break;
|
|
206
|
+
case "env-var-name":
|
|
207
|
+
this.#renderEnvVarNameStep();
|
|
208
|
+
break;
|
|
209
|
+
case "header-name":
|
|
210
|
+
this.#renderHeaderNameStep();
|
|
211
|
+
break;
|
|
212
|
+
case "scope":
|
|
213
|
+
this.#renderScopeStep();
|
|
214
|
+
break;
|
|
215
|
+
case "confirm":
|
|
216
|
+
this.#renderConfirmStep();
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#renderNameStep(): void {
|
|
222
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Step 1: Server Name")));
|
|
223
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
224
|
+
this.#contentContainer.addChild(new Text("Enter a unique name for this server:", 0, 0));
|
|
225
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
226
|
+
|
|
227
|
+
this.#inputField = new Input();
|
|
228
|
+
this.#inputField.setValue(this.#state.name);
|
|
229
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
230
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
231
|
+
|
|
232
|
+
// Show validation error if any
|
|
233
|
+
if (this.#validationError) {
|
|
234
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", `✗ ${sanitize(this.#validationError)}`), 0, 0));
|
|
235
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.#contentContainer.addChild(
|
|
239
|
+
new Text(theme.fg("muted", "[Only letters, numbers, dash, underscore, dot]"), 0, 0),
|
|
240
|
+
);
|
|
241
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to cancel]"), 0, 0));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#renderTransportStep(): void {
|
|
245
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Step 2: Transport Type")));
|
|
246
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
247
|
+
this.#contentContainer.addChild(new Text("Select the transport type:", 0, 0));
|
|
248
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
249
|
+
|
|
250
|
+
const options = [
|
|
251
|
+
{ value: "stdio" as const, label: "stdio (Local process)" },
|
|
252
|
+
{ value: "http" as const, label: "http (HTTP server)" },
|
|
253
|
+
{ value: "sse" as const, label: "sse (Server-Sent Events)" },
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
for (let i = 0; i < options.length; i++) {
|
|
257
|
+
const option = options[i];
|
|
258
|
+
const isSelected = i === this.#selectedIndex;
|
|
259
|
+
const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
260
|
+
const text = isSelected ? theme.fg("accent", option.label) : option.label;
|
|
261
|
+
this.#contentContainer.addChild(new Text(prefix + text, 0, 0));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
265
|
+
this.#contentContainer.addChild(
|
|
266
|
+
new Text(theme.fg("muted", "[↑↓ to navigate, Enter to select, Esc to cancel]"), 0, 0),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#renderCommandStep(): void {
|
|
271
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Step 3: Command")));
|
|
272
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
273
|
+
this.#contentContainer.addChild(new Text("Enter the command to run:", 0, 0));
|
|
274
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
275
|
+
|
|
276
|
+
this.#inputField = new Input();
|
|
277
|
+
this.#inputField.setValue(this.#state.command);
|
|
278
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
279
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
280
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to go back]"), 0, 0));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#renderArgsStep(): void {
|
|
284
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Step 4: Arguments (Optional)")));
|
|
285
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
286
|
+
this.#contentContainer.addChild(new Text("Enter command arguments (space-separated):", 0, 0));
|
|
287
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
288
|
+
|
|
289
|
+
this.#inputField = new Input();
|
|
290
|
+
this.#inputField.setValue(this.#state.args);
|
|
291
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
292
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
293
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Press Enter to skip or continue]"), 0, 0));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#renderUrlStep(): void {
|
|
297
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Step 3: Server URL")));
|
|
298
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
299
|
+
this.#contentContainer.addChild(new Text("Enter the server URL:", 0, 0));
|
|
300
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
301
|
+
|
|
302
|
+
this.#inputField = new Input();
|
|
303
|
+
this.#inputField.setValue(this.#state.url);
|
|
304
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
305
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
306
|
+
|
|
307
|
+
// Show validation error if any
|
|
308
|
+
if (this.#validationError) {
|
|
309
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", `✗ ${sanitize(this.#validationError)}`), 0, 0));
|
|
310
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Must start with http:// or https://]"), 0, 0));
|
|
314
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to go back]"), 0, 0));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#renderAuthLocationStep(): void {
|
|
318
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Step: How to provide the key?")));
|
|
319
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
320
|
+
|
|
321
|
+
const options = [
|
|
322
|
+
{ value: "env" as const, label: "Environment variable" },
|
|
323
|
+
{ value: "header" as const, label: "HTTP header" },
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
for (let i = 0; i < options.length; i++) {
|
|
327
|
+
const option = options[i];
|
|
328
|
+
const isSelected = i === this.#selectedIndex;
|
|
329
|
+
const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
330
|
+
const text = isSelected ? theme.fg("accent", option.label) : option.label;
|
|
331
|
+
this.#contentContainer.addChild(new Text(prefix + text, 0, 0));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
335
|
+
this.#contentContainer.addChild(
|
|
336
|
+
new Text(theme.fg("muted", "[↑↓ to navigate, Enter to select, Esc to go back]"), 0, 0),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#renderEnvVarNameStep(): void {
|
|
341
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Step: Environment Variable Name")));
|
|
342
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
343
|
+
this.#contentContainer.addChild(new Text("Enter the environment variable name:", 0, 0));
|
|
344
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
345
|
+
|
|
346
|
+
this.#inputField = new Input();
|
|
347
|
+
this.#inputField.setValue(this.#state.envVarName);
|
|
348
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
349
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
350
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to go back]"), 0, 0));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#renderHeaderNameStep(): void {
|
|
354
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Step: HTTP Header Name")));
|
|
355
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
356
|
+
this.#contentContainer.addChild(new Text("Enter the HTTP header name:", 0, 0));
|
|
357
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
358
|
+
|
|
359
|
+
this.#inputField = new Input();
|
|
360
|
+
this.#inputField.setValue(this.#state.headerName);
|
|
361
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
362
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
363
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to go back]"), 0, 0));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#renderScopeStep(): void {
|
|
367
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Step: Configuration Scope")));
|
|
368
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
369
|
+
|
|
370
|
+
const options = [
|
|
371
|
+
{ value: "user" as const, label: "User level (~/.omp/mcp.json)" },
|
|
372
|
+
{ value: "project" as const, label: "Project level (.omp/mcp.json)" },
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
for (let i = 0; i < options.length; i++) {
|
|
376
|
+
const option = options[i];
|
|
377
|
+
const isSelected = i === this.#selectedIndex;
|
|
378
|
+
const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
379
|
+
const text = isSelected ? theme.fg("accent", option.label) : option.label;
|
|
380
|
+
this.#contentContainer.addChild(new Text(prefix + text, 0, 0));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
384
|
+
this.#contentContainer.addChild(
|
|
385
|
+
new Text(theme.fg("muted", "[↑↓ to navigate, Enter to select, Esc to go back]"), 0, 0),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#renderConfirmStep(): void {
|
|
390
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Review Configuration")));
|
|
391
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
392
|
+
|
|
393
|
+
// Show summary
|
|
394
|
+
this.#contentContainer.addChild(new Text(`Name: ${theme.fg("accent", this.#state.name)}`, 0, 0));
|
|
395
|
+
this.#contentContainer.addChild(new Text(`Type: ${this.#state.transport}`, 0, 0));
|
|
396
|
+
|
|
397
|
+
if (this.#state.transport === "stdio") {
|
|
398
|
+
this.#contentContainer.addChild(new Text(`Command: ${this.#state.command}`, 0, 0));
|
|
399
|
+
if (this.#state.args) {
|
|
400
|
+
this.#contentContainer.addChild(new Text(`Args: ${this.#state.args}`, 0, 0));
|
|
401
|
+
}
|
|
402
|
+
} else {
|
|
403
|
+
this.#contentContainer.addChild(new Text(`URL: ${sanitize(this.#state.url)}`, 0, 0));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Auth info
|
|
407
|
+
if (this.#state.authMethod === "none") {
|
|
408
|
+
this.#contentContainer.addChild(new Text("Auth: None", 0, 0));
|
|
409
|
+
} else if (this.#state.authMethod === "oauth") {
|
|
410
|
+
this.#contentContainer.addChild(new Text("Auth: OAuth (authenticated)", 0, 0));
|
|
411
|
+
} else if (this.#state.authMethod === "manual") {
|
|
412
|
+
if (this.#state.authLocation === "env") {
|
|
413
|
+
this.#contentContainer.addChild(new Text(`Auth: API key via env (${this.#state.envVarName})`, 0, 0));
|
|
414
|
+
} else {
|
|
415
|
+
this.#contentContainer.addChild(new Text(`Auth: API key via header (${this.#state.headerName})`, 0, 0));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const scopeLabel = this.#state.scope === "user" ? "User level" : "Project level";
|
|
420
|
+
this.#contentContainer.addChild(new Text(`Scope: ${scopeLabel}`, 0, 0));
|
|
421
|
+
|
|
422
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
423
|
+
this.#contentContainer.addChild(new Text("Save this configuration?", 0, 0));
|
|
424
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
425
|
+
|
|
426
|
+
const options = ["Yes", "No"];
|
|
427
|
+
for (let i = 0; i < options.length; i++) {
|
|
428
|
+
const isSelected = i === this.#selectedIndex;
|
|
429
|
+
const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
430
|
+
const text = isSelected ? theme.fg("accent", options[i]) : options[i];
|
|
431
|
+
this.#contentContainer.addChild(new Text(prefix + text, 0, 0));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
435
|
+
this.#contentContainer.addChild(
|
|
436
|
+
new Text(theme.fg("muted", "[↑↓ to navigate, Enter to select, Esc to go back]"), 0, 0),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
handleInput(keyData: string): void {
|
|
441
|
+
// Handle Ctrl+C to cancel wizard immediately
|
|
442
|
+
if (keyData === "\x03") {
|
|
443
|
+
// Ctrl+C pressed - cancel wizard
|
|
444
|
+
this.#onCancelCallback();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Handle Escape (always handled by wizard)
|
|
449
|
+
if (matchesKey(keyData, "escape")) {
|
|
450
|
+
if (this.#currentStep === "name") {
|
|
451
|
+
// Cancel wizard
|
|
452
|
+
this.#onCancelCallback();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// Go back to previous step
|
|
456
|
+
this.#goBack();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// If we have an input field, let it handle the input
|
|
461
|
+
if (this.#inputField) {
|
|
462
|
+
// Handle Enter to proceed
|
|
463
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
464
|
+
this.#saveInputAndProceed();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
// Pass all other keys to the input field
|
|
468
|
+
this.#inputField.handleInput(keyData);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Selector steps - handle Enter
|
|
473
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
474
|
+
this.#selectCurrentOption();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Handle up/down arrows for selectors
|
|
479
|
+
if (matchesKey(keyData, "up")) {
|
|
480
|
+
this.#moveSelection(-1);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (matchesKey(keyData, "down")) {
|
|
484
|
+
this.#moveSelection(1);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#saveInputAndProceed(): void {
|
|
490
|
+
if (!this.#inputField) return;
|
|
491
|
+
|
|
492
|
+
const value = this.#inputField.getValue().trim();
|
|
493
|
+
|
|
494
|
+
switch (this.#currentStep) {
|
|
495
|
+
case "name": {
|
|
496
|
+
// Validate server name
|
|
497
|
+
const nameError = validateServerName(value);
|
|
498
|
+
if (nameError) {
|
|
499
|
+
this.#validationError = nameError;
|
|
500
|
+
this.#renderStep();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
this.#validationError = null;
|
|
504
|
+
this.#state.name = value;
|
|
505
|
+
this.#currentStep = "transport";
|
|
506
|
+
this.#selectedIndex = 0;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case "command":
|
|
510
|
+
if (!value) {
|
|
511
|
+
// Command is required
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
this.#state.command = value;
|
|
515
|
+
this.#currentStep = "args";
|
|
516
|
+
break;
|
|
517
|
+
case "args":
|
|
518
|
+
this.#state.args = value; // Optional
|
|
519
|
+
void this.#testConnectionAndDetectAuth();
|
|
520
|
+
return;
|
|
521
|
+
case "url": {
|
|
522
|
+
// Validate URL
|
|
523
|
+
if (!value) {
|
|
524
|
+
this.#validationError = "URL is required";
|
|
525
|
+
this.#renderStep();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
let parsedUrl: URL;
|
|
529
|
+
try {
|
|
530
|
+
parsedUrl = new URL(value);
|
|
531
|
+
} catch {
|
|
532
|
+
this.#validationError = "Invalid URL format (must start with http:// or https://)";
|
|
533
|
+
this.#renderStep();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
537
|
+
this.#validationError = "URL must use http:// or https:// scheme";
|
|
538
|
+
this.#renderStep();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
this.#validationError = null;
|
|
542
|
+
this.#state.url = value;
|
|
543
|
+
void this.#testConnectionAndDetectAuth();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
case "oauth-auth-url":
|
|
547
|
+
if (!value) return;
|
|
548
|
+
this.#state.oauthAuthUrl = value;
|
|
549
|
+
this.#currentStep = "oauth-token-url";
|
|
550
|
+
break;
|
|
551
|
+
case "oauth-token-url":
|
|
552
|
+
if (!value) return;
|
|
553
|
+
this.#state.oauthTokenUrl = value;
|
|
554
|
+
this.#currentStep = "oauth-client-id";
|
|
555
|
+
break;
|
|
556
|
+
case "oauth-client-id":
|
|
557
|
+
if (!value) return;
|
|
558
|
+
this.#state.oauthClientId = value;
|
|
559
|
+
this.#currentStep = "oauth-client-secret";
|
|
560
|
+
break;
|
|
561
|
+
case "oauth-client-secret":
|
|
562
|
+
this.#state.oauthClientSecret = value; // Optional
|
|
563
|
+
this.#currentStep = "oauth-scopes";
|
|
564
|
+
break;
|
|
565
|
+
case "oauth-scopes":
|
|
566
|
+
this.#state.oauthScopes = value; // Optional
|
|
567
|
+
// Launch OAuth flow
|
|
568
|
+
void this.#launchOAuthFlow();
|
|
569
|
+
return;
|
|
570
|
+
case "apikey":
|
|
571
|
+
if (!value) {
|
|
572
|
+
// API key is required
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
this.#state.apiKey = value;
|
|
576
|
+
// Determine auth location based on transport
|
|
577
|
+
if (this.#state.transport === "stdio") {
|
|
578
|
+
this.#currentStep = "env-var-name";
|
|
579
|
+
} else {
|
|
580
|
+
this.#currentStep = "auth-location";
|
|
581
|
+
this.#selectedIndex = 0;
|
|
582
|
+
}
|
|
583
|
+
break;
|
|
584
|
+
case "env-var-name":
|
|
585
|
+
if (!value) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
this.#state.envVarName = value;
|
|
589
|
+
this.#state.authLocation = "env";
|
|
590
|
+
this.#currentStep = "scope";
|
|
591
|
+
this.#selectedIndex = 0;
|
|
592
|
+
break;
|
|
593
|
+
case "header-name":
|
|
594
|
+
if (!value) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
this.#state.headerName = value;
|
|
598
|
+
this.#state.authLocation = "header";
|
|
599
|
+
this.#currentStep = "scope";
|
|
600
|
+
this.#selectedIndex = 0;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
this.#inputField = null;
|
|
605
|
+
this.#renderStep();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
#selectCurrentOption(): void {
|
|
609
|
+
switch (this.#currentStep) {
|
|
610
|
+
case "transport": {
|
|
611
|
+
const transports: TransportType[] = ["stdio", "http", "sse"];
|
|
612
|
+
this.#state.transport = transports[this.#selectedIndex];
|
|
613
|
+
this.#currentStep = this.#state.transport === "stdio" ? "command" : "url";
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
case "auth-method": {
|
|
617
|
+
const authMethods: Array<"oauth" | "manual"> = ["oauth", "manual"];
|
|
618
|
+
this.#state.authMethod = authMethods[this.#selectedIndex];
|
|
619
|
+
if (this.#state.authMethod === "oauth") {
|
|
620
|
+
this.#currentStep = "oauth-auth-url";
|
|
621
|
+
} else {
|
|
622
|
+
// manual
|
|
623
|
+
this.#currentStep = "apikey";
|
|
624
|
+
}
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
case "oauth-error":
|
|
628
|
+
if (this.#selectedIndex === 0) {
|
|
629
|
+
void this.#launchOAuthFlow();
|
|
630
|
+
} else {
|
|
631
|
+
this.#currentStep = "oauth-auth-url";
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
case "auth-location": {
|
|
635
|
+
const authLocations: Array<"env" | "header"> = ["env", "header"];
|
|
636
|
+
this.#state.authLocation = authLocations[this.#selectedIndex];
|
|
637
|
+
if (this.#state.authLocation === "env") {
|
|
638
|
+
this.#currentStep = "env-var-name";
|
|
639
|
+
} else {
|
|
640
|
+
this.#currentStep = "header-name";
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
case "scope": {
|
|
645
|
+
const scopes: Scope[] = ["user", "project"];
|
|
646
|
+
this.#state.scope = scopes[this.#selectedIndex];
|
|
647
|
+
// Auto-save once scope is selected.
|
|
648
|
+
this.#complete();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
this.#renderStep();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
#moveSelection(delta: number): void {
|
|
657
|
+
const maxIndex = this.#getMaxIndexForCurrentStep();
|
|
658
|
+
this.#selectedIndex = (this.#selectedIndex + delta + maxIndex + 1) % (maxIndex + 1);
|
|
659
|
+
this.#renderStep();
|
|
660
|
+
this.#requestRender();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
#getMaxIndexForCurrentStep(): number {
|
|
664
|
+
switch (this.#currentStep) {
|
|
665
|
+
case "transport":
|
|
666
|
+
return 2; // 3 options
|
|
667
|
+
case "auth-method":
|
|
668
|
+
return 1; // 2 options
|
|
669
|
+
case "oauth-error":
|
|
670
|
+
return 1; // 2 options
|
|
671
|
+
case "auth-location":
|
|
672
|
+
return 1; // 2 options
|
|
673
|
+
case "scope":
|
|
674
|
+
return 1; // 2 options
|
|
675
|
+
case "confirm":
|
|
676
|
+
return 1; // 2 options
|
|
677
|
+
default:
|
|
678
|
+
return 0;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
#goBack(): void {
|
|
683
|
+
// Navigate to previous step
|
|
684
|
+
switch (this.#currentStep) {
|
|
685
|
+
case "transport":
|
|
686
|
+
this.#currentStep = "name";
|
|
687
|
+
break;
|
|
688
|
+
case "command":
|
|
689
|
+
case "url":
|
|
690
|
+
this.#currentStep = "transport";
|
|
691
|
+
this.#selectedIndex = this.#state.transport === "stdio" ? 0 : this.#state.transport === "http" ? 1 : 2;
|
|
692
|
+
break;
|
|
693
|
+
case "args":
|
|
694
|
+
this.#currentStep = "command";
|
|
695
|
+
break;
|
|
696
|
+
case "auth-method":
|
|
697
|
+
// Go back to url or args depending on transport
|
|
698
|
+
if (this.#state.transport === "stdio") {
|
|
699
|
+
this.#currentStep = "args";
|
|
700
|
+
} else {
|
|
701
|
+
this.#currentStep = "url";
|
|
702
|
+
}
|
|
703
|
+
break;
|
|
704
|
+
case "oauth-auth-url":
|
|
705
|
+
case "apikey":
|
|
706
|
+
// Go back to transport-specific connection step
|
|
707
|
+
if (this.#state.transport === "stdio") {
|
|
708
|
+
this.#currentStep = "args";
|
|
709
|
+
} else {
|
|
710
|
+
this.#currentStep = "url";
|
|
711
|
+
}
|
|
712
|
+
break;
|
|
713
|
+
case "auth-location":
|
|
714
|
+
// Go back to API key input
|
|
715
|
+
this.#currentStep = "apikey";
|
|
716
|
+
break;
|
|
717
|
+
case "env-var-name":
|
|
718
|
+
case "header-name":
|
|
719
|
+
// Go back to auth location selection (for HTTP) or directly to apikey (for stdio)
|
|
720
|
+
if (this.#state.transport === "stdio") {
|
|
721
|
+
this.#currentStep = "apikey";
|
|
722
|
+
} else {
|
|
723
|
+
this.#currentStep = "auth-location";
|
|
724
|
+
this.#selectedIndex = this.#state.authLocation === "env" ? 0 : 1;
|
|
725
|
+
}
|
|
726
|
+
break;
|
|
727
|
+
case "oauth-token-url":
|
|
728
|
+
case "oauth-client-id":
|
|
729
|
+
case "oauth-client-secret":
|
|
730
|
+
case "oauth-scopes":
|
|
731
|
+
// Go back through OAuth flow
|
|
732
|
+
if (this.#currentStep === "oauth-token-url") {
|
|
733
|
+
this.#currentStep = "oauth-auth-url";
|
|
734
|
+
} else if (this.#currentStep === "oauth-client-id") {
|
|
735
|
+
this.#currentStep = "oauth-token-url";
|
|
736
|
+
} else if (this.#currentStep === "oauth-client-secret") {
|
|
737
|
+
this.#currentStep = "oauth-client-id";
|
|
738
|
+
} else if (this.#currentStep === "oauth-scopes") {
|
|
739
|
+
this.#currentStep = "oauth-client-secret";
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
case "scope":
|
|
743
|
+
// Go back to last authentication step
|
|
744
|
+
if (this.#state.authMethod === "oauth") {
|
|
745
|
+
this.#currentStep = "oauth-scopes";
|
|
746
|
+
} else {
|
|
747
|
+
// manual - go back to env var name or header name
|
|
748
|
+
if (this.#state.authLocation === "env") {
|
|
749
|
+
this.#currentStep = "env-var-name";
|
|
750
|
+
} else {
|
|
751
|
+
this.#currentStep = "header-name";
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
break;
|
|
755
|
+
case "oauth-error":
|
|
756
|
+
this.#currentStep = "oauth-auth-url";
|
|
757
|
+
break;
|
|
758
|
+
case "confirm":
|
|
759
|
+
this.#currentStep = "scope";
|
|
760
|
+
this.#selectedIndex = this.#state.scope === "user" ? 0 : 1;
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
this.#renderStep();
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
#renderAuthMethodStep(): void {
|
|
768
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Step: Authentication Method")));
|
|
769
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
770
|
+
|
|
771
|
+
const options = [
|
|
772
|
+
{ value: "oauth" as const, label: "OAuth flow (web-based)", desc: "(opens browser)" },
|
|
773
|
+
{ value: "manual" as const, label: "Manual API key/token", desc: "(paste or use shell command)" },
|
|
774
|
+
];
|
|
775
|
+
|
|
776
|
+
for (let i = 0; i < options.length; i++) {
|
|
777
|
+
const option = options[i];
|
|
778
|
+
const isSelected = i === this.#selectedIndex;
|
|
779
|
+
const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
780
|
+
const text = isSelected ? theme.fg("accent", option.label) : option.label;
|
|
781
|
+
this.#contentContainer.addChild(new Text(prefix + text, 0, 0));
|
|
782
|
+
if (!isSelected) {
|
|
783
|
+
this.#contentContainer.addChild(new Text(` ${theme.fg("dim", option.desc)}`, 0, 0));
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
788
|
+
this.#contentContainer.addChild(
|
|
789
|
+
new Text(theme.fg("muted", "[↑↓ to navigate, Enter to select, Esc to go back]"), 0, 0),
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
#renderOAuthAuthUrlStep(): void {
|
|
794
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "OAuth: Authorization URL")));
|
|
795
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
796
|
+
this.#contentContainer.addChild(new Text("Enter the OAuth authorization endpoint:", 0, 0));
|
|
797
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
798
|
+
|
|
799
|
+
this.#inputField = new Input();
|
|
800
|
+
this.#inputField.setValue(this.#state.oauthAuthUrl);
|
|
801
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
802
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
803
|
+
this.#contentContainer.addChild(
|
|
804
|
+
new Text(theme.fg("muted", "e.g., https://auth.example.com/oauth/authorize"), 0, 0),
|
|
805
|
+
);
|
|
806
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
807
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to go back]"), 0, 0));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
#renderOAuthTokenUrlStep(): void {
|
|
811
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "OAuth: Token URL")));
|
|
812
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
813
|
+
this.#contentContainer.addChild(new Text("Enter the OAuth token endpoint:", 0, 0));
|
|
814
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
815
|
+
|
|
816
|
+
this.#inputField = new Input();
|
|
817
|
+
this.#inputField.setValue(this.#state.oauthTokenUrl);
|
|
818
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
819
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
820
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "e.g., https://auth.example.com/oauth/token"), 0, 0));
|
|
821
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
822
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to go back]"), 0, 0));
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
#renderOAuthClientIdStep(): void {
|
|
826
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "OAuth: Client ID")));
|
|
827
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
828
|
+
this.#contentContainer.addChild(new Text("Enter your OAuth client ID:", 0, 0));
|
|
829
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
830
|
+
|
|
831
|
+
this.#inputField = new Input();
|
|
832
|
+
this.#inputField.setValue(this.#state.oauthClientId);
|
|
833
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
834
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
835
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to go back]"), 0, 0));
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
#renderOAuthClientSecretStep(): void {
|
|
839
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "OAuth: Client Secret (Optional)")));
|
|
840
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
841
|
+
this.#contentContainer.addChild(new Text("Enter your OAuth client secret:", 0, 0));
|
|
842
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "(Leave empty for PKCE-only flows)"), 0, 0));
|
|
843
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
844
|
+
|
|
845
|
+
this.#inputField = new Input();
|
|
846
|
+
this.#inputField.setValue(this.#state.oauthClientSecret);
|
|
847
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
848
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
849
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to go back]"), 0, 0));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
#renderOAuthScopesStep(): void {
|
|
853
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "OAuth: Scopes (Optional)")));
|
|
854
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
855
|
+
this.#contentContainer.addChild(new Text("Enter OAuth scopes (space-separated):", 0, 0));
|
|
856
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
857
|
+
|
|
858
|
+
this.#inputField = new Input();
|
|
859
|
+
this.#inputField.setValue(this.#state.oauthScopes);
|
|
860
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
861
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
862
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "e.g., read write"), 0, 0));
|
|
863
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
864
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to go back]"), 0, 0));
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
#renderOAuthErrorStep(): void {
|
|
868
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", "OAuth authentication failed"), 0, 0));
|
|
869
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
870
|
+
this.#contentContainer.addChild(new Text("Choose next action:", 0, 0));
|
|
871
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
872
|
+
|
|
873
|
+
const options = ["Retry OAuth authentication", "Edit OAuth settings"];
|
|
874
|
+
for (let i = 0; i < options.length; i++) {
|
|
875
|
+
const isSelected = i === this.#selectedIndex;
|
|
876
|
+
const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
877
|
+
const text = isSelected ? theme.fg("accent", options[i]) : options[i];
|
|
878
|
+
this.#contentContainer.addChild(new Text(prefix + text, 0, 0));
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
882
|
+
this.#contentContainer.addChild(
|
|
883
|
+
new Text(theme.fg("muted", "[↑↓ to navigate, Enter to select, Esc to go back]"), 0, 0),
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
#renderApiKeyStep(): void {
|
|
888
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "API Key Required")));
|
|
889
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
890
|
+
this.#contentContainer.addChild(new Text("Enter your API key or token:", 0, 0));
|
|
891
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "(Supports !command for password manager)"), 0, 0));
|
|
892
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
893
|
+
|
|
894
|
+
this.#inputField = new Input();
|
|
895
|
+
this.#inputField.setValue(this.#state.apiKey);
|
|
896
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
897
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
898
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to go back]"), 0, 0));
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Test connection and automatically detect if auth is needed.
|
|
903
|
+
*/
|
|
904
|
+
async #testConnectionAndDetectAuth(): Promise<void> {
|
|
905
|
+
const testConfig = this.#buildServerConfig();
|
|
906
|
+
|
|
907
|
+
if (!this.#onTestConnectionCallback) {
|
|
908
|
+
// Skip test, go to scope
|
|
909
|
+
this.#currentStep = "scope";
|
|
910
|
+
this.#selectedIndex = 0;
|
|
911
|
+
this.#renderStep();
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
try {
|
|
916
|
+
// Try to connect - timeout is handled by the transport layer (5 seconds)
|
|
917
|
+
await this.#onTestConnectionCallback(testConfig);
|
|
918
|
+
|
|
919
|
+
// Success! No auth required
|
|
920
|
+
this.#contentContainer.clear();
|
|
921
|
+
this.#contentContainer.addChild(new Text(theme.fg("success", "✓ Connection successful!"), 0, 0));
|
|
922
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
923
|
+
this.#contentContainer.addChild(new Text("No authentication required", 0, 0));
|
|
924
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
925
|
+
|
|
926
|
+
setTimeout(() => {
|
|
927
|
+
this.#state.authMethod = "none";
|
|
928
|
+
this.#currentStep = "scope";
|
|
929
|
+
this.#selectedIndex = 0;
|
|
930
|
+
this.#renderStep();
|
|
931
|
+
}, 1000);
|
|
932
|
+
} catch (error) {
|
|
933
|
+
// Connection failed - check if it's an auth error
|
|
934
|
+
const authResult = analyzeAuthError(error as Error);
|
|
935
|
+
|
|
936
|
+
if (authResult.requiresAuth) {
|
|
937
|
+
// Prefer OAuth first: use error metadata, then well-known discovery fallback.
|
|
938
|
+
let oauth = authResult.authType === "oauth" ? (authResult.oauth ?? null) : null;
|
|
939
|
+
if (!oauth && this.#state.transport !== "stdio" && this.#state.url) {
|
|
940
|
+
try {
|
|
941
|
+
oauth = await discoverOAuthEndpoints(this.#state.url);
|
|
942
|
+
} catch {
|
|
943
|
+
// Ignore discovery failures and fallback to manual auth.
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (oauth) {
|
|
948
|
+
this.#state.oauthAuthUrl = oauth.authorizationUrl;
|
|
949
|
+
this.#state.oauthTokenUrl = oauth.tokenUrl;
|
|
950
|
+
this.#state.oauthClientId = oauth.clientId || "";
|
|
951
|
+
this.#state.oauthScopes = oauth.scopes || "";
|
|
952
|
+
this.#state.authMethod = "oauth";
|
|
953
|
+
|
|
954
|
+
this.#contentContainer.clear();
|
|
955
|
+
this.#contentContainer.addChild(new Text(theme.fg("success", "✓ OAuth detected"), 0, 0));
|
|
956
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
957
|
+
this.#contentContainer.addChild(new Text("Launching browser for authorization...", 0, 0));
|
|
958
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
959
|
+
|
|
960
|
+
void this.#launchOAuthFlow();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// OAuth metadata unavailable: fallback to manual API key.
|
|
965
|
+
this.#contentContainer.clear();
|
|
966
|
+
this.#contentContainer.addChild(new Text(theme.fg("warning", "⚠ Authentication required"), 0, 0));
|
|
967
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
968
|
+
this.#contentContainer.addChild(new Text("OAuth parameters could not be discovered.", 0, 0));
|
|
969
|
+
this.#contentContainer.addChild(new Text("Provide API key/token manually.", 0, 0));
|
|
970
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
971
|
+
this.#currentStep = "apikey";
|
|
972
|
+
this.#renderStep();
|
|
973
|
+
} else {
|
|
974
|
+
// Not an auth error - just a connection failure
|
|
975
|
+
const errorMsg = sanitize(error instanceof Error ? error.message : String(error));
|
|
976
|
+
this.#contentContainer.clear();
|
|
977
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", "✗ Connection failed"), 0, 0));
|
|
978
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
979
|
+
this.#contentContainer.addChild(new Text(errorMsg, 0, 0));
|
|
980
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
981
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "Adding server anyway..."), 0, 0));
|
|
982
|
+
|
|
983
|
+
setTimeout(() => {
|
|
984
|
+
this.#state.authMethod = "none";
|
|
985
|
+
this.#currentStep = "scope";
|
|
986
|
+
this.#selectedIndex = 0;
|
|
987
|
+
this.#renderStep();
|
|
988
|
+
}, 2000);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Build a server config from current wizard state for connection testing (no auth).
|
|
995
|
+
*/
|
|
996
|
+
#buildServerConfig(): MCPServerConfig {
|
|
997
|
+
return this.#buildServerConfigWithAuth(false);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
#buildServerConfigWithAuth(includeAuth: boolean): MCPServerConfig {
|
|
1001
|
+
const transport = this.#state.transport ?? "stdio";
|
|
1002
|
+
|
|
1003
|
+
if (transport === "stdio") {
|
|
1004
|
+
const config: MCPStdioServerConfig = {
|
|
1005
|
+
type: "stdio",
|
|
1006
|
+
command: this.#state.command,
|
|
1007
|
+
timeout: 5000,
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
if (this.#state.args) {
|
|
1011
|
+
config.args = this.#state.args.split(/\s+/).filter(Boolean);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (includeAuth && this.#state.authMethod === "oauth" && this.#state.oauthCredentialId) {
|
|
1015
|
+
config.auth = {
|
|
1016
|
+
type: "oauth",
|
|
1017
|
+
credentialId: this.#state.oauthCredentialId,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (includeAuth && this.#state.authMethod === "manual" && this.#state.apiKey) {
|
|
1022
|
+
config.env = {
|
|
1023
|
+
...(config.env ?? {}),
|
|
1024
|
+
[this.#state.envVarName || "API_KEY"]: this.#state.apiKey,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return config;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// http or sse
|
|
1032
|
+
const config: MCPHttpServerConfig | MCPSseServerConfig = {
|
|
1033
|
+
type: transport,
|
|
1034
|
+
url: this.#state.url,
|
|
1035
|
+
timeout: 5000,
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
if (includeAuth && this.#state.authMethod === "oauth" && this.#state.oauthCredentialId) {
|
|
1039
|
+
config.auth = {
|
|
1040
|
+
type: "oauth",
|
|
1041
|
+
credentialId: this.#state.oauthCredentialId,
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (includeAuth && this.#state.authMethod === "manual" && this.#state.apiKey) {
|
|
1046
|
+
if (this.#state.authLocation === "env") {
|
|
1047
|
+
// For HTTP with env location, store in headers using the env var name as-is
|
|
1048
|
+
config.headers = {
|
|
1049
|
+
...(config.headers ?? {}),
|
|
1050
|
+
[this.#state.headerName || "Authorization"]: this.#state.apiKey,
|
|
1051
|
+
};
|
|
1052
|
+
} else {
|
|
1053
|
+
const headerName = this.#state.headerName || "Authorization";
|
|
1054
|
+
config.headers = {
|
|
1055
|
+
...(config.headers ?? {}),
|
|
1056
|
+
[headerName]: this.#state.apiKey,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return config;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async #launchOAuthFlow(): Promise<void> {
|
|
1065
|
+
if (!this.#onOAuthCallback) {
|
|
1066
|
+
this.#contentContainer.clear();
|
|
1067
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", "OAuth flow not available"), 0, 0));
|
|
1068
|
+
this.#renderStep();
|
|
1069
|
+
this.#requestRender();
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Validate OAuth configuration
|
|
1074
|
+
if (!this.#state.oauthAuthUrl || !this.#state.oauthTokenUrl) {
|
|
1075
|
+
this.#contentContainer.clear();
|
|
1076
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", "OAuth configuration incomplete"), 0, 0));
|
|
1077
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1078
|
+
this.#contentContainer.addChild(new Text("Authorization and Token URLs are required.", 0, 0));
|
|
1079
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1080
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Press Esc to go back]"), 0, 0));
|
|
1081
|
+
this.#requestRender();
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Show "Authenticating..." message
|
|
1086
|
+
this.#contentContainer.clear();
|
|
1087
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "OAuth Authentication"), 0, 0));
|
|
1088
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1089
|
+
this.#contentContainer.addChild(new Text("Launching OAuth flow...", 0, 0));
|
|
1090
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "Browser will open automatically."), 0, 0));
|
|
1091
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1092
|
+
this.#contentContainer.addChild(
|
|
1093
|
+
new Text(theme.fg("warning", "If browser doesn't open, copy the URL from chat."), 0, 0),
|
|
1094
|
+
);
|
|
1095
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1096
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "(Press Esc to cancel)"), 0, 0));
|
|
1097
|
+
this.#requestRender();
|
|
1098
|
+
|
|
1099
|
+
try {
|
|
1100
|
+
// Call OAuth handler
|
|
1101
|
+
const credentialId = await this.#onOAuthCallback(
|
|
1102
|
+
this.#state.oauthAuthUrl,
|
|
1103
|
+
this.#state.oauthTokenUrl,
|
|
1104
|
+
this.#state.oauthClientId,
|
|
1105
|
+
this.#state.oauthClientSecret,
|
|
1106
|
+
this.#state.oauthScopes,
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
// Store credential ID
|
|
1110
|
+
this.#state.oauthCredentialId = credentialId;
|
|
1111
|
+
|
|
1112
|
+
// Show success message
|
|
1113
|
+
this.#contentContainer.clear();
|
|
1114
|
+
this.#contentContainer.addChild(new Text(theme.fg("success", "✓ Authentication successful!"), 0, 0));
|
|
1115
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1116
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "Running connection health check..."), 0, 0));
|
|
1117
|
+
const healthText = new Text(theme.fg("muted", "| Checking server connection..."), 0, 0);
|
|
1118
|
+
this.#contentContainer.addChild(healthText);
|
|
1119
|
+
|
|
1120
|
+
const spinnerFrames = ["|", "/", "-", "\\"];
|
|
1121
|
+
let spinnerIndex = 0;
|
|
1122
|
+
const spinner = setInterval(() => {
|
|
1123
|
+
healthText.setText(
|
|
1124
|
+
theme.fg("muted", `${spinnerFrames[spinnerIndex % spinnerFrames.length]} Checking server connection...`),
|
|
1125
|
+
);
|
|
1126
|
+
spinnerIndex++;
|
|
1127
|
+
this.#requestRender();
|
|
1128
|
+
}, 120);
|
|
1129
|
+
|
|
1130
|
+
let healthPassed = true;
|
|
1131
|
+
let healthError = "";
|
|
1132
|
+
if (this.#onTestConnectionCallback) {
|
|
1133
|
+
try {
|
|
1134
|
+
const { promise: timeoutPromise, reject: timeoutReject } = Promise.withResolvers<never>();
|
|
1135
|
+
const timer = setTimeout(
|
|
1136
|
+
() => timeoutReject(new Error("Health check timed out after 10 seconds")),
|
|
1137
|
+
10_000,
|
|
1138
|
+
);
|
|
1139
|
+
try {
|
|
1140
|
+
await Promise.race([
|
|
1141
|
+
this.#onTestConnectionCallback(this.#buildServerConfigWithAuth(true)),
|
|
1142
|
+
timeoutPromise,
|
|
1143
|
+
]);
|
|
1144
|
+
} finally {
|
|
1145
|
+
clearTimeout(timer);
|
|
1146
|
+
}
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
healthPassed = false;
|
|
1149
|
+
healthError = sanitize(error instanceof Error ? error.message : String(error));
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
clearInterval(spinner);
|
|
1154
|
+
if (healthPassed) {
|
|
1155
|
+
healthText.setText(theme.fg("success", "✓ Health check passed"));
|
|
1156
|
+
} else {
|
|
1157
|
+
healthText.setText(theme.fg("warning", "⚠ Health check failed (will still save config)"));
|
|
1158
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1159
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", healthError), 0, 0));
|
|
1160
|
+
}
|
|
1161
|
+
this.#requestRender();
|
|
1162
|
+
|
|
1163
|
+
// Move to scope selection after short delay
|
|
1164
|
+
setTimeout(
|
|
1165
|
+
() => {
|
|
1166
|
+
this.#currentStep = "scope";
|
|
1167
|
+
this.#selectedIndex = 0;
|
|
1168
|
+
this.#renderStep();
|
|
1169
|
+
this.#requestRender();
|
|
1170
|
+
},
|
|
1171
|
+
healthPassed ? 1000 : 2000,
|
|
1172
|
+
);
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
// Show error with options to retry or go back
|
|
1175
|
+
const errorMsg = sanitize(error instanceof Error ? error.message : String(error));
|
|
1176
|
+
this.#contentContainer.clear();
|
|
1177
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", "✗ OAuth authentication failed"), 0, 0));
|
|
1178
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1179
|
+
this.#contentContainer.addChild(new Text(errorMsg, 0, 0));
|
|
1180
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1181
|
+
|
|
1182
|
+
// Provide helpful tips based on error type
|
|
1183
|
+
if (errorMsg.includes("timeout") || errorMsg.includes("timed out")) {
|
|
1184
|
+
this.#contentContainer.addChild(
|
|
1185
|
+
new Text(theme.fg("muted", "Tip: Complete authorization faster next time"), 0, 0),
|
|
1186
|
+
);
|
|
1187
|
+
} else if (errorMsg.includes("Invalid OAuth URLs")) {
|
|
1188
|
+
this.#contentContainer.addChild(
|
|
1189
|
+
new Text(theme.fg("muted", "Tip: Check that the OAuth URLs are correct"), 0, 0),
|
|
1190
|
+
);
|
|
1191
|
+
} else if (errorMsg.includes("ECONNREFUSED")) {
|
|
1192
|
+
this.#contentContainer.addChild(
|
|
1193
|
+
new Text(theme.fg("muted", "Tip: Verify the OAuth server is accessible"), 0, 0),
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1198
|
+
this.#contentContainer.addChild(new Text(`${theme.fg("accent", "→ ")}Retry`, 0, 0));
|
|
1199
|
+
this.#contentContainer.addChild(new Text(" Edit OAuth settings", 0, 0));
|
|
1200
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1201
|
+
this.#contentContainer.addChild(
|
|
1202
|
+
new Text(theme.fg("muted", "[↑↓ to navigate, Enter to select, Esc to go back]"), 0, 0),
|
|
1203
|
+
);
|
|
1204
|
+
this.#requestRender();
|
|
1205
|
+
|
|
1206
|
+
// Set up as a selector step
|
|
1207
|
+
this.#selectedIndex = 0;
|
|
1208
|
+
this.#currentStep = "oauth-error";
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
#complete(): void {
|
|
1213
|
+
if (!this.#state.scope) return;
|
|
1214
|
+
|
|
1215
|
+
// Build the config
|
|
1216
|
+
const config: MCPServerConfig = this.#buildConfig();
|
|
1217
|
+
|
|
1218
|
+
// Call completion callback
|
|
1219
|
+
this.#onCompleteCallback(this.#state.name, config, this.#state.scope);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
#buildConfig(): MCPServerConfig {
|
|
1223
|
+
if (this.#state.transport === "stdio") {
|
|
1224
|
+
const config: MCPStdioServerConfig = {
|
|
1225
|
+
type: "stdio",
|
|
1226
|
+
command: this.#state.command,
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
if (this.#state.args) {
|
|
1230
|
+
config.args = this.#state.args.split(/\s+/).filter(Boolean);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Add OAuth auth if configured
|
|
1234
|
+
if (this.#state.authMethod === "oauth" && this.#state.oauthCredentialId) {
|
|
1235
|
+
config.auth = {
|
|
1236
|
+
type: "oauth",
|
|
1237
|
+
credentialId: this.#state.oauthCredentialId,
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// Add API key to env if manual auth — use user-chosen env var name
|
|
1242
|
+
if (this.#state.authMethod === "manual" && this.#state.apiKey) {
|
|
1243
|
+
const envKey = this.#state.envVarName || "API_KEY";
|
|
1244
|
+
config.env = {
|
|
1245
|
+
[envKey]: this.#state.apiKey,
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
return config;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// HTTP or SSE — use concrete type
|
|
1253
|
+
const config: MCPHttpServerConfig | MCPSseServerConfig = {
|
|
1254
|
+
type: this.#state.transport!,
|
|
1255
|
+
url: this.#state.url,
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
// Add OAuth auth if configured
|
|
1259
|
+
if (this.#state.authMethod === "oauth" && this.#state.oauthCredentialId) {
|
|
1260
|
+
config.auth = {
|
|
1261
|
+
type: "oauth",
|
|
1262
|
+
credentialId: this.#state.oauthCredentialId,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Add API key using user-chosen header name and auth location
|
|
1267
|
+
if (this.#state.authMethod === "manual" && this.#state.apiKey) {
|
|
1268
|
+
if (this.#state.authLocation === "env") {
|
|
1269
|
+
// Env-based auth for HTTP: store the key in env on the config
|
|
1270
|
+
// HTTP/SSE configs don't have an env field, so use headers as carrier
|
|
1271
|
+
const headerName = this.#state.headerName || "Authorization";
|
|
1272
|
+
config.headers = {
|
|
1273
|
+
[headerName]: this.#state.apiKey,
|
|
1274
|
+
};
|
|
1275
|
+
} else {
|
|
1276
|
+
// Header-based auth: use the user's chosen header name
|
|
1277
|
+
const headerName = this.#state.headerName || "Authorization";
|
|
1278
|
+
config.headers = {
|
|
1279
|
+
[headerName]: this.#state.apiKey,
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
return config;
|
|
1285
|
+
}
|
|
1286
|
+
}
|