From 8e6f40f719df3ddd9549e068d5385efe827a2875 Mon Sep 17 00:00:00 2001
From: masnwilliams <43387599+masnwilliams@users.noreply.github.com>
Date: Thu, 17 Sep 2026 02:14:03 +0000
Subject: [PATCH 1/2] Expose config registry recommendations
---
README.md | 1 +
src/lib/mcp/register.test.ts | 2 +
src/lib/mcp/register.ts | 2 +
src/lib/mcp/tool-names.ts | 1 +
src/lib/mcp/tools/config-registry.test.ts | 194 ++++++++++++++++++++++
src/lib/mcp/tools/config-registry.ts | 121 ++++++++++++++
6 files changed, 321 insertions(+)
create mode 100644 src/lib/mcp/tools/config-registry.test.ts
create mode 100644 src/lib/mcp/tools/config-registry.ts
diff --git a/README.md b/README.md
index 734b371..68605dc 100644
--- a/README.md
+++ b/README.md
@@ -328,6 +328,7 @@ See [Vault payments](docs/vault-payments.md) for both provider flows, safety rul
### Standalone tools
- `get_connection_context` - Inspect the authenticated principal, organization, credential scope, and effective project scope.
+- `resolve_browser_config` - Look up cached Config Registry browser and proxy recommendations, start a missing background analysis, and poll an analysis by ID before creating a browser.
- `computer_action` - Mouse, keyboard, clipboard, and screenshot controls for browser sessions (click, type, press_key, scroll, move, get_position, read_clipboard, write_clipboard, screenshot).
- `browser_curl` - Send HTTP requests through an existing browser session's Chrome network stack.
- `execute_playwright_code` - Execute Playwright/TypeScript code and browser-wide WebMCP helpers against an existing browser session. Does not create or delete browsers - use `manage_browsers` for session lifecycle.
diff --git a/src/lib/mcp/register.test.ts b/src/lib/mcp/register.test.ts
index 41e49d5..4fc90c4 100644
--- a/src/lib/mcp/register.test.ts
+++ b/src/lib/mcp/register.test.ts
@@ -8,6 +8,7 @@ const NON_AUTH_TOOLSETS = [
"profiles",
"docs",
"browsers",
+ "config_registry",
"projects",
"api_keys",
"browser_pools",
@@ -180,6 +181,7 @@ describe("project selection registration", () => {
const projectScopedTools = [
"manage_profiles",
"manage_browsers",
+ "resolve_browser_config",
"manage_browser_pools",
"browser_curl",
"manage_proxies",
diff --git a/src/lib/mcp/register.ts b/src/lib/mcp/register.ts
index bc96712..5af41d3 100644
--- a/src/lib/mcp/register.ts
+++ b/src/lib/mcp/register.ts
@@ -13,6 +13,7 @@ import { registerBrowserCurlTool } from "@/lib/mcp/tools/browser-curl";
import { registerBrowserCapabilities } from "@/lib/mcp/tools/browsers";
import { registerComputerActionTool } from "@/lib/mcp/tools/computer-action";
import { registerConnectionContextTool } from "@/lib/mcp/tools/connection-context";
+import { registerConfigRegistryTool } from "@/lib/mcp/tools/config-registry";
import { registerCredentialProviderTools } from "@/lib/mcp/tools/credential-providers";
import { registerCredentialTools } from "@/lib/mcp/tools/credentials";
import { registerDocsTools } from "@/lib/mcp/tools/docs";
@@ -41,6 +42,7 @@ const mcpToolRegistrations = [
["profiles", registerProfileCapabilities],
["docs", registerDocsTools],
["browsers", registerBrowserCapabilities],
+ ["config_registry", registerConfigRegistryTool],
["projects", registerProjectCapabilities],
["api_keys", registerAPIKeyCapabilities],
["browser_pools", registerBrowserPoolCapabilities],
diff --git a/src/lib/mcp/tool-names.ts b/src/lib/mcp/tool-names.ts
index 6d24c0d..1e4c3bb 100644
--- a/src/lib/mcp/tool-names.ts
+++ b/src/lib/mcp/tool-names.ts
@@ -25,6 +25,7 @@ export const KERNEL_MCP_TOOL_NAMES = [
"manage_vault_wallets",
"manage_vaults",
"open_auth_login",
+ "resolve_browser_config",
"search_docs",
"submit_feedback",
"webmcp",
diff --git a/src/lib/mcp/tools/config-registry.test.ts b/src/lib/mcp/tools/config-registry.test.ts
new file mode 100644
index 0000000..ed3ad0f
--- /dev/null
+++ b/src/lib/mcp/tools/config-registry.test.ts
@@ -0,0 +1,194 @@
+///
+
+import { describe, expect, test } from "bun:test";
+import { connectTestMcp, toolResultJSON } from "@/lib/mcp/mcp-test-fixtures";
+import { registerConfigRegistryTool } from "@/lib/mcp/tools/config-registry";
+
+const recommendation = {
+ type: "recommendation",
+ browser: {
+ stealth: false,
+ headless: false,
+ gpu: false,
+ viewport: { width: 1920, height: 1080 },
+ },
+ proxy: {
+ mode: "managed",
+ create: { type: "isp", config: { country: "US" } },
+ },
+ match_scope: "exact",
+ matched_target: "https://example.com/",
+ evidence: {
+ success_rate: 1,
+ sample_size: 3,
+ accessed: 3,
+ blocked: 0,
+ inconclusive: 0,
+ run_count: 1,
+ last_observed_at: "2026-09-17T00:00:00Z",
+ },
+};
+
+function configRegistryClient(
+ calls: Array<{ action: string; value: unknown }>,
+) {
+ return {
+ configRegistry: {
+ lookup: async (value: unknown) => {
+ calls.push({ action: "lookup", value });
+ return {
+ target: {
+ normalized: "https://example.com/",
+ host: "example.com",
+ domain: "example.com",
+ },
+ recommendation,
+ working_configurations: [recommendation],
+ };
+ },
+ resolve: async (value: unknown) => {
+ calls.push({ action: "resolve", value });
+ return {
+ target: {
+ normalized: "https://example.com/",
+ host: "example.com",
+ domain: "example.com",
+ },
+ analysis: {
+ id: "analysis_123",
+ status: "running",
+ failure: null,
+ created_at: "2026-09-17T00:00:00Z",
+ expires_at: "2026-09-17T00:10:00Z",
+ finished_at: null,
+ },
+ recommendation: null,
+ working_configurations: [],
+ };
+ },
+ analyses: {
+ retrieve: async (value: unknown) => {
+ calls.push({ action: "get_analysis", value });
+ return {
+ target: {
+ normalized: "https://example.com/",
+ host: "example.com",
+ domain: "example.com",
+ },
+ analysis: {
+ id: "analysis_123",
+ status: "completed",
+ failure: null,
+ created_at: "2026-09-17T00:00:00Z",
+ expires_at: "2026-09-17T00:10:00Z",
+ finished_at: "2026-09-17T00:02:00Z",
+ },
+ recommendation,
+ working_configurations: [recommendation],
+ };
+ },
+ },
+ },
+ };
+}
+
+describe("resolve_browser_config", () => {
+ test("returns cached recommendations without starting an analysis", async () => {
+ const calls: Array<{ action: string; value: unknown }> = [];
+ const { client, close } = await connectTestMcp(
+ registerConfigRegistryTool,
+ configRegistryClient(calls),
+ );
+
+ try {
+ const result = toolResultJSON(
+ await client.callTool({
+ name: "resolve_browser_config",
+ arguments: {
+ action: "lookup",
+ url: "https://example.com/",
+ allowed_proxy_countries: ["US"],
+ },
+ }),
+ );
+
+ expect(result.recommendation).toEqual(recommendation);
+ expect(calls).toEqual([
+ {
+ action: "lookup",
+ value: {
+ url: "https://example.com/",
+ allowed_proxy_countries: ["US"],
+ },
+ },
+ ]);
+ } finally {
+ await close();
+ }
+ });
+
+ test("starts and polls a missing analysis", async () => {
+ const calls: Array<{ action: string; value: unknown }> = [];
+ const { client, close } = await connectTestMcp(
+ registerConfigRegistryTool,
+ configRegistryClient(calls),
+ );
+
+ try {
+ const started = toolResultJSON(
+ await client.callTool({
+ name: "resolve_browser_config",
+ arguments: {
+ action: "resolve",
+ url: "https://example.com/",
+ intent: "Sign in through the site's normal account login.",
+ },
+ }),
+ );
+ const completed = toolResultJSON(
+ await client.callTool({
+ name: "resolve_browser_config",
+ arguments: {
+ action: "get_analysis",
+ analysis_id: started.analysis.id,
+ },
+ }),
+ );
+
+ expect(completed.analysis.status).toBe("completed");
+ expect(completed.recommendation).toEqual(recommendation);
+ expect(calls).toEqual([
+ {
+ action: "resolve",
+ value: {
+ url: "https://example.com/",
+ intent: "Sign in through the site's normal account login.",
+ },
+ },
+ { action: "get_analysis", value: "analysis_123" },
+ ]);
+ } finally {
+ await close();
+ }
+ });
+
+ test("requires action-specific identifiers", async () => {
+ const { client, close } = await connectTestMcp(
+ registerConfigRegistryTool,
+ configRegistryClient([]),
+ );
+
+ try {
+ const result = await client.callTool({
+ name: "resolve_browser_config",
+ arguments: { action: "get_analysis" },
+ });
+ expect(result.isError).toBe(true);
+ expect(JSON.stringify(result.content)).toContain(
+ "analysis_id is required",
+ );
+ } finally {
+ await close();
+ }
+ });
+});
diff --git a/src/lib/mcp/tools/config-registry.ts b/src/lib/mcp/tools/config-registry.ts
new file mode 100644
index 0000000..6251ec1
--- /dev/null
+++ b/src/lib/mcp/tools/config-registry.ts
@@ -0,0 +1,121 @@
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { z } from "zod";
+import {
+ defaultMcpDependencies,
+ type McpDependencies,
+} from "@/lib/mcp/dependencies";
+import {
+ errorResponse,
+ jsonResponse,
+ throwToolError,
+} from "@/lib/mcp/responses";
+import {
+ projectForOperation,
+ projectSelectionInputSchema,
+} from "@/lib/mcp/project-selection";
+
+const httpUrlSchema = z
+ .string()
+ .url()
+ .refine(
+ (value) => {
+ const protocol = new URL(value).protocol;
+ return protocol === "http:" || protocol === "https:";
+ },
+ { message: "URL must use http or https." },
+ );
+
+export function registerConfigRegistryTool(
+ server: McpServer,
+ options: McpDependencies = defaultMcpDependencies,
+) {
+ server.tool(
+ "resolve_browser_config",
+ 'Look up or resolve the Config Registry browser and proxy recommendation for a target before creating a browser. Call "lookup" first; it returns cached global knowledge immediately without starting work. If it returns null or type "no_recommendation", call "resolve" once with the intended workload, then poll "get_analysis" with the returned analysis_id at bounded intervals until terminal. Apply the returned browser settings unchanged. For a managed proxy recipe, create that proxy once with manage_proxies and reuse its ID when creating the browser.',
+ {
+ ...projectSelectionInputSchema(),
+ action: z
+ .enum(["lookup", "resolve", "get_analysis"])
+ .describe("Operation to perform."),
+ url: httpUrlSchema
+ .describe("(lookup, resolve) Public target URL.")
+ .optional(),
+ allowed_proxy_countries: z
+ .array(z.string().regex(/^[A-Za-z]{2}$/))
+ .min(1)
+ .max(10)
+ .describe(
+ "(lookup, resolve) ISO 3166 country codes allowed for a returned managed proxy.",
+ )
+ .optional(),
+ intent: z
+ .string()
+ .max(300)
+ .describe(
+ "(resolve) Plain-language workload to exercise while analyzing the target.",
+ )
+ .optional(),
+ analysis_id: z
+ .string()
+ .describe("(get_analysis) Analysis ID returned by resolve.")
+ .optional(),
+ },
+ {
+ title: "Resolve a Kernel browser configuration",
+ readOnlyHint: false,
+ destructiveHint: false,
+ idempotentHint: false,
+ openWorldHint: true,
+ },
+ async (params, extra) => {
+ if (!extra.authInfo) throw new Error("Authentication required");
+ const client = options.createKernelClient(
+ extra.authInfo.token,
+ projectForOperation(extra.authInfo, params),
+ );
+
+ try {
+ switch (params.action) {
+ case "lookup": {
+ if (!params.url) {
+ return errorResponse("Error: url is required for lookup.");
+ }
+ const result = await client.configRegistry.lookup({
+ url: params.url,
+ ...(params.allowed_proxy_countries && {
+ allowed_proxy_countries: params.allowed_proxy_countries,
+ }),
+ });
+ return jsonResponse(result);
+ }
+ case "resolve": {
+ if (!params.url) {
+ return errorResponse("Error: url is required for resolve.");
+ }
+ const result = await client.configRegistry.resolve({
+ url: params.url,
+ ...(params.allowed_proxy_countries && {
+ allowed_proxy_countries: params.allowed_proxy_countries,
+ }),
+ ...(params.intent && { intent: params.intent }),
+ });
+ return jsonResponse(result);
+ }
+ case "get_analysis": {
+ if (!params.analysis_id) {
+ return errorResponse(
+ "Error: analysis_id is required for get_analysis.",
+ );
+ }
+ const result = await client.configRegistry.analyses.retrieve(
+ params.analysis_id,
+ );
+ return jsonResponse(result);
+ }
+ }
+ } catch (error) {
+ throwToolError("resolve_browser_config", params.action, error);
+ }
+ },
+ );
+}
From b2be582ee7384a797f79201f1d23ad7f786da104 Mon Sep 17 00:00:00 2001
From: masnwilliams <43387599+masnwilliams@users.noreply.github.com>
Date: Thu, 17 Sep 2026 02:19:52 +0000
Subject: [PATCH 2/2] Tighten config registry tool validation
---
src/lib/mcp/register.test.ts | 20 ++++++++++++++++++++
src/lib/mcp/register.ts | 1 +
src/lib/mcp/tools/config-registry.test.ts | 9 +++++++++
src/lib/mcp/tools/config-registry.ts | 1 +
4 files changed, 31 insertions(+)
diff --git a/src/lib/mcp/register.test.ts b/src/lib/mcp/register.test.ts
index 4fc90c4..d2a0b77 100644
--- a/src/lib/mcp/register.test.ts
+++ b/src/lib/mcp/register.test.ts
@@ -147,6 +147,26 @@ describe("MCP toolset allowlist", () => {
},
);
+ test("accepts the config registry standalone tool alias", () => {
+ const previousEnabled = process.env.KERNEL_MCP_ENABLED_TOOLSETS;
+ const previousDisabled = process.env.KERNEL_MCP_DISABLED_TOOLSETS;
+ process.env.KERNEL_MCP_ENABLED_TOOLSETS = "resolve_browser_config";
+ delete process.env.KERNEL_MCP_DISABLED_TOOLSETS;
+ try {
+ expect(captureRegistration(false).legacyTools).toEqual([
+ "get_connection_context",
+ "resolve_browser_config",
+ ]);
+ } finally {
+ if (previousEnabled === undefined)
+ delete process.env.KERNEL_MCP_ENABLED_TOOLSETS;
+ else process.env.KERNEL_MCP_ENABLED_TOOLSETS = previousEnabled;
+ if (previousDisabled === undefined)
+ delete process.env.KERNEL_MCP_DISABLED_TOOLSETS;
+ else process.env.KERNEL_MCP_DISABLED_TOOLSETS = previousDisabled;
+ }
+ });
+
test("keeps connection context and only the selected browser controls", () => {
const previousEnabled = process.env.KERNEL_MCP_ENABLED_TOOLSETS;
const previousDisabled = process.env.KERNEL_MCP_DISABLED_TOOLSETS;
diff --git a/src/lib/mcp/register.ts b/src/lib/mcp/register.ts
index 5af41d3..94db920 100644
--- a/src/lib/mcp/register.ts
+++ b/src/lib/mcp/register.ts
@@ -73,6 +73,7 @@ const standaloneToolsetAliases: Partial> = {
exec_command: "shell",
browser_utilities: "browser_curl",
open_auth_login: "auth_connections",
+ resolve_browser_config: "config_registry",
};
function isMcpToolset(value: string): value is McpToolset {
diff --git a/src/lib/mcp/tools/config-registry.test.ts b/src/lib/mcp/tools/config-registry.test.ts
index ed3ad0f..b4a16e3 100644
--- a/src/lib/mcp/tools/config-registry.test.ts
+++ b/src/lib/mcp/tools/config-registry.test.ts
@@ -187,6 +187,15 @@ describe("resolve_browser_config", () => {
expect(JSON.stringify(result.content)).toContain(
"analysis_id is required",
);
+
+ const empty = await client.callTool({
+ name: "resolve_browser_config",
+ arguments: { action: "get_analysis", analysis_id: "" },
+ });
+ expect(empty.isError).toBe(true);
+ expect(JSON.stringify(empty.content)).toContain(
+ "String must contain at least 1 character",
+ );
} finally {
await close();
}
diff --git a/src/lib/mcp/tools/config-registry.ts b/src/lib/mcp/tools/config-registry.ts
index 6251ec1..45ccfc9 100644
--- a/src/lib/mcp/tools/config-registry.ts
+++ b/src/lib/mcp/tools/config-registry.ts
@@ -57,6 +57,7 @@ export function registerConfigRegistryTool(
.optional(),
analysis_id: z
.string()
+ .min(1)
.describe("(get_analysis) Analysis ID returned by resolve.")
.optional(),
},