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/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/sdk/server/index.js";
19
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
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 = "1.10.0";
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
- const server = new Server({ name: "cdp-toolkit", version: VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
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
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
70
- // Filtered to whatever the selected backend can actually run, per REQUIRED_CAPABILITIES:
71
- // under Chrome this is byte-identical to the unfiltered MANIFEST (every capability CDP needs
72
- // is declared); under Firefox the performance/heap/lighthouse tools are simply absent.
73
- tools: MANIFEST.filter((s) => AVAILABLE_NAMES.has(s.name)).map((s) => ({ name: s.name, description: s.description, inputSchema: s.inputSchema })),
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(CallToolRequestSchema, async (request) => {
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 {