# Prowl MCP Server — AI Agent Skill

> **Protocol:** Model Context Protocol (MCP)
> **Server name:** `prowl_mcp`
> **Version:** 1.6.0

Prowl is **the MCP that turns any AI agent into a research analyst** — not a research tool, but the intelligence layer your agent connects to. It exposes **447 marketing intelligence API tools** (across DataForSEO, Majestic, Keywords Everywhere, SearchAPI, SerpAPI, web scraping, SEO, ads, AI, and utility providers) so your agent answers with real numbers instead of guesses, plus the full Prowl pipeline — multi-agent analysis, individual tool invocation, artifact generation, and session management — to any MCP-compatible client.

> **Per-tool reference:** this page describes the MCP surface. For one page per underlying
> tool — input schema, example request, response shape, error table, the tools it chains
> with — see [`PROWL-AI/prowl-docs`](https://github.com/PROWL-AI/prowl-docs), plain markdown
> regenerated from the code on every release.

---

## Authentication

All requests to the MCP server require a **Bearer API key** in the `Authorization` header:

```
Authorization: Bearer prowl_<your_key>
```

API keys are generated from the Prowl web UI under **MCP Home → "Generate your token" (Step 1 of "Set up in 3 steps")** (or `POST /api/mcp/keys` / `POST /api/v1/keys` with JWT). Each key starts with the `prowl_` prefix followed by 32 hex characters (38 characters total).

### Scoped keys (optional)

When creating a key you may restrict:

| Field | Effect |
|-------|--------|
| `allowed_categories` | Allowlist (e.g. `["dataforseo", "web"]`); empty = all categories. Categories follow provider family: `dataforseo`, `seo`, `serpapi_serp`, `searchapi`, `ads`, `ai`, `web`, `utility`, `analyze` (DataForSEO = `dataforseo`, not `seo`). `prowl_analyze` needs `analyze` (or an empty allowlist) |
| `denied_categories` | Blocklist (e.g. `["ads"]`) |
| `daily_spend_limit_usd` | Max billed USD per UTC day for this key |
| `total_spend_limit_usd` | Lifetime spend cap for this key |
| `ip_allowlist` | Only accept requests from listed IPs |

Scoped keys enforce category and spend policy on `prowl_call_tool` **and** `prowl_analyze` (preflight, before any tool runs). Revoked keys → **401**; IP violations → **403**.

The following endpoints are publicly accessible without authentication:
- `/mcp/skill.md` — this document
- `/mcp/health` — server health check (returns JSON with status, version, MCP tool count, underlying API-tool count, cached-session count)

---

## Billing & Wallet

MCP HTTP transport uses **USD wallet billing** (not report quotas):

| Pool | Source | Expiry | Charge order |
|------|--------|--------|--------------|
| **Subscription** | Stripe plan renewal | Burns at period end | 1st |
| **Extra** | Top-up packs ($10 / $50 / $200) | Never expires | 2nd |

- **`prowl_call_tool`** — wallet preflight before invoke; response JSON includes `billing` (`estimated_cost_usd`, `actual_cost_usd`, `debited`, …)
- **`prowl_analyze`** — pre-flight wallet gate + post-query debit for aggregate tool spend
- New signups get **$5 starter credit** in the extra pool (`PROWL_STARTER_CREDIT_USD`, default `5`)
- Insufficient balance → tool error or gate block before expensive runs

**Inspect balance — from MCP, with your API key:**

Call **`prowl_get_wallet`** (free). It is the only surface that answers a
`prowl_` key: the REST route below decodes a **JWT**, which an API key is not.
It also reports which execution modes the key can actually run, so a `deep`
request that would silently resolve to `basic` is knowable before the spend.

**Inspect balance over REST (JWT only — browser session, not an API key):**

```bash
curl -H "Authorization: Bearer <jwt>" https://prowl.chat/api/v1/wallet
```

**Public price catalog:**

```bash
curl https://prowl.chat/api/v1/tools/pricing?limit=50
```

**Top-up packs:** `GET /api/topup/packs`, `POST /api/checkout/topup` (JWT).

---

## Server URL

```
https://<your-prowl-host>/mcp
```

For local development via stdio transport:

```bash
python -m mcp_server
```

---

## Configuration

### Claude Desktop / Cursor (HTTP — Remote)

```json
{
  "mcpServers": {
    "prowl": {
      "url": "https://<your-prowl-host>/mcp",
      "headers": {
        "Authorization": "Bearer prowl_<your_key>"
      }
    }
  }
}
```

### Claude Desktop / Cursor (stdio — Local)

stdio is for local development from a cloned repo. Launch via `uv` so
dependencies resolve, and supply an LLM key in `env`. For hosted use, prefer
the HTTP transport above.

```json
{
  "mcpServers": {
    "prowl": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/0xDEV", "run", "python", "-m", "mcp_server"],
      "env": { "ANTHROPIC_API_KEY": "sk-..." }
    }
  }
}
```

---

## Tools (23 registered tools — 21 logical + 2 legacy aliases → 447 API tools)

> **Recommended flow for agents:** `prowl_search_tools` (or `prowl_list_tools`) → `prowl_tool_info` → `prowl_call_tool` (metered per invocation). Use `prowl_analyze` when you want a full strategy report in one shot (blocking), or the **session tools** (`prowl_start_session` → `prowl_session_status` → `prowl_get_session`) when you'd rather kick off a long deep-research run and poll for it asynchronously.

> **About `session_id`:** Most tools accept an optional `session_id` string. Pass the **same** value across consecutive calls and the server scopes the cached report, conversation history, and circuit-breaker state to that one conversation. Omit `session_id` and the server derives a key from your MCP client identity (`_meta.client_id`) or HTTP `mcp-session-id` / `x-session-id` header. The literal `__default__` is used as the last-resort fallback so stdio transports keep working.

### 1. `prowl_analyze`

Run a full competitive intelligence analysis. This is the core tool — it plans research, makes 20-400 API tool calls in parallel (depending on `execution_mode`: basic/deep/max), and synthesises a structured markdown report.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string (3-2000) | Yes | Natural-language research query. Follow-ups about the previous report (translate, summarise, explain a section) are answered conversationally from the cached report — no need to resend the whole query. Also accepts a domain-less product/business idea ("I want to build a…", "validate this idea before I build it") for pre-launch idea validation — not only a competitor URL or domain. |
| `execution_mode` | `"basic"` \| `"deep"` \| `"max"` | No | `"basic"` (20-100 tool calls, ~30-90s, free) \| `"deep"` (40-300 calls, multi-competitor deep-dive + evidence verification, 3-5min, requires an active Exploit+ subscription) \| `"max"` (60-400 calls, adversarially-verified claims, 5-10min, requires Blackops+). Unauthorized deep/max auto-downgrade to basic with a notice. Default `"basic"`. |
| `session_id` | string (≤128) | No | Pin this call to a chat session for per-session memory (see *About `session_id`* above). |

**Example:**
```json
{
  "query": "Full competitor analysis for stripe.com vs square.com",
  "execution_mode": "deep",
  "session_id": "stripe-analysis-2026-05"
}
```

**Returns:** Full markdown report with SEO, ads, funnels, reviews, pricing, and market trends. On `deep`/`max` runs, extracted claims are cross-checked against tool evidence — refuted or low-confidence claims are flagged and coverage gaps are disclosed honestly rather than glossed over.

**Execution time:** 30 seconds to 5 minutes. The server emits periodic `progress` + `info` events every ~15s so Cloudflare/DigitalOcean proxies don't cut the long-running connection.

---

### 2. `prowl_list_tools`

List all 447 available API tools grouped by category.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `category` | string | No | Filter by category (e.g. `"dataforseo"`, `"searchapi"`, `"serpapi_serp"`, `"web"`, `"seo"`, `"ads"`, `"ai"`, `"utility"`). Omit for all. |

**Returns:** JSON with tools grouped by category and total count.

---

### 3. `prowl_search_tools`

**Semantic** search across the 447-tool catalog with pagination — the fastest way to find a tool when you know *what* you want but not its exact name. Returns compact entries (name, use case, category, provider, top-level params) instead of full schemas, so it's cheap to page through. Follow with `prowl_tool_info` for the full input schema of a chosen tool.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string (≤500) | No | Natural-language description (e.g. `"find competitor backlinks"`, `"scrape a webpage"`). Blank → browse the catalog top-down by category. |
| `limit` | int (1-50) | No | Max results per page (default 10). |
| `offset` | int (≥0) | No | Pagination offset; pass `next_offset` from the previous page. |
| `category` | string | No | Restrict to a category (`"web"`, `"seo"`, `"ads"`, `"ai"`, `"dataforseo"`, `"searchapi"`, `"serpapi_serp"`, `"utility"`). |
| `provider` | string | No | Restrict to a provider (e.g. `"DataForSEO"`, `"SpyFu"`, `"Exa"`). |

**Returns:** JSON `{ query, results[], total, limit, offset, next_offset, degraded, filters }`. `next_offset` is `null` on the last page; `degraded=true` indicates the fallback (empty query or unavailable index) returned a category browse.

---

### 4. `prowl_tool_info`

Get detailed information about a specific API tool, including its full JSON input schema.

> Legacy alias: `prowl_get_tool_info`

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tool_name` | string (1-200) | Yes | Exact tool name (e.g. `"dataforseo_serp_google_organic_live_advanced"`). |

**Returns:** JSON with tool name, description, category, and JSON Schema for parameters.

---

### 5. `prowl_call_tool`

Directly invoke a single API tool with **wallet metering**. Each call debits your USD balance (provider cost × markup). Use for production agent workflows that call specific data APIs.

> Legacy alias: `prowl_test_tool`

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tool_name` | string (1-200) | Yes | Tool name to invoke. |
| `params` | object | No | JSON parameters for the tool. Default: `{}`. |
| `session_id` | string (≤128) | No | Scope the invocation to a chat session (shares the same cache/circuit-breaker state as `prowl_analyze`). |

**Example:**
```json
{
  "tool_name": "extract_domain_from_url",
  "params": {"url": "https://www.example.com/pricing"}
}
```

**Returns:** JSON with `tool_name`, `success`, `data`, `error`, and `execution_time_ms`.

---

### 6. `prowl_get_stats`

Get current session statistics including token usage, cost estimate, tool call counts, and memory usage.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `session_id` | string (≤128) | No | Scope stats to a specific chat session. |

**Returns:** JSON session statistics.

---

### 7. `prowl_generate_artifact`

Generate a visual artifact from the last analysis report. Requires a prior `prowl_analyze` call **on the same `session_id`**.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `artifact_type` | `"infographic"` \| `"pdf"` \| `"pptx"` | Yes | Type of artifact. |
| `theme` | `"prowl"` \| `"prowl-gold"` \| `"prowl-light"` | No | Visual theme. Default: `"prowl"`. |
| `session_id` | string (≤128) | No | Must match the `session_id` used for the `prowl_analyze` call that produced the report. |

**Returns:** JSON with artifact type and file path.

---

### 8. `prowl_export_report`

Export the last analysis report to a file. Requires a prior `prowl_analyze` call **on the same `session_id`**.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `format` | `"markdown"` \| `"html"` | No | Export format. Default: `"markdown"`. |
| `filepath` | string | No | Destination path. Auto-generated if omitted. |
| `session_id` | string (≤128) | No | Scope the export to a specific chat session. |

**Returns:** JSON with format and absolute file path.

---

### 9. `prowl_reset_session`

Reset the agent conversation, cached report, and circuit breakers. Use when switching research topics.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `session_id` | string (≤128) | No | Scope the reset to a specific chat session. When omitted, resets the session identified by the MCP client. |

**Returns:** Confirmation message.

---

## Session tools (async chat sessions)

These four tools let an agent **drive analyses asynchronously** and read back
results by `session_id`. A session is the same chat-session row the web
dashboard shows, so work started over MCP is visible in the UI and vice versa.
Ownership is always enforced — a key can only see sessions belonging to its
user.

### 10. `prowl_start_session`

Start a competitive-intelligence analysis **in the background** and return
immediately with a `session_id`. The run continues on a detached server-side
task, so a dropped SSE stream (or an agent that stops polling) won't cancel it.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string (3-2000) | Yes | Natural-language research query. |
| `execution_mode` | `"basic"` \| `"deep"` \| `"max"` | No | `"basic"` (~30-90s) \| `"deep"` (3-5min, requires an active Exploit+ subscription) \| `"max"` (5-10min, adversarially-verified, requires Blackops+). Unauthorized deep/max auto-downgrade to basic with a notice. Default: `"basic"`. |
| `title` | string (≤200) | No | Friendly session title (defaults to a query snippet). |

**Returns:** JSON `{session_id, status:"running", title, execution_mode, message}`.
Bills exactly like `prowl_analyze` (wallet preflight before the run, debit
after). Use `prowl_session_status` to poll.

**Example:**
```json
{"query": "Full competitor analysis for stripe.com vs square.com", "execution_mode": "deep", "title": "Stripe vs Square"}
```

---

### 11. `prowl_session_status`

Poll a started session. While running, returns live `progress` (0-1) and
`stats`; when finished, includes `status:"completed"` with the full `report`
(or `status:"failed"` with `error`). Falls back to the database for sessions
whose in-memory job has expired or that completed before a server restart.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `session_id` | string (1-128) | Yes | The id returned by `prowl_start_session`. |

**Returns:** JSON `{session_id, status, progress?, stats?, report?, error?}` where
`status` ∈ `queued` / `running` / `completed` / `failed` / `not_found`.

---

### 12. `prowl_get_session`

Fetch the generated report (and optionally full message history) for a
specific session. Reads the persisted report from the database first, falling
back to the in-memory job result.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `session_id` | string (1-128) | Yes | Session to read. |
| `include_messages` | boolean | No | Include the full user/assistant message history. Default: `false`. |

**Returns:** JSON `{session_id, status, report, report_available, messages?}`.

---

### 13. `prowl_list_sessions`

List the caller's chat sessions, newest first. Each entry carries a `status`
(`running` while a background analysis is in flight, otherwise
`completed` / `failed` / `empty`).

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `limit` | integer (1-200) | No | Max sessions to return. Default: `50`. |

**Returns:** JSON `{sessions:[{session_id, title, status, created_at, updated_at, message_count, tool_calls_count}], count, source}`.

---

## Playbook, scheduling & diagnostics tools

These six tools round out the wrapper surface: pick a fixed report shape, manage recurring background analyses, and inspect recent errors for your account. The scheduling and error-feed tools require an authenticated Prowl account (API key) — they return an `Error:` string otherwise.

### 14. `prowl_list_playbooks`

List the available report playbooks. A playbook is a fixed, persona-tuned set of report sections — pass its id as `playbook_id` to `prowl_analyze` to force that exact report structure instead of dynamic, query-driven section selection.

**Input:** None.

**Returns:** Markdown list of playbooks (id, title, target personas, what each covers, section count).

---

### 15. `prowl_schedule_create`

Register a saved analysis that runs automatically and stores the report.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | Yes | Natural-language research query to run on each fire. |
| `trigger_type` | `"interval"` \| `"webhook"` | No | `"interval"` runs on a `cadence` at `run_at_hour` (UTC). `"webhook"` returns a unique URL that runs the report when POSTed. Default: `"interval"`. |
| `execution_mode` | `"basic"` \| `"deep"` \| `"max"` | No | Resolved fresh against the account's subscription tier when the job actually fires (not at creation time) — deep/max auto-downgrade to basic if the required tier isn't active then. Default: `"basic"`. |
| `playbook_id` | string | No | Optional playbook id (see `prowl_list_playbooks`) to fix the report structure. |
| `cadence` | `"daily"` \| `"weekly"` \| `"monthly"` | Required when `trigger_type="interval"` | How often the schedule fires. |
| `run_at_hour` | int (0-23) | No | Hour of day (UTC) an interval schedule fires. Default: `8`. |

**Returns:** Confirmation with the schedule id (and a webhook URL when `trigger_type="webhook"`). Manage with `prowl_schedule_list` / `prowl_schedule_cancel` / `prowl_schedule_resume`.

---

### 16. `prowl_schedule_list`

List the caller's scheduled report runs.

**Input:** None.

**Returns:** Markdown list of schedules: id, active/paused state, trigger type, cadence + next-run time (or webhook), and query snippet.

---

### 17. `prowl_schedule_cancel`

Delete one of the caller's scheduled jobs.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `job_id` | string | Yes | Id of the schedule to delete (from `prowl_schedule_list`). |

**Returns:** Confirmation message, or an error if no schedule with that id is owned by the caller.

---

### 18. `prowl_schedule_pause`

Stop a scheduled job from firing without deleting it. The job keeps its query, cadence and history; `prowl_schedule_resume` puts it back on the clock.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `job_id` | string | Yes | Id of the schedule to pause (from `prowl_schedule_list`). |

**Returns:** Confirmation message, or an error if no schedule with that id is owned by the caller.

---

### 19. `prowl_schedule_resume`

Re-activate a paused scheduled job (e.g. after topping up the wallet).

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `job_id` | string | Yes | Id of the schedule to resume. |

**Returns:** Confirmation message, or an error if no schedule with that id is owned by the caller.

---

### 20. `prowl_get_error_feed`

Recent errors for the caller's account: failed tools, billing, and actions. Read-only; scoped to your own account.

**Input:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `hours` | int (1-720) | No | Lookback window in hours. Default: `24`. |
| `source` | string | No | Filter by source: `"tool_error"`, `"action"`, `"tool_invocation"`, `"billing_outbox"`. |
| `tool_name` | string | No | Filter to errors from a specific tool. |
| `error_class` | string | No | Filter by class: `"network"`, `"server"`, `"auth"`, `"rate_limit"`, `"timeout"`, `"plan_denied"`, `"client"`, `"unknown"`. |
| `severity` | string | No | Filter by severity: `"alert"`, `"warn"`, `"info"`. |
| `limit` | int (1-200) | No | Max events to return. Default: `50`. |

**Returns:** Markdown list of matching error events (timestamp, severity, source, tool, error class, message), or a "no errors" message.

---

### 21. `prowl_get_wallet`

Your wallet balance and the execution modes this key can actually run. Free, read-only, takes no parameters.

This is the only surface that answers a `prowl_` API key. `GET /api/v1/wallet` returns the same balances but decodes a **JWT**, and an API key is not one — so a key-holding client (CLI, CI job, agent) could otherwise learn its balance only from the error that blocked a call it had already committed to.

The `entitlement` block is the other half. `deep` and `max` need an active subscription (or the admin-granted `max_tools_enabled` flag); without one a run does **not** fail — it resolves to `basic` and is reserved and billed as basic. `available_execution_modes` says which modes are real for this key **before** the spend, and `downgraded_modes` names what each unavailable mode would become.

**Input:** none.

**Returns:**
```json
{
  "subscription_balance_usd": 12.40,
  "extra_balance_usd": 5.00,
  "total_available_usd": 17.40,
  "updated_at": "2026-08-15T09:14:22Z",
  "entitlement": {
    "has_active_subscription": true,
    "max_tools_granted": false,
    "available_execution_modes": ["basic", "deep"],
    "downgraded_modes": { "max": "basic" }
  },
  "key_limits": {
    "key_id": "key_...",
    "daily_spend_limit_usd": null,
    "total_spend_limit_usd": 50.0,
    "allowed_categories": [],
    "denied_categories": []
  }
}
```

`key_limits` appears only for a scoped key, and matters for the same reason: a key can be capped well below the wallet, so a batch planned against the balance alone can still be refused partway through.

---

## Resources (3)

Resources provide read-only access to server state via `prowl://` URIs.

| URI | Description |
|-----|-------------|
| `prowl://tools` | Full catalog of all 447 API tools with schemas, grouped by category. |
| `prowl://stats` | Current session statistics snapshot. |
| `prowl://report` | Last generated analysis report (markdown). |

> Resources read from the **default** session cache because FastMCP's resource protocol has no per-call context. To inspect a specific session's state, call the `prowl_get_stats` tool with a `session_id` argument instead.

---

## Prompts (3)

Pre-built prompt templates for common research scenarios.

### `competitor_analysis`

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `domain` | string | Yes | Target domain (e.g. `"example.com"`). |
| `focus_areas` | string | No | Comma-separated focus areas (e.g. `"SEO, pricing, ads"`). |

### `seo_audit`

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `domain` | string | Yes | Target domain to audit. |

### `ad_creative_research`

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `domain` | string | Yes | Target domain to research. |
| `platforms` | string | No | Comma-separated platforms (e.g. `"Facebook, Google, TikTok"`). |

---

## Workflows

### Basic Analysis Workflow

```
1. prowl_analyze(query="Competitor analysis for stripe.com", session_id="s1")
2. prowl_generate_artifact(artifact_type="pdf", theme="prowl", session_id="s1")
3. prowl_export_report(format="html", session_id="s1")
```

### Async Session Workflow (long deep-research, non-blocking)

```
1. prowl_start_session(query="Deep competitor analysis for stripe.com", execution_mode="deep")
                                          → {session_id, status:"running"}
2. prowl_session_status(session_id="...") → poll: running (progress) … → completed (report)
3. prowl_get_session(session_id="...", include_messages=true)  → durable report + history
   prowl_list_sessions()                  → see all your sessions and their status
```

### Tool Discovery Workflow (recommended)

```
1. prowl_search_tools(query="...")        → find tools by intent (or prowl_list_tools to browse)
2. prowl_tool_info(tool_name="...")       → check input schema + estimated cost
3. prowl_call_tool(tool_name="...", params={...})  → invoke with wallet debit
```

### Multi-Query Research Session

```
1. prowl_analyze(query="SEO audit for example.com", session_id="seo-job")
2. prowl_generate_artifact(artifact_type="infographic", session_id="seo-job")
3. prowl_reset_session(session_id="seo-job")
4. prowl_analyze(query="Ad creative research for example.com", session_id="ads-job")
5. prowl_export_report(format="markdown", session_id="ads-job")
```

---

## Error Handling

All tools return structured error messages prefixed with `"Error:"`. Common patterns:

| Error | Cause | Resolution |
|-------|-------|------------|
| `Error: Report limit reached for your account` | Legacy web chat quota (not MCP wallet) | Use MCP wallet / subscribe / top up |
| `Error: API key is not allowed to call tools in category` | Scoped key category restriction | Use a key with broader scopes or different tool |
| `Error: API key spend limit reached` | Per-key daily/total cap hit | Top up wallet or create key with higher limit |
| `Error: No report available` | Artifact/export called before analysis on this `session_id` | Run `prowl_analyze` with the same `session_id` first |
| `Error: Tool 'xyz' not found` | Invalid tool name | Use `prowl_list_tools` to find the correct name |
| `Error: Analysis failed — <ErrorType>: <message>` | Upstream API, network, or planner error | Retry the query; if it persists, call `prowl_reset_session` |

Transport-level errors: HTTP **401** (invalid/revoked key), **403** (IP not allowlisted), JSON-RPC **-32603** (internal).

---

## Session Behavior

The server keeps a **per-client `ProwlAPI` cache** keyed in this order:

1. Explicit `session_id` argument passed to the tool.
2. `_meta.client_id` populated by the MCP SDK from the calling client.
3. HTTP `mcp-session-id` or `x-session-id` request header.
4. The literal `__default__` (last-resort fallback for stdio transports / tests).

Each cached entry has its own `OrchestratorAgent`, last report, conversation history, and circuit-breaker state. Two callers with different `session_id`s do **not** share memory — Cursor window A and Cursor window B can run independent analyses concurrently.

Idle entries are swept after `PROWL_MCP_SESSION_TTL_SEC` seconds (default 7200 = 2h); the cache is bounded by `PROWL_MCP_MAX_SESSIONS` (default 64) with LRU eviction. `prowl_reset_session` evicts the entry immediately.

---

## Tool Categories

The 447 underlying API tools are organised into:

- **dataforseo** (~149) — SERP, keyword research, backlinks, domain analytics, On-Page, Content Analysis, Business Data
- **searchapi** (~100) — Specialised search engines: Google Ads Transparency, Maps, News, Trends, Scholar, YouTube, LinkedIn, Yandex, Baidu, Naver, Bing, Yahoo, DuckDuckGo, Walmart, Amazon, eBay, Etsy
- **serpapi_serp** (~57) — SerpAPI integrations: Google Lens, Reviews, Discover, Finance, Local, Direct Answers; Facebook profiles, X/Twitter timelines, Reddit, Pinterest, Instagram
- **seo** (~55) — SpyFu domain stats, PPC/SEO competitors, KOMBAT, kw rankings; Majestic TrustFlow/CitationFlow backlinks, ref domains, anchor text; Firecrawl scrape; SEO/PPC keyword research
- **web** (~19) — Web scraping, screenshots, niche-media/channel discovery, domain analysis, page extraction
- **ads** (~12) — Ad creative research, spend analysis, campaign data (Meta, Google, TikTok, LinkedIn)
- **ai** (~10) — AI-powered content analysis, summarisation, perplexity_chat
- **utility** (~6) — URL parsing, domain extraction, data formatting, validation

Use `prowl_list_tools(category="dataforseo")` to explore tools in a specific category. The exact tool count and category breakdown is dynamic and available live via `/mcp/health` and the `prowl://tools` resource.

---

## Rate Limits & Billing

- **`prowl_call_tool`** — wallet-gated; each invocation debits USD balance and is logged with `api_key_id` when using a scoped key
- **`prowl_analyze`** — wallet pre-flight gate + aggregate post-query debit; long runs emit heartbeat every ~15s (SSE keep-alive)
- Subscription tiers grant **USD credit bundles** per billing period (Exploit $60, Blackops $120, Syndicate $240); unused subscription balance burns at period end. `deep` execution_mode requires an active Exploit+ subscription; `max` requires Blackops+. A key without the required tier is auto-downgraded to `basic`, never rejected outright.
- Extra credits from top-up packs never expire
- The MCP API key validation cache trims to 256 entries on insertion (LRU when full)

---

## Agent REST API (JWT)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/wallet` | Dual-wallet balance |
| `GET` | `/api/v1/wallet/invocations` | Recent tool calls |
| `GET` | `/api/v1/usage/tools` | Alias for recent invocations |
| `GET` | `/api/v1/usage/summary` | Spend by day/category |
| `GET` | `/api/v1/tools/pricing` | Public catalog (`estimated_billed_usd`) |
| `POST` | `/api/v1/keys` | Create scoped MCP API key |
| `GET` | `/api/v1/keys` | List keys (v1 alias) |
| `DELETE` | `/api/v1/keys/{id}` | Revoke key (v1 alias) |
| `GET` | `/api/mcp/keys` | List keys |
| `DELETE` | `/api/mcp/keys/{id}` | Revoke key |

---

## Health Check

```bash
curl https://<your-prowl-host>/mcp/health
```

Returns JSON like:

```json
{
  "status": "ok",
  "server": "prowl_mcp",
  "version": "1.6.0",
  "transport": "streamable-http",
  "endpoint": "/mcp",
  "mcp_tool_count": 21,
  "mcp_tools": ["prowl_analyze", "prowl_list_tools", "prowl_search_tools", "prowl_start_session", "..."],
  "api_tool_count": 447,
  "cached_sessions": 0,
  "api_status": "lazy_pending",
  "tool_count": 21
}
```

| Field | Meaning |
|-------|---------|
| `status` | `"ok"` when MCP tools are registered, `"degraded"` otherwise |
| `mcp_tool_count` | Number of MCP tools (the `prowl_*` wrappers) |
| `api_tool_count` | Underlying provider tool count delegated to by the wrappers |
| `cached_sessions` | Current per-client `ProwlAPI` instances in the cache |
| `api_status` | `"lazy_pending"` (no session yet), `"ready"` (at least one warmed), or `"error: <details>"` |
