Skip to content

Commit 7b88c6d

Browse files
committed
Fix auth wait guidance and live-view session validation
1 parent 55b2569 commit 7b88c6d

5 files changed

Lines changed: 95 additions & 3 deletions

File tree

src/lib/mcp/tools/auth-connections.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createKernelClient } from "@/lib/mcp/kernel-client";
44
import {
55
AuthLoginStartError,
66
deriveAuthNextAction,
7+
hasLiveAuthFlow,
78
toSafeAuthConnection,
89
waitForAuthConnection,
910
} from "@/lib/mcp/tools/managed-auth-state";
@@ -108,12 +109,16 @@ export function registerAuthConnectionTools(server: McpServer) {
108109
const connection = await client.auth.connections.retrieve(
109110
params.id,
110111
);
112+
const safeConnection = toSafeAuthConnection(connection);
113+
const authFlowInProgress = hasLiveAuthFlow(safeConnection);
111114
return safeJsonResponse({
112-
connection: toSafeAuthConnection(connection),
115+
connection: safeConnection,
113116
instruction:
114-
connection.status === "AUTHENTICATED"
117+
safeConnection.status === "AUTHENTICATED" && !authFlowInProgress
115118
? "Authentication is verified. Use this profile_name when creating the browser."
116-
: "Do not continue the protected action. Ask for consent, then use open_auth_login to authenticate securely.",
119+
: authFlowInProgress
120+
? "Authentication is still pending. Immediately call manage_auth_connections with action=wait and this id again. Do not continue the protected action yet."
121+
: "Do not continue the protected action. Ask for consent, then use open_auth_login to authenticate securely.",
117122
});
118123
}
119124
case "wait": {

src/lib/mcp/tools/auth-login-app.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ describe("managed-auth MCP App registration", () => {
180180
domain_filter: "example.com",
181181
profile_name: "work",
182182
wait_seconds: 25,
183+
previous_flow_expires_at: null,
183184
},
184185
},
185186
});
@@ -210,6 +211,50 @@ describe("managed-auth MCP App registration", () => {
210211
expect(JSON.stringify(result)).not.toContain("app_capability");
211212
});
212213

214+
test("new_login launcher guards wait with any matching pre-flow baseline", async () => {
215+
kernelClientFactory = () => ({
216+
auth: {
217+
connections: {
218+
list: async () => ({
219+
getPaginatedItems: () => [
220+
{
221+
id: "conn_1",
222+
domain: "example.com",
223+
profile_name: "work",
224+
status: "AUTHENTICATED",
225+
flow_status: "SUCCESS",
226+
flow_type: "LOGIN",
227+
flow_expires_at: "2026-01-01T00:00:00Z",
228+
},
229+
],
230+
hasNextPage: () => false,
231+
}),
232+
},
233+
},
234+
});
235+
try {
236+
const { tools } = captureRegistration();
237+
const result = await tools.get("open_auth_login")!.handler(
238+
{
239+
mode: "new_login",
240+
domain: "example.com",
241+
profile_name: "work",
242+
text_only: false,
243+
},
244+
{ authInfo: { token: "unused-api-key" } },
245+
);
246+
expect(result.structuredContent.next_action.arguments).toEqual({
247+
action: "wait",
248+
domain_filter: "example.com",
249+
profile_name: "work",
250+
wait_seconds: 25,
251+
previous_flow_expires_at: "2026-01-01T00:00:00Z",
252+
});
253+
} finally {
254+
resetKernelClientFactory();
255+
}
256+
});
257+
213258
test("app-only tools fail closed on hosts without MCP Apps support", async () => {
214259
const { tools } = captureRegistration({ appsSupport: false });
215260
const calls: Array<[string, any]> = [

src/lib/mcp/tools/auth-login-app.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,32 @@ export function registerAuthLoginApp(server: McpServer) {
231231
await client.auth.connections.retrieve(input.connection_id!),
232232
)
233233
: null;
234+
const previousFlowExpiresAtForNewLogin =
235+
!reauthConnection && input.mode === "new_login"
236+
? await (async () => {
237+
try {
238+
const page = await client.auth.connections.list({
239+
domain: input.domain!,
240+
profile_name: input.profile_name!,
241+
limit: 100,
242+
});
243+
const matches = page
244+
.getPaginatedItems()
245+
.filter(
246+
(item) =>
247+
item.domain === input.domain &&
248+
item.profile_name === input.profile_name,
249+
);
250+
if (matches.length === 1 && !page.hasNextPage()) {
251+
return matches[0].flow_expires_at ?? null;
252+
}
253+
} catch {
254+
// Keep launcher behavior available even if discovery fails;
255+
// a null baseline still guards against unflowed stale auth.
256+
}
257+
return null;
258+
})()
259+
: undefined;
234260
const connection = reauthConnection ?? {
235261
domain: input.domain!,
236262
profile_name: input.profile_name!,
@@ -253,6 +279,7 @@ export function registerAuthLoginApp(server: McpServer) {
253279
domain_filter: input.domain!,
254280
profile_name: input.profile_name!,
255281
wait_seconds: 25,
282+
previous_flow_expires_at: previousFlowExpiresAtForNewLogin,
256283
};
257284
// The delete authorization capability is App-only: issue it solely to
258285
// hosts that declared MCP Apps support, and keep it in _meta (which

src/lib/mcp/tools/live-view-app.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, mock, test } from "bun:test";
22
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3+
import { z } from "zod";
34
import { registerLiveViewApp } from "@/lib/mcp/tools/live-view-app";
45

56
// Tests that exercise API-backed handlers substitute a fake Kernel client.
@@ -78,6 +79,18 @@ function fakeScreenshotClient() {
7879
}
7980

8081
describe("live view MCP App", () => {
82+
test("live view tool schemas reject empty session identifiers", () => {
83+
const { tools } = captureRegistration();
84+
const showSchema = z.object(
85+
tools.get("show_browser_live_view")!.config.inputSchema,
86+
);
87+
expect(showSchema.safeParse({ session_id: "" }).success).toBe(false);
88+
const captureSchema = z.object(
89+
tools.get("capture_live_view_frame")!.config.inputSchema,
90+
);
91+
expect(captureSchema.safeParse({ session_id: "" }).success).toBe(false);
92+
});
93+
8194
test("capture_live_view_frame fails closed on hosts without MCP Apps support", async () => {
8295
redisMarkerPresent = false;
8396
const { tools } = captureRegistration({ appsSupport: false });

src/lib/mcp/tools/live-view-app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ export function registerLiveViewApp(server: McpServer) {
460460
inputSchema: {
461461
session_id: z
462462
.string()
463+
.min(1)
463464
.describe("Browser session ID to display (from manage_browsers)."),
464465
},
465466
annotations: {
@@ -531,6 +532,7 @@ export function registerLiveViewApp(server: McpServer) {
531532
inputSchema: {
532533
session_id: z
533534
.string()
535+
.min(1)
534536
.describe("Browser session ID to capture a frame from."),
535537
},
536538
annotations: {

0 commit comments

Comments
 (0)