From 1225db43f534e2f6cda2112dd142e09c223a6c5a Mon Sep 17 00:00:00 2001 From: Filip Michalsky Date: Sun, 30 Aug 2026 15:30:02 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(ruby):=20webmcp=20example=20=E2=80=94?= =?UTF-8?q?=20canonical=20example=20set=20complete=20(M5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of packages/sdk-python/examples/webmcp.py: page.tools discovers the tools the test site registers via navigator.modelContext, invokes calculateSum, and prints the invocation result. Verified live on Browserbase (4 tools discovered, sum 42, status Completed); local verification is network-blocked on this machine (github.io TLS reset — Python example fails identically), the SDK path is the same either way. Co-Authored-By: Claude Fable 5 --- packages/sdk-ruby/README.md | 4 +-- packages/sdk-ruby/examples/webmcp.rb | 49 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 packages/sdk-ruby/examples/webmcp.rb diff --git a/packages/sdk-ruby/README.md b/packages/sdk-ruby/README.md index ecb7f2734..82fba3e85 100644 --- a/packages/sdk-ruby/README.md +++ b/packages/sdk-ruby/README.md @@ -104,10 +104,10 @@ each runnable as `bundle exec ruby examples/.rb [--browserbase]`: | `page_events.rb` | mirrors the Python example; page.on console events + AI extract | | `context_and_response.rb` | Response/cookies/clipboard/viewport/snapshot tour; no LLM needed | | `custom_llm.rb` | mirrors the Python example; bring-your-own-LLM via `llm.generate` (OPENAI_API_KEY or AI_GATEWAY_API_KEY) | +| `webmcp.rb` | mirrors the Python example; page-registered tools via `page.tools` → invoke → result; no LLM needed | | `demo.rb`, `arctic_observe.rb` | spike walkthroughs (not part of the canonical set) | -The one remaining canonical example is `webmcp`, which needs a WebMCP-enabled -target page (the `page.webmcp_*` methods themselves are wrapped and tested). +That completes the canonical example set from `packages/sdk-{ts,python,go}/examples`. ## Development diff --git a/packages/sdk-ruby/examples/webmcp.rb b/packages/sdk-ruby/examples/webmcp.rb new file mode 100644 index 000000000..0d2ec6e09 --- /dev/null +++ b/packages/sdk-ruby/examples/webmcp.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# WebMCP: pages can register tools (navigator.modelContext); Stagehand lists +# them via page.tools and invokes them. Ruby port of +# packages/sdk-python/examples/webmcp.py — no LLM needed. +# +# ruby examples/webmcp.rb # local Chrome (headed, like the sibling SDKs) +# ruby examples/webmcp.rb --browserbase # Browserbase session + +require "json" + +require_relative "../lib/stagehand" +require_relative "example_helpers" + +WEBMCP_TEST_SITE = "https://browserbase.github.io/stagehand-eval-sites/sites/webmcp-test/" + +browser = + if ARGV.include?("--browserbase") + api_key = ExampleHelpers.env("BROWSERBASE_API_KEY") or abort "Set BROWSERBASE_API_KEY." + puts "Creating a Browserbase session..." + Stagehand::Browserbase.launch(api_key: api_key) + else + puts "Launching local Chrome..." + Stagehand::LocalBrowser.launch(headless: false) + end + +begin + stagehand = Stagehand.create(browser: browser, log_level: ENV.fetch("STAGEHAND_LOG_LEVEL", "warn")) + begin + page = browser.context.pages.first + raise "Stagehand initialized without an active page" if page.nil? + page.goto(WEBMCP_TEST_SITE) + + tools = page.tools(timeout: 5_000) + puts "tools -> #{tools.map(&:name).join(", ")}" + calculate_sum = tools.find { |tool| tool.name == "calculateSum" } + raise "calculateSum was not registered by the page" if calculate_sum.nil? + + invocation = calculate_sum.invoke(input: { a: 19, b: 23 }) + result = invocation.result + + puts JSON.pretty_generate(result.to_wire) + ensure + stagehand.close + end +ensure + browser.close +end +puts "Closed." From e96a6e4677f0ef445821f5f536ec6e3b16cda297 Mon Sep 17 00:00:00 2001 From: Filip Michalsky Date: Sun, 30 Aug 2026 19:10:00 -0400 Subject: [PATCH 2/2] style: oxfmt markdown (fmt:check) Co-Authored-By: Claude Fable 5 --- packages/sdk-ruby/ESTIMATE.md | 44 +++++++++++++++++------------------ packages/sdk-ruby/README.md | 36 ++++++++++++++-------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/sdk-ruby/ESTIMATE.md b/packages/sdk-ruby/ESTIMATE.md index 27da2456e..61c5dcc5f 100644 --- a/packages/sdk-ruby/ESTIMATE.md +++ b/packages/sdk-ruby/ESTIMATE.md @@ -16,34 +16,34 @@ Python and Go SDKs are built. ## What the spike proved (de-risked) -| Risk | Outcome | -|---|---| -| No JSON-Schema→Ruby codegen exists | Custom stdlib-only generator, 274 LOC, covers all 234 defs + method registries, `--check` drift mode wired into `just generate`/`just check` | -| Wire casing | Free — the schema is already snake_case; opaque containers pass through verbatim (fixture round-trip tested) | -| Sync Ruby ↔ bidirectional RPC | Background-reader-thread model (Go-style) works; `websocket-driver` + own TCP/SSL socket, no reactor framework needed | -| Chrome without Playwright | Hand-rolled launcher ported from Python's `browser.py` works headless + headed on macOS | -| Extension packaging | Ruby zip is **byte-identical** to the Python SDK's deterministic archive (same SHA-256) | -| No Browserbase Ruby SDK exists | Hand-rolled 4-endpoint REST client (~130 LOC) suffices, mirroring Go | -| End-to-end | `goto → observe → act → extract` verified live on a local Chrome (transport) and on a Browserbase session (full AI loop incl. extension upload + Model Gateway + session release) | +| Risk | Outcome | +| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| No JSON-Schema→Ruby codegen exists | Custom stdlib-only generator, 274 LOC, covers all 234 defs + method registries, `--check` drift mode wired into `just generate`/`just check` | +| Wire casing | Free — the schema is already snake_case; opaque containers pass through verbatim (fixture round-trip tested) | +| Sync Ruby ↔ bidirectional RPC | Background-reader-thread model (Go-style) works; `websocket-driver` + own TCP/SSL socket, no reactor framework needed | +| Chrome without Playwright | Hand-rolled launcher ported from Python's `browser.py` works headless + headed on macOS | +| Extension packaging | Ruby zip is **byte-identical** to the Python SDK's deterministic archive (same SHA-256) | +| No Browserbase Ruby SDK exists | Hand-rolled 4-endpoint REST client (~130 LOC) suffices, mirroring Go | +| End-to-end | `goto → observe → act → extract` verified live on a local Chrome (transport) and on a Browserbase session (full AI loop incl. extension upload + Model Gateway + session release) | Spike size: ~2,400 hand-written LOC + ~2,000 generated + ~800 tests. Python comparison: ~4,900 hand-written + ~3,600 generated — a fair proxy for the finished Ruby size. ## Work breakdown to production parity -| # | Item | Weeks | -|---|---|---| -| A | Walking skeleton (this spike) | 2.5–3 *(sunk)* | -| B | Full method surface (~62 remaining: context 14, page ~26, locator 17, response 6, clipboard, webmcp, file upload, callback_batch passthrough) — mechanical wrapper + tests per method, batched by namespace | 3–4 | -| C | Client-side LLM (`llm.generate` inbound handler, message/tool unions, custom-LLM example) | 1 | -| D | Validation hardening: strict unions, input ergonomics, RBS signatures, thread-safety soak | 1–1.5 | -| E | 11-example set + `example-parity` compliance | 0.5–1 | -| F | Parity tooling: ast-grep Ruby lane (`@ast-grep/lang-ruby` availability is the biggest unknown; prism-based fallback +0.5–1 wk) + extend `rules/ast-grep/*` | 1–1.5 | -| G | Docs: Ruby tabs across `docs/v4/reference/*` + guides + `sdk-reference.test.ts` | 1 | -| H | Release + CI: turbo task, changesets version proxy, `sync-ruby-version.ts`, extension embedding in the gem, RubyGems trusted publishing + alpha lane, CI matrix (Ruby 3.2–3.4 × macOS/Linux), **Windows launcher** | 1.5–2 | -| I | Beta hardening buffer (real-world sites, large payloads, memory/soak) | 1–1.5 | -| | **Total beyond spike** | **10–13.5** | -| | **Total including spike** | **~12.5–16.5** | +| # | Item | Weeks | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | +| A | Walking skeleton (this spike) | 2.5–3 _(sunk)_ | +| B | Full method surface (~62 remaining: context 14, page ~26, locator 17, response 6, clipboard, webmcp, file upload, callback_batch passthrough) — mechanical wrapper + tests per method, batched by namespace | 3–4 | +| C | Client-side LLM (`llm.generate` inbound handler, message/tool unions, custom-LLM example) | 1 | +| D | Validation hardening: strict unions, input ergonomics, RBS signatures, thread-safety soak | 1–1.5 | +| E | 11-example set + `example-parity` compliance | 0.5–1 | +| F | Parity tooling: ast-grep Ruby lane (`@ast-grep/lang-ruby` availability is the biggest unknown; prism-based fallback +0.5–1 wk) + extend `rules/ast-grep/*` | 1–1.5 | +| G | Docs: Ruby tabs across `docs/v4/reference/*` + guides + `sdk-reference.test.ts` | 1 | +| H | Release + CI: turbo task, changesets version proxy, `sync-ruby-version.ts`, extension embedding in the gem, RubyGems trusted publishing + alpha lane, CI matrix (Ruby 3.2–3.4 × macOS/Linux), **Windows launcher** | 1.5–2 | +| I | Beta hardening buffer (real-world sites, large payloads, memory/soak) | 1–1.5 | +| | **Total beyond spike** | **10–13.5** | +| | **Total including spike** | **~12.5–16.5** | ## Ongoing cost diff --git a/packages/sdk-ruby/README.md b/packages/sdk-ruby/README.md index 82fba3e85..8410c2446 100644 --- a/packages/sdk-ruby/README.md +++ b/packages/sdk-ruby/README.md @@ -50,8 +50,8 @@ end to end but deliberately implements only a sliver of the API surface. and WebMCP (`tools` → invoke/result/cancel). - **Locator** — `page.locator(selector)` → all 17 locator methods (`click/fill/type/hover/scroll_to/count/text_content/inner_text/inner_html/ - input_value/visible?/checked?/centroid/highlight/send_click_event/ - select_option/set_input_files` + `first`/`nth`; file uploads take paths or +input_value/visible?/checked?/centroid/highlight/send_click_event/ +select_option/set_input_files` + `first`/`nth`; file uploads take paths or `Stagehand::FilePayload`, 50 MiB/file). - **Context** — `pages/new_page/active_page/set_active_page/close`, cookies (`cookies/add_cookies/clear_cookies` with String/Regexp filters), clipboard @@ -90,22 +90,22 @@ Browserbase sessions. Ruby ports of the canonical example set (`packages/sdk-{ts,python,go}/examples`), each runnable as `bundle exec ruby examples/.rb [--browserbase]`: -| Example | Notes | -|---|---| -| `act.rb`, `observe.rb`, `extract.rb` | example.com, mirror the Python examples | -| `model_gateway.rb` | Browserbase-only; no model configured (Gateway picks one) | -| `caching.rb` | Browserbase-only; `cache: true` + `metadata.cache` round-trip | -| `custom_logging.rb` | `on_log:` callback appending JSONL to `stagehand.jsonl` | -| `file_upload.rb` | mirrors the Python example; no LLM needed (runs local or Browserbase) | -| `page_interactions.rb` | locator fill/type/click/readers, evaluate, screenshot, history; no LLM needed | -| `hybrid_news.rb` | AI extract + deterministic locators/pagination/screenshot on Hacker News | -| `search_flow.rb` | locator-driven search on DuckDuckGo + AI extraction of the results | -| `batch.rb` | mirrors the Python example; callback batch, no LLM needed | -| `page_events.rb` | mirrors the Python example; page.on console events + AI extract | -| `context_and_response.rb` | Response/cookies/clipboard/viewport/snapshot tour; no LLM needed | -| `custom_llm.rb` | mirrors the Python example; bring-your-own-LLM via `llm.generate` (OPENAI_API_KEY or AI_GATEWAY_API_KEY) | -| `webmcp.rb` | mirrors the Python example; page-registered tools via `page.tools` → invoke → result; no LLM needed | -| `demo.rb`, `arctic_observe.rb` | spike walkthroughs (not part of the canonical set) | +| Example | Notes | +| ------------------------------------ | -------------------------------------------------------------------------------------------------------- | +| `act.rb`, `observe.rb`, `extract.rb` | example.com, mirror the Python examples | +| `model_gateway.rb` | Browserbase-only; no model configured (Gateway picks one) | +| `caching.rb` | Browserbase-only; `cache: true` + `metadata.cache` round-trip | +| `custom_logging.rb` | `on_log:` callback appending JSONL to `stagehand.jsonl` | +| `file_upload.rb` | mirrors the Python example; no LLM needed (runs local or Browserbase) | +| `page_interactions.rb` | locator fill/type/click/readers, evaluate, screenshot, history; no LLM needed | +| `hybrid_news.rb` | AI extract + deterministic locators/pagination/screenshot on Hacker News | +| `search_flow.rb` | locator-driven search on DuckDuckGo + AI extraction of the results | +| `batch.rb` | mirrors the Python example; callback batch, no LLM needed | +| `page_events.rb` | mirrors the Python example; page.on console events + AI extract | +| `context_and_response.rb` | Response/cookies/clipboard/viewport/snapshot tour; no LLM needed | +| `custom_llm.rb` | mirrors the Python example; bring-your-own-LLM via `llm.generate` (OPENAI_API_KEY or AI_GATEWAY_API_KEY) | +| `webmcp.rb` | mirrors the Python example; page-registered tools via `page.tools` → invoke → result; no LLM needed | +| `demo.rb`, `arctic_observe.rb` | spike walkthroughs (not part of the canonical set) | That completes the canonical example set from `packages/sdk-{ts,python,go}/examples`.