Skip to content
Draft
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
73 changes: 73 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: CI

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1

jobs:
test:
name: build & test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
# A hung test would otherwise occupy a runner until GitHub's 6-hour ceiling.
# The suite finishes in seconds; 30 minutes is generous headroom for a cold
# cache on the slowest platform.
timeout-minutes: 30
strategy:
# Don't cancel the other platforms when one fails — the whole point of
# this matrix is seeing which platforms differ.
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- uses: actions/checkout@v4

# GitHub runners ship a stable Rust toolchain, so there's nothing to
# install. Recorded here because it's easy to assume otherwise.
- name: Show toolchain
run: |
rustc --version
cargo --version

# Tinker binds the OS webview through wry/tao, so Linux needs GTK and
# WebKitGTK headers. Cargo cannot declare these. Without them the build
# fails inside gdk-sys with an error that never names the fix — see
# docs/getting-started.md. Keeping this step here means the workflow
# doubles as executable setup documentation.
- name: Install native dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libgtk-3-dev \
libwebkit2gtk-4.1-dev

# macOS ships WebKit and Windows runners ship the WebView2 runtime, so
# neither needs an install step.

- name: Cache cargo registry and target
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
# Cargo.lock is gitignored in this repo, so the key is based on
# Cargo.toml instead. That makes the cache slightly less precise.
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
restore-keys: ${{ runner.os }}-cargo-

# Build before testing. tests/mcp_tests.rs spawns `cargo run -- --mcp`
# as a subprocess and waits a fixed 2s for it to come up; if the binary
# still had to compile at that point the test would be needlessly slow.
- name: Build
run: cargo build --all-targets --verbose

- name: Test
run: cargo test --verbose
220 changes: 181 additions & 39 deletions ROADMAP.md

Large diffs are not rendered by default.

65 changes: 65 additions & 0 deletions docs/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ The MCP server implements the Model Context Protocol specification, using JSON-R
}
```

## Known limitation: tools do not return results

Every tool currently broadcasts its command to the browser and responds with
`"Command '<name>' sent successfully"`. It does **not** return the command's
result. Reads such as `get_console_logs`, `get_core_web_vitals`, `get_page_info`,
and `execute_javascript` trigger the work, but the output is published to the
event bus rather than returned to the caller.

Use the REST API when you need the value back. This is the top priority in
Track A of the [roadmap](../ROADMAP.md).

## Available Methods

### initialize
Expand Down Expand Up @@ -207,6 +218,60 @@ Execute JavaScript code in the page context.
**Arguments:**
- `script` (string, required): JavaScript code to execute

### Recording & Replay

#### start_recording
Start recording browser events. Requires `name` and `start_url`.

#### stop_recording
Stop the active recording. No arguments.

#### save_recording / load_recording
Persist a recording to disk or read one back. Each requires `path`.

#### start_playback / stop_playback
Begin or halt replay of the loaded recording. No arguments.

#### get_playback_state
Current position, speed, and whether playback is running. No arguments.

#### step_playback
Step one event through the recording. Optional `direction` (`forward` or `backward`,
default `forward`). An unrecognised direction is rejected rather than defaulted, so a
typo can't silently step the wrong way during a bisect.

### Console Monitoring

#### start_console_monitoring
Start capturing console output (log, info, warn, error) from the page. No arguments.

#### stop_console_monitoring
Stop capturing console output. No arguments.

#### get_console_logs
Retrieve captured console messages. Optional `level` (`log`, `info`, `warn`, `error`, `debug`);
omit it to get everything. Use this after an interaction to check whether the page reported errors.

#### clear_console_logs
Clear the captured message buffer. No arguments.

### Performance

#### start_performance_monitoring
Start collecting performance metrics. No arguments.

#### stop_performance_monitoring
Stop collecting performance metrics. No arguments.

#### get_core_web_vitals
Get Core Web Vitals for the current page (LCP, FID, CLS, INP, TTFB, FCP). No arguments.

#### get_memory_metrics
Get memory usage (JS heap, DOM nodes, event listeners). No arguments.

#### get_performance_summary
Get an aggregate performance summary. No arguments.

### Network Monitoring

#### start_network_monitoring
Expand Down
11 changes: 10 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ Then ask Claude to control the browser:
- **DOM Interaction**: find_element, click_element, type_text
- **JavaScript**: execute_javascript, get_page_info
- **Network**: start_network_monitoring, stop_network_monitoring, get_network_stats, export_network_har
- **Console**: start_console_monitoring, stop_console_monitoring, get_console_logs, clear_console_logs
- **Performance**: start_performance_monitoring, stop_performance_monitoring, get_core_web_vitals, get_memory_metrics, get_performance_summary
- **Recording & Replay**: start_recording, stop_recording, save_recording, load_recording, start_playback, stop_playback, get_playback_state, step_playback

See [MCP Server Documentation](docs/mcp-server.md) for complete details.

Expand All @@ -189,7 +192,7 @@ Tinker works and is useful, with the caveats below. Status here is kept
honest against the code — see the [roadmap](ROADMAP.md) for a per-module
breakdown citing implementing files and test counts.

**Verified**: August 22, 2026 · ~70 browser commands · `cargo test` → 164 passed, 3 ignored
**Verified**: August 22, 2026 · ~70 browser commands · `cargo test` → 140 passed, 0 failed

### What works

Expand Down Expand Up @@ -222,6 +225,12 @@ breakdown citing implementing files and test counts.
testing (tab order, accessibility) isn't scriptable yet.
- **Assertions are minimal.** Recordings can store expected state, but there's
no authoring UX and no pass/fail surfacing.
- **MCP tools don't return results.** Every tool triggers its command and
replies `"Command '<name>' sent successfully"`. Reads like
`get_console_logs` and `get_core_web_vitals` do not return logs or vitals —
results are published to the event bus instead. Use the REST API when you
need the answer back. Tracked as the top item in Track A of the
[roadmap](ROADMAP.md).

### Getting it running

Expand Down
4 changes: 3 additions & 1 deletion src/browser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ mod console;
pub mod keyboard;
pub mod session;

use self::{
// Publicly re-exported so the MCP server can hold the same shared state the
// engine does and answer reads directly rather than through the event bus.
pub use self::{
tabs::TabManager,
event_viewer::EventViewer,
tab_ui::{TabBar, TabCommand},
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod api;
pub mod browser;
pub mod event;
pub mod mcp;
pub mod platform;
pub mod templates;

Expand Down
26 changes: 17 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ use clap::Parser;
use tracing::{debug, error, info};
use std::{sync::{Arc, Mutex}, env};

mod api;
mod browser;
mod event;
mod mcp;
mod templates;

use crate::{
browser::{BrowserEngine, session::default_session_path},
// The binary links against the library rather than re-declaring its modules.
// Declaring them here as well compiled the whole crate a second time and ran
// every shared test twice, once per target.
use tinker::{
api, mcp,
browser::{session::default_session_path, BrowserEngine},
event::EventSystem,
};

Expand Down Expand Up @@ -233,12 +231,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
if args.mcp {
let command_tx_clone = command_tx.clone();
let event_rx_clone = event_rx.resubscribe();
// Share the engine's own state with the MCP server so read tools can
// return real values. Both live in this process; the server just runs
// on another thread.
let mcp_state = mcp::BrowserState {
console: browser.console_monitor.clone(),
performance: browser.performance_monitor.clone(),
network: browser.network_monitor.clone(),
player: browser.player.clone(),
};
info!("🚀 Starting MCP server on stdio");
info!("📡 MCP server ready for JSON-RPC protocol messages");

// MCP server must run on a separate thread since it blocks on stdin
std::thread::spawn(move || {
let mut mcp_server = mcp::McpServer::new(command_tx_clone, event_rx_clone);
let mut mcp_server = mcp::McpServer::new(command_tx_clone, event_rx_clone)
.with_state(mcp_state);
if let Err(e) = mcp_server.run() {
error!("MCP server error: {}", e);
}
Expand Down
Loading
Loading