Skip to main content
Glama

Scout MCP

Scout is an MCP server for web crawling that improves with use. A capable model like Claude works a site out once. Scout compiles what it learned into a recipe that a 4B model can run with one instruction.

/scout https://mikrotik.com/product/RB433AH price|cpu

Claude reaches the page, maps the site and finds the traps. It then hands back a one-line card that Qwen3.5-4B runs correctly from then on:

Call the tool run with recipe="mikrotik.com/specs" and input="hAP ax2". Reply with the RESULT line only.

RESULT: ok | mikrotik.com/specs | hAP ax2 | url=https://mikrotik.com/product/hap_ax2; title=MikroTik · hAP ax²; price=$99.00; cpu=IPQ-6010; ram=1 GB; max_power=27 W

Install

Claude Code (installs the MCP server and the /scout skill):

/plugin marketplace add sireggserroneous/scout-mcp
/plugin install scout@scout-mcp

Any MCP client. Install uv, then add this to the client's MCP config:

{ "mcpServers": { "scout": { "command": "uvx",
  "args": ["--from", "git+https://github.com/sireggserroneous/scout-mcp", "scout-mcp"] } } }

Pages built by JavaScript need a headless browser. Add "--with", "playwright" to the uvx args, then run uvx playwright install chromium once. If you already run Firecrawl or crawl4ai, set FIRECRAWL_URL or CRAWL4AI_URL and Scout uses it as one more reader.

Related MCP server: mcp-web-search

From a maze to one line

Getting MikroTik specs looks like a one-step job. Here is what Claude actually ran into on the way:

Try

Result

guess mikrotik.com/product/{model}

RB433AH works. hAP ac3 is a 404: that page lives at hap_ac3.

lowercase it, turn spaces into _

Now RB433AH is a 404: that one really is uppercase.

also turn + into plus

CRS354-48G-4S+2Q+RM works (crs354_48g_4splus2qplusrm)…

…but RB5009UG+S+IN lives at rb5009ug_s_in. Same character, opposite rule.

look the url up in the sitemap, comparing both sides with plus and punctuation removed

all four styles work

Then the data: 4 fields, each a label on one line and its value on a later line, on a page that also has a 40-row throughput table, a documents list and a retailer map.

Written out as instructions, that is a 7-step program with a string-normalisation rule, a loop over 566 urls, a branch for "not found", and four lookups in a 7,000-character page. That is too much for a 4B model. So it is never handed to one. Claude compiles it instead:

{"name": "mikrotik.com/specs", "input_name": "model",
 "examples": ["RB433AH", "hAP ac3", "CRS354-48G-4S+2Q+RM", "RB5009UG+S+IN", "CCR2004-1G-12S+2XS"],
 "steps": [
  {"map": "https://mikrotik.com", "filter": "/product/", "same": [["plus", ""], ["[^a-z0-9]", ""]]},
  {"reach": "{url}", "want": "Specification"},
  {"extract": {"price":     "^\\s*-\\s*Suggested price\\s*\\n\\s*(\\S.*)$",
               "cpu":       "^\\s*-\\s*CPU\\s*\\n\\s*(\\S.*)$",
               "ram":       "^\\s*-\\s*Size of RAM\\s*\\n\\s*(\\S.*)$",
               "max_power": "^\\s*-\\s*Max power consumption\\s*\\n\\s*(\\S.*)$"}}]}

compile runs the recipe on every example and saves it only if all of them pass. The two wrong guesses above were caught this way: each one passed on some examples and failed on others. Once saved, every branch lives in code. The small model gets one card and one tool, and its whole job is a single call:

the small model gets

the small model must

without a recipe

7 numbered steps, site_map and reach

normalise strings, scan 566 urls, branch, read a 7k-char page, format a line

with a recipe

1 line, the run tool

make one call, copy one line

Measured on Qwen3.5-4B (Q4_K_M, llama.cpp on a laptop iGPU), on 5 MikroTik models, as an A/B:

exact answer

tool calls

wall time (median)

A: the 7 steps as instructions, site_map + reach

4 / 5

2–4 per model, 15 in all (5 of them wrong turns)

389–671 s (414 s)

B: the card, run

5 / 5

1 per model

22–195 s (88 s)

In arm A, Qwen filtered the sitemap with the raw model name, built a url with a space in it, and once dropped the $ from a price. It reached the right page in the end, but it took 3 to 11 minutes each time, mostly spent reading 566 urls and a 7,000-character page. In arm B the model never sees any of that. One arm-A run did catch a bug in the recipe: the RAM regex had matched a sentence in the product description, not the spec list. The fix (anchoring each field to its list item) went in, the recipe was recompiled with that model as a fifth example, and the arm-B run was repeated on the fixed recipe. Both arms used the same model and the same Scout. Harness: bench/qwen_ab.py.

If a page changes, run fails loudly with a code: NO_MATCH at step 1, EXTRACT_MISS at step 3. The recipe's score drops, and recipes() marks it last run failed … recompile. The next big-model session fixes the recipe. The small model's card stays the same.

The CLI form fits a small model with a shell. It prints one line and exits 1 on failure:

$ scout-mcp run mikrotik.com/specs "CCR2004-16G-2S+"
RESULT: ok | mikrotik.com/specs | CCR2004-16G-2S+ | url=https://mikrotik.com/product/ccr2004_16g_2splus; ... price=$465.00; cpu=AL32400; ram=4 GB; max_power=48 W
$ scout-mcp run mikrotik.com/specs "nope 9"
RESULT: fail | mikrotik.com/specs | nope 9 | NO_MATCH at step 1

Recipe steps

Step

Does

{"input": [[regex, repl], ...], "lower": true}

rewrites the input (slug rules)

{"map": url, "filter": regex, "same": [[regex, repl], ...]}

the site's own urls. same keeps the url whose last segment equals the input after both are rewritten. Last step: returns the list. Otherwise: the first url becomes {url}.

{"reach": "https://…/{input}" | "{url}", "want": regex}

reads a page with the full reader ladder (below)

{"find": regex}

the first link on the page whose url or text matches becomes {url}

{"links": "/chapter/<n>/"}

a link family from families. Last step: every matching link on the page. Otherwise: the first becomes {url}.

{"jsonld": {"price": "offers.price"}, "type": "Product"}

reads the page's schema.org data by dotted path. A name ending in ? is optional.

{"extract": {"field": regex, "optional?": regex}}

group 1 of each regex. Every field must be found, except one whose name ends in ?, which is filled only when the page has it.

How Scout learns: a maze, a Markov chain, and rewrite rules

Every way to reach a page is a move:

  • the readers: direct, firecrawl, crawl4ai, browser

  • url_rewrite: a regex on the full url

  • locale_prefix × listing_root: where a site keeps its index of things, such as /en-us + /products

  • detail_suffix: where the details live, such as /specifications

The idea comes from Markov chains and MarkovJunior-style rewrite rules. Each move takes Scout from one state to the next. The goal state is good information: real content (not a wall, a login page or an error page) that carries what you asked for.

flowchart LR
  U[url + want] --> O[host's ordered moves<br/>winner first, recent failures last]
  O --> J{good information?}
  J -- yes --> W[return page · learn the winner]
  J -- no --> N[next move: reader → url_rewrite → detail_suffix → on-page link]
  N --> J
  N -- maze exhausted --> E[coded error + next steps · logged]
  W -. a big model compiles the path .-> R[recipe: one call for a small model]
  • Each host keeps its readers in order. The reader that last worked goes first, and one that failed goes to the back for six hours. When excalidraw.com gives a plain fetch an empty JavaScript shell, Scout falls back to the browser. On the next visit it starts with the browser.

  • The register stores moves as data. Each move has a scope (*, a domain or a host regex) and a Laplace score (wins + 1) / (tries + 2). A move nobody has tried scores 0.5. A move that loses its first five tries is retired. Agents propose moves with moves(action='propose', …), and real traffic decides which ones stay.

  • Parts compose. A listing url is a locale prefix ("", /en-us, /us/en, …) plus a catalogue root (/products, /collections, /catalog, …). Scout ranks every pairing by the product of the two parts' scores. It puts the locale in the url you gave it, or the one the host taught it, first. Credit is by contrast: when /products wins after /en/products failed, only /en loses. When nothing wins, nothing is scored, because a site with no listing teaches nothing about the parts.

  • Recipes are the compiled layer on top. Every run scores a recipe, the same way moves are scored.

The recipe book it ships with

scout_mcp/recipes.json was distilled from 417 sites that a larger crawler had learned. Every one was checked live again on 2026-10-03 with Scout's own user agent, and only what held up was kept:

  • Shared structure became scored parts. The priors are measured. /products was the root of all 13 verified catalogue indexes. A bare root (no locale) won 8 of 8. The /en prefix the old crawler kept guessing was real on only 4 of 41 sites: the rest were 404s, homepage redirects, or soft 200s serving the homepage. /specifications is the detail page on 5 sites.

  • Per-site facts, only where a site differs (147 sites): 56 need a rendering reader, 14 have a verified listing (7 with a locale), 28 have item url patterns proven by a real catalogue, 6 have a detail suffix, and 59 put up a wall (challenge, robots, refusal or login) that Scout reports before trying.

  • Dropped: unverified listings, including product pages, category pages, tag pages and a census.gov policy page that had all passed as "listings", plus item patterns a small model guessed but a catalogue never proved.

Point SCOUT_DB at a shared path and a whole team, people and agents alike, learns from each other's runs.

These rules came from a sibling project: a recipe chain that rebuilt 16 US state codes from scratch, with 7 or 8 of every 8 sampled sections matching the held text word for word. Its hardest problems were about links, and the fixes were all general:

  • A link's family is its path with the numbers taken out. Chapter_1.html and Chapter_14A.html are one family, and so are NHTOC-I.htm and NHTOC-XIV.htm (Roman numerals). Links with no numbers group as siblings under their parent (/products/*). Only same-site families with 3 or more links count. Static assets, #anchors (each item is read once, not once per anchor) and a page's link to itself are ignored.

  • Sample across the family, not its first links. A list opens with its odd items: a code's first chapter is often a one-line "Repealed". Requiring 2 of 3 good samples at every level compounded into no path at all, so it is 2 at the top and 1 below.

  • Vote on what the family leads to, not on whichever sample came back first. Section pages that also list their neighbours made one sample look like an index; the majority got it right.

  • Look deeper before trusting a weak match. A page that looks like one item, but still has families leading down, is walked further first.

  • Rank by yield through every level: links × share of good samples × what each yields below. Scoring one level at a time made a five-page index outrank 89 chapters of sections.

  • Tell content from menus by function words. The density of the, of, shall, any separates sentences (≈0.35–0.5) from navigation (≈0.15) where capitals and length could not. Every reach result carries this prose score.

  • Skip families that lead to a different kind of document. Statute pages link to the bills that enacted them, and the chain followed them into scanned slip laws.

families(url, want?) runs this walk and returns the ranked families, their samples, and a recipe_hint draft for compile. The links recipe step then lists a family's urls on sites with no sitemap.

Live, on two sites (2026-10-03):

Start

Best chain found by sampling

Estimate

Compiled hint

Idaho statutes index

Title<n> (74 links, 5/5 samples are indexes) → Title<n>/T<n> (34 per title, 3/3 pages)

≈2,516 chapter pages

RESULT: ok … count=19; first=…/Title1/T1CH1

a MikroTik category page

/product/* (22 links, 5/5 pages)

22 products

RESULT: ok … count=22; first=…/product/hex_s_2025

MikroTik added two rules the law sites never needed:

  • A family the page above already shows is navigation. Every MikroTik page carries the same category menu, and without this rule the walk descended from the menu back into the menu.

  • Siblings group under their parent first. The number shape names the family only when one shape covers 80% of them (Title<n>). RB<n> and crs<n>_<n>_<n>_in are both just /product/*.

When a site blocks you

Also from the sibling project. A statute site blocked its crawler's address after a re-read at ten requests a second, and the lessons generalise:

  • A block page can come back as HTTP 200. That site answered Blocked <your IP> … for assistance EMAIL: WebMaster@… as an ordinary page, so a check on status codes alone never saw it and the crawler kept going. Scout reads a page as a block only when it has both halves, a refusal ("blocked", "access denied", "too many requests") and something about the visitor (an IP address, "your IP", a CAPTCHA, "are you a robot"). That way a page that only mentions "blocked" isn't mistaken for one. It checks error bodies and 200 pages alike.

  • A block stops the whole walk. Every reader leaves from the same address, so trying the next one only adds to the load. Scout stops, puts the host on hold for 30 minutes, and slows its pace there for good (5 s or slower). A 429 does the same, for its Retry-After or 2 minutes.

  • The probe is built in. When an automatic hold ends, the next reach is a single probe. If it is refused again, the hold doubles (up to a day) and the pace slows again. You don't need a separate watcher process.

  • Holds are shared. They live in Scout's memory, so every agent pointed at the same SCOUT_DB stays off the site. A held host is not contacted at all: reach, site_map and families return HELD and send no request.

  • The way out is a person. The block page usually names a contact, and Scout keeps it with the host. The BLOCKED error's first next step says who to email and what to say: what reads the site, that it went too fast, the new pace, and an offer to use a bulk download. hold(host, reason='unblock requested <date>') then keeps everyone off until hold(host, lift=true). A hold set that way has no end date, so Scout never probes it.

Official routes and enrollment

When a site walls Scout, the best next step is usually the site's own front door: an open API, a bulk download, a feed, or an API a person signs up for. Scout keeps that per site as its access entry.

  • No signup needed (open_api, bulk, feed, public_json, alternative): the wall's first next step is that route, as a ready reach(...).

  • A person must enroll (enroll_free, enroll_paid, enroll_oauth, commercial): Scout files an enrollment request the first time the wall is hit. enroll(action='list') is the checklist. Each entry says where to sign up, what it costs, the terms, and where the key goes.

  • No route known yet: the error says so, and a big model looks for one (the API, developer program or bulk download) and records it with enroll(action='request', …). Say you need LexisNexis but haven't set Scout up for it: /scout https://www.lexisnexis.com/... hits the login, Claude records LexisNexis's developer program, and it lands on the checklist.

  • Updating Scout: /scout-update works through the checklist with you, along with failing recipes, holds waiting on a site's answer, and recent failures. When you've put a key in place, enroll(action='done') tests it on the API's sample url before marking the site enrolled. From then on recipes call the API with no key in them; Scout adds it.

Keys never pass through a tool. They live in SCOUT_KEY_<NAME> environment variables or in ~/.config/scout-mcp/keys.env (NAME=value). Scout ignores that file unless its mode is 600. A key is added at fetch time, only to its own API's host, and scrubbed by value from every page, url and error Scout returns. OAuth client credentials (NAME_ID and NAME_SECRET) are exchanged for a token, which is cached until it expires.

The book ships with the official routes for 21 sites that challenged Scout, researched and tested on 2026-10-03:

Site

Official route

Signup

Tested from a datacenter address

www.ecfr.gov, ecfr.federalregister.gov

eCFR API (+ GovInfo bulk XML)

none

200 JSON, while the pages challenge

www.drugs.com

DailyMed, openFDA and RxNav (the FDA's own label data)

none

200 JSON

www.nytimes.com

RSS feeds (full text needs the free Developer API key)

none

200 RSS

www.arlingtonma.gov

the town's RSS feeds and GIS data hub

none

200 RSS, 200 JSON

snuggymom.com

WordPress RSS and REST API

none

200 JSON

www.dmp.com

product literature on assets.dmp.com

none

200 PDF

alaskacountyoffices.org

Alaska's state community database (ArcGIS)

none

200 JSON

legiscan.com

LegiScan Public API

free key

—

www.merriam-webster.com

Merriam-Webster Dictionary API

free key (non-commercial)

—

www.britannica.com

Britannica Syndication API

free key (non-commercial)

—

www.digikey.com

DigiKey Product Information v4

free OAuth app

—

www.bhphotovideo.com, www.guitarcenter.com

affiliate programs (product feeds unverified)

free application

—

us.rs-online.com, www.acronymfinder.com

licensed web services

contract

—

webfiles.nycourts.gov, dictionary.cambridge.org, www.arcat.com, www.essentialbuildstore.com, aqcheckin.s3.amazonaws.com

none found: no API, a retired one, an unreachable one, or a private bucket

—

the reason is recorded, so no one searches again

Several "challenged" sites serve their own APIs and feeds without any challenge. A wall in front of the pages is often not a wall in front of the data.

Lessons from a big catalogue: AMD

The sibling crawler's scout went after AMD's catalogue, and every mistake it made became a general rule:

  • A wall can be silent. From some networks amd.com answers no status at all: the plain fetch stalls until it times out, and the browser's HTTP/2 stream is reset. Scout used to call that NETWORK and suggest checking the spelling. A stall or reset from a host that resolves is now SILENT_REFUSAL: the site's edge refusing this client. It is treated as a wall (official route first, the host held so nothing keeps knocking), not as a typo.

  • A map has to work when plain fetches don't. site_map reads sitemaps and walks links through the host's learned reader, not just a plain fetch.

  • Map from the catalogue's root when nothing else looks like a catalogue. AMD's map ran out on blog posts. Walking /en/products found 1,561 product pages. When the sitemap and the walk turn up no catalogue-shaped urls, Scout finds the root with its scored locale × root parts and walks from there. That way the step is a scored move, not a hard-wired rule.

  • Editorial sections are never the items. The scout picked /blogs/ and /newsroom/ as AMD's product pattern and "proved" 689 blog posts. Blog, news, press, events, careers, investors and similar patterns are flagged editorial and ranked last in site_map and families.

  • A title of ===== is not a title. It falls back to the page's first real heading.

  • "… Series" is a category. So are "… Family" and "… Lineup". Recipes pick their examples from single items.

  • A page with children under its own path is a hub, not an item. families counts 3 or more child pages as an index. Partner, request-a-quote and contact pages are flagged with the editorial ones. Support pages are not flagged, because some makers keep their product pages under /support/.

  • Listing a whole site again and again gets you blocked. Four dry runs in an hour, each re-listing amd.com, and its edge answered "Access Denied" to everything. site_map now reuses a host's listing for 12 hours (fresh=true to list again). The edge's denial page names no IP, only an incident reference (Reference #18.…). An incident reference (Akamai Reference #, Cloudflare Ray ID) now counts as the visitor half of a block, so the host is held, not retried.

Download once, read from the archive

Also from the sibling crawler: the way to map a whole site without loading it is to ask the library that already copied it. The Wayback Machine held 1,543 of AMD's product pages, the newest a day old, while amd.com refused every request.

  • Listing from the archive's index. site_map(url, archive='only') lists a site from the Wayback Machine's public index without a single request to the site. Robots rules come from the site's archived robots.txt. With the default archive='auto', the index is used when the site is on hold or listed nothing. amd.com's product section: 1,816 urls in 12 seconds.

  • Reading from the archive. When a site walls Scout (blocked, a silent refusal, a challenge, a refusal, a login, or down), the next rung is the latest archived copy, as the site served it. A held site is read from the archive only, with no contact with the site at all. Every result says so: archived: {captured: 2026-08-21, copy: …}, and recipe RESULT lines carry archived=<date>. The url stays the site's own, so provenance is honest.

  • The archive is never a host's winning reader. Live reading resumes as soon as the site allows it, and robots.txt still binds: a path the site disallows is not read from its archive either. The archive itself is paced at one request every 4.5 seconds, under its limit of about 15 copies a minute. Scout's first try at 2 seconds got it refused by archive.org, so a refusal from the archive now holds the archive too (10 minutes, doubling), and cache jobs wait out the hold, never knocking through it.

  • A local mirror. Every good page is kept on disk for a day (SCOUT_MIRROR_DAYS). Re-runs, parser fixes and recipe compiles read the copy, not the site; reach(fresh=true) reads it again. Scout's pace was already a promise: it only ever slows down for a host, never speeds back up.

It compiles like any other recipe. amd.com/processor-specs ships in the book, built entirely from archived pages:

RESULT: ok | amd.com/processor-specs | EPYC 9575F | url=https://www.amd.com/en/products/processors/server/epyc/9005-series/amd-epyc-9575f.html; title=AMD EPYC™ 9575F; archived=2026-07-10; cores=64; threads=128; boost=Up to 5 GHz; tdp=400W; socket=SP5; launched=10/10/2024

Cached mode: when you'll pull a lot from one site

Asking a site for the same pages again and again is how crawlers get blocked. If you're about to pull a lot from one site, download it once and work from the copy:

cache("https://www.amd.com/en/products/processors/", filter="(9000|9005)-series/.+\\.html$", source="archive")
cache(url, action="status")                       # listed, fetched, failed, eta
run("amd.com/processor-specs", inputs=["Ryzen 9 9950X3D", "EPYC 9575F", ...], cached=true)
  • cache lists the site (sitemaps, link walk, catalogue root, or the Wayback index), then fetches every page once into Scout's copy, in the background, at the host's pace. source='archive' downloads from the Wayback Machine with no request to the site at all. 'auto' reads live, with the archive where the site walls Scout. The copy is kept days (default 7).

  • cached=true on reach, site_map and run reads only the copy and sends nothing anywhere. A page that isn't in it fails with NOT_CACHED and says how to get it. A recipe over a cached site runs at memory speed, so a small model can work through a long input list without the site ever seeing it.

  • run(recipe, inputs=[…]) returns one RESULT line per input. On the command line: scout-mcp cache <url> [filter] [--archive] downloads in the foreground, and scout-mcp run <recipe> - --cached < models.txt prints one line per input.

What a scout checks first

These rules come from the sibling crawler's queue:

  • What already exists. scout(url) returns the site's compiled recipes first. A recipe that passes is the answer, so nobody re-scouts the site.

  • Whose site it is. own_hosts lists the hosts the site links to that carry its name, such as regional sites or a store. A host that carries the name but isn't linked is a reseller lead, not the maker: meanwellsource.com had 1,827 "Mean Well" pages.

  • The sibling the request names. When a page lacks what was asked, one of Scout's next steps climbs to the page that lists it and its siblings, and follows the sibling link that names two-thirds of the request's words. Depot held New York's Criminal Procedure Law and was asked for its Estates, Powers and Trusts Law; the CPL's parent page named it.

Counted is not read: lessons from a law re-read

The sibling crawler landed 17,126 law sections with zero errors. When someone finally read a sample, much of it was broken: 29,299 sections were a website's menu ("Skip navigation Home Documents…"), 23,530 were a heading with no body, and a double-escaped regex had turned every "t" of 9,308 Alabama sections into a space ("he books… o de ermine he accuracy"). Every check had counted; none had read. In Scout:

  • A content gate on recipe values. A value that is page chrome (menu text, raw markup, a stylesheet rule) or has lost common letters fails as BAD_VALUE, at compile and on every run.

  • A regex lint at compile. A double-escaped escape inside a character class ([^\\t] means "not a backslash and not the letter t") is refused with that explanation.

  • The skill says to read the test values yourself. A wrong but plausible value is something only a reader catches.

  • Text decoded the way it was written. Bytes that decode cleanly as UTF-8 are read as UTF-8. Otherwise Scout uses the declared charset, but never UTF-16/32 without a byte-order mark (one state's pages declare UTF-16 and are UTF-8), and falls back to windows-1252 (another state's pages lost every § when read as UTF-8).

  • A site that asks for slower requests gets them, for as long as it asks. That includes a Retry-After given as an HTTP date.

  • A download nobody is running says so. The re-reads died with the one-off script that started them, and nothing noticed. A cache job whose process is gone shows interrupted, and starting it again resumes where it stopped.

The laws of the United States

Scout can read a state's whole code from the legislature's own site and write it out, one JSON line per section:

scout-mcp laws                                   # the states it can read, with their citation form and source
scout-mcp laws us-tx --out texas.jsonl           # every section of the Texas statutes
scout-mcp laws us-nv --limit 5                   # a quick look

Each line is {"citation": "NRS 1.010", "heading": "...", "text": "...", "url": "<the page it came from>"}. A state is a short script in scout_mcp/laws/<state>.json: a sections() generator that reads the legislature's table of contents, API or bulk file and yields each section's own words. Every request goes through Scout's gate (robots.txt and its crawl delay, holds), at the recipe's own pace when that is slower. Text that is page chrome or has lost its letters is skipped. Big bulk files (Texas's code zips, North Dakota's single page) are downloaded once a day to Scout's data folder.

33 ready: AL, AS, CO, CT, GU, IA, IL, LA, MD, ME, MI, MN, MP, MT, ND, NE, NH, NJ, NV, NY, OR, PA, PR, RI, SD, TX, UT, VA, VT, WA, WI, WV, WY. These are the scripts a sibling crawler used in October 2026 to land each code whole: Texas 119,000 sections, Illinois 69,000, Nebraska 55,000, Nevada 50,000. A full state takes from minutes (Texas, 31 downloads) to hours (Illinois and New Hampshire ask for 10 and 11 seconds between requests).

  • New York reads the Senate's Open Legislation API, which needs a free key: put it in SCOUT_KEY_NYSENATE.

  • Louisiana takes its Revised Statutes list from a dated snapshot, because the site only opens a title by form post.

  • Not here yet: AK, AZ, DC, DE, FL, GA, HI, ID, KS, KY, MA, MO, NC and SC are read by the sibling crawler's page-pattern engine and still need porting to scripts. California needs its 1.28 GB bulk zip read entry by entry. Indiana serves its code zip only to browsers.

  • Need permission first: Arkansas, Mississippi, Tennessee and the Virgin Islands publish their official code only through LexisNexis, whose terms forbid automated reading. New Mexico's official site is on a platform whose terms require written consent. Ohio's and Oklahoma's robots.txt files disallow all crawlers. The routes section below says whom to ask.

The text is the law as each site publishes it. It is not an official or certified copy; cite the source url.

Whole catalogues

Seven makers so far: Intel, AMD, Samsung (the US site, ~2,500 SKUs), Honeywell Automation, Functional Devices, QSC and Von Duprin. scout-mcp catalog lists the makers; scout-mcp catalog intel --out intel.jsonl reads one maker's whole catalogue, one JSON line per product: {"model_number", "name", "family", "category", "url", "specs": {label: value}}. family is the product line (AMD EPYC, AMD Ryzen, Intel Xeon, Intel Core) and model_number the model within it, so a shelf of 4,600 Intel SKUs groups into its brands instead of one flat list.

  • Intel lists its SKUs from the Internet Archive's index: ARK draws its lists with JavaScript and the one full list is closed by robots.txt. Each SKU's specifications page is then read live. The Processor Number is the model and the Product Collection's brand the family. Prices are left out; a price is a link to a seller, not a number to copy.

  • AMD resets any client that isn't a browser, so its pages are read from the Internet Archive's latest copies ("archive": "auto" in the maker's script). The site's own sitemap names ~400 product pages, and the spec table's Name row is the model as AMD writes it ("AMD Ryzen™ 9 9950X").

  • Samsung pages list every SKU sold on them (sizes, colours) in the page's own data, and the specs come from the JSON the page itself loads, so one page lands a whole group. The page's feature copy is drawn later by JavaScript, so descriptions are left empty rather than filled with the SEO blurb.

  • Functional Devices runs a commerce store whose guest API lists every product with its spec fields; Scout reads that.

  • Panduit isn't here: its product data only answers requests that claim to come from its own pages.

A recipe field named like a description (description, summary, overview) must also read as one. The content gate refuses a stripped breadcrumb ("HomeAll ProductsLow Voltage…"), a page's script data, and the site's lifecycle notice ("End of commercialization is the last date…"): 1,061 of one maker's descriptions in the sibling crawler were one of those.

Official routes to state codes

The sibling crawler rebuilt about 25 state codes on 2026-10-10. For most of them the best route was not the section pages but something the legislature publishes for whole-code reading. Those routes are now in the book (enroll("list") shows them, and a wall on these hosts leads with them):

Site

Official route

Why it beats the pages

statutes.capitol.texas.gov

one zip of chapter HTML per code

all 30 codes, about 122,000 sections, in 31 requests

olls.info

Colorado's title download (HTML zip)

the online C.R.S. is a commercial platform's

le.utah.gov

XML per title

structured text, current and future versions marked

law.lis.virginia.gov

CSV per title

76 requests instead of 33,000 throttled pages

ndlegis.gov

the whole Century Code on one page

one fetch

nebraskalegislature.gov

range view, 400 sections a request

16 minutes instead of 41 hours at its crawl delay

sdlegislature.gov

Statutes API

a title per request (UTF-16 with no byte-order mark)

mgaleg.maryland.gov

statute text API

an article's section list, then each section

legislature.maine.gov, www.palegis.us

whole titles (.docx, HTML)

one file per title

downloads.leginfo.legislature.ca.gov

pubinfo bulk zip (1.28 GB)

the website answers with a challenge

nmonesource.com

the platform owner's written consent

its terms forbid automated reading

codes.ohio.gov, oklegislature.gov, capitol.tn.gov

none: robots.txt disallows all, or the official copy is on a commercial platform

the request to make is recorded

What else that rebuild taught:

  • A one-line robots.txt still means what it says. Ohio's reads User-agent: * Disallow: / with the newline missing, so parsers find no rules. The intent is plain, so it is treated as disallow-all.

  • Counted is still not read. Virginia's 32,000 sections, Ohio's 12,000 and Minnesota's 6,900 had landed as the legislature's own menus, version banners and footers. Each passed every count. Those phrases are in the content gate, and the missing-letters test now leaves short prose, all-capitals notices and lists alone.

  • More requests at once don't make one host faster. Its pace is fixed by its gap; 39 jobs behind one 2-second gate each moved a page a minute and looked dead.

Structured data, archived block pages, and other things worth knowing

  • schema.org data. Many product pages carry clean Product data as JSON-LD even when their html is a mess. reach returns it as jsonld, and the jsonld recipe step reads dotted paths (offers.price, sku). Title tails like "| CT4000T705SSD5 | crucial.com" are cut from names. crucial.com/ssd ships in the book, built entirely from archived pages while crucial.com's edge rejects Scout: RESULT: ok | crucial.com/ssd | CT4000T705SSD5 | … archived=2026-07-20; name=Crucial T705 4TB PCIe Gen5 NVMe M.2 SSD with heatsink; sku=CT4000T705SSD5; price=565.99; currency=USD

  • An archived copy can itself be a block page. The newest Wayback copy of some AMD pages is Akamai's 403, and of some Crucial pages it's the edge's rejection page, captured with a 200. Scout tells a replayed error (it has a Memento-Datetime) apart from the archive refusing Scout, so it no longer holds the archive by mistake. It then walks back through older, distinct captures until one is real content. The archive pace is 5 seconds, with a 15-minute hold when it refuses.

  • Cloudflare's 2026 wall ("Performing security verification… protect against malicious bots") is read as a challenge, not content.

  • A map started inside a section stays inside it. site_map("https://site/en/products/processors/") lists only that section. The site's root maps the whole site.

  • Function words don't steer. "and", "the", "section", "law" no longer pick which link to follow.

  • A dropped connection to a renderer service gets one retry on a fresh connection before it counts as a failure.

  • Same-name companies and terms of use. Edwards Lifesciences is not Edwards fire safety, simplex.com is not Simplex fire, and sol.com is not Sol-Ark: the skill checks the site names what you mean. robots.txt is not the only rule either. LexisNexis's terms forbid automated access without written permission, so its entry in the book is a permission request, not a route.

Errors that say what to do next

When Scout fails, it returns an error code, a message, and next_steps written as concrete tool calls, plus the tried trail it took through the maze. Any capable model can pick up from there.

Code

Next steps point to

NOT_FOUND

site_map instead of guessing paths

WANT_MISS

loosening want, a detail_suffix move, site_map

JS_SHELL / THIN_CONTENT

adding a rendering reader, the site's JSON endpoints, a print view

REFUSED / SILENT_REFUSAL / CHALLENGE_WALL / LOGIN_REQUIRED

the site's official route, its partner program, the same information published elsewhere

BLOCKED / HELD

who to email to lift the block, hold to keep every agent off until they answer, when Scout probes

ROBOTS_DISALLOWED / RATE_LIMITED

an official feed, waiting for Retry-After

TLS_ERROR / NETWORK / SERVER_ERROR

the cause, retrying later

NO_URLS / NO_LISTING / NO_MATCH / EXTRACT_MISS / TEST_FAILED

what to recompile or propose

Tools

Tool

For

Does

scout(url, want?)

big model

reaches the page, maps url patterns, finds the listing, reports what was learned

reach(url, want?, full?)

big model

reads one page through the learned ladder

site_map(url, filter?)

big model

the site's own urls and their patterns

families(url, want?, depth?)

big model

which link families lead to good pages, by sampling and vote

compile(name, steps, examples, …)

big model

tests a recipe and saves it, returns the small-model card

moves(action, …)

big model

lists, proposes or retires moves

hold(host, reason?, lift?, min_interval?)

big model

keeps every agent off a site (or lets them back), slows Scout's pace there

enroll(action, host?, info?, note?)

big model

records a site's official route, and lists, tests and closes enrollment requests

laws(state?, limit?)

any model

the US state codes Scout reads whole from each legislature; a few sections of one (the whole code: scout-mcp laws <state>)

catalog (command line)

any model

scout-mcp catalog <maker>: a maker's whole catalogue as model, family, category and specs

memory(host?)

big model

reader order, routes, moves and recent failures for a host

recipes(host?)

both

compiled recipes with scores, status and cards

run(recipe, input?, inputs?, cached?)

small model

runs a recipe, returns one RESULT line (one per input)

cache(url, filter?, source?, days?, action?)

big model

downloads a site once (live or from the Wayback Machine) for cached reads

Give the small model only run, or the CLI. One tool means it has nothing to choose between.

Configuration

Variable

Default

SCOUT_DB

~/.local/share/scout-mcp/scout.db

SCOUT_USER_AGENT

ScoutMCP/<version> (+https://github.com/sireggserroneous/scout-mcp)

SCOUT_MIN_INTERVAL

1.0 seconds between requests to one host

SCOUT_MIRROR_DAYS

1: how long a good page is re-read from disk (0 turns the mirror off)

SCOUT_ARCHIVE

https://web.archive.org (off to never read archived copies)

SCOUT_KEYS_FILE

~/.config/scout-mcp/keys.env (mode 600), next to SCOUT_KEY_<NAME> env vars

FIRECRAWL_URL, FIRECRAWL_API_KEY, CRAWL4AI_URL, CRAWL4AI_TOKEN

unset

Develop

uv venv && uv pip install -e . playwright
python -m scout_mcp.scout      # self-checks, no network

There are two modules. scout_mcp/scout.py is the engine, and scout_mcp/server.py holds the MCP tools and the CLI. Seed moves and example recipes are in scout_mcp/recipes.json. MIT licensed.

Available Tools

13 tools
cacheA

Cached mode, for pulling a lot from one site: list it once (filter narrows the urls) and download every page once into Scout's copy, in the background, at the host's pace. source='archive' downloads from the Wayback Machine with no request to the site; 'live' from the site; 'auto' live, with the archive where the site walls Scout. The copy is kept days; reads and recipe runs with cached=true use only it. action=status follows the job; action=stop ends it.

Inputs: url (required), days, action, filter, +2 more.

compileA

Compile what you worked out about a site into a recipe a small model can run in one call. Scout runs it on every example and saves it only if all pass; the reply carries the one-line card for the small model. name: '/'. input_name: what the caller supplies ('model', 'sku'; empty for none). examples: 2+ inputs. steps, run in order, each a one-key object: {"input": [[regex, repl], ...], "lower": true} rewrite the input (slug rules) {"map": "https://site", "filter": "/product/{input}$"} the site's own urls; first match becomes {url} {"reach": "https://site/p/{input}" | "{url}", "want": regex} read a page {"find": regex} first link on the page matching (url or text) becomes {url} {"links": "/chapter//"} a link family from families(); last step = all its links, else the first is {url} {"jsonld": {"price": "offers.price", "sku": "sku"}, "type": "Product"} from the page's schema.org data {"extract": {"field": "regex with one group", "optional?": "..."}} every field must be found, except a name ending in ? Prefer map over a guessed url template: sites are inconsistent about case and slugs.

Inputs: name (required), steps (required), about, examples, +1 more.

enrollA

Official routes and the enrollments they need: the checklist for whoever updates Scout. action=list: open requests, each with a todo that says where to sign up, what it costs, and where the key goes. action=request host info: record a site's official route. info = {route: open_api|bulk|feed|public_json|alternative (no signup) or enroll_free|enroll_paid|enroll_oauth|commercial (a person must enroll), name, signup_url, docs_url, base_url, auth: {kind: none|query|header|bearer|oauth2_client_credentials, name, token_url, headers}, cost, terms, sample (a url that returns the data; leave the key out)}. Keyed routes become an enrollment request. action=applied (signed up, waiting on approval) | done (Scout tests the key on the sample, then marks enrolled) | decline. Keys are never passed here: they go in the keys file or SCOUT_KEY_ env vars.

Inputs: host, info, note, action, +1 more.

familiesA

Which of a page's link families lead to good pages, and how many. Each family (links whose paths differ only in their numbers, like /chapter/1/ and /chapter/14A/) is sampled across its spread, the samples vote on what the family leads to (an index of more links, or pages), indexes are walked one level deeper, and families are ranked by yield through every level. best.recipe_hint is a draft recipe for compile. Use it on sites with no useful sitemap.

Inputs: url (required), want, depth.

holdA

Keep every agent sharing Scout's memory off a host, or let them back. Use it when a site blocked you and a person has asked for the block to be lifted: reason says what was asked and when. min_interval (seconds) slows Scout's pace on that host for good. Scout holds a host by itself after a block page or a 429, and probes once when that ends.

Inputs: host (required), lift, reason, min_interval.

lawsA

US state codes read whole from each legislature's own site. No state: the list of states Scout can read, with each one's citation form and source. A state (e.g. 'us-tx'): its first limit sections (max 50) as citation, heading, text and url. A whole code is tens of thousands of sections: run scout-mcp laws <state> --out file.jsonl for that.

Inputs: limit, state.

memoryB

What Scout has learned about a host: winning reader, reader order, routes (listing, detail_suffix, patterns), the moves that apply, recent failures. No host: recent failures everywhere and the whole register.

Inputs: host.

movesA

The moves register: rewrite rules Scout tries where its built-in moves fail, each scored (wins+1)/(tries+2) on real reaches and retired after 5 straight losses. action=list [kind, host] | propose kind scope spec [note] | retire id. kinds: url_rewrite {pattern, repl} (regex on the full url; repl is literal text plus \1 groups) · locale_prefix {prefix} and listing_root {path}, which compose into listing paths (/en-us + /products) and are ranked, never retired · listing_path {path} (a whole path) · detail_suffix {suffix}. scope: '*', a domain ('example.com' covers www.example.com) or a host regex. Propose only what you saw work.

Inputs: id, host, kind, note, +3 more.

reachA

Reach one page and judge whether it is good information. Tries the host's learned readers winner-first, then scored url rewrites and detail sub-pages. Returns ok, markdown (excerpt unless full=true), the trail tried, and on failure an error {code, message, next_steps}. A page read today comes from Scout's mirror (fresh=true reads it again). When the site walls Scout, or is on hold, the latest Wayback Machine copy is read instead, and archived says when it was captured. cached=true reads only Scout's copy of the site (see cache) and sends no request.

Inputs: url (required), full, want, fresh, +1 more.

recipesC

The compiled recipes (optionally for one host): what each takes, its score, whether its last run failed, and the one-line card a small model runs.

Inputs: host.

runB

Run a compiled recipe. Reply with the RESULT line. inputs runs it over many inputs (one RESULT line each); cached=true reads only Scout's copy of the site, sending no request (use after cache).

Inputs: recipe (required), input, cached, inputs.

scoutC

Scout a url: reach the page (as markdown), map the site's url patterns, find its listing page, and return what Scout has learned about the host. want is an optional regex the page must carry (a model number, 'price|$', a heading); without it, good = real content that is not a wall. On failure page.error says why and what to try.

Inputs: url (required), full, want.

site_mapA

A site's real urls (robots.txt sitemaps, a polite link walk, the catalogue's root) with their url patterns. filter is a regex. A listing is reused for 12 hours; fresh=true lists the site again (only when it changed: re-listing a whole site run after run is how crawlers get blocked). archive='only' lists the site from the Wayback Machine's index without a single request to it; 'auto' does so when the site is on hold or listed nothing. Use it instead of guessing paths.

Inputs: url (required), fresh, limit, filter, +1 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.4/5.0

    Scored across 13 tools

    Disambiguation3/5

    The umbrella `scout` tool overlaps with `reach`, `site_map`, and `memory`, since it reaches a page, maps site patterns, and returns learned host data. Descriptions help distinguish the targeted tools, but an agent could still misselect `scout` when a more specific tool would suffice.

    Naming Consistency3/5

    All names are lowercase and mostly single words, with one snake_case compound (`site_map`). However, the set mixes verbs (`scout`, `reach`, `compile`, `run`) and nouns (`memory`, `moves`, `recipes`, `families`), so there is no predictable verb_noun pattern.

    Tool Count5/5

    With 13 tools, the count sits comfortably in the ideal 3–15 range for a complex web-scouting server. Each tool appears to cover a distinct phase of discovery, reading, rule management, caching, or enrollment.

    Completeness4/5

    The surface covers discovery, page reading, site mapping, link families, memory, rewrite rules, recipe compilation/execution, caching, blocking, enrollment, and legal codes. Minor lifecycle gaps exist, such as no explicit recipe deletion or cache clearing, but agents can work around them.

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      C
      maintenance
      Enables AI agents to read web pages reliably, returning clean markdown content, hyperlinks, and metadata without navigation or ad noise.
      3
      5 npm
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables agents to extract article text from public URLs or HTML, verify results against acceptance contracts, and fall back across extractors while ranking future attempts using local observed outcomes.
      MIT