pi-soly 1.9.3 → 1.11.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/ask/index.ts +12 -11
- package/ask/picker.ts +356 -76
- package/ask/prompt.ts +6 -2
- package/ask/tests/picker.test.ts +273 -82
- package/codemap.ts +276 -0
- package/hotreload.ts +239 -0
- package/init.ts +302 -0
- package/mcp/CHANGELOG.md +384 -0
- package/mcp/LICENSE +21 -0
- package/mcp/OAUTH.md +355 -0
- package/mcp/README.md +410 -0
- package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
- package/mcp/__tests__/cli.upstream-test.ts +97 -0
- package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
- package/mcp/__tests__/config.upstream-test.ts +303 -0
- package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
- package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
- package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
- package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
- package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
- package/mcp/__tests__/errors.upstream-test.ts +218 -0
- package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
- package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
- package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
- package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
- package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
- package/mcp/__tests__/logger.upstream-test.ts +175 -0
- package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
- package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
- package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
- package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
- package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
- package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
- package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
- package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
- package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
- package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
- package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
- package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
- package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
- package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
- package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
- package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
- package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
- package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
- package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
- package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
- package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
- package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
- package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
- package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
- package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
- package/mcp/agent-dir.ts +20 -0
- package/mcp/app-bridge.bundle.js +67 -0
- package/mcp/cli.js +184 -0
- package/mcp/commands.ts +422 -0
- package/mcp/config.ts +666 -0
- package/mcp/consent-manager.ts +64 -0
- package/mcp/direct-tools.ts +439 -0
- package/mcp/elicitation-handler.ts +347 -0
- package/mcp/errors.ts +219 -0
- package/mcp/glimpse-ui.ts +80 -0
- package/mcp/host-html-template.ts +427 -0
- package/mcp/index.ts +362 -0
- package/mcp/init.ts +362 -0
- package/mcp/lifecycle.ts +93 -0
- package/mcp/logger.ts +169 -0
- package/mcp/mcp-auth-flow.ts +559 -0
- package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
- package/mcp/mcp-auth.ts +302 -0
- package/mcp/mcp-auth.upstream-test.ts +373 -0
- package/mcp/mcp-callback-server.ts +372 -0
- package/mcp/mcp-callback-server.upstream-test.ts +416 -0
- package/mcp/mcp-oauth-provider.ts +369 -0
- package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
- package/mcp/mcp-panel.ts +829 -0
- package/mcp/mcp-setup-panel.ts +580 -0
- package/mcp/metadata-cache.ts +201 -0
- package/mcp/notify.ts +111 -0
- package/mcp/npx-resolver.ts +424 -0
- package/mcp/oauth-handler.ts +57 -0
- package/mcp/onboarding-state.ts +68 -0
- package/mcp/package.json +106 -0
- package/mcp/panel-keys.ts +37 -0
- package/mcp/proxy-modes.ts +949 -0
- package/mcp/resource-tools.ts +17 -0
- package/mcp/sampling-handler.ts +268 -0
- package/mcp/server-manager.ts +545 -0
- package/mcp/state.ts +41 -0
- package/mcp/tool-metadata.ts +216 -0
- package/mcp/tool-registrar.ts +46 -0
- package/mcp/tool-result-renderer.ts +161 -0
- package/mcp/types.ts +448 -0
- package/mcp/ui-resource-handler.ts +146 -0
- package/mcp/ui-server.ts +623 -0
- package/mcp/ui-session.ts +386 -0
- package/mcp/ui-stream-types.ts +89 -0
- package/mcp/utils.ts +129 -0
- package/mcp/vitest.config.ts +14 -0
- package/migrate.ts +258 -0
- package/notification.ts +218 -0
- package/notifications-log.ts +83 -0
- package/package.json +20 -3
- package/status.ts +140 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
authenticate: vi.fn(),
|
|
5
|
+
supportsOAuth: vi.fn(),
|
|
6
|
+
lazyConnect: vi.fn(),
|
|
7
|
+
updateServerMetadata: vi.fn(),
|
|
8
|
+
updateMetadataCache: vi.fn(),
|
|
9
|
+
getFailureAgeSeconds: vi.fn(),
|
|
10
|
+
updateStatusBar: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("../mcp-auth-flow.ts", () => ({
|
|
14
|
+
authenticate: mocks.authenticate,
|
|
15
|
+
supportsOAuth: mocks.supportsOAuth,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("../init.ts", () => ({
|
|
19
|
+
lazyConnect: mocks.lazyConnect,
|
|
20
|
+
updateServerMetadata: mocks.updateServerMetadata,
|
|
21
|
+
updateMetadataCache: mocks.updateMetadataCache,
|
|
22
|
+
getFailureAgeSeconds: mocks.getFailureAgeSeconds,
|
|
23
|
+
updateStatusBar: mocks.updateStatusBar,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
describe("proxy auto auth", () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.resetModules();
|
|
29
|
+
mocks.authenticate.mockReset().mockResolvedValue("authenticated");
|
|
30
|
+
mocks.supportsOAuth.mockReset().mockReturnValue(true);
|
|
31
|
+
mocks.lazyConnect.mockReset().mockResolvedValue(false);
|
|
32
|
+
mocks.updateServerMetadata.mockReset();
|
|
33
|
+
mocks.updateMetadataCache.mockReset();
|
|
34
|
+
mocks.getFailureAgeSeconds.mockReset().mockReturnValue(null);
|
|
35
|
+
mocks.updateStatusBar.mockReset();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("auto-authenticates and retries executeConnect once", async () => {
|
|
39
|
+
const { executeConnect } = await import("../proxy-modes.ts");
|
|
40
|
+
|
|
41
|
+
let current: any;
|
|
42
|
+
const connected = {
|
|
43
|
+
status: "connected",
|
|
44
|
+
tools: [{ name: "search", description: "Search" }],
|
|
45
|
+
resources: [],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const manager = {
|
|
49
|
+
connect: vi
|
|
50
|
+
.fn()
|
|
51
|
+
.mockImplementationOnce(async () => {
|
|
52
|
+
current = { status: "needs-auth" };
|
|
53
|
+
return current;
|
|
54
|
+
})
|
|
55
|
+
.mockImplementationOnce(async () => {
|
|
56
|
+
current = connected;
|
|
57
|
+
return current;
|
|
58
|
+
}),
|
|
59
|
+
close: vi.fn(async () => {
|
|
60
|
+
current = undefined;
|
|
61
|
+
}),
|
|
62
|
+
getConnection: vi.fn(() => current),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const state = {
|
|
66
|
+
config: {
|
|
67
|
+
settings: { autoAuth: true, toolPrefix: "server" },
|
|
68
|
+
mcpServers: {
|
|
69
|
+
demo: { url: "https://api.example.com/mcp", auth: "oauth" },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
manager,
|
|
73
|
+
toolMetadata: new Map(),
|
|
74
|
+
failureTracker: new Map(),
|
|
75
|
+
ui: { setStatus: vi.fn() },
|
|
76
|
+
} as any;
|
|
77
|
+
|
|
78
|
+
const result = await executeConnect(state, "demo");
|
|
79
|
+
|
|
80
|
+
expect(mocks.authenticate).toHaveBeenCalledWith(
|
|
81
|
+
"demo",
|
|
82
|
+
"https://api.example.com/mcp",
|
|
83
|
+
state.config.mcpServers.demo,
|
|
84
|
+
);
|
|
85
|
+
expect(manager.close).toHaveBeenCalledWith("demo");
|
|
86
|
+
expect(manager.connect).toHaveBeenCalledTimes(2);
|
|
87
|
+
expect(result.content[0].text).toContain("demo (1 tools)");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("fails fast for non-ui browser auth when autoAuth is enabled", async () => {
|
|
91
|
+
const { executeConnect } = await import("../proxy-modes.ts");
|
|
92
|
+
|
|
93
|
+
const manager = {
|
|
94
|
+
connect: vi.fn(async () => ({ status: "needs-auth" })),
|
|
95
|
+
close: vi.fn(async () => {}),
|
|
96
|
+
getConnection: vi.fn(() => ({ status: "needs-auth" })),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const state = {
|
|
100
|
+
config: {
|
|
101
|
+
settings: { autoAuth: true },
|
|
102
|
+
mcpServers: {
|
|
103
|
+
demo: { url: "https://api.example.com/mcp", auth: "oauth" },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
manager,
|
|
107
|
+
toolMetadata: new Map(),
|
|
108
|
+
failureTracker: new Map(),
|
|
109
|
+
ui: undefined,
|
|
110
|
+
} as any;
|
|
111
|
+
|
|
112
|
+
const result = await executeConnect(state, "demo");
|
|
113
|
+
|
|
114
|
+
expect(mocks.authenticate).not.toHaveBeenCalled();
|
|
115
|
+
expect(result.content[0].text).toContain("auth-start");
|
|
116
|
+
expect(result.content[0].text).toContain("/mcp-auth demo");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("uses custom authRequiredMessage for non-ui autoAuth failures", async () => {
|
|
120
|
+
const { executeConnect } = await import("../proxy-modes.ts");
|
|
121
|
+
|
|
122
|
+
const state = {
|
|
123
|
+
config: {
|
|
124
|
+
settings: {
|
|
125
|
+
autoAuth: true,
|
|
126
|
+
authRequiredMessage: "Reconnect ${server} from the host app.",
|
|
127
|
+
},
|
|
128
|
+
mcpServers: {
|
|
129
|
+
demo: { url: "https://api.example.com/mcp", auth: "oauth" },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
manager: {
|
|
133
|
+
connect: vi.fn(async () => ({ status: "needs-auth" })),
|
|
134
|
+
close: vi.fn(async () => {}),
|
|
135
|
+
getConnection: vi.fn(() => ({ status: "needs-auth" })),
|
|
136
|
+
},
|
|
137
|
+
toolMetadata: new Map(),
|
|
138
|
+
failureTracker: new Map(),
|
|
139
|
+
ui: undefined,
|
|
140
|
+
} as any;
|
|
141
|
+
|
|
142
|
+
const result = await executeConnect(state, "demo");
|
|
143
|
+
|
|
144
|
+
expect(mocks.authenticate).not.toHaveBeenCalled();
|
|
145
|
+
expect(result.content[0].text).toBe("Reconnect demo from the host app.");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("runs URL elicitations returned by proxy tool calls", async () => {
|
|
149
|
+
const { UrlElicitationRequiredError } = await import("@modelcontextprotocol/sdk/types.js");
|
|
150
|
+
const { executeCall } = await import("../proxy-modes.ts");
|
|
151
|
+
const error = new UrlElicitationRequiredError([{
|
|
152
|
+
mode: "url",
|
|
153
|
+
message: "Connect your account",
|
|
154
|
+
elicitationId: "connect-1",
|
|
155
|
+
url: "https://example.com/connect",
|
|
156
|
+
}]);
|
|
157
|
+
const connection = {
|
|
158
|
+
status: "connected",
|
|
159
|
+
client: { callTool: vi.fn().mockRejectedValue(error) },
|
|
160
|
+
};
|
|
161
|
+
const manager = {
|
|
162
|
+
getConnection: vi.fn(() => connection),
|
|
163
|
+
handleUrlElicitationRequired: vi.fn().mockResolvedValue("accept"),
|
|
164
|
+
touch: vi.fn(),
|
|
165
|
+
incrementInFlight: vi.fn(),
|
|
166
|
+
decrementInFlight: vi.fn(),
|
|
167
|
+
};
|
|
168
|
+
const state = {
|
|
169
|
+
config: { settings: {}, mcpServers: { demo: { command: "demo" } } },
|
|
170
|
+
manager,
|
|
171
|
+
toolMetadata: new Map([["demo", [{
|
|
172
|
+
name: "demo_search",
|
|
173
|
+
originalName: "search",
|
|
174
|
+
description: "Search",
|
|
175
|
+
inputSchema: { type: "object", properties: {} },
|
|
176
|
+
}]]]),
|
|
177
|
+
failureTracker: new Map(),
|
|
178
|
+
completedUiSessions: [],
|
|
179
|
+
} as any;
|
|
180
|
+
|
|
181
|
+
const result = await executeCall(state, "demo_search", {}, "demo");
|
|
182
|
+
|
|
183
|
+
expect(manager.handleUrlElicitationRequired).toHaveBeenCalledWith("demo", error);
|
|
184
|
+
expect(result.details).toMatchObject({ error: "url_elicitation_required", action: "accept" });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("auto-authenticates and retries executeCall once", async () => {
|
|
188
|
+
const { executeCall } = await import("../proxy-modes.ts");
|
|
189
|
+
|
|
190
|
+
let current: any = { status: "needs-auth" };
|
|
191
|
+
const connected = {
|
|
192
|
+
status: "connected",
|
|
193
|
+
client: {
|
|
194
|
+
callTool: vi.fn(async () => ({
|
|
195
|
+
isError: false,
|
|
196
|
+
content: [{ type: "text", text: "ok" }],
|
|
197
|
+
})),
|
|
198
|
+
},
|
|
199
|
+
tools: [{ name: "search", description: "Search" }],
|
|
200
|
+
resources: [],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const manager = {
|
|
204
|
+
connect: vi.fn(async () => {
|
|
205
|
+
current = connected;
|
|
206
|
+
return connected;
|
|
207
|
+
}),
|
|
208
|
+
close: vi.fn(async () => {
|
|
209
|
+
current = undefined;
|
|
210
|
+
}),
|
|
211
|
+
getConnection: vi.fn(() => current),
|
|
212
|
+
touch: vi.fn(),
|
|
213
|
+
incrementInFlight: vi.fn(),
|
|
214
|
+
decrementInFlight: vi.fn(),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const state = {
|
|
218
|
+
config: {
|
|
219
|
+
settings: { autoAuth: true, toolPrefix: "server" },
|
|
220
|
+
mcpServers: {
|
|
221
|
+
demo: { url: "https://api.example.com/mcp", auth: "oauth" },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
manager,
|
|
225
|
+
toolMetadata: new Map([
|
|
226
|
+
[
|
|
227
|
+
"demo",
|
|
228
|
+
[
|
|
229
|
+
{
|
|
230
|
+
name: "demo_search",
|
|
231
|
+
originalName: "search",
|
|
232
|
+
description: "Search",
|
|
233
|
+
inputSchema: { type: "object", properties: {} },
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
],
|
|
237
|
+
]),
|
|
238
|
+
failureTracker: new Map(),
|
|
239
|
+
ui: { setStatus: vi.fn() },
|
|
240
|
+
completedUiSessions: [],
|
|
241
|
+
} as any;
|
|
242
|
+
|
|
243
|
+
const result = await executeCall(state, "demo_search", { q: "hello" }, "demo");
|
|
244
|
+
|
|
245
|
+
expect(mocks.authenticate).toHaveBeenCalledWith(
|
|
246
|
+
"demo",
|
|
247
|
+
"https://api.example.com/mcp",
|
|
248
|
+
state.config.mcpServers.demo,
|
|
249
|
+
);
|
|
250
|
+
expect(manager.connect).toHaveBeenCalledTimes(1);
|
|
251
|
+
expect(result.content[0].text).toContain("ok");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { executeCall, executeSearch } from "../proxy-modes.ts";
|
|
3
|
+
import type { McpExtensionState } from "../state.ts";
|
|
4
|
+
|
|
5
|
+
function createState(): McpExtensionState {
|
|
6
|
+
return {
|
|
7
|
+
config: {
|
|
8
|
+
mcpServers: {
|
|
9
|
+
demo: { command: "npx", args: ["demo"] },
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
toolMetadata: new Map([
|
|
13
|
+
[
|
|
14
|
+
"demo",
|
|
15
|
+
[
|
|
16
|
+
{
|
|
17
|
+
name: "demo_search",
|
|
18
|
+
originalName: "search",
|
|
19
|
+
description: "Search demo records",
|
|
20
|
+
inputSchema: { type: "object", properties: {} },
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
],
|
|
24
|
+
]),
|
|
25
|
+
manager: {
|
|
26
|
+
getConnection: () => undefined,
|
|
27
|
+
},
|
|
28
|
+
failureTracker: new Map(),
|
|
29
|
+
} as unknown as McpExtensionState;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("proxy discovery", () => {
|
|
33
|
+
it("searches MCP tools only", () => {
|
|
34
|
+
const result = executeSearch(createState(), "read");
|
|
35
|
+
|
|
36
|
+
expect(result.content[0].text).toBe('No tools matching "read"');
|
|
37
|
+
expect(result.details).toMatchObject({ count: 0, matches: [] });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("rejects regex queries longer than the safety cap", () => {
|
|
41
|
+
const result = executeSearch(createState(), "a".repeat(257), true);
|
|
42
|
+
|
|
43
|
+
expect(result.details).toMatchObject({ error: "query_too_long", maxLength: 256 });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("reports malformed regex queries separately from unsafe patterns", () => {
|
|
47
|
+
const result = executeSearch(createState(), "[", true);
|
|
48
|
+
|
|
49
|
+
expect(result.details).toMatchObject({ error: "invalid_pattern" });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("rejects catastrophic-backtracking regex queries", () => {
|
|
53
|
+
const result = executeSearch(createState(), "(a+)+$", true);
|
|
54
|
+
|
|
55
|
+
expect(result.details).toMatchObject({ error: "unsafe_pattern", safetyStatus: "vulnerable" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("accepts safe regex queries", () => {
|
|
59
|
+
const result = executeSearch(createState(), "^demo_[a-z]+$", true);
|
|
60
|
+
|
|
61
|
+
expect(result.details).toMatchObject({ count: 1, query: "^demo_[a-z]+$" });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("keeps non-regex searches unaffected by the regex length cap", () => {
|
|
65
|
+
const result = executeSearch(createState(), "search terms ".repeat(40), false);
|
|
66
|
+
|
|
67
|
+
expect(result.details).not.toMatchObject({ error: "query_too_long" });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("tells callers to invoke native Pi tools directly", async () => {
|
|
71
|
+
const result = await executeCall(
|
|
72
|
+
createState(),
|
|
73
|
+
"read",
|
|
74
|
+
undefined,
|
|
75
|
+
undefined,
|
|
76
|
+
() => [{ name: "read", description: "Read a file" } as any],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(result.content[0].text).toBe(
|
|
80
|
+
'"read" is a native Pi tool. Call read directly instead of using mcp({ tool: "read" }).',
|
|
81
|
+
);
|
|
82
|
+
expect(result.details).toMatchObject({ error: "native_tool", requestedTool: "read" });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
completeAuthFromInput: vi.fn(),
|
|
5
|
+
startAuth: vi.fn(),
|
|
6
|
+
supportsOAuth: vi.fn(),
|
|
7
|
+
lazyConnect: vi.fn(),
|
|
8
|
+
updateServerMetadata: vi.fn(),
|
|
9
|
+
updateMetadataCache: vi.fn(),
|
|
10
|
+
getFailureAgeSeconds: vi.fn(),
|
|
11
|
+
updateStatusBar: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("../mcp-auth-flow.ts", () => ({
|
|
15
|
+
authenticate: vi.fn(),
|
|
16
|
+
completeAuthFromInput: mocks.completeAuthFromInput,
|
|
17
|
+
startAuth: mocks.startAuth,
|
|
18
|
+
supportsOAuth: mocks.supportsOAuth,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("../init.ts", () => ({
|
|
22
|
+
lazyConnect: mocks.lazyConnect,
|
|
23
|
+
updateServerMetadata: mocks.updateServerMetadata,
|
|
24
|
+
updateMetadataCache: mocks.updateMetadataCache,
|
|
25
|
+
getFailureAgeSeconds: mocks.getFailureAgeSeconds,
|
|
26
|
+
updateStatusBar: mocks.updateStatusBar,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
function createState(overrides: Record<string, unknown> = {}) {
|
|
30
|
+
return {
|
|
31
|
+
config: {
|
|
32
|
+
settings: {},
|
|
33
|
+
mcpServers: {
|
|
34
|
+
demo: { url: "https://api.example.com/mcp", auth: "oauth" },
|
|
35
|
+
bearer: { url: "https://api.example.com/mcp", auth: "bearer" },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
manager: { close: vi.fn(async () => {}) },
|
|
39
|
+
toolMetadata: new Map(),
|
|
40
|
+
failureTracker: new Map([["demo", Date.now()]]),
|
|
41
|
+
...overrides,
|
|
42
|
+
} as any;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("manual OAuth proxy actions", () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.resetModules();
|
|
48
|
+
mocks.completeAuthFromInput.mockReset().mockResolvedValue("authenticated");
|
|
49
|
+
mocks.startAuth.mockReset().mockResolvedValue({
|
|
50
|
+
authorizationUrl: "https://auth.example.com/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A19876%2Fcallback",
|
|
51
|
+
});
|
|
52
|
+
mocks.supportsOAuth.mockReset().mockImplementation((definition) => definition.auth === "oauth");
|
|
53
|
+
mocks.updateStatusBar.mockReset();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns copyable instructions and authorization URL", async () => {
|
|
57
|
+
const { executeAuthStart } = await import("../proxy-modes.ts");
|
|
58
|
+
const state = createState();
|
|
59
|
+
|
|
60
|
+
const result = await executeAuthStart(state, "demo");
|
|
61
|
+
|
|
62
|
+
expect(mocks.startAuth).toHaveBeenCalledWith("demo", "https://api.example.com/mcp", state.config.mcpServers.demo);
|
|
63
|
+
expect(result.content[0].text).toContain("Open this URL in your local browser");
|
|
64
|
+
expect(result.content[0].text).toContain("https://auth.example.com/authorize");
|
|
65
|
+
expect(result.content[0].text).toContain("auth-complete");
|
|
66
|
+
expect(result.details).toMatchObject({ mode: "auth-start", server: "demo" });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("rejects auth-start for non-OAuth servers", async () => {
|
|
70
|
+
const { executeAuthStart } = await import("../proxy-modes.ts");
|
|
71
|
+
|
|
72
|
+
const result = await executeAuthStart(createState(), "bearer");
|
|
73
|
+
|
|
74
|
+
expect(mocks.startAuth).not.toHaveBeenCalled();
|
|
75
|
+
expect(result.content[0].text).toContain("not configured for OAuth");
|
|
76
|
+
expect(result.details).toMatchObject({ error: "oauth_not_supported" });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("completes auth from a copied redirect URL and resets connection state", async () => {
|
|
80
|
+
const { executeAuthComplete } = await import("../proxy-modes.ts");
|
|
81
|
+
const state = createState();
|
|
82
|
+
|
|
83
|
+
const result = await executeAuthComplete(state, "demo", "http://localhost:19876/callback?code=abc&state=state");
|
|
84
|
+
|
|
85
|
+
expect(mocks.completeAuthFromInput).toHaveBeenCalledWith("demo", "http://localhost:19876/callback?code=abc&state=state");
|
|
86
|
+
expect(state.manager.close).toHaveBeenCalledWith("demo");
|
|
87
|
+
expect(state.failureTracker.has("demo")).toBe(false);
|
|
88
|
+
expect(mocks.updateStatusBar).toHaveBeenCalledWith(state);
|
|
89
|
+
expect(result.content[0].text).toContain("OAuth authentication successful");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { executeUiMessages } from "../proxy-modes.ts";
|
|
3
|
+
import type { McpExtensionState } from "../state.ts";
|
|
4
|
+
|
|
5
|
+
function createState(prompts: string[]): McpExtensionState {
|
|
6
|
+
return {
|
|
7
|
+
completedUiSessions: [
|
|
8
|
+
{
|
|
9
|
+
serverName: "interactive-visualizer",
|
|
10
|
+
toolName: "show_visualization",
|
|
11
|
+
completedAt: new Date("2026-03-12T16:00:00Z"),
|
|
12
|
+
reason: "done",
|
|
13
|
+
messages: {
|
|
14
|
+
prompts,
|
|
15
|
+
notifications: [],
|
|
16
|
+
intents: [],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
} as McpExtensionState;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("executeUiMessages", () => {
|
|
24
|
+
it("normalizes canonical handoff prompts into structured intents", () => {
|
|
25
|
+
const state = createState([
|
|
26
|
+
'visualization_annotations_submitted\n{"visualizationId":"flow","annotations":[{"id":"a1","kind":"pin","text":"Check this"}]}',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const result = executeUiMessages(state);
|
|
30
|
+
expect(result.content[0]).toMatchObject({
|
|
31
|
+
type: "text",
|
|
32
|
+
text: expect.stringContaining("visualization_annotations_submitted"),
|
|
33
|
+
});
|
|
34
|
+
expect(result.content[0]).toMatchObject({
|
|
35
|
+
text: expect.not.stringContaining("### Prompts:\n- visualization_annotations_submitted"),
|
|
36
|
+
});
|
|
37
|
+
expect(result.details).toMatchObject({
|
|
38
|
+
intents: [
|
|
39
|
+
{
|
|
40
|
+
intent: "visualization_annotations_submitted",
|
|
41
|
+
params: {
|
|
42
|
+
visualizationId: "flow",
|
|
43
|
+
annotations: [{ id: "a1", kind: "pin", text: "Check this" }],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
handoffs: [
|
|
48
|
+
{
|
|
49
|
+
intent: "visualization_annotations_submitted",
|
|
50
|
+
params: {
|
|
51
|
+
visualizationId: "flow",
|
|
52
|
+
annotations: [{ id: "a1", kind: "pin", text: "Check this" }],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
cleared: true,
|
|
57
|
+
});
|
|
58
|
+
expect(state.completedUiSessions).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("preserves ordinary prompts as prompts", () => {
|
|
62
|
+
const state = createState(["Please analyze this flow"]);
|
|
63
|
+
const result = executeUiMessages(state);
|
|
64
|
+
expect(result.content[0]).toMatchObject({
|
|
65
|
+
text: expect.stringContaining("### Prompts:\n- Please analyze this flow"),
|
|
66
|
+
});
|
|
67
|
+
expect(result.details).toMatchObject({
|
|
68
|
+
prompts: ["Please analyze this flow"],
|
|
69
|
+
intents: [],
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|