Skip to content
Merged
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
8 changes: 4 additions & 4 deletions src/lib/mcp/analytics-context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const MCP_INTENT_ARGUMENT_DESCRIPTION =
"Why this tool is being called and how it fits the user's overall goal, in 15-25 words, " +
"third person. Used for product analytics. Never restate argument values, and never " +
"include credentials, tokens, URLs, file contents, or personal data. Example: " +
'"Inspecting a running browser session to diagnose a checkout automation that stopped ' +
"why this tool is being called and how it fits the user's overall goal, in 15-25 words, " +
"third person. used for product analytics. never restate argument values, and never " +
"include credentials, tokens, urls, file contents, or personal data. example: " +
'"inspecting a running browser session to diagnose a checkout automation that stopped ' +
'responding partway through the flow."';
2 changes: 1 addition & 1 deletion src/lib/mcp/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,7 @@ describe("instrumentMcpAnalytics (SDK integration)", () => {
});
expect(rejectedVaultInput.isError).toBe(true);
expect(JSON.stringify(rejectedVaultInput)).toContain(
"Invalid vault tool input",
"invalid vault tool input",
);
expect(JSON.stringify(rejectedVaultInput)).not.toContain(
"never-echo-this",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/mcp/browser-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
test("browser create config rejects an empty start URL", () => {
expect(buildBrowserCreateConfig({ start_url: "" })).toEqual({
ok: false,
error: "Error: start_url must be a valid URL.",
error: "error: start_url must be a valid url.",
});
});

Expand Down
8 changes: 4 additions & 4 deletions src/lib/mcp/browser-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function configValue<T>(value: T): BrowserConfigResult<T> {
}

function configError<T>(message: string): BrowserConfigResult<T> {
return { ok: false, error: `Error: ${message}` };
return { ok: false, error: `error: ${message}` };
}

function buildBrowserStartUrl(
Expand All @@ -103,7 +103,7 @@ function buildBrowserStartUrl(
try {
new URL(startUrl);
} catch {
return configError("start_url must be a valid URL.");
return configError("start_url must be a valid url.");
}

return configValue(startUrl);
Expand All @@ -113,7 +113,7 @@ function buildBrowserProfile(
params: BrowserProfileParams,
): BrowserConfigResult<BrowserProfileConfig | undefined> {
if (params.profile_name && params.profile_id) {
return configError("Cannot specify both profile_name and profile_id.");
return configError("cannot specify both profile_name and profile_id.");
}
if (
params.save_profile_changes !== undefined &&
Expand All @@ -138,7 +138,7 @@ function buildBrowserExtensions(
params: BrowserExtensionParams,
): BrowserConfigResult<BrowserExtensionConfig | undefined> {
if (params.extension_id && params.extension_name) {
return configError("Cannot specify both extension_id and extension_name.");
return configError("cannot specify both extension_id and extension_name.");
}
if (!params.extension_id && !params.extension_name)
return configValue(undefined);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/mcp/project-selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ describe("projectIDForOperation", () => {
scopes: [],
};
expect(() => connectionContextFromAuthInfo(info)).toThrow(
"Kernel connection scope is unavailable",
"KERNEL connection scope is unavailable",
);
});
});
6 changes: 3 additions & 3 deletions src/lib/mcp/project-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { z } from "zod";
import type { McpConnectionContext } from "@/lib/mcp/auth-context";

const DEFAULT_PROJECT_DESCRIPTION =
"Optional project name or ID used to scope this operation. On organization-wide connections, omit it to use the API's organization-wide or default-project behavior. On project-scoped connections, omit it or pass the fixed project returned by get_connection_context.";
"optional project name or id used to scope this operation. on organization-wide connections, omit it to use the api's organization-wide or default-project behavior. on project-scoped connections, omit it or pass the fixed project returned by get_connection_context.";

const DEFAULT_PROJECT_ID_DESCRIPTION =
"Deprecated: use `project` instead. Optional project ID used to scope this operation. On organization-wide connections, omit it to use the API's organization-wide or default-project behavior. On project-scoped connections, omit it or pass the fixed project ID returned by get_connection_context.";
"deprecated: use `project` instead. optional project id used to scope this operation. on organization-wide connections, omit it to use the api's organization-wide or default-project behavior. on project-scoped connections, omit it or pass the fixed project id returned by get_connection_context.";

export type ProjectSelection = {
project?: string;
Expand Down Expand Up @@ -46,7 +46,7 @@ export function connectionContextFromAuthInfo(
): McpConnectionContext {
const context = authInfo.extra?.connectionContext;
if (!context || typeof context !== "object" || !("scope" in context)) {
throw new Error("Kernel connection scope is unavailable");
throw new Error("KERNEL connection scope is unavailable");
}
return context as McpConnectionContext;
}
Expand Down
90 changes: 66 additions & 24 deletions src/lib/mcp/prompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,54 @@ function schemaNames(value: unknown): string[] {
]);
}

function connectAdvertisedMcp() {
return connectTestMcp((server) => {
registerMcpCapabilities(server, {
mcpApps: true,
vaults: true,
search: true,
});
instrumentMcpAnalytics(server, null);
}, {});
}

async function advertisedTexts(
mcp: Awaited<ReturnType<typeof connectAdvertisedMcp>>,
) {
const promptText = async (name: string, args: Record<string, string>) => {
const result = await mcp.client.getPrompt({ name, arguments: args });
const content = result.messages[0].content;
return content.type === "text" ? content.text : "";
};
const { tools } = await mcp.client.listTools();
const { prompts } = await mcp.client.listPrompts();
const texts = [...describedText(tools), ...describedText(prompts)];
for (const concept of ["browsers", "apps", "overview"]) {
texts.push(await promptText("kernel-concepts", { concept }));
}
texts.push(
await promptText("debug-browser-session", {
session_id: "session_123",
issue_description: "page fails to load",
}),
);
return texts;
}

// Removes the machine-readable parts of advertised prose: code, quoted literal
// values, URLs, paths, and constant-style identifiers such as env vars and
// error codes.
function stripLiterals(text: string) {
return text
.replace(/```[\s\S]*?```/g, "")
.replace(/`[^`]*`/g, "")
.replace(/"[^"]*"/g, "")
.replace(/https?:\/\/\S+/g, "")
.replace(/(?<![\w.])\/[\w\-.{}:/*]+/g, "")
.replace(/\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g, "")
.replace(/\bKERNEL\b/g, "");
}

describe("mcp browser positioning", () => {
test("describes site compatibility with authorization language", async () => {
const mcp = await connectTestMcp(registerBrowserCapabilities, {});
Expand All @@ -64,32 +112,10 @@ describe("mcp browser positioning", () => {
});

test("keeps advertised metadata and prompts free of access-evasion language", async () => {
const mcp = await connectTestMcp((server) => {
registerMcpCapabilities(server, {
mcpApps: true,
vaults: true,
search: true,
});
instrumentMcpAnalytics(server, null);
}, {});
const promptText = async (name: string, args: Record<string, string>) => {
const result = await mcp.client.getPrompt({ name, arguments: args });
const content = result.messages[0].content;
return content.type === "text" ? content.text : "";
};
const mcp = await connectAdvertisedMcp();
try {
const { tools } = await mcp.client.listTools();
const { prompts } = await mcp.client.listPrompts();
const texts = [...describedText(tools), ...describedText(prompts)];
for (const concept of ["browsers", "apps", "overview"]) {
texts.push(await promptText("kernel-concepts", { concept }));
}
texts.push(
await promptText("debug-browser-session", {
session_id: "session_123",
issue_description: "page fails to load",
}),
);
const texts = await advertisedTexts(mcp);

expect(
texts.flatMap((text) => text.match(BLOCKED_LANGUAGE) ?? []),
Expand All @@ -110,6 +136,22 @@ describe("mcp browser positioning", () => {
}
});

test("keeps advertised metadata and prompts in brand casing", async () => {
const mcp = await connectAdvertisedMcp();
try {
const prose = (await advertisedTexts(mcp)).map(stripLiterals);

expect(prose.flatMap((text) => text.match(/\S*[A-Z]\S*/g) ?? [])).toEqual(
[],
);
expect(prose.flatMap((text) => text.match(/\bkernel\b/g) ?? [])).toEqual(
[],
);
} finally {
await mcp.close();
}
});

test.each(["browsers", "apps", "overview"])(
"keeps the %s concept prompt focused on browser workflows",
async (concept) => {
Expand Down
Loading
Loading