chore: make this repo self-contained

Split out of the agent repository into its own project. The two commits
that introduced and reworked the extension are preserved.

The extension could not build on its own: prebuild called
`python ../../tools/generate_tool_defs.py`, and the 31 tool schemas lived
outside the tree — in a directory that was under no version control at all.

- tools/schema/*.json and tools/generate_tool_defs.py now live here. They
  describe browser tools, so they belong with the extension.
- The generator writes TypeScript into this repo and, when the agent repo is
  checked out next to it, the Python tool definitions there as well. The
  presence check looks for agent/tools.py: checking only for the directory
  was useless — it is almost always true, and the generator then wrote into an
  arbitrary neighbouring folder (verified, then fixed).
- Added .gitignore: dist/, node_modules/, and *.token — the MCP bridge token
  is a runtime secret.

Verified: `npm run build` from a clean checkout with and without the agent
repo present; tsc reports no errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 18:39:24 +02:00
co-authored by Claude Opus 5
parent 72d6a393fb
commit 35b3af97a2
34 changed files with 844 additions and 2 deletions
+128
View File
@@ -0,0 +1,128 @@
"""Generator: tools/schema/*.json → Python ToolSpecs + TypeScript types.
Liest alle JSON-Dateien aus tools/schema/, erzeugt:
1. agent/browser_tool_defs.py — Liste von Bedrock-toolSpec-Dicts für den Broker
2. extension/src/shared/tool-schemas.generated.ts — TypeScript-Typen + Registry
Aufruf:
python tools/generate_tool_defs.py
Im esbuild-Step der Extension als Pre-Build verdrahtet (package.json "prebuild").
"""
import json
import os
from pathlib import Path
SCHEMA_DIR = Path(__file__).parent / "schema"
#: Wurzel dieses Repos (tools/ liegt direkt darunter).
_ROOT = Path(__file__).parent.parent
#: TypeScript-Ziel — immer vorhanden, dies ist das eigene Repo.
TS_OUTPUT = _ROOT / "src" / "shared" / "tool-schemas.generated.ts"
#: Python-Ziel im NACHBAR-Repo (agent/). Die Schemata beschreiben Browser-Tools
#: und gehoeren damit zur Extension; der Python-Broker konsumiert nur das
#: Erzeugnis, das dort eingecheckt ist. Liegen die Repos nicht nebeneinander,
#: wird der Python-Teil uebersprungen statt abzubrechen — die Extension muss
#: allein baubar bleiben.
PY_OUTPUT = _ROOT.parent / "browser_tool_defs.py"
def load_schemas() -> list[dict]:
schemas = []
for f in sorted(SCHEMA_DIR.glob("*.json")):
with open(f, encoding="utf-8") as fh:
schemas.append(json.load(fh))
return schemas
def generate_python(schemas: list[dict]) -> str:
lines = [
'"""Auto-generated browser tool definitions for the Bedrock Converse API.',
'',
'DO NOT EDIT — generated by tools/generate_tool_defs.py from tools/schema/*.json.',
'"""',
'',
'BROWSER_TOOL_SPECS: list[dict] = [',
]
for s in schemas:
spec = {
"toolSpec": {
"name": s["name"],
"description": s["description"],
"inputSchema": {"json": s["inputSchema"]},
}
}
lines.append(f" {json.dumps(spec, ensure_ascii=False)},")
lines.append("]")
lines.append("")
lines.append("# Mapping: tool name -> requires_confirmation flag")
lines.append("BROWSER_TOOL_CONFIRMATION: dict[str, bool] = {")
for s in schemas:
lines.append(f' "{s["name"]}": {str(s.get("requires_confirmation", False))},')
lines.append("}")
lines.append("")
lines.append(f"BROWSER_TOOL_NAMES: list[str] = {json.dumps([s['name'] for s in schemas])}")
lines.append("")
return "\n".join(lines)
def generate_typescript(schemas: list[dict]) -> str:
lines = [
"/**",
" * Auto-generated browser tool schemas and types.",
" * DO NOT EDIT — generated by tools/generate_tool_defs.py from tools/schema/*.json.",
" */",
"",
"export interface BrowserToolSchema {",
" name: string;",
" description: string;",
" requires_confirmation: boolean;",
" inputSchema: Record<string, unknown>;",
"}",
"",
"export const BROWSER_TOOL_SCHEMAS: BrowserToolSchema[] = [",
]
for s in schemas:
lines.append(f" {json.dumps(s, ensure_ascii=False)},")
lines.append("];")
lines.append("")
lines.append("export const BROWSER_TOOL_NAMES = [")
for s in schemas:
lines.append(f' "{s["name"]}",')
lines.append("] as const;")
lines.append("")
lines.append("export type BrowserToolName = typeof BROWSER_TOOL_NAMES[number];")
lines.append("")
lines.append("export const REQUIRES_CONFIRMATION: Record<string, boolean> = {")
for s in schemas:
lines.append(f' "{s["name"]}": {str(s.get("requires_confirmation", False)).lower()},')
lines.append("};")
lines.append("")
return "\n".join(lines)
def main():
schemas = load_schemas()
print(f"Loaded {len(schemas)} tool schemas from {SCHEMA_DIR}")
py_code = generate_python(schemas)
# NICHT nur auf das Verzeichnis pruefen — das existiert fast immer und der
# Generator schrieb dann in ein beliebiges Nachbarverzeichnis (gemessen).
# Der Marker macht daraus eine echte Aussage: liegt hier das Agenten-Repo?
if (PY_OUTPUT.parent / "tools.py").is_file():
PY_OUTPUT.write_text(py_code, encoding="utf-8")
print(f"Generated Python: {PY_OUTPUT} ({len(py_code)} bytes)")
else:
print(f"Skipped Python output: {PY_OUTPUT.parent} not found "
f"(agent repo not checked out next to this one)")
ts_code = generate_typescript(schemas)
TS_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
TS_OUTPUT.write_text(ts_code, encoding="utf-8")
print(f"Generated TypeScript: {TS_OUTPUT} ({len(ts_code)} bytes)")
if __name__ == "__main__":
main()
+37
View File
@@ -0,0 +1,37 @@
{
"name": "browser_batch",
"description": "Runs several browser tools in one call, in order, returning all results. Saves one model round-trip per step — the dominant cost of form filling and navigation. Use when the whole sequence is known in advance. Do NOT use when a later step depends on what an earlier one returns.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"actions": {
"type": "array",
"description": "Steps in order: [{name, input}, ...].",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the browser tool to run, e.g. 'browser_click', 'browser_type', 'browser_computer'. Must be one of the available browser tools, and not 'browser_batch'."
},
"input": {
"type": "object",
"description": "Argument object for that tool — exactly what you would pass in a single call. Omit or use {} for tools without arguments."
}
},
"required": [
"name"
]
}
},
"stopOnError": {
"type": "boolean",
"description": "Stop at the first failing step (default true)."
}
},
"required": [
"actions"
]
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "browser_click",
"description": "Click an element addressed by ref_id or CSS selector.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector of the element to click, e.g. 'button[type=submit]' or '#login'."
},
"ref_id": {
"type": "string",
"description": "Stable ref_id from browser_read_page or browser_find."
},
"trusted": {
"type": "boolean",
"description": "Force real CDP events for this one call even if the extension is configured for synthetic…"
}
}
}
}
+88
View File
@@ -0,0 +1,88 @@
{
"name": "browser_computer",
"description": "Operates mouse and keyboard at pixel coordinates via CDP (isTrusted events). Workflow: action='screenshot' first, read coordinates off that image, then act. Use only for canvas, maps, drag&drop and anything that ignores selectors — for normal HTML browser_find + browser_click are cheaper and more reliable.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"screenshot",
"mouse_move",
"left_click",
"right_click",
"middle_click",
"double_click",
"triple_click",
"left_click_drag",
"left_mouse_down",
"left_mouse_up",
"scroll",
"type",
"key",
"hold_key",
"wait",
"cursor_position"
],
"description": "What to do. Pixel coordinates come from a screenshot."
},
"coordinate": {
"type": "array",
"items": {
"type": "integer"
},
"minItems": 2,
"maxItems": 2,
"description": "[x, y] in screenshot pixels."
},
"start_coordinate": {
"type": "array",
"items": {
"type": "integer"
},
"minItems": 2,
"maxItems": 2,
"description": "[x, y] where a drag begins."
},
"text": {
"type": "string",
"description": "Text to type, or key name for key presses (e.g. 'Return', 'ctrl+a')."
},
"scroll_direction": {
"type": "string",
"enum": [
"up",
"down",
"left",
"right"
],
"description": "up | down | left | right"
},
"scroll_amount": {
"type": "integer",
"description": "Number of wheel clicks."
},
"duration": {
"type": "number",
"description": "Seconds to hold or wait."
},
"modifiers": {
"type": "array",
"items": {
"type": "string",
"enum": [
"ctrl",
"alt",
"shift",
"meta"
]
},
"description": "Held modifier keys, e.g. ['ctrl','shift']."
}
},
"required": [
"action"
]
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "browser_drag",
"description": "Drag one element onto another with the real mouse: press the left button over the centre of the source, move to the centre of the target in several intermediate steps, and release there.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"sourceSelector": {
"type": "string",
"description": "CSS selector of the element to pick up. The drag starts at its centre."
},
"targetSelector": {
"type": "string",
"description": "CSS selector of the drop target. The button is released at its centre."
}
},
"required": [
"sourceSelector",
"targetSelector"
]
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "browser_execute_js",
"description": "Run JavaScript in the page's MAIN world and return the result. Same realm as the site's own scripts, so you can reach the application's window globals — frameworks, state stores, config objects, jQuery, data layers — which an isolated content script cannot see.",
"requires_confirmation": true,
"inputSchema": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "JavaScript source to evaluate in the page. The last expression's value is returned, e.g."
}
},
"required": [
"code"
]
}
}
+28
View File
@@ -0,0 +1,28 @@
{
"name": "browser_file_upload",
"description": "Attaches local files to an <input type=\"file\"> via CDP, as if picked in the OS dialog, and fires a change event. Paths must be absolute on the machine running the browser. Never try to open the native file dialog by clicking — it cannot be operated.",
"requires_confirmation": true,
"inputSchema": {
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector of the file input."
},
"ref_id": {
"type": "string",
"description": "ref_id of the file input. Preferred."
},
"files": {
"type": "array",
"items": {
"type": "string"
},
"description": "Absolute paths on the browser machine."
}
},
"required": [
"files"
]
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "browser_find",
"description": "Finds elements by plain-language description instead of a CSS selector. Matches label text, accessible name, placeholder, aria-label, title, role and type, ranked by fit. Returns ref_id (for browser_click/type/select), role, text, tag and coordinates. Start here instead of reading the whole page.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "What you are looking for, in plain language."
},
"limit": {
"type": "integer",
"description": "Maximum number of candidates (default 5)."
}
},
"required": [
"query"
]
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"name": "browser_form_input",
"description": "Fill a form field: handles input, textarea, select, checkbox, radio. For select elements, matches by value or visible text.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector of the form element"
},
"ref_id": {
"type": "string",
"description": "Stable ref_id from read_page"
},
"value": {
"type": "string",
"description": "Value to set (for select: option value or text; for checkbox/radio: 'true'/'false')"
}
},
"required": [
"value"
]
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "browser_get_page_info",
"description": "Get current page URL, title, viewport dimensions, scroll position, and document height.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "browser_get_text",
"description": "Get the text content of the page or a specific element (Readability-style extraction).",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector (default: body)"
},
"maxLength": {
"type": "number",
"description": "Max characters to return (default 8000)"
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "browser_go_back",
"description": "Navigate back in the active tab's history.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "browser_go_forward",
"description": "Navigate forward in the active tab's history.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {}
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"name": "browser_highlight",
"description": "Visually highlight an element on the page (for debugging/demonstration).",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector of the element to highlight"
},
"color": {
"type": "string",
"description": "Outline color (default '#ff6b35')"
},
"duration": {
"type": "number",
"description": "Highlight duration in ms (default 3000)"
}
},
"required": [
"selector"
]
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "browser_hover",
"description": "Move the real mouse pointer onto an element and leave it there.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector of the element to hover, e.g. 'nav .menu-item:first-child'."
},
"ref_id": {
"type": "string",
"description": "Stable ref_id from browser_read_page or browser_find."
}
}
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"name": "browser_key",
"description": "Press a single key, optionally with modifiers, as a real keystroke through the DevTools Protocol — trusted keydown/keyup with the correct key code, so keyboard shortcuts, form submission via Enter and focus traversal via Tab all behave as they would for a human.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Key name in DOM KeyboardEvent.key notation, e.g."
},
"modifiers": {
"type": "array",
"items": {
"type": "string",
"enum": [
"ctrl",
"alt",
"shift",
"meta"
]
},
"description": "Modifier keys held while the key is pressed, e.g."
}
},
"required": [
"key"
]
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "browser_navigate",
"description": "Navigate the active tab to a URL, or open a URL in a new tab.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "Target URL to navigate to"
},
"newTab": {
"type": "boolean",
"description": "If true, open in a new tab instead of the active one"
}
},
"required": [
"url"
]
}
}
+29
View File
@@ -0,0 +1,29 @@
{
"name": "browser_read_console",
"description": "Read captured console logs (log, warn, error, info) from the active page.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"clear": {
"type": "boolean",
"description": "Clear the log buffer after reading (default false)"
},
"level": {
"type": "string",
"enum": [
"all",
"log",
"warn",
"error",
"info"
],
"description": "Filter by log level (default 'all')"
},
"limit": {
"type": "number",
"description": "Max entries to return (default 50)"
}
}
}
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "browser_read_network",
"description": "Read the network requests the active page has made. Captured live through the DevTools Protocol — the same data the Network tab of DevTools shows: URL, HTTP method, status code, resource type, timing and size.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"filter": {
"type": "string",
"description": "Only return requests whose URL contains this substring, e.g. '/api/' or 'graphql'."
},
"method": {
"type": "string",
"description": "Only return requests with this HTTP method, e.g. 'GET', 'POST', 'PUT', 'DELETE'."
},
"limit": {
"type": "integer",
"description": "Maximum number of entries to return, most recent first (default 50)."
},
"includeBody": {
"type": "boolean",
"description": "Include the response body of matching requests (default false)."
}
}
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"name": "browser_read_page",
"description": "Reads the accessibility tree of the page: a compact view of what is on screen and interactive. Cheaper and more precise than a screenshot for normal HTML. Every interactive node carries a ref_id — pass it to browser_click/type/select instead of guessing a selector. Fields report label, role and editable.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"viewportOnly": {
"type": "boolean",
"description": "Only what is currently visible (default true)."
},
"maxDepth": {
"type": "number",
"description": "Maximum nesting depth."
},
"maxTokens": {
"type": "number",
"description": "Budget; the tree is cut off when reached."
},
"includeHidden": {
"type": "boolean",
"description": "Include invisible elements (default false)."
},
"selector": {
"type": "string",
"description": "Read only this subtree, e.g. a compose form."
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "browser_reload",
"description": "Reload the current page in the active tab.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "browser_resize_window",
"description": "Resize the browser window holding the active tab. Changes the viewport, so every pixel coordinate from earlier screenshots becomes invalid — take a fresh screenshot afterwards.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"width": {
"type": "integer",
"description": "Window width in pixels, e.g. 1280 for a desktop layout or 390 to force a mobile layout."
},
"height": {
"type": "integer",
"description": "Window height in pixels, e.g. 900. Omit to keep the current height."
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "browser_screenshot",
"description": "Captures a JPEG of the page as an image you can look at. Read pixel coordinates off it for browser_computer; origin (0,0) is top-left, x grows right, y grows down. Viewport only by default — exactly the area browser_computer can reach. Expensive in tokens: prefer browser_read_page for normal HTML.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"fullPage": {
"type": "boolean",
"description": "Whole page instead of the viewport. Coordinates then no longer match browser_computer."
},
"maxWidth": {
"type": "integer",
"description": "Scale down to this width in pixels."
}
}
}
}
+31
View File
@@ -0,0 +1,31 @@
{
"name": "browser_scroll",
"description": "Scroll the page, or a specific scrollable element, by a pixel amount.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"direction": {
"type": "string",
"enum": [
"up",
"down",
"left",
"right"
],
"description": "Scroll direction."
},
"amount": {
"type": "number",
"description": "Pixels to scroll (default 500)."
},
"selector": {
"type": "string",
"description": "CSS selector of the scrollable element (default: the page itself)."
}
},
"required": [
"direction"
]
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"name": "browser_select",
"description": "Select an option in a <select> element by value or visible text.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector of the <select> element"
},
"ref_id": {
"type": "string",
"description": "Stable ref_id from read_page"
},
"value": {
"type": "string",
"description": "Option value or visible text to select"
}
},
"required": [
"value"
]
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "browser_tabs_close",
"description": "Close a tab by its ID. Get IDs from browser_tabs_list. Use it to tidy up tabs you opened while working; closing a tab does not affect the pages themselves.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"tabId": {
"type": "number",
"description": "The ID of the tab to close, as reported by browser_tabs_list."
}
},
"required": [
"tabId"
]
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "browser_tabs_create",
"description": "Open a new tab with the given URL.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "URL to open in the new tab"
}
},
"required": [
"url"
]
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "browser_tabs_list",
"description": "List all open browser tabs with their IDs, titles, URLs, and active state.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "browser_tabs_select",
"description": "Switch to a tab by its ID (makes it the active tab).",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"tabId": {
"type": "number",
"description": "The tab ID to activate"
}
},
"required": [
"tabId"
]
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "browser_type",
"description": "Types text into an input, textarea or contenteditable as real CDP keystrokes, so masks, validation and framework state react as for a human. A target is REQUIRED (ref_id or selector) — without one the text would land in whatever happens to be focused, which in forms is usually the wrong field. The result reports typed_into (where the text actually went) and sets target_mismatch=true if that is not the element you asked for.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"selector": { "type": "string", "description": "CSS selector of the field." },
"ref_id": { "type": "string", "description": "ref_id from browser_find or browser_read_page. Preferred; wins over selector." },
"text": { "type": "string", "description": "Text to type, character by character." },
"clear": { "type": "boolean", "description": "Clear the field first (default true)." },
"submit": { "type": "boolean", "description": "Press Enter afterwards (default false). Submits most forms." },
"use_focus": { "type": "boolean", "description": "Type into the focused element without a target. Only when you set that focus yourself and mean it." }
},
"required": ["text"]
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "browser_wait",
"description": "Wait for an element to appear or a timeout to elapse.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"selector": {
"type": "string",
"description": "CSS selector to wait for (if omitted, just waits for timeout)"
},
"timeout": {
"type": "number",
"description": "Max milliseconds to wait (default 5000)"
},
"visible": {
"type": "boolean",
"description": "Wait until element is visible, not just present (default true)"
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "browser_zoom",
"description": "Set the zoom level of the active tab.",
"requires_confirmation": false,
"inputSchema": {
"type": "object",
"properties": {
"level": {
"type": "number",
"description": "Zoom factor (1.0 = 100%, 1.5 = 150%, 0.5 = 50%)"
}
},
"required": [
"level"
]
}
}