Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions .fgp/skill.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
{
"$schema": "https://fgp.dev/schemas/skill.json",
"name": "browser-gateway",
"version": "1.0.0",
"description": "Fast, high-performance browser automation via Chrome DevTools Protocol",
"author": {
"name": "Wolfgang Schoenberger",
"email": "support@fgp.dev"
},
"repository": "https://github.com/fast-gateway-protocol/browser",
"homepage": "https://fgp.dev",
"license": "MIT",
"keywords": ["browser", "automation", "chrome", "cdp", "playwright"],
"category": "browser-automation",

"binary": {
"type": "rust",
"cargo_package": "fgp-browser",
"build_command": "cargo build --release",
"executable": "target/release/browser-gateway"
},

"distribution": {
"prebuilt": {
"darwin-arm64": "https://github.com/fast-gateway-protocol/browser/releases/download/v${VERSION}/browser-gateway-darwin-arm64",
"darwin-x64": "https://github.com/fast-gateway-protocol/browser/releases/download/v${VERSION}/browser-gateway-darwin-x64",
"linux-x64": "https://github.com/fast-gateway-protocol/browser/releases/download/v${VERSION}/browser-gateway-linux-x64"
Comment on lines +25 to +27
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VERSION placeholder in distribution URLs uses ${VERSION} syntax but the version field is defined as "0.1.0". Ensure that the system performing substitution correctly handles the version format (with or without the 'v' prefix) since the URLs include "v${VERSION}".

Copilot uses AI. Check for mistakes.
},
"homebrew": {
"tap": "fast-gateway-protocol/fgp",
"formula": "browser-gateway"
}
},

"daemon": {
"name": "browser",
"socket_path": "${FGP_HOME}/services/browser/daemon.sock",
"pid_file": "${FGP_HOME}/services/browser/daemon.pid",
"log_file": "${FGP_HOME}/services/browser/daemon.log",
"start_command": ["${SKILL_BIN}", "start"],
"stop_command": ["${SKILL_BIN}", "stop"],
"health_method": "health"
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The daemon health_method references "health" but this method is not defined in the methods array. The methods list includes browser., session., and state.* methods but no "health" method. Either add the health method to the methods array or update the health_method reference to match an existing method name.

Copilot uses AI. Check for mistakes.
},

"methods": [
{
"name": "health",
"description": "Check daemon health status",
"params": {}
},
{
"name": "browser.open",
"description": "Navigate to a URL",
"params": {
"url": { "type": "string", "required": true, "description": "URL to navigate to" },
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.snapshot",
"description": "Get accessibility tree (ARIA snapshot)",
"params": {
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.screenshot",
"description": "Capture PNG screenshot",
"params": {
"path": { "type": "string", "required": true, "description": "Output file path" },
"full_page": { "type": "boolean", "description": "Capture full scrollable page (default: false)" },
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.click",
"description": "Click an element by selector",
"params": {
"selector": { "type": "string", "required": true, "description": "CSS selector or ARIA ref" },
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.fill",
"description": "Fill an input field",
"params": {
"selector": { "type": "string", "required": true, "description": "CSS selector" },
"value": { "type": "string", "required": true, "description": "Value to fill" },
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.press",
"description": "Press a key",
"params": {
"key": { "type": "string", "required": true, "description": "Key to press (e.g., Enter, Tab)" },
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.select",
"description": "Select a dropdown option",
"params": {
"selector": { "type": "string", "required": true, "description": "CSS selector" },
"value": { "type": "string", "required": true, "description": "Option value" },
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.check",
"description": "Check/uncheck a checkbox",
"params": {
"selector": { "type": "string", "required": true, "description": "CSS selector" },
"checked": { "type": "boolean", "description": "Check state (default: true)" },
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.hover",
"description": "Hover over an element",
"params": {
"selector": { "type": "string", "required": true, "description": "CSS selector" },
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.scroll",
"description": "Scroll the page or element",
"params": {
"direction": { "type": "string", "enum": ["up", "down", "left", "right"], "description": "Scroll direction (default: down)" },
"amount": { "type": "integer", "description": "Pixels to scroll" },
"selector": { "type": "string", "description": "Element to scroll (optional)" },
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.press_combo",
"description": "Press key with modifiers (e.g., Ctrl+A)",
"params": {
"key": { "type": "string", "required": true, "description": "Key to press" },
"modifiers": {
"type": "array",
"items": { "type": "string", "enum": ["ctrl", "alt", "shift", "meta"] },
"description": "Modifier keys"
},
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.upload",
"description": "Upload a file to an input element",
"params": {
"selector": { "type": "string", "required": true, "description": "File input selector" },
"path": { "type": "string", "required": true, "description": "Path to file to upload" },
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.session.new",
"description": "Create a new isolated browser session",
"params": {
"session_id": { "type": "string", "description": "Custom session ID (optional)" }
}
},
{
"name": "browser.session.list",
"description": "List active browser sessions",
"params": {}
},
{
"name": "browser.session.close",
"description": "Close a browser session",
"params": {
"session_id": { "type": "string", "required": true, "description": "Session ID to close" }
}
},
{
"name": "browser.state.save",
"description": "Save browser state (cookies, localStorage)",
"params": {
"name": { "type": "string", "required": true, "description": "State name" },
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.state.load",
"description": "Load saved browser state",
"params": {
"name": { "type": "string", "required": true, "description": "State name" },
"session_id": { "type": "string", "description": "Session ID (optional)" }
}
},
{
"name": "browser.state.list",
"description": "List saved browser states",
"params": {}
}
],

"requirements": {
"chrome": {
"type": "browser",
"names": ["Google Chrome", "Chromium"],
"min_version": "90",
"install_hint": "brew install --cask google-chrome"
}
},

"exports": {
"mcp": {
"enabled": true,
"command": "${SKILL_BIN}",
"args": ["mcp"],
"tools_prefix": "fgp_browser"
},
"claude": {
"enabled": true,
"skill_name": "browser-fgp",
"triggers": ["browser", "navigate", "screenshot", "click", "fill", "automation", "web page"]
},
"cursor": {
"enabled": true,
"server_name": "fgp-browser"
},
"windsurf": {
"enabled": false
},
"continue_dev": {
"enabled": false
}
}
}
8 changes: 4 additions & 4 deletions src/browser/aria.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ fn is_interactive_node(node: &CdpAxNode) -> bool {
.role
.as_ref()
.and_then(|role| role.value.as_ref())
.map_or(false, |value| {
.is_some_and(|value| {
let role_str = json_as_str(value).unwrap_or("");
matches!(
role_str,
Expand Down Expand Up @@ -96,7 +96,7 @@ fn is_focusable(node: &CdpAxNode) -> bool {
&& p.value
.value
.as_ref()
.and_then(|v| json_as_bool(v))
.and_then(json_as_bool)
.unwrap_or(false)
})
})
Expand Down Expand Up @@ -267,7 +267,7 @@ fn convert_node_ref(node: &CdpAxNode, counter: &mut usize) -> AriaNode {
&& p.value
.value
.as_ref()
.and_then(|v| json_as_bool(v))
.and_then(json_as_bool)
.unwrap_or(false)
})
})
Expand All @@ -282,7 +282,7 @@ fn convert_node_ref(node: &CdpAxNode, counter: &mut usize) -> AriaNode {
&& p.value
.value
.as_ref()
.and_then(|v| json_as_bool(v))
.and_then(json_as_bool)
.unwrap_or(false)
})
})
Expand Down
2 changes: 1 addition & 1 deletion src/browser/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,7 @@ impl BrowserClient {
})
.collect();

headless_dirs.sort_by(|a, b| b.file_name().cmp(&a.file_name()));
headless_dirs.sort_by_key(|b| std::cmp::Reverse(b.file_name()));

for dir in headless_dirs {
let binary = dir.path().join(subdir_name).join("chrome-headless-shell");
Expand Down
6 changes: 3 additions & 3 deletions src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use fgp_daemon::service::MethodInfo;
use fgp_daemon::FgpService;
use serde_json::Value;
use std::collections::HashMap;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::runtime::Runtime;
use tokio::sync::RwLock;
Expand Down Expand Up @@ -75,7 +75,7 @@ impl BrowserService {

async fn get_or_init_client(
client: &Arc<RwLock<Option<Arc<BrowserClient>>>>,
user_data_dir: &PathBuf,
user_data_dir: &Path,
headless: bool,
) -> Result<Arc<BrowserClient>> {
if let Some(existing) = client.read().await.as_ref() {
Expand All @@ -84,7 +84,7 @@ impl BrowserService {

let mut client_lock = client.write().await;
if client_lock.is_none() {
let new_client = BrowserClient::new(user_data_dir.clone(), headless).await?;
let new_client = BrowserClient::new(user_data_dir.to_path_buf(), headless).await?;
*client_lock = Some(Arc::new(new_client));
}

Expand Down
2 changes: 1 addition & 1 deletion tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ fn test_version_command() {
#[test]
fn test_crate_compiles() {
// This test passes if the crate compiles successfully
assert!(true);
// The existence of this function proves compilation succeeds
}