diff --git a/Cargo.lock b/Cargo.lock index d290b0a..3b72bba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1592,6 +1592,7 @@ dependencies = [ name = "plumb-mcp" version = "0.0.1" dependencies = [ + "plumb-cdp", "plumb-core", "plumb-format", "rmcp", diff --git a/crates/plumb-cli/tests/mcp_stdio.rs b/crates/plumb-cli/tests/mcp_stdio.rs index 56809b9..48afd3a 100644 --- a/crates/plumb-cli/tests/mcp_stdio.rs +++ b/crates/plumb-cli/tests/mcp_stdio.rs @@ -130,7 +130,7 @@ fn mcp_initialize_and_tools_list() { .unwrap_or_else(|| panic!("lint_url tool missing: got {tools:?}")); assert_eq!( lint_url["description"], - "Lint a URL with Plumb. Walking-skeleton accepts plumb-fake:// URLs only." + "Lint a URL with Plumb. Accepts http(s):// and plumb-fake:// URLs." ); assert_eq!( lint_url["inputSchema"]["properties"]["url"]["type"], diff --git a/crates/plumb-mcp/AGENTS.md b/crates/plumb-mcp/AGENTS.md index 3b995b8..d1ccb9d 100644 --- a/crates/plumb-mcp/AGENTS.md +++ b/crates/plumb-mcp/AGENTS.md @@ -45,7 +45,8 @@ Use the `09-mcp-tool-author` subagent for cookie-cutter execution. ## Depends on - `plumb-core` (types; `test-fake` feature enabled so `lint_url` can - serve the canned snapshot until the real CDP driver lands). + serve the canned snapshot for `plumb-fake://` URLs). +- `plumb-cdp` (drives Chromium for real `http(s)://` URLs in `lint_url`). - `plumb-format` (mcp_compact). - `rmcp` (server + macros + transport-io + schemars features). - `tokio`, `serde`, `serde_json`, `schemars`, `thiserror`, `tracing`. diff --git a/crates/plumb-mcp/Cargo.toml b/crates/plumb-mcp/Cargo.toml index 4fd4a06..dfb61f8 100644 --- a/crates/plumb-mcp/Cargo.toml +++ b/crates/plumb-mcp/Cargo.toml @@ -14,8 +14,9 @@ keywords.workspace = true categories.workspace = true [dependencies] -# `test-fake` is enabled so the walking-skeleton `lint_url` tool can -# serve the canned snapshot until the real Chromium driver lands. +# `test-fake` keeps the canned snapshot path available for +# `plumb-fake://` URLs alongside the real Chromium driver path. +plumb-cdp = { workspace = true } plumb-core = { workspace = true, features = ["test-fake"] } plumb-format = { workspace = true } rmcp = { workspace = true } diff --git a/crates/plumb-mcp/src/lib.rs b/crates/plumb-mcp/src/lib.rs index 2f86009..c0a9cf5 100644 --- a/crates/plumb-mcp/src/lib.rs +++ b/crates/plumb-mcp/src/lib.rs @@ -7,8 +7,9 @@ //! //! - `echo` — smoke-tests the transport. //! - `lint_url` — lints a URL and returns violations in the MCP-compact -//! shape from `docs/local/prd.md` §14.2. Walking-skeleton accepts -//! `plumb-fake://` URLs only. +//! shape from `docs/local/prd.md` §14.2. Accepts `http(s)://` URLs +//! (driven by `plumb_cdp::ChromiumDriver`) and `plumb-fake://` URLs +//! (served from the canned snapshot). //! - `explain_rule` — returns the canonical markdown documentation and //! metadata for a built-in rule by id. //! @@ -28,7 +29,8 @@ pub use explain::rule_ids as documented_rule_ids; use std::io; -use plumb_core::{Config, PlumbSnapshot, register_builtin, run}; +use plumb_cdp::{BrowserDriver, ChromiumDriver, ChromiumOptions, Target, is_fake_url}; +use plumb_core::{Config, PlumbSnapshot, ViewportKey, register_builtin, run}; use plumb_format::mcp_compact; use rmcp::{ RoleServer, ServerHandler, ServiceExt, @@ -67,7 +69,7 @@ pub struct EchoArgs { /// Arguments to the `lint_url` tool. #[derive(Debug, Deserialize, JsonSchema)] pub struct LintUrlArgs { - /// URL to lint. Walking-skeleton accepts `plumb-fake://` URLs only. + /// URL to lint. Accepts `http(s)://` and `plumb-fake://` URLs. pub url: String, } @@ -96,12 +98,26 @@ impl PlumbServer { } async fn lint_url(&self, args: LintUrlArgs) -> Result { - if !args.url.starts_with("plumb-fake://") { - return Ok(CallToolResult::success(vec![Content::text( - "lint_url currently only accepts plumb-fake:// URLs in the walking skeleton.", - )])); - } - let snapshot = PlumbSnapshot::canned(); + let snapshot = if is_fake_url(&args.url) { + PlumbSnapshot::canned() + } else { + let target = Target { + url: args.url.clone(), + viewport: ViewportKey::new("desktop"), + width: 1280, + height: 800, + device_pixel_ratio: 1.0, + }; + let driver = ChromiumDriver::new(ChromiumOptions::default()); + match driver.snapshot(target).await { + Ok(snap) => snap, + Err(err) => { + return Ok(CallToolResult::error(vec![Content::text(format!( + "lint_url failed: {err}" + ))])); + } + } + }; let config = Config::default(); let violations = run(&snapshot, &config); let (text, structured) = mcp_compact(&violations); @@ -202,7 +218,7 @@ impl ServerHandler for PlumbServer { tool_descriptor::("echo", "Echo a message — smoke test the MCP transport."), tool_descriptor::( "lint_url", - "Lint a URL with Plumb. Walking-skeleton accepts plumb-fake:// URLs only.", + "Lint a URL with Plumb. Accepts http(s):// and plumb-fake:// URLs.", ), tool_descriptor::( "explain_rule", diff --git a/docs/src/mcp.md b/docs/src/mcp.md index 0c2e43f..63d3f9e 100644 --- a/docs/src/mcp.md +++ b/docs/src/mcp.md @@ -32,12 +32,12 @@ For local development against a source checkout: } ``` -## Walking-skeleton tools +## Tools | Tool | Description | |------|-------------| | `echo` | Smoke-test the transport. Echoes the `message` arg back. | -| `lint_url` | Lint a URL. Accepts `plumb-fake://hello` only until the Chromium driver lands. | +| `lint_url` | Lint a URL. Accepts `http(s)://` URLs (driven by the bundled Chromium driver) and `plumb-fake://hello` (canned snapshot for tests). On a Chromium launch failure the response is returned with `isError: true` and a single text block carrying the typed driver error. | | `explain_rule` | Return canonical documentation and metadata for a Plumb rule by id. Args: `{ "rule_id": "/" }`. | The response shape follows the MCP `content` + `structuredContent`