@mehmoodqureshi/chrome-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/shared/download.d.ts +15 -0
- package/dist/shared/download.js +0 -0
- package/dist/shared/protocol.d.ts +114 -0
- package/dist/shared/protocol.js +55 -0
- package/dist/src/bridge/auth.d.ts +32 -0
- package/dist/src/bridge/auth.js +76 -0
- package/dist/src/bridge/connection.d.ts +48 -0
- package/dist/src/bridge/connection.js +192 -0
- package/dist/src/bridge/datadir.d.ts +8 -0
- package/dist/src/bridge/datadir.js +22 -0
- package/dist/src/bridge/server.d.ts +58 -0
- package/dist/src/bridge/server.js +178 -0
- package/dist/src/cli.d.ts +11 -0
- package/dist/src/cli.js +93 -0
- package/dist/src/config.d.ts +42 -0
- package/dist/src/config.js +188 -0
- package/dist/src/executor/cdp-executor.d.ts +131 -0
- package/dist/src/executor/cdp-executor.js +422 -0
- package/dist/src/executor/extension-executor.d.ts +102 -0
- package/dist/src/executor/extension-executor.js +124 -0
- package/dist/src/executor/manager.d.ts +43 -0
- package/dist/src/executor/manager.js +94 -0
- package/dist/src/executor/select.d.ts +23 -0
- package/dist/src/executor/select.js +53 -0
- package/dist/src/executor/stub-executor.d.ts +60 -0
- package/dist/src/executor/stub-executor.js +118 -0
- package/dist/src/executor/types.d.ts +192 -0
- package/dist/src/executor/types.js +24 -0
- package/dist/src/mcp/envelopes.d.ts +13 -0
- package/dist/src/mcp/envelopes.js +30 -0
- package/dist/src/mcp/helpers.d.ts +37 -0
- package/dist/src/mcp/helpers.js +71 -0
- package/dist/src/mcp/markdown-extract.d.ts +9 -0
- package/dist/src/mcp/markdown-extract.js +61 -0
- package/dist/src/mcp/server.d.ts +18 -0
- package/dist/src/mcp/server.js +82 -0
- package/dist/src/mcp/tools.d.ts +32 -0
- package/dist/src/mcp/tools.js +267 -0
- package/dist/src/mcp/validators.d.ts +32 -0
- package/dist/src/mcp/validators.js +104 -0
- package/dist/src/security/policy.d.ts +48 -0
- package/dist/src/security/policy.js +155 -0
- package/docs/BLUEPRINT.md +596 -0
- package/extension-dist/background.js +567 -0
- package/extension-dist/manifest.json +12 -0
- package/extension-dist/options.html +32 -0
- package/extension-dist/options.js +37 -0
- package/package.json +69 -0
- package/scripts/postinstall.js +50 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/mcp/helpers.ts — the high-level tools composed SERVER-SIDE from executor
|
|
4
|
+
* primitives. They never touch the wire directly: `extract_links` and
|
|
5
|
+
* `read_as_markdown` read via primitives; `fill_form` sequences fill+click.
|
|
6
|
+
* (Only `download_file` is privileged and lives on the executor.)
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.extractLinks = extractLinks;
|
|
10
|
+
exports.readAsMarkdown = readAsMarkdown;
|
|
11
|
+
exports.fillForm = fillForm;
|
|
12
|
+
const markdown_extract_1 = require("./markdown-extract");
|
|
13
|
+
/**
|
|
14
|
+
* Collect anchors from the page (or a subtree). Implemented as a single page
|
|
15
|
+
* eval so it is one round-trip; falls back to parsing getHtml if eval is denied.
|
|
16
|
+
*/
|
|
17
|
+
async function extractLinks(ex, args) {
|
|
18
|
+
const root = args.selector ? JSON.stringify(args.selector) : 'null';
|
|
19
|
+
const expr = `(() => {
|
|
20
|
+
const root = ${root} ? document.querySelector(${root}) : document;
|
|
21
|
+
if (!root) return [];
|
|
22
|
+
const here = location.origin;
|
|
23
|
+
return [...root.querySelectorAll('a[href]')].map(a => ({
|
|
24
|
+
href: a.href, text: (a.textContent || '').trim().slice(0, 200),
|
|
25
|
+
})).filter(l => l.href && (${args.sameOriginOnly ? 'l.href.startsWith(here)' : 'true'}));
|
|
26
|
+
})()`;
|
|
27
|
+
const res = await ex.eval(expr, { tabId: args.tabId });
|
|
28
|
+
if (res.ok && Array.isArray(res.value)) {
|
|
29
|
+
return { links: res.value };
|
|
30
|
+
}
|
|
31
|
+
// Fallback: parse hrefs out of the HTML (e.g. when eval is policy-denied).
|
|
32
|
+
const { html } = await ex.getHtml(args.selector ? { selector: args.selector } : undefined, {
|
|
33
|
+
tabId: args.tabId,
|
|
34
|
+
});
|
|
35
|
+
const links = [];
|
|
36
|
+
const re = /<a[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
|
|
37
|
+
let m;
|
|
38
|
+
while ((m = re.exec(html)) !== null) {
|
|
39
|
+
links.push({ href: m[1], text: m[2].replace(/<[^>]+>/g, '').trim().slice(0, 200) });
|
|
40
|
+
}
|
|
41
|
+
return { links };
|
|
42
|
+
}
|
|
43
|
+
/** Read a page (or subtree) as readable markdown. */
|
|
44
|
+
async function readAsMarkdown(ex, args) {
|
|
45
|
+
const { html } = await ex.getHtml(args.selector ? { selector: args.selector } : undefined, {
|
|
46
|
+
tabId: args.tabId,
|
|
47
|
+
});
|
|
48
|
+
return (0, markdown_extract_1.htmlToMarkdown)(html);
|
|
49
|
+
}
|
|
50
|
+
/** Fill a set of fields (keyed by selector) and optionally submit. */
|
|
51
|
+
async function fillForm(ex, args) {
|
|
52
|
+
let filled = 0;
|
|
53
|
+
for (const [selector, value] of Object.entries(args.fields)) {
|
|
54
|
+
const target = { selector };
|
|
55
|
+
if (typeof value === 'boolean') {
|
|
56
|
+
// Checkbox/radio: a click toggles it.
|
|
57
|
+
await ex.click(target, { tabId: args.tabId });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
await ex.fill(target, value, { tabId: args.tabId });
|
|
61
|
+
}
|
|
62
|
+
filled++;
|
|
63
|
+
}
|
|
64
|
+
let submitted = false;
|
|
65
|
+
if (args.submitSelector) {
|
|
66
|
+
await ex.click({ selector: args.submitSelector }, { tabId: args.tabId });
|
|
67
|
+
submitted = true;
|
|
68
|
+
}
|
|
69
|
+
return { filled, submitted };
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/mcp/markdown-extract.ts — a dependency-free HTML → readable-markdown
|
|
3
|
+
* reducer. Not a full Readability port, but it drops non-content chrome
|
|
4
|
+
* (script/style/nav/header/footer/aside/form), and converts the common block
|
|
5
|
+
* and inline structure (headings, lists, links, emphasis, code, blockquote,
|
|
6
|
+
* hr) to markdown, then collapses whitespace.
|
|
7
|
+
*/
|
|
8
|
+
/** Convert an HTML string to readable markdown. */
|
|
9
|
+
export declare function htmlToMarkdown(html: string): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/mcp/markdown-extract.ts — a dependency-free HTML → readable-markdown
|
|
4
|
+
* reducer. Not a full Readability port, but it drops non-content chrome
|
|
5
|
+
* (script/style/nav/header/footer/aside/form), and converts the common block
|
|
6
|
+
* and inline structure (headings, lists, links, emphasis, code, blockquote,
|
|
7
|
+
* hr) to markdown, then collapses whitespace.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.htmlToMarkdown = htmlToMarkdown;
|
|
11
|
+
/** Convert an HTML string to readable markdown. */
|
|
12
|
+
function htmlToMarkdown(html) {
|
|
13
|
+
let s = html;
|
|
14
|
+
// Drop comments and whole non-content subtrees.
|
|
15
|
+
s = s.replace(/<!--[\s\S]*?-->/g, '');
|
|
16
|
+
s = s.replace(/<(script|style|noscript|head|svg|nav|header|footer|aside|form|template)[\s\S]*?<\/\1>/gi, '');
|
|
17
|
+
// Headings.
|
|
18
|
+
for (let i = 1; i <= 6; i++) {
|
|
19
|
+
const re = new RegExp(`<\\s*h${i}[^>]*>([\\s\\S]*?)<\\/h${i}>`, 'gi');
|
|
20
|
+
s = s.replace(re, (_m, t) => `\n\n${'#'.repeat(i)} ${strip(t)}\n\n`);
|
|
21
|
+
}
|
|
22
|
+
// Lists: ordered items get "1.", unordered get "-". (Flat; nesting is lossy.)
|
|
23
|
+
s = s.replace(/<\s*ol[^>]*>([\s\S]*?)<\/ol>/gi, (_m, inner) => {
|
|
24
|
+
let n = 0;
|
|
25
|
+
return '\n' + inner.replace(/<\s*li[^>]*>([\s\S]*?)<\/li>/gi, (_x, t) => `\n${++n}. ${strip(t)}`) + '\n';
|
|
26
|
+
});
|
|
27
|
+
s = s.replace(/<\s*li[^>]*>([\s\S]*?)<\/li>/gi, (_m, t) => `\n- ${strip(t)}`);
|
|
28
|
+
// Blockquote, hr, code.
|
|
29
|
+
s = s.replace(/<\s*blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_m, t) => `\n\n> ${strip(t)}\n\n`);
|
|
30
|
+
s = s.replace(/<\s*hr[^>]*\/?\s*>/gi, '\n\n---\n\n');
|
|
31
|
+
s = s.replace(/<\s*(pre|code)[^>]*>([\s\S]*?)<\/\1>/gi, (_m, _tag, t) => `\`${strip(t)}\``);
|
|
32
|
+
// Inline emphasis.
|
|
33
|
+
s = s.replace(/<\s*(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, (_m, _t, t) => `**${strip(t)}**`);
|
|
34
|
+
s = s.replace(/<\s*(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, (_m, _t, t) => `_${strip(t)}_`);
|
|
35
|
+
// Links.
|
|
36
|
+
s = s.replace(/<\s*a[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_m, href, t) => {
|
|
37
|
+
const label = strip(t);
|
|
38
|
+
return label ? `[${label}](${href})` : '';
|
|
39
|
+
});
|
|
40
|
+
// Block boundaries → newlines.
|
|
41
|
+
s = s.replace(/<\s*(p|br|div|section|article|tr|table|ul|ol)[^>]*>/gi, '\n');
|
|
42
|
+
// Remove remaining tags, decode entities, collapse blank runs.
|
|
43
|
+
s = strip(s);
|
|
44
|
+
s = s.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n');
|
|
45
|
+
return s.trim();
|
|
46
|
+
}
|
|
47
|
+
function strip(fragment) {
|
|
48
|
+
return decodeEntities(fragment.replace(/<[^>]+>/g, '')).replace(/[ \t]+/g, ' ').trim();
|
|
49
|
+
}
|
|
50
|
+
function decodeEntities(s) {
|
|
51
|
+
return s
|
|
52
|
+
.replace(/ /g, ' ')
|
|
53
|
+
.replace(/&/g, '&')
|
|
54
|
+
.replace(/</g, '<')
|
|
55
|
+
.replace(/>/g, '>')
|
|
56
|
+
.replace(/"/g, '"')
|
|
57
|
+
.replace(/'/g, "'")
|
|
58
|
+
.replace(/—/g, '—')
|
|
59
|
+
.replace(/…/g, '…');
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=markdown-extract.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/mcp/server.ts — MCP server bootstrap over stdio.
|
|
3
|
+
*
|
|
4
|
+
* Wires the SDK `Server` to a `StdioServerTransport` so an MCP host (Claude)
|
|
5
|
+
* drives chrome-mcp over JSON-RPC on stdin/stdout. CRITICAL: in stdio mode
|
|
6
|
+
* NOTHING may be written to stdout except the JSON-RPC stream — all diagnostics
|
|
7
|
+
* go to stderr via `logErr`.
|
|
8
|
+
*/
|
|
9
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
|
+
/** stderr only — never stdout in stdio mode. */
|
|
11
|
+
export declare function logErr(message: string): void;
|
|
12
|
+
/** Build a fresh `Server` with the full tool surface registered (no transport). */
|
|
13
|
+
export declare function createServer(): Server;
|
|
14
|
+
/** Start over stdio. Idempotent. */
|
|
15
|
+
export declare function startMcpServer(): Promise<void>;
|
|
16
|
+
/** Stop and release the transport. Idempotent, best-effort. */
|
|
17
|
+
export declare function stopMcpServer(): Promise<void>;
|
|
18
|
+
export declare function isMcpServerRunning(): boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/mcp/server.ts — MCP server bootstrap over stdio.
|
|
4
|
+
*
|
|
5
|
+
* Wires the SDK `Server` to a `StdioServerTransport` so an MCP host (Claude)
|
|
6
|
+
* drives chrome-mcp over JSON-RPC on stdin/stdout. CRITICAL: in stdio mode
|
|
7
|
+
* NOTHING may be written to stdout except the JSON-RPC stream — all diagnostics
|
|
8
|
+
* go to stderr via `logErr`.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.logErr = logErr;
|
|
12
|
+
exports.createServer = createServer;
|
|
13
|
+
exports.startMcpServer = startMcpServer;
|
|
14
|
+
exports.stopMcpServer = stopMcpServer;
|
|
15
|
+
exports.isMcpServerRunning = isMcpServerRunning;
|
|
16
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
17
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
18
|
+
const tools_1 = require("./tools");
|
|
19
|
+
const SERVER_NAME = 'chrome-mcp';
|
|
20
|
+
const SERVER_VERSION = '0.1.0';
|
|
21
|
+
let server = null;
|
|
22
|
+
let transport = null;
|
|
23
|
+
/** stderr only — never stdout in stdio mode. */
|
|
24
|
+
function logErr(message) {
|
|
25
|
+
process.stderr.write(`[chrome-mcp] ${message}\n`);
|
|
26
|
+
}
|
|
27
|
+
/** Build a fresh `Server` with the full tool surface registered (no transport). */
|
|
28
|
+
function createServer() {
|
|
29
|
+
const srv = new index_js_1.Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
|
|
30
|
+
(0, tools_1.registerTools)(srv);
|
|
31
|
+
srv.onerror = (err) => {
|
|
32
|
+
logErr(`server error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
33
|
+
};
|
|
34
|
+
return srv;
|
|
35
|
+
}
|
|
36
|
+
/** Start over stdio. Idempotent. */
|
|
37
|
+
async function startMcpServer() {
|
|
38
|
+
if (server) {
|
|
39
|
+
logErr('startMcpServer called but already running; ignoring.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const srv = createServer();
|
|
43
|
+
const tx = new stdio_js_1.StdioServerTransport();
|
|
44
|
+
try {
|
|
45
|
+
await srv.connect(tx);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
logErr(`failed to connect stdio transport: ${String(err)}`);
|
|
49
|
+
server = null;
|
|
50
|
+
transport = null;
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
server = srv;
|
|
54
|
+
transport = tx;
|
|
55
|
+
logErr(`${SERVER_NAME} v${SERVER_VERSION} connected over stdio.`);
|
|
56
|
+
}
|
|
57
|
+
/** Stop and release the transport. Idempotent, best-effort. */
|
|
58
|
+
async function stopMcpServer() {
|
|
59
|
+
const srv = server;
|
|
60
|
+
if (!srv)
|
|
61
|
+
return;
|
|
62
|
+
server = null;
|
|
63
|
+
const tx = transport;
|
|
64
|
+
transport = null;
|
|
65
|
+
try {
|
|
66
|
+
await srv.close();
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
logErr(`error closing server: ${String(err)}`);
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
await tx?.close();
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
logErr(`error closing transport: ${String(err)}`);
|
|
76
|
+
}
|
|
77
|
+
logErr('MCP server stopped.');
|
|
78
|
+
}
|
|
79
|
+
function isMcpServerRunning() {
|
|
80
|
+
return server !== null;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/mcp/tools.ts — the MCP tool surface: the advertised catalog
|
|
3
|
+
* (`TOOL_DEFINITIONS`), the name→handler dispatch (`TOOL_HANDLERS`), the
|
|
4
|
+
* never-throw firewall (`dispatchToolCall`), and `registerTools()` which wires
|
|
5
|
+
* both onto a `Server`.
|
|
6
|
+
*
|
|
7
|
+
* Each handler: validate args → **policy-gate against the relevant URL** → call
|
|
8
|
+
* the active Executor (or a server-side helper) → serialize via an envelope.
|
|
9
|
+
* Nothing here throws to the transport: `dispatchToolCall` renders any thrown
|
|
10
|
+
* `Error` as an `isError` result.
|
|
11
|
+
*/
|
|
12
|
+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
13
|
+
import { type CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
+
import type { Executor } from '../executor/types';
|
|
15
|
+
import { type Policy } from '../security/policy';
|
|
16
|
+
export interface ToolDefinition {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
inputSchema: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export declare const TOOL_DEFINITIONS: ToolDefinition[];
|
|
22
|
+
interface ToolCtx {
|
|
23
|
+
ex: Executor;
|
|
24
|
+
policy: Policy;
|
|
25
|
+
}
|
|
26
|
+
type ToolHandler = (args: Record<string, unknown>, ctx: ToolCtx) => Promise<CallToolResult>;
|
|
27
|
+
export declare const TOOL_HANDLERS: Record<string, ToolHandler>;
|
|
28
|
+
export declare function dispatchToolCall(name: string, rawArgs: unknown): Promise<CallToolResult>;
|
|
29
|
+
/** Assert the catalog and the dispatch table describe the same tool set. */
|
|
30
|
+
export declare function assertNoDrift(): void;
|
|
31
|
+
export declare function registerTools(server: Server): void;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/mcp/tools.ts — the MCP tool surface: the advertised catalog
|
|
4
|
+
* (`TOOL_DEFINITIONS`), the name→handler dispatch (`TOOL_HANDLERS`), the
|
|
5
|
+
* never-throw firewall (`dispatchToolCall`), and `registerTools()` which wires
|
|
6
|
+
* both onto a `Server`.
|
|
7
|
+
*
|
|
8
|
+
* Each handler: validate args → **policy-gate against the relevant URL** → call
|
|
9
|
+
* the active Executor (or a server-side helper) → serialize via an envelope.
|
|
10
|
+
* Nothing here throws to the transport: `dispatchToolCall` renders any thrown
|
|
11
|
+
* `Error` as an `isError` result.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.TOOL_HANDLERS = exports.TOOL_DEFINITIONS = void 0;
|
|
15
|
+
exports.dispatchToolCall = dispatchToolCall;
|
|
16
|
+
exports.assertNoDrift = assertNoDrift;
|
|
17
|
+
exports.registerTools = registerTools;
|
|
18
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
19
|
+
const types_1 = require("../executor/types");
|
|
20
|
+
const manager_1 = require("../executor/manager");
|
|
21
|
+
const policy_1 = require("../security/policy");
|
|
22
|
+
const envelopes_1 = require("./envelopes");
|
|
23
|
+
const helpers_1 = require("./helpers");
|
|
24
|
+
const validators_1 = require("./validators");
|
|
25
|
+
const TARGET_PROPS = {
|
|
26
|
+
selector: { type: 'string', description: 'CSS selector (exactly one of selector|ref)' },
|
|
27
|
+
ref: { type: 'string', description: 'Element ref from a prior read (exactly one of selector|ref)' },
|
|
28
|
+
};
|
|
29
|
+
const obj = (properties, required = []) => ({
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties,
|
|
32
|
+
required,
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
});
|
|
35
|
+
exports.TOOL_DEFINITIONS = [
|
|
36
|
+
{ name: 'tabs_list', description: 'List open browser tabs.', inputSchema: obj({}) },
|
|
37
|
+
{ name: 'tab_select', description: 'Make a tab active by tabId.', inputSchema: obj({ tabId: { type: 'string' } }, ['tabId']) },
|
|
38
|
+
{ name: 'tab_new', description: 'Open a new tab, optionally at a URL.', inputSchema: obj({ url: { type: 'string' } }) },
|
|
39
|
+
{ name: 'tab_close', description: 'Close a tab by tabId.', inputSchema: obj({ tabId: { type: 'string' } }, ['tabId']) },
|
|
40
|
+
{ name: 'navigate', description: 'Navigate the active (or given) tab to a URL.', inputSchema: obj({ url: { type: 'string' }, tabId: { type: 'string' }, waitUntil: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle'] } }, ['url']) },
|
|
41
|
+
{ name: 'back', description: 'Go back in history.', inputSchema: obj({ tabId: { type: 'string' } }) },
|
|
42
|
+
{ name: 'forward', description: 'Go forward in history.', inputSchema: obj({ tabId: { type: 'string' } }) },
|
|
43
|
+
{ name: 'reload', description: 'Reload the active (or given) tab.', inputSchema: obj({ tabId: { type: 'string' }, waitUntil: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle'] } }) },
|
|
44
|
+
{ name: 'click', description: 'Click an element.', inputSchema: obj({ ...TARGET_PROPS, tabId: { type: 'string' }, button: { type: 'string', enum: ['left', 'right', 'middle'] }, clickCount: { type: 'number' } }) },
|
|
45
|
+
{ name: 'type', description: 'Type text into an element.', inputSchema: obj({ ...TARGET_PROPS, text: { type: 'string' }, tabId: { type: 'string' }, clear: { type: 'boolean' }, pressEnter: { type: 'boolean' }, keyEvents: { type: 'boolean' } }, ['text']) },
|
|
46
|
+
{ name: 'press', description: 'Press a key (with optional modifiers).', inputSchema: obj({ key: { type: 'string' }, modifiers: { type: 'array', items: { type: 'string' } }, tabId: { type: 'string' } }, ['key']) },
|
|
47
|
+
{ name: 'hover', description: 'Hover over an element.', inputSchema: obj({ ...TARGET_PROPS, tabId: { type: 'string' } }) },
|
|
48
|
+
{ name: 'scroll', description: 'Scroll the page or to an element.', inputSchema: obj({ ...TARGET_PROPS, x: { type: 'number' }, y: { type: 'number' }, deltaX: { type: 'number' }, deltaY: { type: 'number' }, tabId: { type: 'string' } }) },
|
|
49
|
+
{ name: 'screenshot', description: 'Capture a PNG screenshot (page or element).', inputSchema: obj({ ...TARGET_PROPS, fullPage: { type: 'boolean' }, tabId: { type: 'string' } }) },
|
|
50
|
+
{ name: 'get_text', description: 'Get visible text of the page or an element.', inputSchema: obj({ ...TARGET_PROPS, tabId: { type: 'string' } }) },
|
|
51
|
+
{ name: 'get_html', description: 'Get HTML of the page or an element.', inputSchema: obj({ ...TARGET_PROPS, outer: { type: 'boolean' }, tabId: { type: 'string' } }) },
|
|
52
|
+
{ name: 'eval', description: 'Evaluate JavaScript in the page (disabled in safe-mode).', inputSchema: obj({ expression: { type: 'string' }, awaitPromise: { type: 'boolean' }, tabId: { type: 'string' } }, ['expression']) },
|
|
53
|
+
{ name: 'wait_for', description: 'Wait for a selector or text to appear/disappear.', inputSchema: obj({ selector: { type: 'string' }, textContains: { type: 'string' }, gone: { type: 'boolean' }, timeoutMs: { type: 'number' }, tabId: { type: 'string' } }) },
|
|
54
|
+
{ name: 'extract_links', description: 'Extract anchors from the page or a subtree.', inputSchema: obj({ selector: { type: 'string' }, sameOriginOnly: { type: 'boolean' }, tabId: { type: 'string' } }) },
|
|
55
|
+
{ name: 'read_as_markdown', description: 'Read the page (or subtree) as readable markdown.', inputSchema: obj({ selector: { type: 'string' }, tabId: { type: 'string' } }) },
|
|
56
|
+
{ name: 'fill_form', description: 'Fill multiple fields (keyed by selector) and optionally submit.', inputSchema: obj({ fields: { type: 'object' }, submitSelector: { type: 'string' }, tabId: { type: 'string' } }, ['fields']) },
|
|
57
|
+
{ name: 'download_file', description: 'Download a file by URL or from a link element.', inputSchema: obj({ url: { type: 'string' }, ...TARGET_PROPS, suggestedName: { type: 'string' }, tabId: { type: 'string' } }) },
|
|
58
|
+
{ name: 'chrome_status', description: 'Report backend/session status.', inputSchema: obj({}) },
|
|
59
|
+
];
|
|
60
|
+
/** Resolve the URL the policy should be evaluated against (the active tab). */
|
|
61
|
+
async function activeUrl(ex) {
|
|
62
|
+
try {
|
|
63
|
+
const tabs = await ex.tabsList();
|
|
64
|
+
return tabs.find((t) => t.active)?.url ?? tabs[0]?.url ?? 'about:blank';
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return 'about:blank';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Policy chokepoint. `urlOverride` is the destination for navigation. */
|
|
71
|
+
async function gate(ctx, method, urlOverride) {
|
|
72
|
+
const url = urlOverride ?? (await activeUrl(ctx.ex));
|
|
73
|
+
(0, policy_1.assertUrlAllowed)(url, method, ctx.policy);
|
|
74
|
+
}
|
|
75
|
+
const tabId = (args) => (0, validators_1.optionalString)(args, 'tabId');
|
|
76
|
+
const waitUntil = (args) => (0, validators_1.optionalString)(args, 'waitUntil');
|
|
77
|
+
exports.TOOL_HANDLERS = {
|
|
78
|
+
tabs_list: async (_a, ctx) => (0, envelopes_1.jsonResult)(await ctx.ex.tabsList()),
|
|
79
|
+
tab_select: async (a, ctx) => {
|
|
80
|
+
await gate(ctx, 'tab_select');
|
|
81
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.tabSelect((0, validators_1.requireString)(a, 'tabId')));
|
|
82
|
+
},
|
|
83
|
+
tab_new: async (a, ctx) => {
|
|
84
|
+
await gate(ctx, 'tab_new');
|
|
85
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.tabNew((0, validators_1.optionalString)(a, 'url')));
|
|
86
|
+
},
|
|
87
|
+
tab_close: async (a, ctx) => {
|
|
88
|
+
await gate(ctx, 'tab_close');
|
|
89
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.tabClose((0, validators_1.requireString)(a, 'tabId')));
|
|
90
|
+
},
|
|
91
|
+
navigate: async (a, ctx) => {
|
|
92
|
+
const url = (0, validators_1.requireString)(a, 'url');
|
|
93
|
+
await gate(ctx, 'navigate', url);
|
|
94
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.navigate({ url, tabId: tabId(a), waitUntil: waitUntil(a) }));
|
|
95
|
+
},
|
|
96
|
+
back: async (a, ctx) => {
|
|
97
|
+
await gate(ctx, 'back');
|
|
98
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.back(tabId(a)));
|
|
99
|
+
},
|
|
100
|
+
forward: async (a, ctx) => {
|
|
101
|
+
await gate(ctx, 'forward');
|
|
102
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.forward(tabId(a)));
|
|
103
|
+
},
|
|
104
|
+
reload: async (a, ctx) => {
|
|
105
|
+
await gate(ctx, 'reload');
|
|
106
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.reload({ tabId: tabId(a), waitUntil: waitUntil(a) }));
|
|
107
|
+
},
|
|
108
|
+
click: async (a, ctx) => {
|
|
109
|
+
const t = (0, validators_1.requireTarget)(a);
|
|
110
|
+
await gate(ctx, 'click');
|
|
111
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.click(t, {
|
|
112
|
+
tabId: tabId(a),
|
|
113
|
+
button: (0, validators_1.optionalString)(a, 'button'),
|
|
114
|
+
clickCount: (0, validators_1.optionalNumber)(a, 'clickCount', { min: 1, max: 3 }),
|
|
115
|
+
}));
|
|
116
|
+
},
|
|
117
|
+
type: async (a, ctx) => {
|
|
118
|
+
const t = (0, validators_1.requireTarget)(a);
|
|
119
|
+
await gate(ctx, 'type');
|
|
120
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.type(t, (0, validators_1.requireString)(a, 'text'), {
|
|
121
|
+
tabId: tabId(a),
|
|
122
|
+
clear: (0, validators_1.optionalBoolean)(a, 'clear'),
|
|
123
|
+
pressEnter: (0, validators_1.optionalBoolean)(a, 'pressEnter'),
|
|
124
|
+
keyEvents: (0, validators_1.optionalBoolean)(a, 'keyEvents'),
|
|
125
|
+
}));
|
|
126
|
+
},
|
|
127
|
+
press: async (a, ctx) => {
|
|
128
|
+
await gate(ctx, 'press');
|
|
129
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.press((0, validators_1.requireString)(a, 'key'), {
|
|
130
|
+
tabId: tabId(a),
|
|
131
|
+
modifiers: (0, validators_1.optionalStringArray)(a, 'modifiers'),
|
|
132
|
+
}));
|
|
133
|
+
},
|
|
134
|
+
hover: async (a, ctx) => {
|
|
135
|
+
const t = (0, validators_1.requireTarget)(a);
|
|
136
|
+
await gate(ctx, 'hover');
|
|
137
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.hover(t, { tabId: tabId(a) }));
|
|
138
|
+
},
|
|
139
|
+
scroll: async (a, ctx) => {
|
|
140
|
+
await gate(ctx, 'scroll');
|
|
141
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.scroll({
|
|
142
|
+
tabId: tabId(a),
|
|
143
|
+
x: (0, validators_1.optionalNumber)(a, 'x'),
|
|
144
|
+
y: (0, validators_1.optionalNumber)(a, 'y'),
|
|
145
|
+
deltaX: (0, validators_1.optionalNumber)(a, 'deltaX'),
|
|
146
|
+
deltaY: (0, validators_1.optionalNumber)(a, 'deltaY'),
|
|
147
|
+
target: (0, validators_1.optionalTarget)(a),
|
|
148
|
+
}));
|
|
149
|
+
},
|
|
150
|
+
screenshot: async (a, ctx) => {
|
|
151
|
+
await gate(ctx, 'screenshot');
|
|
152
|
+
const shot = await ctx.ex.screenshot({
|
|
153
|
+
tabId: tabId(a),
|
|
154
|
+
fullPage: (0, validators_1.optionalBoolean)(a, 'fullPage'),
|
|
155
|
+
target: (0, validators_1.optionalTarget)(a),
|
|
156
|
+
});
|
|
157
|
+
const caption = shot.truncated ? `(truncated; full height ${shot.fullHeight}px)` : undefined;
|
|
158
|
+
return (0, envelopes_1.imageResult)(shot.dataBase64, shot.mimeType, caption);
|
|
159
|
+
},
|
|
160
|
+
get_text: async (a, ctx) => {
|
|
161
|
+
await gate(ctx, 'get_text');
|
|
162
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.getText((0, validators_1.optionalTarget)(a), { tabId: tabId(a) }));
|
|
163
|
+
},
|
|
164
|
+
get_html: async (a, ctx) => {
|
|
165
|
+
await gate(ctx, 'get_html');
|
|
166
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.getHtml((0, validators_1.optionalTarget)(a), { tabId: tabId(a), outer: (0, validators_1.optionalBoolean)(a, 'outer') }));
|
|
167
|
+
},
|
|
168
|
+
eval: async (a, ctx) => {
|
|
169
|
+
await gate(ctx, 'eval');
|
|
170
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.eval((0, validators_1.requireString)(a, 'expression'), {
|
|
171
|
+
tabId: tabId(a),
|
|
172
|
+
awaitPromise: (0, validators_1.optionalBoolean)(a, 'awaitPromise'),
|
|
173
|
+
}));
|
|
174
|
+
},
|
|
175
|
+
wait_for: async (a, ctx) => {
|
|
176
|
+
await gate(ctx, 'wait_for');
|
|
177
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.waitFor({
|
|
178
|
+
tabId: tabId(a),
|
|
179
|
+
selector: (0, validators_1.optionalString)(a, 'selector'),
|
|
180
|
+
textContains: (0, validators_1.optionalString)(a, 'textContains'),
|
|
181
|
+
gone: (0, validators_1.optionalBoolean)(a, 'gone'),
|
|
182
|
+
timeoutMs: (0, validators_1.optionalNumber)(a, 'timeoutMs', { min: 0, max: 120_000 }),
|
|
183
|
+
}));
|
|
184
|
+
},
|
|
185
|
+
extract_links: async (a, ctx) => {
|
|
186
|
+
await gate(ctx, 'get_text'); // read of page content
|
|
187
|
+
return (0, envelopes_1.jsonResult)(await (0, helpers_1.extractLinks)(ctx.ex, {
|
|
188
|
+
selector: (0, validators_1.optionalString)(a, 'selector'),
|
|
189
|
+
sameOriginOnly: (0, validators_1.optionalBoolean)(a, 'sameOriginOnly'),
|
|
190
|
+
tabId: tabId(a),
|
|
191
|
+
}));
|
|
192
|
+
},
|
|
193
|
+
read_as_markdown: async (a, ctx) => {
|
|
194
|
+
await gate(ctx, 'get_text');
|
|
195
|
+
return (0, envelopes_1.textResult)(await (0, helpers_1.readAsMarkdown)(ctx.ex, { selector: (0, validators_1.optionalString)(a, 'selector'), tabId: tabId(a) }));
|
|
196
|
+
},
|
|
197
|
+
fill_form: async (a, ctx) => {
|
|
198
|
+
await gate(ctx, 'type'); // mutating
|
|
199
|
+
const fields = a.fields;
|
|
200
|
+
if (typeof fields !== 'object' || fields === null || Array.isArray(fields)) {
|
|
201
|
+
throw new validators_1.McpToolError('"fields" must be an object mapping selector -> string|boolean');
|
|
202
|
+
}
|
|
203
|
+
return (0, envelopes_1.jsonResult)(await (0, helpers_1.fillForm)(ctx.ex, {
|
|
204
|
+
fields: fields,
|
|
205
|
+
submitSelector: (0, validators_1.optionalString)(a, 'submitSelector'),
|
|
206
|
+
tabId: tabId(a),
|
|
207
|
+
}));
|
|
208
|
+
},
|
|
209
|
+
download_file: async (a, ctx) => {
|
|
210
|
+
await gate(ctx, 'download_file');
|
|
211
|
+
const url = (0, validators_1.optionalString)(a, 'url');
|
|
212
|
+
const target = (0, validators_1.optionalTarget)(a);
|
|
213
|
+
if (!url && !target)
|
|
214
|
+
throw new validators_1.McpToolError('provide "url" or a target (selector|ref)');
|
|
215
|
+
return (0, envelopes_1.jsonResult)(await ctx.ex.download({ url, target, tabId: tabId(a), suggestedName: (0, validators_1.optionalString)(a, 'suggestedName') }));
|
|
216
|
+
},
|
|
217
|
+
chrome_status: async (_a, ctx) => (0, envelopes_1.jsonResult)(ctx.ex.status()),
|
|
218
|
+
};
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Dispatch (never-throw firewall)
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
function errMessage(err) {
|
|
223
|
+
if (err instanceof validators_1.McpToolError || err instanceof types_1.ExecutorError)
|
|
224
|
+
return err.message;
|
|
225
|
+
if (err instanceof Error)
|
|
226
|
+
return `internal error: ${err.message}`;
|
|
227
|
+
return `internal error: ${String(err)}`;
|
|
228
|
+
}
|
|
229
|
+
async function dispatchToolCall(name, rawArgs) {
|
|
230
|
+
const handler = exports.TOOL_HANDLERS[name];
|
|
231
|
+
if (!handler)
|
|
232
|
+
return (0, envelopes_1.errorResult)(`unknown tool: ${name}`);
|
|
233
|
+
try {
|
|
234
|
+
const mgr = (0, manager_1.getManager)();
|
|
235
|
+
const ex = await mgr.ensureReady();
|
|
236
|
+
return await handler((0, validators_1.asArgs)(rawArgs), { ex, policy: mgr.policy });
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
return (0, envelopes_1.errorResult)(errMessage(err));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** Assert the catalog and the dispatch table describe the same tool set. */
|
|
243
|
+
function assertNoDrift() {
|
|
244
|
+
const defs = new Set(exports.TOOL_DEFINITIONS.map((d) => d.name));
|
|
245
|
+
const handlers = new Set(Object.keys(exports.TOOL_HANDLERS));
|
|
246
|
+
for (const n of defs)
|
|
247
|
+
if (!handlers.has(n))
|
|
248
|
+
throw new Error(`tool "${n}" is advertised but has no handler`);
|
|
249
|
+
for (const n of handlers)
|
|
250
|
+
if (!defs.has(n))
|
|
251
|
+
throw new Error(`handler "${n}" has no advertised definition`);
|
|
252
|
+
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Wiring
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
function registerTools(server) {
|
|
257
|
+
assertNoDrift();
|
|
258
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
259
|
+
tools: exports.TOOL_DEFINITIONS.map((d) => ({
|
|
260
|
+
name: d.name,
|
|
261
|
+
description: d.description,
|
|
262
|
+
inputSchema: d.inputSchema,
|
|
263
|
+
})),
|
|
264
|
+
}));
|
|
265
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => dispatchToolCall(req.params.name, req.params.arguments));
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/mcp/validators.ts — lightweight, dependency-free runtime guards for tool
|
|
3
|
+
* arguments. The JSON Schema in `tools.ts` is the advertised contract; these
|
|
4
|
+
* guards defend each handler from malformed input and throw `McpToolError` with
|
|
5
|
+
* an actionable message (rendered as a structured `isError` result upstream).
|
|
6
|
+
*/
|
|
7
|
+
import type { Target } from '../executor/types';
|
|
8
|
+
/**
|
|
9
|
+
* Thrown when a tool request can't be fulfilled for a caller-actionable reason
|
|
10
|
+
* (bad args, exactly-one-of violation, …). The dispatch firewall converts it to
|
|
11
|
+
* an `isError` result, so it never tears down the transport.
|
|
12
|
+
*/
|
|
13
|
+
export declare class McpToolError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
/** Coerce raw tool args into a plain object, rejecting non-objects. */
|
|
17
|
+
export declare function asArgs(raw: unknown): Record<string, unknown>;
|
|
18
|
+
export declare function requireString(args: Record<string, unknown>, key: string): string;
|
|
19
|
+
export declare function optionalString(args: Record<string, unknown>, key: string): string | undefined;
|
|
20
|
+
export declare function optionalBoolean(args: Record<string, unknown>, key: string): boolean | undefined;
|
|
21
|
+
export declare function optionalNumber(args: Record<string, unknown>, key: string, bounds?: {
|
|
22
|
+
min?: number;
|
|
23
|
+
max?: number;
|
|
24
|
+
}): number | undefined;
|
|
25
|
+
export declare function optionalStringArray(args: Record<string, unknown>, key: string): string[] | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Require EXACTLY ONE of `selector` | `ref`. Returns a normalized `Target`.
|
|
28
|
+
* Both-present and neither-present are both errors — the contract is one-of.
|
|
29
|
+
*/
|
|
30
|
+
export declare function requireTarget(args: Record<string, unknown>): Target;
|
|
31
|
+
/** Like `requireTarget` but the target is optional (whole-page reads). */
|
|
32
|
+
export declare function optionalTarget(args: Record<string, unknown>): Target | undefined;
|