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
4 changes: 2 additions & 2 deletions bun.lock

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"@modelcontextprotocol/core": "2.0.0",
"@modelcontextprotocol/server": "2.0.0",
"@onkernel/managed-auth-react": "0.5.5",
"@onkernel/sdk": "0.141.0",
"@onkernel/sdk": "0.148.0",
"@posthog/mcp": "0.17.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/redis": "^4.0.11",
Expand Down
1 change: 1 addition & 0 deletions src/app/[transport]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const ENTITLEMENT_GATED_TOOLS = new Set([
"manage_vaults",
"manage_vault_wallets",
"manage_vault_cards",
"manage_vault_payment_requests",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"manage_vault_credentials",
"manage_vault_items",
"manage_vault_provider_configs",
Expand Down
1 change: 1 addition & 0 deletions src/lib/mcp/prompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const BLOCKED_LANGUAGE =
const BLOCKED_NAMES = new RegExp(`${BLOCKED_LANGUAGE.source}|captcha`, "i");
const API_FIELD_NAMES = new Set([
"manage_browsers:stealth",
"manage_browsers:fingerprint",
"manage_browsers:captcha",
"manage_browser_pools:stealth",
"manage_browser_pools:captcha",
Expand Down
5 changes: 4 additions & 1 deletion src/lib/mcp/register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ const EXPECTED_ANNOTATIONS: Record<string, [boolean, boolean, boolean]> = {
manage_vault_items: [false, true, true],
manage_vault_credentials: [false, true, true],
manage_vault_cards: [false, true, true],
manage_vault_wallets: [false, false, true],
manage_vault_payment_requests: [false, true, true],
manage_vault_wallets: [false, true, true],
manage_vault_provider_configs: [false, true, true],
};

Expand Down Expand Up @@ -224,6 +225,7 @@ describe("MCP toolset allowlist", () => {
"manage_vault_provider_configs",
"manage_vault_wallets",
"manage_vault_cards",
"manage_vault_payment_requests",
"manage_vault_credentials",
"manage_vault_items",
"manage_vaults",
Expand Down Expand Up @@ -297,6 +299,7 @@ describe("project selection registration", () => {
"manage_vaults",
"manage_vault_wallets",
"manage_vault_cards",
"manage_vault_payment_requests",
"manage_vault_credentials",
"manage_vault_items",
"open_auth_login",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/mcp/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ export const telemetryEventCategories = [
"monitor",
] as const;

export const TELEMETRY_EVENT_CATALOG = `event categories: console (console output and uncaught exceptions), network (request/response metadata), page (navigation and lifecycle), interaction (clicks, keys, scrolls), control (agent-driven api calls), platform (KERNEL platform lifecycle and service events), connection (cdp/live-view attach/detach), system (vm health), screenshot (periodic monitor screenshots), captcha (verification prompts a site displayed during the session), monitor (telemetry collector health; captured automatically with any cdp category). high-signal event types: console_error, network_loading_failed, network_response with non-2xx status, system_oom_kill, service_crashed, monitor_disconnected (telemetry gap — treat following events as incomplete).`;
export const TELEMETRY_EVENT_CATALOG = `event categories: console (console output and uncaught exceptions), network (request/response metadata), page (navigation and lifecycle), interaction (clicks, keys, scrolls), control (agent-driven api calls), platform (KERNEL platform lifecycle and service events), connection (cdp/live-view attach/detach), system (vm health), screenshot (periodic monitor screenshots), captcha (verification prompts a site displayed during the session), monitor (telemetry collector health; captured automatically with any cdp category). high-signal event types: console_error, network_loading_failed, network_response with non-2xx status, system_oom_kill, service_crashed, monitor_disconnected (telemetry gap — treat following events as incomplete). browser_proxy_error data.code can be payment_window_expired when the proxy reports that a payment window has expired; unrecognized proxy codes appear as unknown with the original value in raw_code.`;
1 change: 1 addition & 0 deletions src/lib/mcp/tool-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const KERNEL_MCP_TOOL_NAMES = [
"manage_vault_cards",
"manage_vault_credentials",
"manage_vault_items",
"manage_vault_payment_requests",
"manage_vault_provider_configs",
"manage_vault_wallets",
"manage_vaults",
Expand Down
30 changes: 28 additions & 2 deletions src/lib/mcp/tools/api-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,32 @@ async function callAPIKeys(apiKeys: unknown, args: Record<string, unknown>[]) {
}

describe("manage_api_keys SDK parity", () => {
test("advertises last-used sorting and null ordering", async () => {
const { client, close } = await connectTestMcp(
registerAPIKeyCapabilities,
{},
);
try {
const tool = (await client.listTools()).tools.find(
({ name }) => name === "manage_api_keys",
);
expect(tool?.inputSchema.properties?.sort_by).toMatchObject({
enum: ["created_at", "name", "expires_at", "last_used_at"],
description: expect.stringContaining(
"keys missing that value sort last in either direction",
),
});
expect(tool?.description).toContain(
"delete asynchronously ends browser sessions",
);
expect(tool?.description).toContain(
"grace-period or natural expiry does not end existing browsers",
);
} finally {
await close();
}
});

test("rejects an empty api_key_id in the schema", async () => {
let called = false;
const [result] = await callAPIKeys(
Expand Down Expand Up @@ -106,7 +132,7 @@ describe("manage_api_keys SDK parity", () => {
name: "staging",
query: "stage",
status: "all",
sort_by: "expires_at",
sort_by: "last_used_at",
sort_direction: "asc",
limit: 5,
offset: 0,
Expand All @@ -123,7 +149,7 @@ describe("manage_api_keys SDK parity", () => {
name: "staging",
query: "stage",
status: "all",
sort_by: "expires_at",
sort_by: "last_used_at",
sort_direction: "asc",
limit: 5,
offset: 0,
Expand Down
8 changes: 5 additions & 3 deletions src/lib/mcp/tools/api-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function registerAPIKeyCapabilities(
"manage_api_keys",
{
description:
'manage KERNEL api keys. use "create" to create an org-wide, project-scoped, or policy-scoped key, "list" to discover masked keys, "get" to retrieve one masked key, "update" to rename a key, "rotate" to issue a replacement with an optional grace period, or "delete" to revoke a key. list and get include last_used_at, an approximate timestamp accurate within about 20 minutes; it is null when the key has not authenticated a request since last-use tracking began. create and rotate return the new plaintext key once; handle it as a secret and never repeat it in chat.',
'manage KERNEL api keys. use "create" to create an org-wide, project-scoped, or policy-scoped key, "list" to discover masked keys, "get" to retrieve one masked key, "update" to rename a key, "rotate" to issue a replacement with an optional grace period, or "delete" to revoke a key. list and get include last_used_at, an approximate timestamp accurate within about 20 minutes; it is null when the key has not authenticated a request since last-use tracking began. create and rotate return the new plaintext key once; handle it as a secret and never repeat it in chat. delete asynchronously ends browser sessions attributed to the revoked key and discards unsaved profile changes; if cleanup cannot be registered, deletion is not committed. rotate with expire_in_days=0 does the same immediate browser cleanup for the old key; grace-period or natural expiry does not end existing browsers, and an immediate rotation is not committed if cleanup cannot be registered.',
inputSchema: z.object({
action: z
.enum(["create", "list", "get", "update", "rotate", "delete"])
Expand Down Expand Up @@ -115,8 +115,10 @@ export function registerAPIKeyCapabilities(
.describe("(list) key status filter. defaults to active.")
.optional(),
sort_by: z
.enum(["created_at", "name", "expires_at"])
.describe("(list) field to sort by.")
.enum(["created_at", "name", "expires_at", "last_used_at"])
.describe(
"(list) field to sort by. when sorting by expires_at or last_used_at, keys missing that value sort last in either direction.",
)
.optional(),
sort_direction: z
.enum(["asc", "desc"])
Expand Down
21 changes: 18 additions & 3 deletions src/lib/mcp/tools/apps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ describe("manage_apps invocation contract", () => {

try {
const tools = await client.listTools();
expect(
tools.tools.some((tool) => tool.name === "manage_apps"),
).toBeTrue();
const appTool = tools.tools.find((tool) => tool.name === "manage_apps");
expect(appTool).toBeDefined();
expect(appTool?.description).toContain("leased_at");
expect(appTool?.description).toContain("replay metadata");

const result = await client.callTool({
name: "manage_apps",
Expand Down Expand Up @@ -110,6 +111,7 @@ describe("manage_apps invocation contract", () => {
cdp_ws_url: "wss://api.onkernel.com/cdp",
webdriver_ws_url: "wss://api.onkernel.com/webdriver",
created_at: "2026-08-08T00:00:00Z",
leased_at: "2026-08-08T00:05:00Z",
headless: false,
stealth: true,
memory: "2GiB",
Expand All @@ -121,6 +123,12 @@ describe("manage_apps invocation contract", () => {
locale: "en-US",
locale_source: "override",
},
replay: {
replay_id: "rpl_123",
replay_view_url: "https://replay.example/view/rpl_123",
started_at: "2026-08-08T00:00:01Z",
finished_at: null,
},
},
],
};
Expand Down Expand Up @@ -149,12 +157,19 @@ describe("manage_apps invocation contract", () => {
session_id: "brr_123",
browser_live_view_url:
"https://api.onkernel.com/browser/live/signed",
leased_at: "2026-08-08T00:05:00Z",
location: {
timezone: "America/New_York",
timezone_source: "default",
locale: "en-US",
locale_source: "override",
},
replay: {
replay_id: "rpl_123",
replay_view_url: "https://replay.example/view/rpl_123",
started_at: "2026-08-08T00:00:01Z",
finished_at: null,
},
},
],
});
Expand Down
8 changes: 7 additions & 1 deletion src/lib/mcp/tools/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function registerAppCapabilities(
"manage_apps",
{
description:
'manage KERNEL apps when an agent needs to discover deployed app actions, invoke an app, or inspect deployment/invocation state. use "list_apps" before invoking an unknown app. "invoke" starts an action asynchronously and returns an invocation_id immediately. use "list_invocations" to find runs, "list_invocation_browsers" to discover their browser sessions, and "get_invocation" after a short delay to inspect one state. do not poll indefinitely; if a run is still active, report its id. "delete_invocation_browsers" terminates every active browser created by one invocation. deployment and invocation event streams are intentionally represented by bounded list/get calls rather than an open sse stream.',
'manage KERNEL apps when an agent needs to discover deployed app actions, invoke an app, or inspect deployment/invocation state. use "list_apps" before invoking an unknown app. "invoke" starts an action asynchronously and returns an invocation_id immediately. use "list_invocations" to find runs, "list_invocation_browsers" to discover their browser sessions, and "get_invocation" after a short delay to inspect one state. list_invocation_browsers includes leased_at only for leased pooled sessions, showing when each was last leased, and replay metadata when recording started with the browser. do not poll indefinitely; if a run is still active, report its id. "delete_invocation_browsers" terminates every active browser created by one invocation. deployment and invocation event streams are intentionally represented by bounded list/get calls rather than an open sse stream.',
inputSchema: z.object({
...projectSelectionInputSchema(),
action: z
Expand Down Expand Up @@ -280,9 +280,15 @@ export function registerAppCapabilities(
browsers: browsers.browsers.map((browser) => ({
session_id: browser.session_id,
browser_live_view_url: browser.browser_live_view_url,
...(browser.leased_at !== undefined && {
leased_at: browser.leased_at,
}),
...(browser.location !== undefined && {
location: browser.location,
}),
...(browser.replay !== undefined && {
replay: browser.replay,
}),
})),
});
}
Expand Down
Loading
Loading