mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-05 02:45:28 +00:00
fix(tools): stop unusable browser tools from crowding out browser_navigate
Tool retrieval returns a top-K (8 by default), and @playwright/mcp alone exposes 30 tools. Measured across three typical browsing requests on a local setup, the agent was handed browser_drop, browser_handle_dialog, browser_close, browser_console_messages and the whole xy-mouse family -- while browser_navigate was missing in two cases out of three. On "click the login button", six of the eight slots went to mouse primitives. Without the tool that opens a page, every other browser tool is dead weight. So the model answered that it could not reach the internet, and that answer was correct. It reads as a refusal or a hallucination, which is what makes it expensive to diagnose: the browser is installed, the MCP server is connected and reports 30 tools, and calling browser_navigate by hand works fine. Two guards: MCP_INDEX_DENIED keeps 18 tools out of the index -- pointer primitives (browser_click and browser_hover work off the accessibility snapshot and need no coordinates), debugging aids, session plumbing, and browser_run_code_unsafe, which runs arbitrary JavaScript in the page. They stay connected and callable; they just no longer compete for a retrieval slot. MCP_COMPANIONS pulls browser_navigate and browser_snapshot in whenever any browser tool is retrieved. The server prefix comes from the hit itself rather than a constant, so the rule holds under any server id -- verified with a second prefix. Matching is on the bare tool name, after the server prefix, so neither guard depends on the browser being registered as builtin_browser. Measured on the same three queries after the change: browser_navigate present three times out of three, no denied tool leaking through, and every slot filled with something that works -- navigate, snapshot, find, click, type, press_key, select_option. The running app indexes 12 MCP tools instead of 30, and the agent then completed a real navigate-then-read task it had previously refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d8a2059df8
commit
830f7cd6d7
1 changed files with 48 additions and 0 deletions
|
|
@ -61,6 +61,40 @@ ASSISTANT_ALWAYS_AVAILABLE = frozenset({
|
||||||
"ui_control",
|
"ui_control",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# MCP tools that must never be indexed for retrieval.
|
||||||
|
#
|
||||||
|
# Retrieval returns a top-K (8 by default), and @playwright/mcp alone exposes
|
||||||
|
# 30 tools. In practice that means the agent is handed browser_drop,
|
||||||
|
# browser_handle_dialog or browser_close while browser_navigate — the only tool
|
||||||
|
# that opens a page — does not surface at all. The model then reports that it
|
||||||
|
# cannot browse the web, which is accurate but reads as a refusal.
|
||||||
|
#
|
||||||
|
# Matched against the bare tool name (the part after the server prefix), so
|
||||||
|
# this holds whatever server id the browser is registered under. Denied tools
|
||||||
|
# stay connected and callable; they just no longer compete for a slot.
|
||||||
|
MCP_INDEX_DENIED = frozenset({
|
||||||
|
# Low-level pointer primitives — browser_click/browser_hover work off the
|
||||||
|
# accessibility snapshot and need no coordinates.
|
||||||
|
"browser_mouse_click_xy", "browser_mouse_down", "browser_mouse_up",
|
||||||
|
"browser_mouse_move_xy", "browser_mouse_drag_xy", "browser_mouse_wheel",
|
||||||
|
"browser_drag", "browser_drop",
|
||||||
|
# Debugging aids, not task tools.
|
||||||
|
"browser_console_messages", "browser_network_request",
|
||||||
|
"browser_network_requests", "browser_evaluate",
|
||||||
|
# Arbitrary JavaScript in the page. Deliberately out of reach.
|
||||||
|
"browser_run_code_unsafe",
|
||||||
|
# Session plumbing the agent should not spend its budget on.
|
||||||
|
"browser_close", "browser_resize", "browser_tabs",
|
||||||
|
"browser_handle_dialog", "browser_file_upload",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Tools that are worthless on their own. If retrieval surfaces any browser
|
||||||
|
# tool, the agent also needs the two that open a page and read it back —
|
||||||
|
# otherwise it receives, say, browser_find with no way to reach a page first.
|
||||||
|
MCP_COMPANIONS = {
|
||||||
|
"browser_": ("browser_navigate", "browser_snapshot"),
|
||||||
|
}
|
||||||
|
|
||||||
COLLECTION_NAME = "odysseus_tool_index"
|
COLLECTION_NAME = "odysseus_tool_index"
|
||||||
|
|
||||||
# ── Tool description registry ──
|
# ── Tool description registry ──
|
||||||
|
|
@ -266,6 +300,10 @@ class ToolIndex:
|
||||||
if len(name_desc) == 2:
|
if len(name_desc) == 2:
|
||||||
name = name_desc[0].strip()
|
name = name_desc[0].strip()
|
||||||
desc = name_desc[1].strip()
|
desc = name_desc[1].strip()
|
||||||
|
# Keep denied tools out of the index entirely, so they
|
||||||
|
# cannot crowd out the ones that do the actual work.
|
||||||
|
if name.rsplit("__", 1)[-1] in MCP_INDEX_DENIED:
|
||||||
|
continue
|
||||||
# Include server identity in the indexed text so RAG can
|
# Include server identity in the indexed text so RAG can
|
||||||
# distinguish "list_emails for server-a" from "list_emails for server-b"
|
# distinguish "list_emails for server-a" from "list_emails for server-b"
|
||||||
server_ctx = f" (server: {current_server})" if current_server else ""
|
server_ctx = f" (server: {current_server})" if current_server else ""
|
||||||
|
|
@ -522,6 +560,16 @@ class ToolIndex:
|
||||||
base = set(always_include or ALWAYS_AVAILABLE)
|
base = set(always_include or ALWAYS_AVAILABLE)
|
||||||
retrieved = self.retrieve(query, k=k)
|
retrieved = self.retrieve(query, k=k)
|
||||||
base.update(retrieved)
|
base.update(retrieved)
|
||||||
|
# Pull in the companions of anything retrieved. The server prefix is
|
||||||
|
# taken from the hit itself rather than hardcoded, so this keeps
|
||||||
|
# working if the browser is registered under a different id.
|
||||||
|
for name in retrieved:
|
||||||
|
prefix, _, bare = name.rpartition("__")
|
||||||
|
for marker, companions in MCP_COMPANIONS.items():
|
||||||
|
if bare.startswith(marker):
|
||||||
|
base.update(
|
||||||
|
f"{prefix}__{c}" if prefix else c for c in companions
|
||||||
|
)
|
||||||
# Keyword-based force-include for common intents. Match on word
|
# Keyword-based force-include for common intents. Match on word
|
||||||
# boundaries, not raw substrings, so short hints like "fix", "line",
|
# boundaries, not raw substrings, so short hints like "fix", "line",
|
||||||
# "serve", "reply" or "unread" don't fire inside unrelated words
|
# "serve", "reply" or "unread" don't fire inside unrelated words
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue