# hardly (pypi · hardly)

Generic HAR analysis MCP server: index and query captures without burning tokens on raw files.

- Trust score: 80/100 (high trust)
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-11-04

## Components

- pypi · `hardly`: 80/100 (this document), [markdown](https://verifymcp.io/servers/b1tninja-hardly/hardly.md), [page](https://verifymcp.io/servers/b1tninja-hardly/hardly)

## Channel facts

- Registry: `pypi`
- Package: `hardly`
- Version: `1.0.0`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-11-04.

- **Supply Chain Security**: 100/100
  - No malware found by supply-chain analysis.
  - No known CVEs affecting this package version or its production dependencies.
  - Runs hatchling.build at install time, a recognised build step with no custom scripting around it.
  - 1 of 23 dependencies flagged as unhealthy.
- **Provenance & Transparency**: 100/100
  - Source repository is publicly reachable at the declared URL.
  - Cryptographically verified build provenance (signed, bound to b1tninja/hardly).
  - Clear OSI-approved license (MPL-2.0).
  - Actively maintained (last published 6 days ago).
  - Publishes a security disclosure policy (SECURITY.md).
- **Schema Quality & AI Usability**: 73/100
  - 100% of prompts and resources have a non-trivial description (not blank, and not just the item's name).
  - AI-judged instruction clarity (good).
  - Context-footprint check failed: tool/resource definitions use about 11084 tokens (~136/item across 81 items; 71 tools + 10 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 20/100
  - Stability observed for 6 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 71/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 0% of tool parameters carry a description.
  - Structured output schemas are declared (100% of tools); any adoption earns full credit.
- **Tool Safety**: 75/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - 0 of 7 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation; "hardly_send_catalog_verify" implies "send" and declares no destructiveHint at all, which the MCP spec reads as destructive by default.
  - An AI judge read all 73 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 100/100
  - Implements a current MCP spec version (2026-07-28).

## Install

### How do I install the hardly MCP server?

hardly runs locally as a PyPI package, launched with uvx hardly. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add b1tninja-hardly -- uvx hardly
```

### Cursor

```json
{
  "mcpServers": {
    "b1tninja-hardly": {
      "command": "uvx",
      "args": [
        "hardly"
      ]
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "b1tninja-hardly": {
      "command": "uvx",
      "args": [
        "hardly"
      ]
    }
  }
}
```

### Codex

```bash
codex mcp add b1tninja-hardly -- uvx hardly
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "b1tninja-hardly": {
      "type": "local",
      "command": [
        "uvx",
        "hardly"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add b1tninja-hardly --command uvx --arg hardly
```

### Hermes

```yaml
mcp_servers:
  b1tninja-hardly:
    command: "uvx"
    args: ["hardly"]
```

### Netclaw

```json
{
  "McpServers": {
    "b1tninja-hardly": {
      "Transport": "stdio",
      "Command": "uvx",
      "Arguments": [
        "hardly"
      ]
    }
  }
}
```

### Vellum

```bash
assistant mcp add b1tninja-hardly -t stdio -c uvx -a hardly
```

### Other

```json
{
  "mcpServers": {
    "b1tninja-hardly": {
      "command": "uvx",
      "args": [
        "hardly"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-11-03 (score 80, +1)

No change was recorded against any check on this day. Stability & Change Management went from 13 to 17. That category is still filling its 30-day observation window: 4 days of observed history at the previous scan, 5 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-11-02 (score 79, +2)

- [functional improvement] Stability: unverified → 0.13

### 2026-10-30 (score 77, +15)

- [security improvement] Malware scan: unverified → pass

### 2026-10-29 (score 62)

First indexed and scored.

## MCP tools (71)

### `hardly_server_status` (~101 tokens)

Hardly Server Status

Server version, feature flags, tool names and browser availability; sections: capabilities (default), browser_setup. Call when a tool seems missing (restart the MCP server) or capture fails.

\``capabilities``: version, features, tools, capture_available (small, ~3 KB).
\``browser_setup``: Playwright package and browser binaries, with the exact install commands.
Example: hardly_server_status(sections=['browser_setup']).

Input parameters:

- `sections`

Output parameters:

- `result` (string)

### `hardly_guide_help` (~76 tokens)

Hardly Guide Help

Categorized catalog of tools and workflows; pass topic to filter. Use when you do not know which tool fits (topics: portal, tokens, capture, modes, or a tool name); for a task-specific plan use hardly_guide_task_plan. Example: hardly_guide_help(topic='capture').

Input parameters:

- `topic`

Output parameters:

- `result` (string)

### `hardly_guide_mode` (~150 tokens)

Hardly Guide Mode

List the three operating modes (archive, headless, interactive), or get the playbook for one mode, or auto-pick a mode from goal / har_path / url. Example: hardly_guide_mode(har_path='/data/capture.har').

With no argument it lists the modes and when to use each. With ``mode`` it returns that mode's
step-by-step playbook; with goal / har_path / url (and no mode) a mode is chosen for you.
\``hardly_guide_task_plan`` is the better first call for a full ordered plan.

Input parameters:

- `goal`
- `har_path`
- `mode`
- `url`

Output parameters:

- `result` (string)

### `hardly_guide_task_plan` (~129 tokens)

Hardly Guide Task Plan

FIRST CALL for a new task: ordered plan of hardly tool calls with example arguments, environment state and recommended tools for the goal. Example: hardly_guide_task_plan(goal='reverse engineer the login flow', har_path='/data/capture.har').

Pass what you know: goal (free text, e.g. 'build a client SDK'), har_path, url. Small output
(~2 KB). For one mode's playbook use hardly_guide_mode, for the tool catalog hardly_guide_help.

Input parameters:

- `goal`
- `har_path`
- `url`

Output parameters:

- `result` (string)

### `hardly_session_open` (~105 tokens)

Hardly Session Open

Index a HAR file (or open a saved index) into a queryable session and return session_id plus summary counts. Start here for any HAR; never Read the raw HAR. Writes nothing (save an index with hardly_write_session_copy). Example: hardly_session_open(har_path='/data/capture.har').

Opening the same path again returns the same session; force=true re-ingests the HAR.

Input parameters:

- `force` (boolean)
- `har_path` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_close` (~50 tokens)

Hardly Session Close

Close an open session to free memory (the session is discarded; files saved with hardly_write_session_copy stay). Example: hardly_session_close(session_id='S').

Input parameters:

- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_list` (~41 tokens)

Hardly Session List

List open HAR sessions (session_id, har_path, index_path). Use when you lost a session_id. No arguments. Example: hardly_session_list().

Output parameters:

- `result` (string)

### `hardly_write_session_copy` (~153 tokens)

Hardly Write Session Copy

Writes a file: save a copy of the session's source HAR (format='har') or its SQLite index (format='index') to output_path. Never in place; refuses to overwrite unless overwrite=true. Example: hardly_write_session_copy(session_id='S', output_path='/data/keep.har').

Writes only inside the working directory, the OS temp dir or HARDLY_WRITE_DIRS (path_not_allowed).

An ephemeral capture's HAR is already deleted (code har_ephemeral_gone): save the index instead,
or capture again with har_output_path.

Input parameters:

- `format` (string)
- `output_path` (string, required)
- `overwrite` (boolean)
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_har_file_check` (~145 tokens)

Hardly Har File Check

Diagnose a HAR file on disk (truncated/omitted bodies, sanitised cookies, clock skew, noise); no session needed. Findings carry a fix_hint such as recapture flags. Example: hardly_har_file_check(har_path='/data/capture.har').

Session-level body counts are hardly_session_body_coverage. Optional ``config`` (object): checks,
exclude, severity_overrides, thresholds{max_entry_bytes, truncation_ratio, clock_skew_s,
noise_ratio, min_entries}, strict, fail_on, host, ignore_hosts, ignore_paths, max_findings, fix.

Input parameters:

- `config`
- `har_path` (string, required)

Output parameters:

- `result` (string)

### `hardly_write_har_pruned` (~142 tokens)

Hardly Write Har Pruned

Writes a file: a pruned copy of a HAR (drop hosts, mime globs, known noise); never in place, refuses an existing output_path unless overwrite=true. Example: hardly_write_har_pruned(har_path='/data/a.har', output_path='/data/a.small.har', drop_noise=true).

Writes only inside the working directory, the OS temp dir or HARDLY_WRITE_DIRS (path_not_allowed).

Input parameters:

- `drop_hosts`
- `drop_mime_types`
- `drop_noise` (boolean)
- `har_path` (string, required)
- `output_path` (string, required)
- `overwrite` (boolean)

Output parameters:

- `result` (string)

### `hardly_write_har_scrubbed` (~129 tokens)

Hardly Write Har Scrubbed

Writes a file: a scrubbed copy of a HAR with secret values, cookies and auth headers replaced by ***REDACTED*** (structure and shapes kept). Never in place; needs overwrite=true to replace. Example: hardly_write_har_scrubbed(har_path='/data/a.har', output_path='/data/a.scrubbed.har').

Writes only inside the working directory, the OS temp dir or HARDLY_WRITE_DIRS (path_not_allowed).

Input parameters:

- `har_path` (string, required)
- `output_path` (string, required)
- `overwrite` (boolean)

Output parameters:

- `result` (string)

### `hardly_write_har_split` (~138 tokens)

Hardly Write Har Split

Writes files: split a HAR into one file per host (by='host') or per page (by='page') inside output_dir, streaming. Example: hardly_write_har_split(har_path='/data/a.har', output_dir='/data/parts').

Writes only inside the working directory, the OS temp dir or HARDLY_WRITE_DIRS (path_not_allowed).

Use for huge captures, then hardly_session_open each part. Refuses to overwrite existing parts unless overwrite=true.

Input parameters:

- `by` (string)
- `har_path` (string, required)
- `output_dir` (string, required)
- `overwrite` (boolean)

Output parameters:

- `result` (string)

### `hardly_write_har_merged` (~126 tokens)

Hardly Write Har Merged

Writes a file: merge several HARs into output_path, prefixing page ids and dropping exact duplicates (dedupe=true). Needs overwrite=true to replace. Example: hardly_write_har_merged(har_paths=['/data/a.har','/data/b.har'], output_path='/data/all.har').

Writes only inside the working directory, the OS temp dir or HARDLY_WRITE_DIRS (path_not_allowed).

Input parameters:

- `dedupe` (boolean)
- `har_paths` (array, required)
- `output_path` (string, required)
- `overwrite` (boolean)

Output parameters:

- `result` (string)

### `hardly_catalog_list` (~184 tokens)

Hardly Catalog List

List targets from a content-neutral catalog file (JSON/YAML), filtered; no network. detail='summary' returns counts instead of rows. Tags and roles are defined by your project (docs/catalog.md). Example: hardly_catalog_list(catalog_path='/data/targets.json', detail='summary').

One row per endpoint: target, tags, groups, role, kind, status, redacted url, gate classes,
stack names. Filters: ``tag`` (comma-separated, all required), ``group`` (object of key->value),
\``role``, ``status``, ``target_id``. hardly assigns no meaning to tags, group keys or roles.

Input parameters:

- `catalog_path` (string, required)
- `detail` (string)
- `group`
- `role`
- `status`
- `tag`
- `target_id`

Output parameters:

- `result` (string)

### `hardly_write_catalog_record` (~223 tokens)

Hardly Write Catalog Record

Writes a file: add or update one target in a catalog file in place (atomic, no network). URLs are redacted and validated. create=true makes the file if missing. Example: hardly_write_catalog_record(catalog_path='/data/targets.json', target={'id':'t1','endpoints':[{'role':'api','url':'https://example.com/api'}]}, create=true).

Writes only inside the working directory, the OS temp dir or HARDLY_WRITE_DIRS (path_not_allowed).

Only that one target changes (there is no overwrite flag; merge=false replaces the target).
\``target`` is {id, name, tags[], groups{}, endpoints:[{role, url, kind?, status?, gate_classes?,
stack?, notes?, capture?:{recipe_ref, har_ref}}]}. merge=true unions tags, overlays groups and
merges endpoints by (role, url); merge=false replaces the target.

Input parameters:

- `catalog_path` (string, required)
- `create` (boolean)
- `merge` (boolean)
- `target` (object, required)

Output parameters:

- `result` (string)

### `hardly_send_catalog_verify` (~266 tokens)

Hardly Send Catalog Verify

LIVE (confirm-gated): politely verify catalog endpoints (verified|blocked|dead|needs_browser). Without confirm=true it returns only the plan and sends nothing; write_back=true also records statuses in catalog_path. Example: hardly_send_catalog_verify(catalog_path='/data/targets.json', confirm=true, write_back=true).

Refuses loopback, private, link-local and metadata hosts, also on redirects (host_not_allowed; local test targets: HARDLY_ALLOW_PRIVATE_HOSTS=1).

Per endpoint it runs one robots-aware, honest-UA fetch and records status, gate classes and
stack names, never bodies or tokens. Per-host delay, request budget, resumable (already-checked
endpoints are skipped unless force / recheck_after_seconds). Stops a host on any gate per
docs/gate-policy.md; never evades.

Input parameters:

- `catalog_path` (string, required)
- `confirm` (boolean)
- `delay_seconds` (number)
- `force` (boolean)
- `group`
- `max_endpoints`
- `max_requests` (integer)
- `recheck_after_seconds`
- `role`
- `status`
- `tag`
- `target_id`
- `write_back` (boolean)

Output parameters:

- `result` (string)

### `hardly_browser_start` (~312 tokens)

Hardly Browser Start

Launch a real browser that records a HAR (needs the capture extra); returns capture_id. headed=true (default) is interactive: ASK THE PERSON to use the window, then call hardly_browser_stop. Example: hardly_browser_start(url='https://example.com', headed=true, channel='chrome').

Refuses loopback, private, link-local and metadata start URLs (host_not_allowed; HARDLY_ALLOW_PRIVATE_HOSTS=1 for a local test site).

headed=false is headless: drive it with hardly_browser_inspect / hardly_browser_interact /
hardly_browser_run_steps. For a one-shot unattended load prefer hardly_browser_capture_discover.
Requires ``pip install -e ".[capture]"`` + ``playwright install chromium`` (see
hardly_server_status). Prefer channel='chrome' for bot walls. url_filter is a Playwright glob.
profile keeps cookies. Without har_output_path the HAR is ephemeral (memory only). same_tab
(default true) forces target=_blank into the current tab. trace=true writes a ``.trace.zip``
beside the HAR. Waits for a capture slot (HARDLY_CAPTURE_SLOTS); the result carries ``slot``.

Input parameters:

- `channel`
- `har_output_path`
- `headed` (boolean)
- `label`
- `omit_content` (boolean)
- `profile`
- `same_tab` (boolean)
- `trace` (boolean)
- `url`
- `url_filter`

Output parameters:

- `result` (string)

### `hardly_browser_stop` (~123 tokens)

Hardly Browser Stop

Stop a running browser capture, flush the HAR and optionally open it as a session. Call when the person says they are done (interactive) or your steps are complete; then continue with hardly_session_site_brief(session_id). Example: hardly_browser_stop(open_session=true).

An ephemeral capture (started without har_output_path) lives only in the returned session; save
it with hardly_write_session_copy(format='index'). Omit capture_id for the latest running one.

Input parameters:

- `capture_id`
- `force` (boolean)
- `open_session` (boolean)

Output parameters:

- `result` (string)

### `hardly_capture_list` (~88 tokens)

Hardly Capture List

List browser captures (running and persisted) with ids and states, or the status of one capture_id. Poll it while a person drives an interactive capture. Example: hardly_capture_list(capture_id='c1').

Use to find a capture_id after a restart. With capture_id the list holds exactly that capture
(state, har_output_path, request counts).

Input parameters:

- `capture_id`

Output parameters:

- `result` (string)

### `hardly_browser_capture_discover` (~341 tokens)

Hardly Browser Capture Discover

LIVE (confirm-gated): unattended time-boxed browser capture of a URL; analyze=true also discovers its APIs and returns session_id plus a brief. Without confirm=true it returns only the plan. Example: hardly_browser_capture_discover(url='https://example.com', wait_seconds=8, analyze=true, confirm=true).

Refuses loopback, private, link-local and metadata start URLs (host_not_allowed; HARDLY_ALLOW_PRIVATE_HOSTS=1 for a local test site).

analyze=false: load the URL, wait wait_seconds (default 20), stop; returns the raw HAR
(har_output_path) and, with open_session=true, a session. analyze=true (headless only): optional
\``steps`` (goto/wait/aria/click/fill/find_click...) then stop, open a session and return a brief
(default wait 5). If the brief shows a wall or captcha, STOP and switch to interactive
hardly_browser_start(headed=true, channel='chrome') with a person. budget_seconds is a hard
per-call budget (env HARDLY_CAPTURE_BUDGET). exclude_noise=true aborts analytics/ad/font/
map-tile/heavy-media requests while loading (default off: blocking can break sites).

Input parameters:

- `analyze` (boolean)
- `budget_seconds`
- `channel`
- `confirm` (boolean)
- `exclude_noise` (boolean)
- `har_output_path`
- `headed` (boolean)
- `open_session` (boolean)
- `steps`
- `url` (string, required)
- `url_filter`
- `wait_seconds`

Output parameters:

- `result` (string)

### `hardly_browser_interact` (~204 tokens)

Hardly Browser Interact

Drive the running browser tab: action=goto (needs url), click, fill (needs value) or press (key, default Enter). Prefer ref from hardly_browser_inspect(sections=['aria']). Example: hardly_browser_interact(action='click', ref='e12').

Target one element per call with ``ref``, else ``css`` / ``xpath`` (from
hardly_browser_inspect(sections=['elements'])), or ``text`` / ``role`` with ``name`` (click).
Never type real credentials yourself: ask the person to use an interactive capture.
Omit capture_id for the latest running capture.

Input parameters:

- `action` (string, required)
- `capture_id`
- `css`
- `key`
- `name`
- `ref`
- `role`
- `text`
- `timeout_seconds` (number)
- `url`
- `value`
- `xpath`

Output parameters:

- `result` (string)

### `hardly_browser_inspect` (~196 tokens)

Hardly Browser Inspect

Read the running browser tab without navigating; sections: url (default), elements (css/xpath locators), aria (accessibility YAML with [ref=eN] handles). Example: hardly_browser_inspect(sections=['aria'], selector='main').

\``url``: current URL and title (cheap check of where a click landed). ``elements``: visible
links/buttons/inputs with locators; ``query`` is a CSS selector override, ``limit`` max rows
(default 40, max 100). ``aria``: rendered ARIA tree, ``mode='ai'`` (default) asks for refs,
\``selector`` scopes it (default body); the refs feed hardly_browser_interact. For offline HAR
HTML bodies use hardly_entry_outline.

Input parameters:

- `capture_id`
- `limit`
- `mode`
- `query`
- `sections`
- `selector`

Output parameters:

- `result` (string)

### `hardly_browser_run_steps` (~223 tokens)

Hardly Browser Run Steps

Run a scripted list of steps on the running browser tab in one call (cheaper than many single calls). Use once you know the selectors; hardly_session_plan_steps suggests steps from a session. Example: hardly_browser_run_steps(steps=[{'op':'goto','url':'https://example.com'}]).

Each step: ``{"op":"goto"|"wait"|"elements"|"click"|"fill"|"press"|"url"|"aria", ...}``.
Example steps::

    [
      {"op":"goto","url":"https://example.com/search"},
      {"op":"wait","ms":1000},
      {"op":"click","text":"I Accept"},
      {"op":"fill","css":"#SearchOnName","value":"EXAMPLE"},
      {"op":"click","css":"input[type=submit]"},
      {"op":"wait","ms":2000},
      {"op":"elements","limit":30}
    ]

Input parameters:

- `capture_id`
- `steps` (array, required)
- `stop_on_error` (boolean)

Output parameters:

- `result` (string)

### `hardly_write_screenshot` (~112 tokens)

Hardly Write Screenshot

Writes a file: save a PNG screenshot of the running browser tab to output_path. Use to show a person or inspect a wall; refuses to overwrite unless overwrite=true. Example: hardly_write_screenshot(output_path='/tmp/page.png', full_page=true).

Writes only inside the working directory, the OS temp dir or HARDLY_WRITE_DIRS (path_not_allowed).

Input parameters:

- `capture_id`
- `full_page` (boolean)
- `output_path` (string, required)
- `overwrite` (boolean)

Output parameters:

- `result` (string)

### `hardly_session_plan_steps` (~92 tokens)

Hardly Session Plan Steps

Suggest browser steps (goto/fill/click) from a session's story, ready for hardly_browser_run_steps. Heuristic: refine selectors with hardly_browser_inspect first. Nothing is run or written (hardly_write_export format=plan_steps saves them). Example: hardly_session_plan_steps(session_id='S').

Input parameters:

- `host`
- `limit`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_overview` (~135 tokens)

Hardly Session Overview

Headline COUNTS of a capture: requests per server, method and status, plus main_host (the app origin, not CDN/analytics). Run right after hardly_session_open; pass main_host as host= to later tools. Example: hardly_session_overview(session_id='S').

Distributions (content types, sizes, timing) are hardly_session_traffic_stats; findings with
severity are hardly_session_report; a digest of a server-rendered site is hardly_session_site_brief.

Input parameters:

- `exclude_noise` (boolean)
- `host`
- `limit`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_traffic_stats` (~142 tokens)

Hardly Session Traffic Stats

DISTRIBUTIONS of a capture: MIME mix, status classes, body sizes, initiator types, timing and payload_kinds. Each has sample entry_ids. Example: hardly_session_traffic_stats(session_id='S', host='app.example.com').

Counts per server/method/status are hardly_session_overview; slow requests
hardly_session_slow_requests; repeated calls hardly_session_duplicates. ``kind`` filters the
payload_kinds list; ``limit`` caps it (default 80).

Input parameters:

- `exclude_noise` (boolean)
- `host`
- `kind`
- `limit`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_body_coverage` (~106 tokens)

Hardly Session Body Coverage

Count entries with usable body text versus empty or truncated. Use before trusting schema/client output, or when bodies look missing. Example: hardly_session_body_coverage(session_id='S').

Useful after Playwright captures that used to omit XHR bodies (size=-1). For problems in the HAR
file itself use hardly_har_file_check.

Input parameters:

- `exclude_noise` (boolean)
- `host`
- `limit`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_issues` (~76 tokens)

Hardly Session Issues

List capture-quality issues: empty bodies, 4xx/5xx, redirects without Location. Check before trusting other results; for HAR-file problems use hardly_har_file_check. Example: hardly_session_issues(session_id='S').

Input parameters:

- `host`
- `limit`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_duplicates` (~82 tokens)

Hardly Session Duplicates

Find repeated method+path_template groups (polling, retries). Use to separate noise from real calls before documenting. Example: hardly_session_duplicates(session_id='S', min_count=5).

Input parameters:

- `exclude_noise` (boolean)
- `host`
- `limit`
- `min_count` (integer)
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_slow_requests` (~91 tokens)

Hardly Session Slow Requests

List the slowest requests (time_ms) first. Use for performance questions; for the general traffic shape use hardly_session_traffic_stats. Example: hardly_session_slow_requests(session_id='S', min_elapsed_seconds=1).

Input parameters:

- `exclude_noise` (boolean)
- `host`
- `limit`
- `min_elapsed_seconds`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_report` (~199 tokens)

Hardly Session Report

One-pass evidence index (access, auth, stack, data, forms): FINDINGS with severity info|notice|blocker and the drill-down tool for each. Start here for any unfamiliar capture; detail='summary' is ~1 KB. Example: hardly_session_report(session_id='S', detail='summary').

Runs the existing detectors once and returns findings {section, kind, severity, label, entry_ids,
names/shapes only, lookup}. detail: summary (counts per section + blockers) | standard |
full (more findings + the drill-down tool per finding). ``categories`` restricts the sections
(access, auth, stack, data, forms). Never returns secret values. explain=true adds canned prose.
Writes nothing: hardly_write_export(format='report') saves it.

Input parameters:

- `categories`
- `detail` (string)
- `explain` (boolean)
- `host`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_site_brief` (~117 tokens)

Hardly Session Site Brief

One-shot DIGEST of a server-rendered site: journey (story), forms, routes, reused values, cookies and issues in a compact result. Run this first on a freshly opened session of an HTML portal, then drill down with the tools it names. Example: hardly_session_site_brief(session_id='S').

Findings with severities are hardly_session_report. Writes nothing: hardly_write_export
(format='site_brief') saves it as Markdown.

Input parameters:

- `host`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_story` (~139 tokens)

Hardly Session Story

Annotated user-journey NARRATIVE: the session stitched into steps (role, forms, labels, fields) with duplicates collapsed. Prefer it over the plain list hardly_session_timeline when reversing portals or building a client. Example: hardly_session_story(session_id='S', host='app.example.com').

It is the input to hardly_client_build. include_related=true adds non-noise traffic from sibling
hosts on the same apex so SPA captures tell a full story.

Input parameters:

- `exclude_noise` (boolean)
- `host`
- `include_related` (boolean)
- `limit`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_timeline` (~101 tokens)

Hardly Session Timeline

Plain ordered LIST of requests in time order for a host, optionally from a path_prefix; no annotation. Raw timeline for login/MFA sequences; hardly_session_story is the condensed narrative. Example: hardly_session_timeline(session_id='S', path_prefix='/login').

Input parameters:

- `exclude_noise` (boolean)
- `host`
- `limit`
- `offset`
- `path_prefix`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_redirect_history` (~89 tokens)

Hardly Session Redirect History

List the RECORDED 3xx redirect hops with matched follow-up entry ids; sends nothing. Use to understand login/canonicalisation redirects; the live twin that follows a URL is hardly_send_redirect_walk. Example: hardly_session_redirect_history(session_id='S').

Input parameters:

- `explain` (boolean)
- `host`
- `limit`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_trace_value` (~117 tokens)

Hardly Session Trace Value

Trace a field name or exact value across a capture: where it first appears and where it is reused. With neither name nor value, list every dynamic value (CSRF, ViewState, session cookies, tokens) reused from earlier responses. Example: hardly_session_trace_value(session_id='S', name='csrf_token').

Values are never echoed back, only entry ids, location and shape.

Input parameters:

- `host`
- `limit`
- `name`
- `session_id` (string, required)
- `value`

Output parameters:

- `result` (string)

### `hardly_session_sql` (~78 tokens)

Hardly Session Sql

Run a read-only SELECT against the session's SQLite index. Escape hatch for questions no named tool answers; try those first. Example: hardly_session_sql(session_id='S', sql='select host, count(*) from entries group by host').

Input parameters:

- `limit`
- `session_id` (string, required)
- `sql` (string, required)

Output parameters:

- `result` (string)

### `hardly_session_compare` (~97 tokens)

Hardly Session Compare

Compare endpoint templates (and credential maps) between two sessions: routes that appeared and disappeared. Use for logged out vs in, or before and after a site change. Example: hardly_session_compare(session_id='A', other_session_id='B').

Input parameters:

- `exclude_noise` (boolean)
- `host`
- `include_credentials` (boolean)
- `other_session_id` (string, required)
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_entry_search` (~204 tokens)

Hardly Entry Search

Find entries by host, path, method, status, body text, header or content_kind; returns small paged rows with entry_ids. Use to locate a specific request, then hardly_entry_get. Example: hardly_entry_search(session_id='S', body_contains='token', limit=10).

\``content_kind`` uses the classifier: json, jsonl, jsonp, csv, html, html_table (or table), pdf,
image, css, javascript, ... Result rows include ``content_kind`` / hints. exclude_noise=false
also returns OPTIONS preflights and tracker traffic.

Input parameters:

- `body_contains`
- `content_kind`
- `exclude_noise` (boolean)
- `header_contains`
- `header_name`
- `host`
- `limit`
- `method`
- `mime_contains`
- `offset`
- `path_contains`
- `session_id` (string, required)
- `status`

Output parameters:

- `result` (string)

### `hardly_entry_get` (~95 tokens)

Hardly Entry Get

Show one entry with redacted headers and truncated bodies (max_body_chars, default 4000). Use after entry_search/endpoint_list gives an entry_id; for large bodies use hardly_entry_body_query instead of raising max_body_chars. Example: hardly_entry_get(session_id='S', entry_id=12).

Input parameters:

- `entry_id` (integer, required)
- `max_body_chars`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_entry_around` (~137 tokens)

Hardly Entry Around

Show the requests just before and after one entry in TIME (a click and the XHRs that followed). Use with a navigation or grid-click entry_id to find the API it triggered; inspect rows with positive delta_ms. Example: hardly_entry_around(session_id='S', entry_id=30, after=8).

What triggered an entry by the browser's own initiator data is hardly_entry_initiators.

Input parameters:

- `after` (integer)
- `before` (integer)
- `entry_id` (integer, required)
- `exclude_noise` (boolean)
- `host`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_entry_initiators` (~104 tokens)

Hardly Entry Initiators

Show the initiator parent and children of an entry (HAR _initiator/pageref): what triggered a request and what it triggered. Use to find who called an API; for time order use hardly_entry_around. Example: hardly_entry_initiators(session_id='S', entry_id=30).

Input parameters:

- `child_limit`
- `entry_id` (integer, required)
- `exclude_noise` (boolean)
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_entry_compare` (~87 tokens)

Hardly Entry Compare

Diff headers and JSON body keys between two entries of one session. Use to see what changed between a failing and a working request, or pre/post login. Example: hardly_entry_compare(session_id='S', entry_id=3, other_entry_id=9).

Input parameters:

- `entry_id` (integer, required)
- `other_entry_id` (integer, required)
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_entry_build_curl` (~103 tokens)

Hardly Entry Build Curl

Build a curl command for one entry and return it as text (not executed); secrets are redacted by default. Use to show a person a single request; for a full client use hardly_client_build. Example: hardly_entry_build_curl(session_id='S', entry_id=12).

Input parameters:

- `entry_id` (integer, required)
- `redact` (boolean)
- `session_id` (string, required)
- `use_env_placeholders` (boolean)

Output parameters:

- `result` (string)

### `hardly_entry_body_query` (~154 tokens)

Hardly Entry Body Query

Search inside one large body without loading it: JSONPath-lite ($.a[*].b, ..key) or regex, paged (pass next_offset while has_more). Use instead of hardly_entry_get for big payloads; sensitive values come back as shapes. Example: hardly_entry_body_query(session_id='S', entry_id=12, jsonpath='$.items[*].id').

Input parameters:

- `context` (integer)
- `entry_id` (integer, required)
- `ignore_case` (boolean)
- `jsonpath`
- `limit`
- `max_chars`
- `offset`
- `regex`
- `session_id` (string, required)
- `side` (string)

Output parameters:

- `result` (string)

### `hardly_entry_outline` (~127 tokens)

Hardly Entry Outline

Offline outline of an HTML/XML body from a HAR entry. format: markdown (headings/tables/forms/links), tree (tag tree) or aria (YAML); omit for all three. Example: hardly_entry_outline(session_id='S', entry_id=5, format='markdown').

Use instead of reading raw HTML; for the live tab use hardly_browser_inspect(sections=['aria']).

Input parameters:

- `entry_id` (integer, required)
- `format`
- `max_depth` (integer)
- `session_id` (string, required)
- `side` (string)

Output parameters:

- `result` (string)

### `hardly_entry_dependencies` (~118 tokens)

Hardly Entry Dependencies

Trace what one request depends on: the earlier response behind each header, cookie, hidden field or value. Returns ordered minimal steps plus inputs the caller must supply. Offline; names and ids only. Example: hardly_entry_dependencies(session_id='S', entry_id=20).

Use before writing a client for a multi-step flow; to track one value across the capture use
hardly_session_trace_value.

Input parameters:

- `entry_id` (integer, required)
- `host`
- `max_depth` (integer)
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_endpoint_list` (~101 tokens)

Hardly Endpoint List

List grouped endpoints (METHOD + path template) with counts, paged by limit/offset. The main API surface view for JSON APIs; for server-rendered pages use hardly_session_site_brief or hardly_page_forms. Example: hardly_endpoint_list(session_id='S', host='api.example.com').

Input parameters:

- `exclude_noise` (boolean)
- `host`
- `limit`
- `offset`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_endpoint_schema` (~134 tokens)

Hardly Endpoint Schema

Request/response shapes of one endpoint template. Sections: schema (default; JSON field names and types), param_roles (each field static, dynamic or sensitive). Example: hardly_endpoint_schema(session_id='S', method='GET', path_template='/api/items/{id}').

Use after hardly_endpoint_list. param_roles decides which parameters a client must compute versus
hard-code. host defaults to the main host. Names and types only.

Input parameters:

- `host`
- `limit`
- `method` (string, required)
- `path_template` (string, required)
- `sections`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_spec_contract_check` (~85 tokens)

Hardly Spec Contract Check

Compare a capture with a previously written OpenAPI file and report drift (new/removed endpoints, status, field, parameter, auth changes). Offline; removed means not observed. Example: hardly_spec_contract_check(session_id='S', spec_path='/data/api.yaml').

Input parameters:

- `host`
- `session_id` (string, required)
- `spec_path` (string, required)

Output parameters:

- `result` (string)

### `hardly_tech_stack` (~138 tokens)

Hardly Tech Stack

Fingerprint web frameworks, CMS, GIS stacks, UI toolkits and data-grid widgets from header/cookie names, paths and body previews. Absent technology gives count 0. Example: hardly_tech_stack(session_id='S', explain=true).

explain=true adds SDK implications. Data-grid frameworks, JSON envelope conventions (OData,
JSON:API, HAL, Relay) and paging/sort styles are under ``data_grids``. For CDN/WAF products use
hardly_gate_bot_protection.

Input parameters:

- `explain` (boolean)
- `host`
- `limit`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_endpoint_graphql` (~77 tokens)

Hardly Endpoint Graphql

Detect GraphQL operations (operationName, query, mutation) behind shared URLs, with evidence entry ids. Use when POSTs share one URL; then hardly_entry_get for a sample. Example: hardly_endpoint_graphql(session_id='S').

Input parameters:

- `host`
- `limit`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_endpoint_streams` (~101 tokens)

Hardly Endpoint Streams

Summarise non-JSON stream/binary formats: gRPC(-web), protobuf, MessagePack, CSV/TSV, SSE, WebSocket frames, with evidence entry ids. Shapes only. Use when endpoints show odd content types. Example: hardly_endpoint_streams(session_id='S').

Input parameters:

- `exclude_noise` (boolean)
- `host`
- `kind`
- `limit`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_endpoint_pagination` (~74 tokens)

Hardly Endpoint Pagination

Recognise cursor, next-link and Link-header pagination, with evidence entry ids. Shapes only. Use before writing a client loop over results. Example: hardly_endpoint_pagination(session_id='S', host='api.example.com').

Input parameters:

- `host`
- `limit`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_endpoint_arcgis` (~76 tokens)

Hardly Endpoint Arcgis

List ArcGIS REST endpoints (MapServer/FeatureServer/etc.) seen in the capture: layer ids, parameter names, paging evidence. Sends nothing; the live twin is hardly_send_arcgis_explore. Example: hardly_endpoint_arcgis(session_id='S').

Input parameters:

- `host`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_page_list` (~69 tokens)

Hardly Page List

List HAR pageref groups (browser page loads) with document hints. Use to split a capture into navigations. Example: hardly_page_list(session_id='S').

Input parameters:

- `exclude_noise` (boolean)
- `host`
- `limit`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_page_forms` (~134 tokens)

Hardly Page Forms

Extract HTML forms and their inputs with signals (hidden fields, tokens, links, handlers). Use to reverse server-rendered pages instead of dumping HTML; entry_id gives the full inventory of one response, omit it to scan the session. Example: hardly_page_forms(session_id='S', entry_id=5).

For link/handler/label-first workflows and search links use hardly_page_ui (same scan).

Input parameters:

- `entry_id`
- `exclude_noise` (boolean)
- `host`
- `limit`
- `offset`
- `session_id` (string, required)
- `side` (string)

Output parameters:

- `result` (string)

### `hardly_page_ui` (~188 tokens)

Hardly Page Ui

Inventory UI controls. Sections: links, handlers (onclick/onsubmit), labels (default all three), search_links (ranked links likely to lead to a search page). Example: hardly_page_ui(session_id='S', sections=['search_links'], keywords=['search']).

Same scan as hardly_page_forms, projected to the UI parts; handler_functions names feed
hardly_page_embedded_routes. entry_id inventories one response, omit it to scan the session.
search_links takes ``keywords`` (the site's domain vocabulary): repeat per hop until a form with
inputs appears; its next_step feeds hardly_browser_run_steps.

Input parameters:

- `entry_id`
- `exclude_noise` (boolean)
- `host`
- `keywords`
- `limit`
- `offset`
- `sections`
- `session_id` (string, required)
- `side` (string)

Output parameters:

- `result` (string)

### `hardly_page_embedded_routes` (~164 tokens)

Hardly Page Embedded Routes

Mine endpoints the page embeds. Sections: script_routes (URL path literals in JavaScript), data_attrs (endpoints and config in HTML data-* attributes); default both. Example: hardly_page_embedded_routes(session_id='S', sections=['data_attrs'], entry_id=5).

Use when search works but the detail URL is unknown, or on JS-heavy pages to find where the page
gets its data; then hardly_entry_around on the click entry. data_attrs reports dataset keys,
value kinds, embedded endpoint URLs and JSON config, never free text.

Input parameters:

- `entry_id`
- `explain` (boolean)
- `host`
- `limit`
- `offset`
- `sections`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_page_tables` (~80 tokens)

Hardly Page Tables

List HTML data tables: headers, row/column counts, column kinds and a masked first row (cell values never returned). Use to understand result grids before scraping or modelling them. Example: hardly_page_tables(session_id='S', entry_id=14).

Input parameters:

- `entry_id`
- `host`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_auth_report` (~206 tokens)

Hardly Auth Report

Authentication evidence; sections: quick (default; auth paths, token responses, header names), patterns, credentials, secret_names, cookies. Names, shapes and entry ids only, never values. Example: hardly_auth_report(session_id='S', sections=['credentials','cookies']).

quick: a first auth check. patterns: named schemes (bearer+refresh, OIDC+PKCE, SAML POST,
double-submit CSRF, signed requests; ``pattern_kinds`` filters). credentials: password fields,
session cookies and flags, anti-forgery names, OAuth params, token shapes and a hypothesised
login_flow (use before implementing login in a client). secret_names: sensitive header/field/
query NAMES. cookies: per-server timeline of Set-Cookie / Cookie names.

Input parameters:

- `explain` (boolean)
- `host`
- `limit`
- `pattern_kinds`
- `sections`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_gate_bot_protection` (~262 tokens)

Hardly Gate Bot Protection

Detect gates in a capture: bot walls, captcha, login, rate limits, 401/403/429 challenges and CDN/WAF bot-protection products. Never evades. Example: hardly_gate_bot_protection(session_id='S', explain=true).

Call when requests are blocked, before any retry: every gate is a STOP sign (re-capture
interactively with a person). barriers: each barrier (environment_blocked, bot_wall, captcha,
proof_of_work, waiting_room, click_through_terms, login, paywall, rate_limit) with evidence
names, entry_ids and a policy action (stop | accept_click_through | unknown_rerun).
http_challenges: WWW-Authenticate schemes, 429/423/Retry-After lockout signals and captcha
widget markup. bot_protection: vendors seen (Cloudflare, Akamai, Imperva, DataDome, HUMAN, ...)
with a state each (blocked / challenged / clearance_seen / present). Challenge nonces are never
returned; a CDN header on a normal page is informational, not a wall.

Input parameters:

- `explain` (boolean)
- `host`
- `limit`
- `sections`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_send_entry` (~185 tokens)

Hardly Send Entry

LIVE (confirm-gated): replay ONE captured request. Without confirm=true it returns only the plan and sends nothing, so tell the person what will be sent first. Sensitive HAR headers are skipped unless given in header_overrides. Example: hardly_send_entry(session_id='S', entry_id=12, confirm=true).

Refuses loopback, private, link-local and metadata hosts, also on redirects (host_not_allowed; local test targets: HARDLY_ALLOW_PRIVATE_HOSTS=1).

For an ordered series use hardly_send_entry_series; to learn which elements are required
hardly_send_entry_ablation. The offline curl text is hardly_entry_build_curl.

Input parameters:

- `body_override`
- `confirm` (boolean)
- `entry_id` (integer, required)
- `header_overrides`
- `session_id` (string, required)
- `timeout_seconds` (number)

Output parameters:

- `result` (string)

### `hardly_send_entry_ablation` (~276 tokens)

Hardly Send Entry Ablation

LIVE (confirm-gated): find which headers, cookies, params, body fields and prior steps a request truly needs by replaying it with ONE element removed at a time. Without confirm=true it returns only the plan. Example: hardly_send_entry_ablation(session_id='S', entry_ids=[12], confirm=true).

Refuses loopback, private, link-local and metadata hosts, also on redirects (host_not_allowed; local test targets: HARDLY_ALLOW_PRIVATE_HOSTS=1).

Reports REQUIRED vs OPTIONAL names only, never bodies. entry_ids may be an ordered flow (earlier
ids are prior steps, the last is the target). Secrets only via ``overrides``
{"headers":{},"cookies":{},"query":{},"body":{}}; missing ones are listed under needs_override.
GET/HEAD only unless allow_unsafe=true. Hard stop on 429 / Retry-After / gate stop; captcha token
fields are never sent. Budget-skipped items appear under not_tested.

Input parameters:

- `allow_gates`
- `allow_unsafe` (boolean)
- `confirm` (boolean)
- `delay_seconds` (number)
- `entry_ids` (array, required)
- `max_requests` (integer)
- `overrides`
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_send_entry_series` (~222 tokens)

Hardly Send Entry Series

LIVE (confirm-gated): replay an ordered series and report the first step whose status, content-type or body shape diverges. Without confirm=true it returns the dry-run plan and missing inputs. Example: hardly_send_entry_series(session_id='S', entry_id=20, confirm=true).

Refuses loopback, private, link-local and metadata hosts, also on redirects (host_not_allowed; local test targets: HARDLY_ALLOW_PRIVATE_HOSTS=1).

Use to verify a client flow works. Secrets only via ``env`` {name: value} or HARDLY_INPUT_<NAME>
env vars; values are never printed. GET/HEAD only unless allow_unsafe; halts on 429/Retry-After/
gates.

Input parameters:

- `allow_gates`
- `allow_unsafe` (boolean)
- `confirm` (boolean)
- `delay_seconds` (number)
- `entry_id`
- `entry_ids`
- `env`
- `max_requests` (integer)
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `hardly_send_site_crawl` (~339 tokens)

Hardly Send Site Crawl

LIVE (confirm-gated): curl-first, robots-aware, polite crawl that finds candidate pages (search forms first). Without confirm=true it returns only the plan. Use when headless Chromium is blocked or static HTML suffices; it stops at gates and never evades them. Example: hardly_send_site_crawl(url='https://example.com', keywords=['search'], confirm=true).

Refuses loopback, private, link-local and metadata hosts, also on redirects (host_not_allowed; local test targets: HARDLY_ALLOW_PRIVATE_HOSTS=1).

Follows only links found in fetched HTML (never guesses hosts or paths), ranks them with
\``keywords`` (nouns), honours robots.txt, waits delay_seconds between requests per host, strips
session ids, and stops at gates and on 429/Retry-After. Caps: max_pages <= 40, depth <= 4.
External registrable domains are only recorded unless follow_external=true (one hop). Returns
candidates, per-page form field NAMES, gate classes and needs_browser pages (with `next` advice
when explain=true); no bodies, URLs redacted. For needs_browser pages use
hardly_browser_capture_discover with a find_click step.

Input parameters:

- `confirm` (boolean)
- `delay_seconds` (number)
- `depth` (integer)
- `explain` (boolean)
- `follow_external` (boolean)
- `keywords`
- `max_pages` (integer)
- `respect_robots` (boolean)
- `timeout_seconds` (number)
- `url` (string, required)
- `user_agent`

Output parameters:

- `result` (string)

### `hardly_send_arcgis_explore` (~188 tokens)

Hardly Send Arcgis Explore

LIVE (confirm-gated): politely explore an ArcGIS REST service or layer URL (GET only, at most 7 requests). Without confirm=true it returns only the plan, so show the person the URL first. For captured traffic use hardly_endpoint_arcgis. Example: hardly_send_arcgis_explore(url='https://host/arcgis/rest/services/X/MapServer', confirm=true).

Refuses loopback, private, link-local and metadata hosts, also on redirects (host_not_allowed; local test targets: HARDLY_ALLOW_PRIVATE_HOSTS=1).

Returns layers, fields (personal-data-like and id fields FLAGGED), query templates and a one-row
sample as field names + masked shapes, never values. Stops on 429 and on token-required
(498/499); never guesses tokens.

Input parameters:

- `confirm` (boolean)
- `url` (string, required)

Output parameters:

- `result` (string)

### `hardly_send_redirect_walk` (~184 tokens)

Hardly Send Redirect Walk

LIVE (confirm-gated): explain a redirect loop (ERR_TOO_MANY_REDIRECTS) by following the chain by hand with and without cookies. Without confirm=true it returns only the plan. Reports statuses, redacted URLs and cookie names only. Example: hardly_send_redirect_walk(url='https://example.com', confirm=true).

Refuses loopback, private, link-local and metadata hosts, also on redirects (host_not_allowed; local test targets: HARDLY_ALLOW_PRIVATE_HOSTS=1).

Reports the loop shape: www<->apex or http<->https flips, trailing-slash fights, growing return
URLs, cookie-dependent redirects, and whether the alternate host resolves. Redirects already in
a capture are hardly_session_redirect_history.

Input parameters:

- `confirm` (boolean)
- `max_hops` (integer)
- `url` (string, required)

Output parameters:

- `result` (string)

### `hardly_write_export` (~348 tokens)

Hardly Write Export

Writes a file: export the session as format openapi, postman, api_markdown, site_brief, report, client_python or plan_steps to output_path. Secrets are redacted or placeholders; refuses to overwrite unless overwrite=true. Example: hardly_write_export(session_id='S', format='openapi', output_path='/tmp/api.yaml', host='api.example.com').

Writes only inside the working directory, the OS temp dir or HARDLY_WRITE_DIRS (path_not_allowed).

openapi: OpenAPI 3 (JSON or YAML by file extension; input to hardly_spec_contract_check).
postman: Collection v2.1 with {{placeholders}} (``title`` names it). api_markdown: API.md of the
endpoints (``max_endpoints``, default 200). site_brief: the hardly_session_site_brief digest as
Markdown. report: the hardly_session_report findings (``detail`` summary|standard|full,
\``categories``; .json path = JSON, else Markdown; a directory gets <har>.report.md).
client_python: the starter urllib client of hardly_client_build (``entry_ids``, ``class_name``).
plan_steps: the JSON steps of hardly_session_plan_steps (``limit``).

Input parameters:

- `categories`
- `class_name`
- `detail` (string)
- `entry_ids`
- `exclude_noise` (boolean)
- `format` (string, required)
- `host`
- `limit`
- `max_endpoints`
- `output_path` (string, required)
- `overwrite` (boolean)
- `session_id` (string, required)
- `title`

Output parameters:

- `result` (string)

### `hardly_client_build` (~117 tokens)

Hardly Client Build

Build a minimal Python (urllib) client sketch from story steps or chosen entry_ids and return it as text; secrets become PLACEHOLDER_* values. Writes nothing (hardly_write_export format=client_python saves it). Example: hardly_client_build(session_id='S', entry_ids=[12, 14]).

Use as the last step of SDK work, after hardly_session_story / hardly_session_site_brief.

Input parameters:

- `class_name`
- `entry_ids`
- `host`
- `session_id` (string, required)

Output parameters:

- `result` (string)

## Diagnostics

Captured diagnostic sections: Provenance, Install scripts, Dependencies. The full working is on the page: https://verifymcp.io/servers/b1tninja-hardly/hardly#diagnostics

## Score history

- 2026-11-04: 80
- 2026-11-03: 80
- 2026-11-02: 79
- 2026-11-01: 77
- 2026-10-31: 77
- 2026-10-30: 77
- 2026-10-29: 62

## Common questions

### What is the hardly MCP server?

hardly is an MCP server listed in the public MCP registry as io.github.b1tninja/hardly. Generic HAR analysis MCP server: index and query captures without burning tokens on raw files. This page covers its PyPI package (hardly).

### Is the hardly MCP server safe to use?

hardly scores 80 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 4 November 2026. Its build provenance is signed and verified. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the hardly MCP server expose?

hardly exposes 71 tools: hardly_server_status, hardly_guide_help, hardly_guide_mode, hardly_guide_task_plan, hardly_session_open, and 66 more. Their descriptions and schemas cost roughly 10,295 tokens of context every time the server is loaded.

### Is the hardly MCP server still maintained?

hardly is still listed as active in the MCP registry. We last reached this channel on 4 November 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

### What licence is the hardly MCP server under?

hardly declares the MPL-2.0 licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.

## Links

- PyPI project: https://pypi.org/project/hardly/
- Socket report: https://socket.dev/pypi/package/hardly
- Repository: https://github.com/b1tninja/hardly
- Changelog RSS feed: https://verifymcp.io/servers/b1tninja-hardly/hardly.xml
- Changelog JSON feed: https://verifymcp.io/servers/b1tninja-hardly/hardly.json
- HTML version of this page: https://verifymcp.io/servers/b1tninja-hardly/hardly
