use rmcp::handler::server::tool::ToolRouter; use rmcp::handler::server::wrapper::Parameters; use rmcp::model::*; use rmcp::{ServerHandler, tool, tool_handler, tool_router}; use tokio::sync::mpsc; use super::commands::{McpCommand, send_cmd}; use super::params::*; pub struct McpServer { tool_router: ToolRouter, cmd_tx: mpsc::Sender, } impl McpServer { pub fn new(cmd_tx: mpsc::Sender) -> Self { Self { tool_router: Self::tool_router(), cmd_tx, } } } #[tool_router] impl McpServer { #[tool( description = r#"Fetch a URL and return the page as token-optimized markdown. Supports GET/POST/PUT/PATCH/DELETE, auth profiles, custom headers, CSS selector extraction, pagination, and binary downloads. USE WHEN: loading a new page by URL, calling an HTTP API, or downloading a binary asset. NOT WHEN: following a link already listed by browser39_links (use browser39_click for that — it preserves Referer and is cheaper). Effect: read for GET, external-mutate for POST/PUT/PATCH/DELETE. Selector parameter accepts CSS only. See docs/selectors.md. Common failures: - INVALID_URL → URL didn't parse; ensure scheme (https://) is present. - TIMEOUT → server slow or unreachable; retryable, or raise timeout in config. - HTTP_ERROR → 4xx/5xx response. Inspect status; for 401/403 attach an auth_profile. - AUTH_PROFILE_DOMAIN_MISMATCH → the requested URL is outside the profile's allowed domains; pick a different profile or extend `domains` in config. - SELECTOR_NOT_FOUND → CSS selector matched zero elements; re-fetch without selector to see the page, or pick from content_selectors in the response. - Page came back giant → set `max_tokens` and use `show_selectors_first: true` (default) to preview section sizes before committing tokens."# )] async fn browser39_fetch( &self, Parameters(params): Parameters, ) -> Result { page_cmd(&self.cmd_tx, |tx| McpCommand::Fetch { params, tx }).await } #[tool( description = r#"Follow a link on the current page by index number or visible link text. USE WHEN: navigating to a link returned by browser39_fetch or browser39_links. NOT WHEN: clicking a button that is not an `` (use browser39_dom_query with a script that calls `.click()`), or submitting a form (use browser39_submit). Effect: read (GET request). Common failures: - NO_PAGE → no current page; call browser39_fetch first. - LINK_NOT_FOUND → index out of range, or text didn't match any link substring (case-insensitive). Call browser39_links to see what is actually on the page; the link list is authoritative. - HTTP_ERROR / TIMEOUT → target URL failed; retryable."# )] async fn browser39_click( &self, Parameters(params): Parameters, ) -> Result { page_cmd(&self.cmd_tx, |tx| McpCommand::Click { params, tx }).await } #[tool( description = r#"List every link on the current page with its index, visible text, and href. Cheap — reads from session cache, no network request. USE WHEN: deciding which link to follow; resolving ambiguous click(text=...) calls; building a navigation plan. NOT WHEN: you already know the URL — use browser39_fetch directly. Effect: read. Common failures: - NO_PAGE → no current page; call browser39_fetch first."# )] async fn browser39_links(&self) -> Result { data_cmd(&self.cmd_tx, |tx| McpCommand::Links { tx }).await } #[tool( description = r#"Query the DOM of the current page using a CSS selector OR a JavaScript expression. Exactly one of `selector` or `script` is required. USE WHEN: extracting structured data, or running custom DOM logic. NOT WHEN: matching by visible text — CSS does not support `:contains()`; use a script: `Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Submit'))`. NOT WHEN: you want to navigate — element.click() and form.submit() inside script DO trigger navigation, but the result shape is different; use browser39_click or browser39_submit when navigation is the goal. Effect: read in selector mode; local-mutate or external-mutate in script mode (scripts can fill fields, dispatch events, or trigger requests via .click()/.submit()). Selector parameter accepts CSS only. See docs/selectors.md. Script mode runs against the static parsed HTML in a deno_core sandbox with full DOM API: traversal (parentElement, children, closest, matches), lookup (getElementById, getElementsByClassName/TagName), mutation (createElement, appendChild, setAttribute, innerHTML/textContent setters), events (addEventListener, dispatchEvent, new Event), and globals (console.log, setTimeout, atob/btoa, localStorage). Console output is captured. Common failures: - NO_PAGE → no current page; call browser39_fetch first. - DOM_QUERY_ERROR → invalid CSS selector or JS exception; the message will name the line/symbol. Reading the relevant section with browser39_fetch + selector first usually clarifies the structure. - Script returned undefined → ensure the last expression is the value you want; the engine returns the final expression, not all intermediate results."# )] async fn browser39_dom_query( &self, Parameters(params): Parameters, ) -> Result { data_cmd(&self.cmd_tx, |tx| McpCommand::DomQuery { params, tx }).await } #[tool( description = r#"Fill form field(s) by CSS selector. Works on ``, `