cdp-toolkit 1.10.0 → 2.0.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/CHANGELOG.md +60 -0
- package/README.md +61 -12
- package/dist/cli.js +1233 -312
- package/dist/index.js +11 -11
- package/dist/mcp.js +1314 -320
- package/package.json +4 -2
- package/src/bidi/driver.ts +219 -29
- package/src/bidi/launch.ts +51 -1
- package/src/bidi/marionette.ts +276 -0
- package/src/bidi/session-lease.ts +265 -0
- package/src/cli.ts +10 -0
- package/src/install/browser.ts +211 -0
- package/src/install/harness.ts +224 -0
- package/src/install/wizard.ts +412 -0
- package/src/manifest.ts +297 -297
- package/src/mcp.ts +123 -11
- package/src/toolDocs.ts +468 -0
- package/src/toolGroups.ts +49 -0
- package/src/tools/navigation.ts +4 -2
package/src/mcp.ts
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
*
|
|
16
16
|
* stdout is the JSON-RPC channel, all diagnostics go to stderr only.
|
|
17
17
|
*/
|
|
18
|
-
import { Server } from "@modelcontextprotocol/
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
18
|
+
import { Server } from "@modelcontextprotocol/server";
|
|
19
|
+
import type { Tool } from "@modelcontextprotocol/server";
|
|
20
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
21
21
|
import { TOOLS, TOOL_NAMES, BASE } from "./index.ts";
|
|
22
22
|
import { MANIFEST } from "./manifest.ts";
|
|
23
23
|
import { resolveBackend, getOrCreateFirefoxSession, disposeFirefoxSession } from "./backend.ts";
|
|
@@ -25,8 +25,10 @@ import { disposeBrowserSession } from "./tools/browser-session.ts";
|
|
|
25
25
|
import { toolAvailability } from "./capabilities.ts";
|
|
26
26
|
import { FIREFOX_TOOLS } from "./firefox-tools.ts";
|
|
27
27
|
import { leaseFromArgs, markLongLivedProcess, withLeaseScope } from "./leases.ts";
|
|
28
|
+
import { TOOL_GROUP, TOOL_GROUPS, GROUP_TOOLS, PROFILES, type ProfileName } from "./toolGroups.ts";
|
|
29
|
+
import { TOOL_DOCS } from "./toolDocs.ts";
|
|
28
30
|
|
|
29
|
-
const VERSION = "
|
|
31
|
+
const VERSION = "2.0.0";
|
|
30
32
|
|
|
31
33
|
// Backend + (Firefox only) attach endpoint are read once at startup (MCP has no per-call notion
|
|
32
34
|
// of backend): --browser flag / CDP_BROWSER env, else "chrome" (zero behavior change for existing
|
|
@@ -59,24 +61,134 @@ function auditCoverage(): void {
|
|
|
59
61
|
// cap and front-loaded (grammar + leases before origin) so nothing critical is clipped.
|
|
60
62
|
const INSTRUCTIONS = [
|
|
61
63
|
"cdp-toolkit drives a real browser over Chrome DevTools Protocol (or Firefox WebDriver-BiDi): open and drive pages, click/fill/type, screenshot and accessibility-snapshot, read console + network, set cookies, emulate devices, run Lighthouse and performance traces, record screencasts. Conventions shared across its tools:",
|
|
64
|
+
"TOOL AVAILABILITY: the server starts on the core profile (12 everyday tools); the rest are organized into groups you can discover and activate with `browser_tools` (or start with CDP_TOOL_PROFILE=full for everything at once). `describe_tool` returns full docs for any tool by name, including hidden ones. Hidden tools are still callable by name even while unlisted.",
|
|
62
65
|
"TARGET SELECTOR (the `target` param, unless a tool says otherwise): 'active' (default = first page) | 'index:N' (0-based) | 'url:<substr>' | 'title:<substr>' | 'label:<name>' (exact, from the origin ledger or a live lease) | a 32-hex '<targetId>'. Four Chrome-only tools (evaluate_script, list_network_requests, get_network_request, list_console_messages) also accept 'worker:<substr>' to reach a service/shared worker — e.g. an MV3 extension's background worker (worker:<extension-id> or worker:background.js); an idle-evicted worker is started first (see each tool's `wake`).",
|
|
63
66
|
"LEASES (the `lease` param): claim_page, and new_page{claim:true}, mint an opaque token. Omit it for a tab THIS process already holds. It is required for a tab held by ANOTHER process, or one claimed explicitly. Under CDP_REQUIRE_LEASE the gate auto-acquires a lease for any tab this process drives — no token is surfaced, so pass `target`, not `lease` — while an explicit claim:true still demands its token on every later call. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all.",
|
|
64
67
|
"ORIGIN: list_pages and list_leases tag each tab's `origin` as 'agent' (this toolkit created it) or 'unknown' — never 'human', because the toolkit cannot prove a person opened a tab. An 'agent' tab stays findable after its creator releases the lease or dies.",
|
|
65
68
|
].join("\n\n");
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
// Progressive disclosure: start with a lean profile (default "core") and reveal
|
|
71
|
+
// further tool groups on demand via the browser_tools meta-tool, which fires
|
|
72
|
+
// notifications/tools/list_changed so the client re-fetches tools/list. This keeps
|
|
73
|
+
// the baseline tools/list payload — and thus the context tokens the toolkit costs —
|
|
74
|
+
// a fraction of the full 45-tool surface until a group is actually needed. Set
|
|
75
|
+
// CDP_TOOL_PROFILE=full to advertise everything at startup (prior behavior).
|
|
76
|
+
const START_PROFILE: ProfileName = process.env.CDP_TOOL_PROFILE === "full" ? "full" : "core";
|
|
77
|
+
const activeGroups = new Set<string>(PROFILES[START_PROFILE]);
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
/** Always-listed meta-tools that drive disclosure; never part of a group. */
|
|
80
|
+
const META_TOOLS: ReadonlyArray<{ name: string; description: string; inputSchema: Tool["inputSchema"] }> = [
|
|
81
|
+
{
|
|
82
|
+
name: "browser_tools",
|
|
83
|
+
description:
|
|
84
|
+
"Manage which cdp-toolkit tool groups are exposed (progressive disclosure). Most tools are hidden until their group is activated; activating one reveals its tools via tools/list_changed. Use action:list_groups first to see what exists.",
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: {
|
|
88
|
+
action: {
|
|
89
|
+
type: "string",
|
|
90
|
+
enum: ["list_groups", "activate", "deactivate", "set_profile"],
|
|
91
|
+
description: "list_groups: show every group, its tools, and active state; activate/deactivate: toggle one group; set_profile: switch wholesale.",
|
|
92
|
+
},
|
|
93
|
+
group: { type: "string", enum: [...TOOL_GROUPS], description: "Group for activate/deactivate." },
|
|
94
|
+
profile: { type: "string", enum: ["core", "full"], description: "Profile for set_profile (core=lean, full=all groups)." },
|
|
95
|
+
},
|
|
96
|
+
required: ["action"],
|
|
97
|
+
additionalProperties: false,
|
|
98
|
+
} as Tool["inputSchema"],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "describe_tool",
|
|
102
|
+
description: "Return the full description and per-parameter docs for any cdp-toolkit tool by name — including tools not currently listed. Call this before using an unfamiliar tool; the live tools/list carries only terse summaries.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: { name: { type: "string", description: "Tool name, e.g. take_screenshot." } },
|
|
106
|
+
required: ["name"],
|
|
107
|
+
additionalProperties: false,
|
|
108
|
+
} as Tool["inputSchema"],
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
/** Human-readable dump of every group, its tools, and whether it's currently active. */
|
|
113
|
+
function renderGroups(): string {
|
|
114
|
+
const lines = [`profile start=${START_PROFILE}; active groups: ${[...activeGroups].join(", ") || "(none)"}`];
|
|
115
|
+
for (const g of TOOL_GROUPS) {
|
|
116
|
+
const shown = GROUP_TOOLS[g].filter((n) => AVAILABLE_NAMES.has(n));
|
|
117
|
+
lines.push(`${activeGroups.has(g) ? "[on] " : "[off]"} ${g} (${shown.length}): ${shown.join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** describe_tool body: full prose for one tool from the on-demand docs map. */
|
|
123
|
+
function renderToolDoc(toolName: string): string {
|
|
124
|
+
const doc = TOOL_DOCS[toolName];
|
|
125
|
+
if (!doc) return `unknown tool: ${toolName}`;
|
|
126
|
+
const group = TOOL_GROUP[toolName];
|
|
127
|
+
const params = Object.entries(doc.params);
|
|
128
|
+
const body = params.length ? params.map(([p, d]) => `- ${p}: ${d || "(no description)"}`).join("\n") : "(no parameters)";
|
|
129
|
+
return `${toolName}${group ? ` [group: ${group}]` : ""}\n\n${doc.description}\n\nParameters:\n${body}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const server = new Server({ name: "cdp-toolkit", version: VERSION }, { capabilities: { tools: { listChanged: true } }, instructions: INSTRUCTIONS });
|
|
133
|
+
|
|
134
|
+
server.setRequestHandler('tools/list', async () => ({
|
|
135
|
+
// Two-axis disclosure: (1) the always-present meta-tools; (2) manifest tools the
|
|
136
|
+
// selected backend can run AND whose group is currently active. Descriptions/schemas
|
|
137
|
+
// here are the compressed form — full prose is served on demand by describe_tool.
|
|
138
|
+
tools: [
|
|
139
|
+
...META_TOOLS,
|
|
140
|
+
...MANIFEST
|
|
141
|
+
.filter((s) => AVAILABLE_NAMES.has(s.name) && activeGroups.has(TOOL_GROUP[s.name] ?? ""))
|
|
142
|
+
.map((s) => ({ name: s.name, description: s.description, inputSchema: s.inputSchema as Tool["inputSchema"] })),
|
|
143
|
+
],
|
|
74
144
|
}));
|
|
75
145
|
|
|
76
|
-
server.setRequestHandler(
|
|
146
|
+
server.setRequestHandler('tools/call', async (request) => {
|
|
77
147
|
const { name } = request.params;
|
|
78
148
|
const args = (request.params.arguments ?? {}) as unknown;
|
|
79
149
|
|
|
150
|
+
// Meta-tools are handled at the MCP layer (they mutate what tools/list advertises)
|
|
151
|
+
// and never enter the browser dispatch or a lease scope.
|
|
152
|
+
if (name === "describe_tool") {
|
|
153
|
+
const toolName = typeof (args as { name?: unknown }).name === "string" ? (args as { name: string }).name : "";
|
|
154
|
+
const known = Boolean(TOOL_DOCS[toolName]);
|
|
155
|
+
return { content: [{ type: "text" as const, text: renderToolDoc(toolName) }], isError: !known };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (name === "browser_tools") {
|
|
159
|
+
const a = args as { action?: unknown; group?: unknown; profile?: unknown };
|
|
160
|
+
const action = typeof a.action === "string" ? a.action : "";
|
|
161
|
+
const before = [...activeGroups].sort().join(",");
|
|
162
|
+
|
|
163
|
+
if (action === "list_groups") {
|
|
164
|
+
return { content: [{ type: "text" as const, text: renderGroups() }] };
|
|
165
|
+
}
|
|
166
|
+
if (action === "activate" || action === "deactivate") {
|
|
167
|
+
const group = typeof a.group === "string" ? a.group : "";
|
|
168
|
+
if (!(TOOL_GROUPS as readonly string[]).includes(group)) {
|
|
169
|
+
return { content: [{ type: "text" as const, text: `unknown group: ${group}. Known: ${TOOL_GROUPS.join(", ")}` }], isError: true };
|
|
170
|
+
}
|
|
171
|
+
if (action === "activate") activeGroups.add(group);
|
|
172
|
+
else activeGroups.delete(group);
|
|
173
|
+
} else if (action === "set_profile") {
|
|
174
|
+
const profile = a.profile === "full" ? "full" : a.profile === "core" ? "core" : "";
|
|
175
|
+
if (!profile) return { content: [{ type: "text" as const, text: `unknown profile: ${String(a.profile)}. Known: core, full` }], isError: true };
|
|
176
|
+
activeGroups.clear();
|
|
177
|
+
for (const g of PROFILES[profile]) activeGroups.add(g);
|
|
178
|
+
} else {
|
|
179
|
+
return { content: [{ type: "text" as const, text: `unknown action: ${action}. Use list_groups | activate | deactivate | set_profile.` }], isError: true };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if ([...activeGroups].sort().join(",") !== before) {
|
|
183
|
+
try {
|
|
184
|
+
await server.sendToolListChanged();
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(`[cdp-toolkit] WARN: sendToolListChanged failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { content: [{ type: "text" as const, text: renderGroups() }] };
|
|
190
|
+
}
|
|
191
|
+
|
|
80
192
|
if (!AVAILABLE_NAMES.has(name)) {
|
|
81
193
|
const gap = AVAILABILITY.unavailable.find((u) => u.name === name);
|
|
82
194
|
return {
|