Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ NEXT_PUBLIC_CLERK_DOMAIN=<x>.clerk.accounts.dev
# API Configuration - Only needed to set the Kernel SDK base url
API_BASE_URL=<x>

# Public origin of this MCP server. The Managed Auth MCP App connects only to
# the narrowly scoped same-origin relay. Local development: http://localhost:3002
MANAGED_AUTH_APP_ORIGIN=https://mcp.onkernel.com

# Mintlify API Configuration - Only needed for the search_docs tool call
MINTLIFY_ASSISTANT_API_TOKEN=mint_dsc_<x>
MINTLIFY_DOMAIN=<x>

# Optional MCP toolset gating. Comma-separated values.
# Example: api_keys hides manage_api_keys so deployments can opt out of key management.
# Supported: apps, api_keys, browser_pools, browsers, computer, docs, extensions, playwright, profiles, projects, proxies, replays, shell
# Supported: apps, api_keys, auth_connections, browser_pools, browsers, computer, docs, extensions, playwright, profiles, projects, proxies, replays, shell
# KERNEL_MCP_DISABLED_TOOLSETS=api_keys

# Redis Configuration
Expand Down
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,20 @@ jobs:
steps:
- uses: actions/checkout@v4

# Pin Bun: the managed-auth App bundle check is byte-exact and Bun's
# minifier output can change between releases. Regenerate the bundle
# with this exact version when bumping it.
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.3"

- run: bun install --frozen-lockfile

- name: Check managed-auth App bundle
run: bun run check:managed-auth-app

- name: Type check
run: bunx tsc --noEmit
run: bunx tsc --noEmit --incremental false

- name: Test
run: bun test
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/lib/mcp/apps/generated/managed-auth-app.ts
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,11 @@ Many other MCP-capable tools accept:

Configure these values wherever the tool expects MCP server settings.

## Tools (16 total)
## Tools (17 model-facing, plus 2 app-only helpers)

Each Kernel feature has a single `manage_*` tool with an `action` parameter, keeping the tool set small and consistent. Five standalone tools handle high-frequency workflows.
Each Kernel feature has a single `manage_*` tool with an `action` parameter, keeping the tool set small and consistent. Standalone tools handle high-frequency and interactive workflows.

Two additional Managed Auth helpers (`begin_auth_login` and `get_auth_login_status`) are marked app-only (`_meta.ui.visibility: ["app"]`); they refuse to execute on hosts that do not declare MCP Apps support.

Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_DISABLED_TOOLSETS` to a comma-separated list. For example, `KERNEL_MCP_DISABLED_TOOLSETS=api_keys` prevents `manage_api_keys` from being registered.

Expand All @@ -272,7 +274,7 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_
- `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 managed auth connections; start login flows (returns a hosted URL and live view); submit MFA codes or SSO selections.
- `manage_auth_connections` - List and get sanitized managed-auth connections. Use domain-filtered `list` as the discovery entrypoint; connection mutations stay in the secure App boundary.
- `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.

Expand All @@ -283,6 +285,7 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_
- `execute_playwright_code` - Execute Playwright/TypeScript code against an existing browser session. Does not create or delete browsers - use `manage_browsers` for session lifecycle.
- `exec_command` - Run shell commands inside a browser VM. Returns decoded stdout/stderr.
- `search_docs` - Search Kernel platform documentation and guides.
- `open_auth_login` - Open a secure interactive Managed Auth MCP App after user consent. Credentials and MFA never enter MCP/model traffic.

## Resources

Expand Down Expand Up @@ -327,6 +330,20 @@ Assistant: I'll create a browser session, then execute Playwright code against i
Returns: { success: true, result: "Example Domain" }
```

### Use managed authentication for a protected site

1. Call `manage_auth_connections` with `action: "list"` and `domain_filter`.
2. Fetch all pages. If multiple profiles match, ask the user which profile to use.
3. If the selected connection needs auth, explain why and wait for consent before calling `open_auth_login`.
4. Immediately follow the launcher's `next_action`: long-poll `manage_auth_connections` with `action: "wait"`, repeating while it reports `pending`.
5. The user enters credentials/MFA only in the secure MCP App. Never ask for them in chat.
6. When the wait returns `authenticated`, continue the pending task and create the browser with the verified `profile_name`.

If no App appears, ask the user to confirm that before retrying `open_auth_login` with
`text_only: true`. That compatibility path returns a capability-bearing hosted login URL as
user-audience text. Returning the URL does not mean login succeeded; always verify the
connection afterward.

### Set up browser profiles for authentication

```
Expand Down
10 changes: 10 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
},
"scripts": {
"dev": "next dev -p 3002",
"build": "next build",
"build:managed-auth-app": "bun scripts/build-managed-auth-app.mjs",
"check:managed-auth-app": "bun scripts/build-managed-auth-app.mjs --check",
"build": "bun run build:managed-auth-app && next build",
"start": "next start -p 3002",
"lint": "next lint",
"test": "bun test",
"format": "prettier --write \"**/*.{ts,js,json,md}\"",
"format:check": "prettier --check \"**/*.{ts,js,json,md}\""
},
Expand All @@ -34,6 +37,7 @@
"@clerk/themes": "^2.4.19",
"@mcp-ui/server": "^5.10.0",
"@modelcontextprotocol/sdk": "1.26.0",
"@onkernel/managed-auth-react": "0.4.1",
"@onkernel/sdk": "^0.78.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/redis": "^4.0.11",
Expand All @@ -55,6 +59,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.11",
"@types/bun": "^1.3.14",
"@types/jszip": "^3.4.1",
"@types/node": "^20",
"@types/react": "^19",
Expand Down
89 changes: 89 additions & 0 deletions scripts/build-managed-auth-app.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Builds the managed-auth MCP App into a single self-contained HTML bundle.
// The --check mode is byte-exact, and Bun's minifier output can change between
// releases, so the bundle is only reproducible with the Bun version pinned in
// .github/workflows/ci.yml (currently 1.3.3). Regenerate the bundle with that
// exact version: bun run build:managed-auth-app
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";

const root = resolve(import.meta.dirname, "..");
const entrypoint = join(root, "src/lib/mcp/apps/managed-auth-entry.tsx");
const generatedPath = join(
root,
"src/lib/mcp/apps/generated/managed-auth-app.ts",
);
const check = process.argv.includes("--check");
const temp = await mkdtemp(join(tmpdir(), "kernel-managed-auth-app-"));

try {
const build = await Bun.build({
entrypoints: [entrypoint],
outdir: temp,
target: "browser",
format: "esm",
minify: true,
splitting: false,
sourcemap: "none",
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
});

if (!build.success) {
for (const log of build.logs) console.error(log);
process.exitCode = 1;
} else {
let javascript = "";
let css = "";
for (const output of build.outputs) {
if (output.path.endsWith(".js")) javascript += await output.text();
if (output.path.endsWith(".css")) css += await output.text();
}
if (!javascript)
throw new Error("Bun did not emit managed-auth JavaScript");

const escapeScript = (value) => value.replaceAll("</script", "<\\/script");
const escapeStyle = (value) => value.replaceAll("</style", "<\\/style");
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Kernel Managed Authentication</title>
<style>${escapeStyle(css)}
html,body,#root{margin:0;min-height:100%;background:var(--color-background-primary,transparent)}
.kernel-app-loading,.kernel-app-status,.kernel-app-fallback{font-family:ui-sans-serif,system-ui,sans-serif;padding:16px;text-align:center}
.kernel-app-actions{display:flex;flex-direction:column;align-items:center;gap:8px;padding:0 16px 16px}
.kernel-app-button{appearance:none;border:0;border-radius:8px;background:#81b300;color:#fff;cursor:pointer;font:600 14px ui-sans-serif,system-ui,sans-serif;padding:10px 16px}
.kernel-app-button:hover{background:#709c00}
</style>
</head>
<body><div id="root"><div class="kernel-app-loading">Preparing secure login…</div></div>
<script type="module">${escapeScript(javascript)}</script>
</body>
</html>`;
const generated = `// Generated by scripts/build-managed-auth-app.mjs. Do not edit.\nexport const MANAGED_AUTH_APP_HTML = ${JSON.stringify(html)};\n`;

if (check) {
let current = "";
try {
current = await readFile(generatedPath, "utf8");
} catch {
// Report the same actionable stale-bundle error below.
}
if (current !== generated) {
console.error(
"Managed-auth App bundle is stale. Run: bun run build:managed-auth-app",
);
process.exitCode = 1;
}
} else {
await mkdir(resolve(generatedPath, ".."), { recursive: true });
await writeFile(generatedPath, generated);
console.log(`Generated ${generatedPath}`);
}
}
} finally {
await rm(temp, { recursive: true, force: true });
}
32 changes: 32 additions & 0 deletions src/app/[transport]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ import { verifyToken } from "@clerk/nextjs/server";
import { NextRequest } from "next/server";
import { isValidJwtFormat } from "@/lib/auth-utils";
import { registerMcpCapabilities } from "@/lib/mcp/register";
import { initializeDeclaresMcpApps } from "@/lib/mcp/tools/auth-login-app";
import { markMcpAppsClient } from "@/lib/redis";

// The streamable-HTTP transport is stateless (one McpServer per request), so
// the client's declared MCP Apps capability is invisible to later tool calls.
// Observe initialize here — where the bearer token is available — and record
// the capability per token so app-only tools can fail closed on hosts that
// never declared MCP Apps support.
async function recordMcpAppsCapability(
req: NextRequest,
token: string,
ttlSeconds: number,
): Promise<void> {
if (req.method !== "POST") return;
try {
const body = await req.clone().json();
if (!initializeDeclaresMcpApps(body)) return;
await markMcpAppsClient({ token, ttlSeconds });
} catch (error) {
// Never block the initialize handshake on the marker; gated tools fail
// closed if the record is missing.
console.error("Failed to record MCP Apps capability:", error);
}
Comment thread
rgarcia marked this conversation as resolved.
}

export async function OPTIONS(_req: NextRequest): Promise<Response> {
return new Response(null, {
Expand Down Expand Up @@ -59,6 +83,9 @@ async function handleAuthenticatedRequest(req: NextRequest): Promise<Response> {
}

if (!isValidJwtFormat(token)) {
// Static API keys have no expiry; use a long sliding TTL (refreshed on
// every gated app-only call and on re-initialize).
await recordMcpAppsCapability(req, token, 24 * 60 * 60);
const authHandler = withMcpAuth(
handler,
async () => ({
Expand Down Expand Up @@ -87,6 +114,11 @@ async function handleAuthenticatedRequest(req: NextRequest): Promise<Response> {
);
}

// The marker is keyed by the session (sid), which survives access-token
// refresh, so bind it to the long sliding TTL (refreshed on every gated
// app-only call) rather than this one token's lifetime.
await recordMcpAppsCapability(req, token, 24 * 60 * 60);

// Create authenticated handler with auth info
const authHandler = withMcpAuth(
handler,
Expand Down
Loading
Loading