diff --git a/README.md b/README.md
index 062e5a1e..090e342c 100644
--- a/README.md
+++ b/README.md
@@ -313,7 +313,7 @@ Call `get_connection_context` before deciding whether to create or select a proj
- `manage_replays` - Start, stop, and list MP4 video replay recordings for a browser session. Session-scoped: start once, run your automation, then stop. Requires a paid Kernel plan.
- `manage_extensions` - List and delete uploaded browser extensions.
- `manage_apps` - List/search apps, invoke actions, get/list/delete deployments, and get invocation results.
-- `manage_auth_connections` - Create, list, get, delete, login, submit, and wait for managed-auth connections in every client. Use domain-filtered `list` for discovery. App-capable clients additionally receive `open_auth_login`; the programmatic actions remain available there too.
+- `manage_auth_connections` - Create, list, get, update, delete, login, submit, inspect timelines, and wait for managed-auth connections in every client. Supports health-check and automatic re-auth settings, managed-auth browser configuration, and canonical interaction-bound field/choice submissions. Use domain-filtered `list` for discovery. App-capable clients additionally receive `open_auth_login`; the programmatic actions remain available there too.
- `manage_credentials` - Create, list, get, update, and delete stored credentials; fetch a current TOTP code for credentials with a configured totp_secret.
- `manage_credential_providers` - Create, list, get, update, and delete external credential providers (e.g. 1Password); list available items and test the provider connection.
@@ -380,7 +380,7 @@ Returns: { success: true, result: "Example Domain" }
Example: “Log me into my Hacker News account and update my profile to add a random emoji at the bottom.” The agent should discover `news.ycombinator.com`, open the App when needed, wait for authentication, then continue the profile edit without asking for credentials or a profile name in chat.
-The secure App defaults `record_session` and `browser_telemetry.enabled` to `true`, recording replay video plus the operational telemetry categories (`control`, `connection`, `system`, and `captcha`) for managed-auth browser sessions. Callers can explicitly disable either setting. The programmatic `manage_auth_connections` create/login actions preserve the API’s opt-in and inheritance behavior when these parameters are omitted.
+The secure App defaults `record_session` and `browser_telemetry.enabled` to `true`, recording replay video plus the operational telemetry categories (`control`, `connection`, `system`, and `captcha`) for managed-auth browser sessions. Callers can explicitly disable either setting. The programmatic `manage_auth_connections` create, update, and login actions pass browser telemetry through the API’s current nested `browser.telemetry` configuration while preserving defaults and inheritance when the MCP parameter is omitted.
### Set up browser profiles for authentication
diff --git a/bun.lock b/bun.lock
index 1f07b3c8..57c052b8 100644
--- a/bun.lock
+++ b/bun.lock
@@ -9,8 +9,8 @@
"@clerk/nextjs": "^7.7.1",
"@clerk/themes": "^2.4.19",
"@modelcontextprotocol/sdk": "1.26.0",
- "@onkernel/managed-auth-react": "0.4.1",
- "@onkernel/sdk": "^0.90.0",
+ "@onkernel/managed-auth-react": "0.5.1",
+ "@onkernel/sdk": "^0.97.0",
"@posthog/mcp": "0.10.1",
"@types/jsonwebtoken": "^9.0.10",
"@types/redis": "^4.0.11",
@@ -147,9 +147,9 @@
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.11", "", { "os": "win32", "cpu": "x64" }, "sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w=="],
- "@onkernel/managed-auth-react": ["@onkernel/managed-auth-react@0.4.1", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-8p+pMljBRQMKLiBFWbyJQ2ohYflsomWYGgQtlF2sbb4b2w/z+CBsnxUiBs1q23h/W1OtHsbw/jKGX51ZCjNYzA=="],
+ "@onkernel/managed-auth-react": ["@onkernel/managed-auth-react@0.5.1", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-tRnx91QTqlop2otlXyOxmI+jHodAMCW0dytWyk4hvu26cjACqH0387S0nP4IfgBVkZW6rwdNoBYKOheQfIxMgA=="],
- "@onkernel/sdk": ["@onkernel/sdk@0.90.0", "", {}, "sha512-KvOusR4JVrb1ifT1sqjUlZkoj5EFcxVu+nKrBWLYWGyZgJSu626f7uJEphigCrDVUYpuzimOGNsvXS9ILtjx6Q=="],
+ "@onkernel/sdk": ["@onkernel/sdk@0.97.0", "", {}, "sha512-Mi9//b9l81tl+eBPV3TpYwU9noriW7c0lFdJ8z5dj1SOQwdPB3nwdQDIfpewxzMeOBNng/qA2jkLkhopXWLbDw=="],
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="],
diff --git a/package.json b/package.json
index 73061f56..a649ff76 100644
--- a/package.json
+++ b/package.json
@@ -39,8 +39,8 @@
"@clerk/nextjs": "^7.7.1",
"@clerk/themes": "^2.4.19",
"@modelcontextprotocol/sdk": "1.26.0",
- "@onkernel/managed-auth-react": "0.4.1",
- "@onkernel/sdk": "^0.90.0",
+ "@onkernel/managed-auth-react": "0.5.1",
+ "@onkernel/sdk": "^0.97.0",
"@posthog/mcp": "0.10.1",
"@types/jsonwebtoken": "^9.0.10",
"@types/redis": "^4.0.11",
diff --git a/src/lib/mcp/apps/generated/managed-auth-app.ts b/src/lib/mcp/apps/generated/managed-auth-app.ts
index e1bf9f40..c2cf1b50 100644
--- a/src/lib/mcp/apps/generated/managed-auth-app.ts
+++ b/src/lib/mcp/apps/generated/managed-auth-app.ts
@@ -1,2 +1,2 @@
// Generated by scripts/build-managed-auth-app.mjs. Do not edit.
-export const MANAGED_AUTH_APP_HTML = "\n\n
\n\n\nKernel Managed Authentication\n\n\n\n\n\n";
+export const MANAGED_AUTH_APP_HTML = "\n\n\n\n\nKernel Managed Authentication\n\n\n\n\n\n";
diff --git a/src/lib/mcp/apps/managed-auth-terminal.test.ts b/src/lib/mcp/apps/managed-auth-terminal.test.ts
index 9b3b82e0..e92a2308 100644
--- a/src/lib/mcp/apps/managed-auth-terminal.test.ts
+++ b/src/lib/mcp/apps/managed-auth-terminal.test.ts
@@ -4,7 +4,7 @@ import {
isTerminalFailure,
} from "@/lib/mcp/apps/managed-auth-terminal";
-// Mirrors the hosted UI (@onkernel/managed-auth-react 0.4.1): FAILED/CANCELED
+// Mirrors the hosted UI (@onkernel/managed-auth-react): FAILED/CANCELED
// render StepError with the actual safe error code so ERROR_DISPLAY copy
// survives; EXPIRED renders StepExpired.
describe("managed-auth terminal view", () => {
diff --git a/src/lib/mcp/tools/apps.test.ts b/src/lib/mcp/tools/apps.test.ts
index df0d57e6..a4a96cb4 100644
--- a/src/lib/mcp/tools/apps.test.ts
+++ b/src/lib/mcp/tools/apps.test.ts
@@ -108,6 +108,7 @@ describe("manage_apps invocation contract", () => {
created_at: "2026-08-08T00:00:00Z",
headless: false,
stealth: true,
+ memory: "2GiB",
timeout_seconds: 600,
region: "us-east",
},
diff --git a/src/lib/mcp/tools/auth-connections.test-fixtures.ts b/src/lib/mcp/tools/auth-connections.test-fixtures.ts
index c520f548..ede19bd6 100644
--- a/src/lib/mcp/tools/auth-connections.test-fixtures.ts
+++ b/src/lib/mcp/tools/auth-connections.test-fixtures.ts
@@ -25,6 +25,22 @@ export function connection(overrides: Partial = {}): ManagedAuth {
hosted_url: "https://managed-auth.onkernel.com/login/conn_1?code=secret",
live_view_url: "https://live.example/secret",
browser_session_id: "browser_secret",
+ interaction_id: "mai_secret",
+ fields: [
+ {
+ id: "field_password",
+ ref: "password",
+ type: "password",
+ reason: "missing",
+ },
+ ],
+ choices: [
+ {
+ id: "work-account",
+ label: "Work account",
+ type: "account",
+ },
+ ],
discovered_fields: [
{
label: "Password",
@@ -129,6 +145,9 @@ const forbiddenKeys = [
"jwt",
"authorization",
"credential",
+ "interaction_id",
+ "fields",
+ "choices",
"discovered_fields",
"mfa_options",
"pending_sso_buttons",
diff --git a/src/lib/mcp/tools/auth-connections.test.ts b/src/lib/mcp/tools/auth-connections.test.ts
index cc2f9614..58808d73 100644
--- a/src/lib/mcp/tools/auth-connections.test.ts
+++ b/src/lib/mcp/tools/auth-connections.test.ts
@@ -15,37 +15,65 @@ import {
describe("manage_auth_connections programmatic surface", () => {
afterEach(resetKernelClientFactory);
- test("keeps every legacy action and adds wait", () => {
+ test("exposes every managed-auth action and current input", () => {
const { schema } = captureHandler();
expect(schema?.action.safeParse("list").success).toBe(true);
expect(schema?.action.safeParse("get").success).toBe(true);
expect(schema?.action.safeParse("create").success).toBe(true);
+ expect(schema?.action.safeParse("update").success).toBe(true);
expect(schema?.action.safeParse("delete").success).toBe(true);
expect(schema?.action.safeParse("login").success).toBe(true);
expect(schema?.action.safeParse("submit").success).toBe(true);
+ expect(schema?.action.safeParse("timeline").success).toBe(true);
expect(schema?.action.safeParse("wait").success).toBe(true);
+ expect(schema?.interaction_id).toBeDefined();
+ expect(schema?.field_values).toBeDefined();
+ expect(schema?.selected_choice_id).toBeDefined();
expect(schema?.fields).toBeDefined();
expect(schema?.mfa_option_id).toBeDefined();
expect(schema?.sso_button_selector).toBeDefined();
expect(schema?.allowed_domains).toBeDefined();
expect(schema?.login_url).toBeDefined();
- expect(schema?.credential_name).toBeDefined();
- expect(schema?.credential_provider).toBeDefined();
- expect(schema?.credential_path).toBeDefined();
+ expect(schema?.credential_name.description).toContain("create, update");
+ expect(schema?.credential_provider.description).toContain("create, update");
+ expect(schema?.credential_path.description).toContain("create, update");
+ expect(schema?.credential_auto.description).toContain("create, update");
+ expect(schema?.health_checks).toBeDefined();
+ expect(schema?.auto_reauth).toBeDefined();
+ expect(schema?.browser_stealth).toBeDefined();
+ expect(schema?.query).toBeDefined();
+ expect(schema?.timeline_type).toBeDefined();
expect(schema?.record_session).toBeDefined();
expect(schema?.flow_checkpoint).toBeDefined();
expect(schema?.proxy_id.safeParse("").success).toBe(false);
expect(schema?.proxy_name.safeParse("").success).toBe(false);
+ expect(schema?.proxy_mode.safeParse("direct").success).toBe(true);
expect(schema?.browser_telemetry.safeParse({ enabled: true }).success).toBe(
true,
);
+ expect(
+ schema?.browser_telemetry.safeParse({
+ enabled: true,
+ browser: {
+ platform: { enabled: true },
+ control: {
+ cdp: { excluded_methods: ["Input.dispatchMouseEvent"] },
+ },
+ },
+ export: {
+ otlp: { destination: { name: "managed-auth-telemetry" } },
+ },
+ }).success,
+ ).toBe(true);
expect(
schema?.browser_telemetry.safeParse({
enabled: false,
browser: { network: { enabled: true } },
}).success,
).toBe(false);
- expect(schema?.sign_in_option_id).toBeUndefined();
+ expect(schema?.selected_choice_id.safeParse("").success).toBe(false);
+ expect(schema?.sign_in_option_id.safeParse("").success).toBe(false);
+ expect(schema?.sso_provider).toBeDefined();
});
test("publishes browser telemetry categories without JSON Schema references", async () => {
@@ -137,7 +165,7 @@ describe("manage_auth_connections programmatic surface", () => {
extra,
);
expect(createBody).not.toHaveProperty("record_session");
- expect(createBody).not.toHaveProperty("browser_telemetry");
+ expect(createBody).not.toHaveProperty("browser");
await handler({ action: "login", id: "conn_1" }, extra);
expect(loginBody).toBeUndefined();
@@ -157,9 +185,11 @@ describe("manage_auth_connections programmatic surface", () => {
);
expect(createBody).toMatchObject({
record_session: true,
- browser_telemetry: {
- enabled: true,
- browser: { network: { enabled: true } },
+ browser: {
+ telemetry: {
+ enabled: true,
+ browser: { network: { enabled: true } },
+ },
},
});
@@ -174,13 +204,236 @@ describe("manage_auth_connections programmatic surface", () => {
);
expect(loginBody).toEqual({
record_session: false,
- browser_telemetry: { enabled: false },
+ browser: { telemetry: { enabled: false } },
});
} finally {
kernelClientMock.factory = () => unusedKernelClient;
}
});
+ test("forwards current connection settings on update", async () => {
+ const { handler } = captureHandler();
+ let updateBody: unknown;
+ kernelClientMock.factory = () => ({
+ auth: {
+ connections: {
+ update: async (_id: string, body: unknown) => {
+ updateBody = body;
+ return connection();
+ },
+ },
+ },
+ });
+ try {
+ await handler(
+ {
+ action: "update",
+ id: "conn_1",
+ allowed_domains: [],
+ credential_provider: "team-1password",
+ credential_path: "Vault/Login",
+ login_url: "",
+ health_check_interval: 1200,
+ health_checks: false,
+ auto_reauth: false,
+ save_credentials: false,
+ record_session: true,
+ browser_stealth: false,
+ proxy_mode: "direct",
+ browser_telemetry: {
+ enabled: true,
+ browser: { platform: { enabled: true } },
+ export: {
+ otlp: { destination: { name: "managed-auth-telemetry" } },
+ },
+ },
+ },
+ { authInfo: { token: "test-token" } },
+ );
+ expect(updateBody).toEqual({
+ allowed_domains: [],
+ credential: {
+ provider: "team-1password",
+ path: "Vault/Login",
+ },
+ login_url: "",
+ health_check_interval: 1200,
+ health_checks: false,
+ auto_reauth: false,
+ save_credentials: false,
+ record_session: true,
+ browser: {
+ stealth: false,
+ proxy: { mode: "direct" },
+ telemetry: {
+ enabled: true,
+ browser: { platform: { enabled: true } },
+ export: {
+ otlp: { destination: { name: "managed-auth-telemetry" } },
+ },
+ },
+ },
+ });
+ } finally {
+ kernelClientMock.factory = () => unusedKernelClient;
+ }
+ });
+
+ test("submits canonical fields and choices with interaction identity", async () => {
+ const { handler } = captureHandler();
+ const submitBodies: unknown[] = [];
+ kernelClientMock.factory = () => ({
+ auth: {
+ connections: {
+ submit: async (_id: string, body: unknown) => {
+ submitBodies.push(body);
+ return { accepted: true };
+ },
+ },
+ },
+ });
+ try {
+ const extra = { authInfo: { token: "test-token" } };
+ await handler(
+ {
+ action: "submit",
+ id: "conn_1",
+ interaction_id: "mai_1",
+ field_values: { field_email: "user@example.com" },
+ },
+ extra,
+ );
+ await handler(
+ {
+ action: "submit",
+ id: "conn_1",
+ interaction_id: "mai_2",
+ selected_choice_id: "work-account",
+ },
+ extra,
+ );
+ await handler(
+ {
+ action: "submit",
+ id: "conn_1",
+ interaction_id: "mai_3",
+ field_values: { field_email: "user@example.com" },
+ selected_choice_id: "work-account",
+ },
+ extra,
+ );
+ expect(submitBodies).toEqual([
+ {
+ interaction_id: "mai_1",
+ field_values: { field_email: "user@example.com" },
+ },
+ {
+ interaction_id: "mai_2",
+ selected_choice_id: "work-account",
+ },
+ {
+ interaction_id: "mai_3",
+ field_values: { field_email: "user@example.com" },
+ selected_choice_id: "work-account",
+ },
+ ]);
+
+ const missingInteraction = await handler(
+ {
+ action: "submit",
+ id: "conn_1",
+ selected_choice_id: "work-account",
+ },
+ extra,
+ );
+ expect(missingInteraction.isError).toBe(true);
+ expect(missingInteraction.content[0].text).toContain(
+ "interaction_id is required",
+ );
+ } finally {
+ kernelClientMock.factory = () => unusedKernelClient;
+ }
+ });
+
+ test.each([
+ [
+ { interaction_id: "mai_1", fields: { email: "user@example.com" } },
+ "interaction_id requires field_values or selected_choice_id",
+ ],
+ [
+ {
+ interaction_id: "mai_1",
+ field_values: { field_email: "user@example.com" },
+ fields: { email: "user@example.com" },
+ },
+ "field_values and selected_choice_id cannot be combined with legacy input fields",
+ ],
+ [
+ {
+ interaction_id: "mai_1",
+ selected_choice_id: "work-account",
+ sso_button_selector: "xpath=//button",
+ },
+ "field_values and selected_choice_id cannot be combined with legacy input fields",
+ ],
+ [
+ {
+ interaction_id: "mai_1",
+ selected_choice_id: "work-account",
+ sso_provider: "google",
+ },
+ "field_values and selected_choice_id cannot be combined with legacy input fields",
+ ],
+ [
+ {
+ interaction_id: "mai_1",
+ selected_choice_id: "work-account",
+ mfa_option_id: "sms",
+ },
+ "field_values and selected_choice_id cannot be combined with legacy input fields",
+ ],
+ [
+ {
+ interaction_id: "mai_1",
+ selected_choice_id: "work-account",
+ sign_in_option_id: "personal-account",
+ },
+ "field_values and selected_choice_id cannot be combined with legacy input fields",
+ ],
+ [
+ { sso_button_selector: "xpath=//button", sso_provider: "google" },
+ "sso_button_selector cannot be combined with other input types",
+ ],
+ [
+ { sso_button_selector: "xpath=//button", mfa_option_id: "sms" },
+ "sso_button_selector cannot be combined with other input types",
+ ],
+ [
+ { sso_provider: "google", mfa_option_id: "sms" },
+ "sso_provider cannot be combined with mfa_option_id or sign_in_option_id",
+ ],
+ [
+ { sign_in_option_id: "work-account", fields: { email: "value" } },
+ "sign_in_option_id cannot be combined with fields or mfa_option_id",
+ ],
+ [
+ { sign_in_option_id: "work-account", mfa_option_id: "sms" },
+ "sign_in_option_id cannot be combined with fields or mfa_option_id",
+ ],
+ ])(
+ "rejects incompatible submit shapes",
+ async (submitParams, expectedError) => {
+ const { handler } = captureHandler();
+ const result = await handler(
+ { action: "submit", id: "conn_1", ...submitParams },
+ { authInfo: { token: "test-token" } },
+ );
+
+ expect(result.isError).toBe(true);
+ expect(result.content[0].text).toContain(expectedError);
+ },
+ );
+
test("legacy actions keep their established raw response shapes", async () => {
const { handler } = captureHandler();
const calls = { create: 0, login: 0, submit: 0, delete: 0, retrieve: 0 };
@@ -236,8 +489,10 @@ describe("manage_auth_connections programmatic surface", () => {
const got = await handler({ action: "get", id: "conn_1" }, extra);
const gotJson = JSON.parse(got.content[0].text);
expect(gotJson.status).toBe("AUTHENTICATED");
+ expect(gotJson.interaction_id).toBe("mai_secret");
+ expect(gotJson.fields).toHaveLength(1);
+ expect(gotJson.choices).toHaveLength(1);
expect(gotJson.discovered_fields).toHaveLength(1);
- expect(gotJson.interaction).toBeUndefined();
// login returns the raw hosted flow response.
const login = await handler({ action: "login", id: "conn_1" }, extra);
@@ -279,6 +534,7 @@ describe("manage_auth_connections programmatic surface", () => {
connections: {
list: async (params: Record) => {
expect(params.domain).toBe("example.com");
+ expect(params.query).toBe("work");
return {
getPaginatedItems: () => [connection()],
has_more: false,
@@ -290,7 +546,11 @@ describe("manage_auth_connections programmatic surface", () => {
});
try {
const result = await handler(
- { action: "list", domain_filter: "example.com" },
+ {
+ action: "list",
+ domain_filter: "example.com",
+ query: "work",
+ },
{ authInfo: { token: "test-token" } },
);
const json = JSON.parse(result.content[0].text);
@@ -306,6 +566,53 @@ describe("manage_auth_connections programmatic surface", () => {
}
});
+ test("timeline returns the paginated event history", async () => {
+ const { handler } = captureHandler();
+ kernelClientMock.factory = () => ({
+ auth: {
+ connections: {
+ timeline: async (id: string, params: Record) => {
+ expect(id).toBe("conn_1");
+ expect(params).toEqual({
+ type: "health_check",
+ limit: 10,
+ offset: 20,
+ });
+ return {
+ getPaginatedItems: () => [
+ {
+ id: "check_1",
+ type: "health_check",
+ status: "AUTHENTICATED",
+ timestamp: "2026-08-31T00:00:00Z",
+ },
+ ],
+ has_more: false,
+ next_offset: null,
+ };
+ },
+ },
+ },
+ });
+ try {
+ const result = await handler(
+ {
+ action: "timeline",
+ id: "conn_1",
+ timeline_type: "health_check",
+ limit: 10,
+ offset: 20,
+ },
+ { authInfo: { token: "test-token" } },
+ );
+ const json = JSON.parse(result.content[0].text);
+ expect(json.items).toHaveLength(1);
+ expect(json.items[0].id).toBe("check_1");
+ } finally {
+ kernelClientMock.factory = () => unusedKernelClient;
+ }
+ });
+
test("legacy validation errors are preserved", async () => {
const { handler } = captureHandler();
const extra = { authInfo: { token: "test-token" } };
@@ -331,9 +638,21 @@ describe("manage_auth_connections programmatic surface", () => {
"require credential_provider",
],
[{ action: "get" }, "id is required for get"],
+ [{ action: "update" }, "id is required for update"],
+ [{ action: "update", id: "conn_1" }, "at least one connection setting"],
[{ action: "delete" }, "id is required for delete"],
[{ action: "login" }, "id is required for login"],
+ [{ action: "timeline" }, "id is required for timeline"],
[{ action: "submit", id: "conn_1" }, "submit requires at least one of"],
+ [
+ {
+ action: "login",
+ id: "conn_1",
+ proxy_id: "proxy_1",
+ proxy_mode: "direct",
+ },
+ "exactly one of proxy_id, proxy_name, or proxy_mode",
+ ],
];
for (const [params, message] of cases) {
const result = await handler(params, extra);
diff --git a/src/lib/mcp/tools/auth-connections.ts b/src/lib/mcp/tools/auth-connections.ts
index 192055a0..9230d7a6 100644
--- a/src/lib/mcp/tools/auth-connections.ts
+++ b/src/lib/mcp/tools/auth-connections.ts
@@ -37,16 +37,26 @@ export function registerAuthConnectionTools(server: McpServer) {
// manage_auth_connections -- Manage Kernel managed auth connections
server.tool(
"manage_auth_connections",
- 'Manage reusable authenticated profiles for third-party websites. Before a browser task that needs a user account, call "list" with the exact domain_filter and inspect every page. If one relevant connection is AUTHENTICATED, create the browser with its profile_name. If multiple relevant accounts exist, ask the user which one to use. If authentication is needed and open_auth_login is available, prefer that secure App so credentials and MFA never enter chat: a direct user request to log in is already consent; if login is only discovered incidentally, ask first. For a new App login, choose a concise stable profile name derived from the service unless the user specified one. The programmatic actions remain available for every client: "create" a connection, "login" to start a hosted flow, "submit" fields/MFA/SSO, "get" status, "delete", or "wait" for completion. After authentication, resume the original task with manage_browsers using the verified profile_name.',
+ 'Manage reusable authenticated profiles for third-party websites. Before a browser task that needs a user account, call "list" with the exact domain_filter and inspect every page. If one relevant connection is AUTHENTICATED, create the browser with its profile_name. If multiple relevant accounts exist, ask the user which one to use. If authentication is needed and open_auth_login is available, prefer that secure App so credentials and MFA never enter chat: a direct user request to log in is already consent; if login is only discovered incidentally, ask first. For a new App login, choose a concise stable profile name derived from the service unless the user specified one. The programmatic actions remain available for every client: "create" or "update" a connection, "login" to start a hosted flow, "submit" fields or choices, "get" status, inspect the "timeline", "delete", or "wait" for completion. Prefer interaction_id with canonical field_values or selected_choice_id when the connection returns fields or choices. After authentication, resume the original task with manage_browsers using the verified profile_name.',
{
...projectSelectionInputSchema(),
action: z
- .enum(["create", "list", "get", "delete", "login", "submit", "wait"])
+ .enum([
+ "create",
+ "list",
+ "get",
+ "update",
+ "delete",
+ "login",
+ "submit",
+ "timeline",
+ "wait",
+ ])
.describe("Operation to perform."),
id: z
.string()
.describe(
- "Auth connection ID. Required for get, delete, login, submit.",
+ "Auth connection ID. Required for get, update, delete, login, submit, and timeline.",
)
.optional(),
domain: z
@@ -62,79 +72,133 @@ export function registerAuthConnectionTools(server: McpServer) {
allowed_domains: z
.array(z.string())
.describe(
- "(create) Additional domains valid for this auth flow. Common SSO providers (Google, Microsoft, Okta, Auth0, Apple, GitHub, Facebook, LinkedIn, Cognito, OneLogin, Ping) are allowed by default.",
+ "(create, update) Additional hostname roots valid for credential entry. Exact hostnames and their subdomains are allowed; leading www. and *. are normalized away. An omitted or empty list leaves credential entry unrestricted.",
)
.optional(),
credential_name: z
.string()
.describe(
- "(create) Name of a pre-stored Kernel credential to use for automatic login.",
+ "(create, update) Name of a pre-stored Kernel credential to use for automatic login.",
)
.optional(),
credential_provider: z
.string()
.describe(
- "(create) External credential provider name (e.g. '1password'). Use with credential_path or credential_auto.",
+ "(create, update) External credential provider name (e.g. '1password'). Use with credential_path or credential_auto.",
)
.optional(),
credential_path: z
.string()
.describe(
- "(create) Provider-specific item path (e.g. 'VaultName/ItemName').",
+ "(create, update) Provider-specific item path (e.g. 'VaultName/ItemName').",
)
.optional(),
credential_auto: z
.boolean()
.describe(
- "(create) If true, the provider auto-looks up credentials by domain.",
+ "(create, update) If true, the provider auto-looks up credentials by domain.",
)
.optional(),
login_url: z
.string()
.describe(
- "(create) Optional explicit login page URL to skip discovery.",
+ "(create, update) Optional explicit login page URL to skip discovery. On update, use an empty string to clear it.",
)
.optional(),
health_check_interval: z
.number()
.int()
+ .min(300)
+ .max(86400)
.describe(
- "(create) Seconds between automatic re-auth checks. Plan-dependent minimum, max 86400.",
+ "(create, update) Seconds between automatic health checks. Plan-dependent minimum, max 86400.",
+ )
+ .optional(),
+ health_checks: z
+ .boolean()
+ .describe(
+ "(create, update) Enable scheduled authentication health checks. Defaults to true on create.",
+ )
+ .optional(),
+ auto_reauth: z
+ .boolean()
+ .describe(
+ "(create, update) Permit automatic re-authentication after a scheduled health check detects an expired session. Defaults to true on create and has no effect when health_checks is false.",
)
.optional(),
save_credentials: z
.boolean()
.describe(
- "(create) Save credentials after each successful login. Default true.",
+ "(create, update) Save credentials after each successful login. Defaults to true on create.",
)
.optional(),
record_session: z
.boolean()
.describe(
- "(create) Set the connection default for recording replay video of future login, reauth, and health-check browser sessions. (login) Override that default for this login only. Omitted preserves the API default/inheritance behavior.",
+ "(create, update) Set the connection default for recording replay video of future login, reauth, and health-check browser sessions. (login) Override that default for this login only. Omitted preserves the API default or inherited value.",
)
.optional(),
browser_telemetry: managedAuthBrowserTelemetrySchema
.describe(
- "(create) Set the connection default for browser telemetry. (login) Override it for this login only. Use { enabled: true } for the default operational categories (control, connection, system, captcha); add browser category flags to opt into console, network, page, interaction, or screenshot capture. Omitted preserves API default/inheritance behavior.",
+ "(create, update) Set the connection default for browser telemetry. (login) Override it for this login only. Use { enabled: true } for the default operational categories (control, connection, system, captcha); browser category settings can opt into console, network, page, interaction, screenshot, or platform capture, tune control CDP exclusions, and configure OTLP export. Omitted preserves the API default or inherited value.",
+ )
+ .optional(),
+ browser_stealth: z
+ .boolean()
+ .describe(
+ "(create, update, login) Whether managed-auth browser sessions use stealth mode. Defaults to true on create; omitted on update or login preserves or inherits the connection setting.",
)
.optional(),
proxy_id: z
.string()
.min(1)
- .describe("(create, login) Proxy ID to route the auth flow through.")
+ .describe(
+ "(create, update, login) Proxy ID to route managed-auth browser sessions through.",
+ )
.optional(),
proxy_name: z
.string()
.min(1)
- .describe("(create, login) Proxy name to route the auth flow through.")
+ .describe(
+ "(create, update, login) Proxy name to route managed-auth browser sessions through.",
+ )
+ .optional(),
+ proxy_mode: z
+ .enum(["direct", "default"])
+ .describe(
+ "(create, update, login) Proxy mode. direct disables proxy egress; default restores the stealth-derived default. Cannot be combined with proxy_id or proxy_name.",
+ )
.optional(),
domain_filter: z.string().describe("(list) Filter by domain.").optional(),
+ query: z
+ .string()
+ .describe("(list) Search by connection ID, domain, or profile name.")
+ .optional(),
...paginationParams,
+ interaction_id: z
+ .string()
+ .min(1)
+ .describe(
+ "(submit) Opaque interaction ID returned with canonical fields and choices. Required with field_values or selected_choice_id.",
+ )
+ .optional(),
+ field_values: z
+ .record(z.string(), z.string())
+ .describe(
+ "(submit) Canonical map of field ID to value. Use with interaction_id when `get` returns fields.",
+ )
+ .optional(),
+ selected_choice_id: z
+ .string()
+ .min(1)
+ .describe(
+ "(submit) Canonical choice ID. Use with interaction_id when `get` returns choices.",
+ )
+ .optional(),
fields: z
.record(z.string(), z.string())
.describe(
- "(submit) Map of field name to value (e.g. { mfa_code: '123456' }). Look at discovered_fields from `get` to know what to provide.",
+ "(submit, legacy) Map of discovered field name to value. Prefer interaction_id and field_values when canonical fields are present.",
)
.optional(),
mfa_option_id: z
@@ -143,12 +207,29 @@ export function registerAuthConnectionTools(server: McpServer) {
"(submit) ID of the MFA option to use, from mfa_options on the connection.",
)
.optional(),
+ sign_in_option_id: z
+ .string()
+ .min(1)
+ .describe(
+ "(submit, legacy) Sign-in option ID from sign_in_options. Prefer selected_choice_id when canonical choices are present.",
+ )
+ .optional(),
sso_button_selector: z
.string()
.describe(
- "(submit) XPath of an SSO button to click instead of submitting fields.",
+ "(submit, legacy) XPath of an ODA SSO button. Cannot be combined with sso_provider.",
)
.optional(),
+ sso_provider: z
+ .string()
+ .describe(
+ "(submit, legacy) Provider from pending_sso_buttons for a CUA SSO choice. Cannot be combined with sso_button_selector.",
+ )
+ .optional(),
+ timeline_type: z
+ .enum(["login", "reauth", "health_check"])
+ .describe("(timeline) Filter events by type.")
+ .optional(),
wait_seconds: z
.number()
.int()
@@ -182,15 +263,84 @@ export function registerAuthConnectionTools(server: McpServer) {
projectForOperation(extra.authInfo, params),
);
+ const proxySelectors = [
+ params.proxy_id,
+ params.proxy_name,
+ params.proxy_mode,
+ ].filter((value) => value !== undefined);
const buildProxy = () =>
- params.proxy_id || params.proxy_name
+ proxySelectors.length === 1
? {
...(params.proxy_id && { id: params.proxy_id }),
...(params.proxy_name && { name: params.proxy_name }),
+ ...(params.proxy_mode && { mode: params.proxy_mode }),
}
: undefined;
+ const buildBrowser = () => {
+ const proxy = buildProxy();
+ return params.browser_stealth !== undefined ||
+ params.browser_telemetry !== undefined ||
+ proxy
+ ? {
+ ...(params.browser_stealth !== undefined && {
+ stealth: params.browser_stealth,
+ }),
+ ...(params.browser_telemetry !== undefined && {
+ telemetry: params.browser_telemetry,
+ }),
+ ...(proxy && { proxy }),
+ }
+ : undefined;
+ };
+ const buildCredential = () => {
+ const hasName = !!params.credential_name;
+ const hasProvider = !!params.credential_provider;
+ const hasPath = !!params.credential_path;
+ const autoTrue = params.credential_auto === true;
+ if (hasName && (hasProvider || hasPath || autoTrue)) {
+ return {
+ error:
+ "credential_name cannot be combined with credential_provider, credential_path, or credential_auto. Use one of: { credential_name } for Kernel credentials, { credential_provider, credential_path } for an external provider item, or { credential_provider, credential_auto: true } for provider domain lookup.",
+ };
+ }
+ if ((hasPath || autoTrue) && !hasProvider) {
+ return {
+ error:
+ "credential_path and credential_auto require credential_provider.",
+ };
+ }
+ if (hasPath && autoTrue) {
+ return {
+ error:
+ "credential_path and credential_auto: true are alternatives — provide exactly one.",
+ };
+ }
+ if (hasProvider && !hasPath && !autoTrue) {
+ return {
+ error:
+ "credential_provider requires either credential_path or credential_auto: true.",
+ };
+ }
+ return {
+ credential:
+ hasName || hasProvider
+ ? {
+ ...(hasName && { name: params.credential_name }),
+ ...(hasProvider && { provider: params.credential_provider }),
+ ...(hasPath && { path: params.credential_path }),
+ ...(autoTrue && { auto: true }),
+ }
+ : undefined,
+ };
+ };
try {
+ if (proxySelectors.length > 1) {
+ return errorResponse(
+ "Error: provide exactly one of proxy_id, proxy_name, or proxy_mode.",
+ );
+ }
+
switch (params.action) {
case "create": {
if (!params.domain || !params.profile_name) {
@@ -198,46 +348,13 @@ export function registerAuthConnectionTools(server: McpServer) {
"Error: domain and profile_name are required for create.",
);
}
- const hasName = !!params.credential_name;
- const hasProvider = !!params.credential_provider;
- const hasPath = !!params.credential_path;
- const autoTrue = params.credential_auto === true;
- if (hasName && (hasProvider || hasPath || autoTrue)) {
- return errorResponse(
- "Error: credential_name cannot be combined with credential_provider, credential_path, or credential_auto. Use one of: { credential_name } for Kernel credentials, { credential_provider, credential_path } for an external provider item, or { credential_provider, credential_auto: true } for provider domain lookup.",
- );
- }
- if ((hasPath || autoTrue) && !hasProvider) {
- return errorResponse(
- "Error: credential_path and credential_auto require credential_provider.",
- );
- }
- if (hasPath && autoTrue) {
- return errorResponse(
- "Error: credential_path and credential_auto: true are alternatives — provide exactly one.",
- );
- }
- if (hasProvider && !hasPath && !autoTrue) {
- return errorResponse(
- "Error: credential_provider requires either credential_path or credential_auto: true.",
- );
- }
- const credential =
- hasName || hasProvider
- ? {
- ...(hasName && { name: params.credential_name }),
- ...(hasProvider && {
- provider: params.credential_provider,
- }),
- ...(hasPath && { path: params.credential_path }),
- ...(autoTrue && { auto: true }),
- }
- : undefined;
- const proxy = buildProxy();
+ const { credential, error } = buildCredential();
+ if (error) return errorResponse(`Error: ${error}`);
+ const browser = buildBrowser();
const connection = await client.auth.connections.create({
domain: params.domain,
profile_name: params.profile_name,
- ...(params.allowed_domains && {
+ ...(params.allowed_domains !== undefined && {
allowed_domains: params.allowed_domains,
}),
...(credential && { credential }),
@@ -245,16 +362,19 @@ export function registerAuthConnectionTools(server: McpServer) {
...(params.health_check_interval !== undefined && {
health_check_interval: params.health_check_interval,
}),
+ ...(params.health_checks !== undefined && {
+ health_checks: params.health_checks,
+ }),
+ ...(params.auto_reauth !== undefined && {
+ auto_reauth: params.auto_reauth,
+ }),
...(params.save_credentials !== undefined && {
save_credentials: params.save_credentials,
}),
...(params.record_session !== undefined && {
record_session: params.record_session,
}),
- ...(params.browser_telemetry !== undefined && {
- browser_telemetry: params.browser_telemetry,
- }),
- ...(proxy && { proxy }),
+ ...(browser && { browser }),
});
if (!connection)
return errorResponse("Failed to create auth connection");
@@ -264,6 +384,7 @@ export function registerAuthConnectionTools(server: McpServer) {
const page = await client.auth.connections.list({
...(params.profile_name && { profile_name: params.profile_name }),
...(params.domain_filter && { domain: params.domain_filter }),
+ ...(params.query && { query: params.query }),
...(params.limit !== undefined && { limit: params.limit }),
...(params.offset !== undefined && { offset: params.offset }),
});
@@ -277,6 +398,54 @@ export function registerAuthConnectionTools(server: McpServer) {
);
return jsonResponse(connection);
}
+ case "update": {
+ if (!params.id)
+ return errorResponse("Error: id is required for update.");
+ const { credential, error } = buildCredential();
+ if (error) return errorResponse(`Error: ${error}`);
+ const browser = buildBrowser();
+ const hasUpdate =
+ params.allowed_domains !== undefined ||
+ credential !== undefined ||
+ params.login_url !== undefined ||
+ params.health_check_interval !== undefined ||
+ params.health_checks !== undefined ||
+ params.auto_reauth !== undefined ||
+ params.save_credentials !== undefined ||
+ params.record_session !== undefined ||
+ browser !== undefined;
+ if (!hasUpdate) {
+ return errorResponse(
+ "Error: update requires at least one connection setting.",
+ );
+ }
+ const connection = await client.auth.connections.update(params.id, {
+ ...(params.allowed_domains !== undefined && {
+ allowed_domains: params.allowed_domains,
+ }),
+ ...(credential && { credential }),
+ ...(params.login_url !== undefined && {
+ login_url: params.login_url,
+ }),
+ ...(params.health_check_interval !== undefined && {
+ health_check_interval: params.health_check_interval,
+ }),
+ ...(params.health_checks !== undefined && {
+ health_checks: params.health_checks,
+ }),
+ ...(params.auto_reauth !== undefined && {
+ auto_reauth: params.auto_reauth,
+ }),
+ ...(params.save_credentials !== undefined && {
+ save_credentials: params.save_credentials,
+ }),
+ ...(params.record_session !== undefined && {
+ record_session: params.record_session,
+ }),
+ ...(browser && { browser }),
+ });
+ return jsonResponse(connection);
+ }
case "delete": {
if (!params.id)
return errorResponse("Error: id is required for delete.");
@@ -286,22 +455,17 @@ export function registerAuthConnectionTools(server: McpServer) {
case "login": {
if (!params.id)
return errorResponse("Error: id is required for login.");
- const proxy = buildProxy();
+ const browser = buildBrowser();
const hasOverrides =
- !!proxy ||
- params.record_session !== undefined ||
- params.browser_telemetry !== undefined;
+ browser !== undefined || params.record_session !== undefined;
const response = await client.auth.connections.login(
params.id,
hasOverrides
? {
- ...(proxy && { proxy }),
+ ...(browser && { browser }),
...(params.record_session !== undefined && {
record_session: params.record_session,
}),
- ...(params.browser_telemetry !== undefined && {
- browser_telemetry: params.browser_telemetry,
- }),
}
: undefined,
);
@@ -310,27 +474,99 @@ export function registerAuthConnectionTools(server: McpServer) {
case "submit": {
if (!params.id)
return errorResponse("Error: id is required for submit.");
- const hasFields =
+ const hasCanonicalFields =
+ !!params.field_values &&
+ Object.keys(params.field_values).length > 0;
+ const hasLegacyFields =
!!params.fields && Object.keys(params.fields).length > 0;
+ const hasCanonicalSubmission =
+ hasCanonicalFields || !!params.selected_choice_id;
+ const hasLegacySubmission =
+ hasLegacyFields ||
+ !!params.mfa_option_id ||
+ !!params.sign_in_option_id ||
+ !!params.sso_button_selector ||
+ !!params.sso_provider;
+ if (!hasCanonicalSubmission && !hasLegacySubmission) {
+ return errorResponse(
+ "Error: submit requires at least one of field_values, selected_choice_id, fields, mfa_option_id, sign_in_option_id, sso_button_selector, or sso_provider.",
+ );
+ }
+ if (params.interaction_id && !hasCanonicalSubmission) {
+ return errorResponse(
+ "Error: interaction_id requires field_values or selected_choice_id.",
+ );
+ }
+ if (hasCanonicalSubmission && hasLegacySubmission) {
+ return errorResponse(
+ "Error: field_values and selected_choice_id cannot be combined with legacy input fields.",
+ );
+ }
+ if (hasCanonicalSubmission && !params.interaction_id) {
+ return errorResponse(
+ "Error: interaction_id is required with field_values or selected_choice_id.",
+ );
+ }
if (
- !hasFields &&
- !params.mfa_option_id &&
- !params.sso_button_selector
- )
+ params.sso_button_selector &&
+ (params.sso_provider ||
+ params.mfa_option_id ||
+ params.sign_in_option_id)
+ ) {
return errorResponse(
- "Error: submit requires at least one of fields (non-empty), mfa_option_id, or sso_button_selector.",
+ "Error: sso_button_selector cannot be combined with other input types.",
);
+ }
+ if (
+ params.sso_provider &&
+ (params.mfa_option_id || params.sign_in_option_id)
+ ) {
+ return errorResponse(
+ "Error: sso_provider cannot be combined with mfa_option_id or sign_in_option_id.",
+ );
+ }
+ if (
+ params.sign_in_option_id &&
+ (hasLegacyFields || params.mfa_option_id)
+ ) {
+ return errorResponse(
+ "Error: sign_in_option_id cannot be combined with fields or mfa_option_id.",
+ );
+ }
const response = await client.auth.connections.submit(params.id, {
- ...(hasFields && { fields: params.fields }),
+ ...(params.interaction_id && {
+ interaction_id: params.interaction_id,
+ }),
+ ...(hasCanonicalFields && { field_values: params.field_values }),
+ ...(params.selected_choice_id && {
+ selected_choice_id: params.selected_choice_id,
+ }),
+ ...(hasLegacyFields && { fields: params.fields }),
...(params.mfa_option_id && {
mfa_option_id: params.mfa_option_id,
}),
+ ...(params.sign_in_option_id && {
+ sign_in_option_id: params.sign_in_option_id,
+ }),
...(params.sso_button_selector && {
sso_button_selector: params.sso_button_selector,
}),
+ ...(params.sso_provider && {
+ sso_provider: params.sso_provider,
+ }),
});
return jsonResponse(response);
}
+ case "timeline": {
+ if (!params.id)
+ return errorResponse("Error: id is required for timeline.");
+ const page = await client.auth.connections.timeline(params.id, {
+ ...(params.timeline_type && { type: params.timeline_type }),
+ ...(params.limit !== undefined && { limit: params.limit }),
+ ...(params.offset !== undefined && { offset: params.offset }),
+ });
+ return paginatedJsonResponse(page);
+ }
case "wait": {
if (!params.id && (!params.domain_filter || !params.profile_name)) {
return errorResponse(
diff --git a/src/lib/mcp/tools/auth-login-app.test.ts b/src/lib/mcp/tools/auth-login-app.test.ts
index a5f99bc6..51ce4acf 100644
--- a/src/lib/mcp/tools/auth-login-app.test.ts
+++ b/src/lib/mcp/tools/auth-login-app.test.ts
@@ -509,6 +509,9 @@ describe("managed-auth MCP App registration", () => {
expect(MANAGED_AUTH_APP_HTML).not.toContain("kernel-app-loading");
expect(MANAGED_AUTH_APP_HTML).toContain("MutationObserver");
expect(MANAGED_AUTH_APP_HTML).toContain("preventScroll");
+ expect(MANAGED_AUTH_APP_HTML).toContain("interaction_id");
+ expect(MANAGED_AUTH_APP_HTML).toContain("field_values");
+ expect(MANAGED_AUTH_APP_HTML).toContain("selected_choice_id");
expect(MANAGED_AUTH_APP_HTML).toContain(
'input:not([type="hidden"]):not([type="submit"])',
);
diff --git a/src/lib/mcp/tools/managed-auth-start.test.ts b/src/lib/mcp/tools/managed-auth-start.test.ts
index 55f50675..d7b59011 100644
--- a/src/lib/mcp/tools/managed-auth-start.test.ts
+++ b/src/lib/mcp/tools/managed-auth-start.test.ts
@@ -58,11 +58,11 @@ describe("managed-auth start/resume state machine", () => {
});
expect(calls.createParams).toMatchObject({
record_session: true,
- browser_telemetry: { enabled: true },
+ browser: { telemetry: { enabled: true } },
});
expect(calls.loginParams).toEqual({
record_session: true,
- browser_telemetry: { enabled: true },
+ browser: { telemetry: { enabled: true } },
});
});
@@ -78,11 +78,11 @@ describe("managed-auth start/resume state machine", () => {
});
expect(calls.createParams).toMatchObject({
record_session: false,
- browser_telemetry: { enabled: false },
+ browser: { telemetry: { enabled: false } },
});
expect(calls.loginParams).toEqual({
record_session: false,
- browser_telemetry: { enabled: false },
+ browser: { telemetry: { enabled: false } },
});
});
diff --git a/src/lib/mcp/tools/managed-auth-state.ts b/src/lib/mcp/tools/managed-auth-state.ts
index 1f654fb2..5ac500d4 100644
--- a/src/lib/mcp/tools/managed-auth-state.ts
+++ b/src/lib/mcp/tools/managed-auth-state.ts
@@ -446,6 +446,13 @@ export async function beginAuthLogin(
const recordSession = input.record_session ?? true;
const browserTelemetry = input.browser_telemetry ?? { enabled: true };
+ const proxy =
+ input.proxy_id || input.proxy_name
+ ? {
+ ...(input.proxy_id && { id: input.proxy_id }),
+ ...(input.proxy_name && { name: input.proxy_name }),
+ }
+ : undefined;
let connection: ManagedAuth;
if (input.mode === "new_login") {
@@ -457,13 +464,10 @@ export async function beginAuthLogin(
save_credentials: input.save_credentials,
}),
record_session: recordSession,
- browser_telemetry: browserTelemetry,
- ...((input.proxy_id || input.proxy_name) && {
- proxy: {
- ...(input.proxy_id && { id: input.proxy_id }),
- ...(input.proxy_name && { name: input.proxy_name }),
- },
- }),
+ browser: {
+ telemetry: browserTelemetry,
+ ...(proxy && { proxy }),
+ },
});
} catch (error) {
const existingId = conflictExistingId(error);
@@ -498,14 +502,6 @@ export async function beginAuthLogin(
};
}
- const proxy =
- input.proxy_id || input.proxy_name
- ? {
- ...(input.proxy_id && { id: input.proxy_id }),
- ...(input.proxy_name && { name: input.proxy_name }),
- }
- : undefined;
-
// Capture the latest server timeline identity before starting. A signed
// "after" checkpoint then identifies the new event even if it reaches a
// terminal state before either the App or model polls once.
@@ -518,8 +514,10 @@ export async function beginAuthLogin(
try {
const login = await client.auth.connections.login(connection.id, {
record_session: recordSession,
- browser_telemetry: browserTelemetry,
- ...(proxy && { proxy }),
+ browser: {
+ telemetry: browserTelemetry,
+ ...(proxy && { proxy }),
+ },
});
let current = withLoginState(connection, login);
try {
diff --git a/src/lib/mcp/tools/managed-auth-telemetry.ts b/src/lib/mcp/tools/managed-auth-telemetry.ts
index 615bd4c4..efa6f4b8 100644
--- a/src/lib/mcp/tools/managed-auth-telemetry.ts
+++ b/src/lib/mcp/tools/managed-auth-telemetry.ts
@@ -6,22 +6,97 @@ function telemetryCategorySchema() {
});
}
+const cdpCommandMethodSchema = z.enum([
+ "Input.dispatchMouseEvent",
+ "Input.dispatchKeyEvent",
+ "Input.insertText",
+ "Input.imeSetComposition",
+ "Input.dispatchTouchEvent",
+ "Input.dispatchDragEvent",
+ "Input.cancelDragging",
+ "Input.emulateTouchFromMouseEvent",
+ "Input.synthesizePinchGesture",
+ "Input.synthesizeScrollGesture",
+ "Input.synthesizeTapGesture",
+ "DOM.setFileInputFiles",
+ "DOM.focus",
+ "DOM.scrollIntoViewIfNeeded",
+ "Page.bringToFront",
+ "Page.captureScreenshot",
+ "Page.captureSnapshot",
+ "Page.handleJavaScriptDialog",
+ "Page.navigate",
+ "Page.navigateToHistoryEntry",
+ "Page.reload",
+ "Page.printToPDF",
+ "Page.startScreencast",
+ "Page.stopScreencast",
+ "Page.stopLoading",
+ "Page.close",
+ "Page.setWebLifecycleState",
+ "Target.activateTarget",
+ "Target.closeTarget",
+ "Target.createTarget",
+ "Target.createBrowserContext",
+ "Target.disposeBrowserContext",
+ "Target.openDevTools",
+ "Browser.cancelDownload",
+ "Browser.close",
+ "Browser.setWindowBounds",
+ "Browser.setContentsSize",
+ "Autofill.trigger",
+]);
+
+const telemetryControlSchema = z.object({
+ enabled: z.boolean().optional(),
+ cdp: z
+ .object({
+ excluded_methods: z.array(cdpCommandMethodSchema).optional(),
+ })
+ .optional(),
+});
+
const telemetryCategoriesSchema = z.object({
captcha: telemetryCategorySchema().optional(),
connection: telemetryCategorySchema().optional(),
console: telemetryCategorySchema().optional(),
- control: telemetryCategorySchema().optional(),
+ control: telemetryControlSchema.optional(),
interaction: telemetryCategorySchema().optional(),
network: telemetryCategorySchema().optional(),
page: telemetryCategorySchema().optional(),
+ platform: telemetryCategorySchema().optional(),
screenshot: telemetryCategorySchema().optional(),
system: telemetryCategorySchema().optional(),
});
+const telemetryDestinationSchema = z
+ .object({
+ id: z.string().min(1).optional(),
+ name: z.string().min(1).optional(),
+ })
+ .superRefine((destination, context) => {
+ if (!!destination.id === !!destination.name) {
+ context.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "telemetry destination requires exactly one of id or name.",
+ });
+ }
+ });
+
export const managedAuthBrowserTelemetrySchema = z
.object({
enabled: z.boolean().optional(),
browser: telemetryCategoriesSchema.optional(),
+ export: z
+ .object({
+ otlp: z
+ .object({
+ enabled: z.boolean().optional(),
+ destination: telemetryDestinationSchema.optional(),
+ })
+ .optional(),
+ })
+ .optional(),
})
.superRefine((telemetry, context) => {
if (