Metadata-Version: 2.4
Name: ca-mindcase-mcp
Version: 1.0.0
Summary: Unofficial, non-affiliated semantic voice/review harvest tools over the Mindcase API (shared core + MCP server and CLI surfaces)
Author: ContextArchitect
License: MIT
Project-URL: Homepage, https://pypi.org/project/ca-mindcase-mcp/
Project-URL: Issues, https://pypi.org/project/ca-mindcase-mcp/
Keywords: mcp,model-context-protocol,claude,harvest,reviews,voice-of-customer,amazon,reddit
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Environment :: Console
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests<3,>=2.31
Requires-Dist: mcp>=1.2.0
Provides-Extra: test
Requires-Dist: pytest>=8.0; extra == "test"
Dynamic: license-file

# ca-mindcase-mcp — semantic Mindcase harvest tools

`ca-mindcase-mcp` is an **unofficial, non-affiliated** third-party client for the
[Mindcase](https://mindcase.co) job API, built as one shared core with two thin
surfaces:

- a **stdio MCP server** for Claude Desktop, and
- a **CLI** for server and batch use.

> **Not affiliated with Mindcase.** This is an independent tool that talks to the
> public Mindcase API. It is not produced or endorsed by Mindcase, and it is a
> different package from Mindcase's own official `mindcase-mcp` server on PyPI.
> The distribution name is `ca-mindcase-mcp`; the import package is `mindcase`.

The tools are **semantic, not passthrough**. Every provider quirk real-world
harvest testing surfaced, and every mechanical screen this project committed to,
lives inside the tools, so a consuming agent cannot skip them. A raw API
passthrough would push that knowledge back into prompt prose and recreate the
failure class it exposed (enforcement over prose).

This tool covers **voice and review harvest only**; ad intelligence is out of
scope by design.

## Architecture

```
mindcase/
  _client.py      raw HTTP client (INSIDE-BOUNDARY, underscore-private)
  screens.py      pure screens (contamination, reddit thread rank, youtube sorts)
  tools.py        semantic tools = the public import API (MindcaseTools)
  mcp_server.py   stdio MCP surface (imports the official mcp SDK)
  cli.py          CLI surface + smoke subcommand
```

The **public import API is the tools layer** (`mindcase.tools`, re-exported from
`mindcase`). A downstream orchestrator imports it and inherits the
mechanical screens by construction:

```python
from mindcase import MindcaseTools
tools = MindcaseTools()
result = tools.amazon_reviews_screened("B0XXXXXXXX")
```

The raw HTTP client lives in the underscore-private `mindcase._client` and has
no MCP dependency, but it is not part of the public contract.

## Threat model: the enforcement boundary (a BOUNDED claim)

The mechanical screens are enforced through every shipped surface -- the MCP
tools, the CLI subcommands, and the documented public import API
(`mindcase.tools`). The enforcement target is any consumer following the
documentation, including LLM agents. Deliberate access to underscore-private
internals, or direct HTTP with the API key, is outside this contract and is not
prevented by any Python package.

**What is claimed, precisely.** This package enforces:

- **One ingestibility decision.** Whether a harvest may be ingested derives from a
  single `CompletenessState` computed at one client chokepoint (`derive_completeness`);
  every surface (a tool's primary rows, Amazon `ingest_allowed`, the CLI exit code)
  keys on that state, not on a raw provider status.
- **Two-signal wallet detection.** Wallet exhaustion is detected by (a) marker
  extraction over an anchored reason-field set (`error`/`message`/`notice`), read
  from BOTH the `/results` payload and the job resource on every terminal status,
  and (b) a balance reconciliation after every harvest, calibrated for the known
  dedicated-tool agents and **fail-closed (audit-only) for any uncalibrated agent**
  reached via `run_agent`.
- **The screened-Amazon partition.** `ingest_rows` is the only top-level
  row-carrying field; the full partition and any partial rows live in `audit`.

**What is NOT claimed.** The silent-ingest class (a wallet-degraded or non-complete
harvest presenting as ingestible) is **not claimed to be provably closed.** Its
review arc is the evidence: three successive closure architectures -- quantified
status gates, then centralized state derivation, then centralized marker
extraction plus a balance backstop -- each shipped, and each was found by a cold
adversarial reviewer to be re-enterable through a deeper channel (a status the
gate didn't enumerate; a reason field the completed path didn't read; then a
marker *source*, a control-flow branch around the gate, and an uncalibrated agent
rate). The current fixes close the three named channels **conservative-only** (a
verified meta-test asserts they only ever move a harvest toward *withheld*), but
we do not assert no further channel exists.

**Why shipping is still safe.** Every residual vector fails conservative: the raw
provider response is cached to disk as ground truth, the full partition is visible
in `audit`, and any harvest is re-runnable. So the residual exposure is *a degraded
harvest appearing ingestible until audit inspection* -- never silent data loss, and
never an unrecoverable state.

Concretely, within the boundary: `run_agent` refuses every provider group that
has a dedicated tool (so no raw path, including a drifted slug like
`amazon/reviews-v2`, reaches a guarded agent); `amazon_reviews_screened` exposes
`ingest_rows` as the ONLY top-level row-carrying field (empty unless the harvest
is COMPLETE AND the pool screened CLEAN) while the full partition -- admitted /
quarantined rows with reasons, counts, metrics, and any wallet-partial rows --
lives inside a single `audit` container that is inspection data, never ingest;
every non-screened harvest tool likewise carries its primary rows only when
COMPLETE and moves a partial into `audit.partial_rows`; a harvest whose raw
response cannot be persisted fails rather than silently returning trimmed rows
only. A downstream orchestrator imports `mindcase.tools` and inherits all of this.

**Ingestibility is a single derived state, not a status check.** Whether a
harvest may be ingested is decided in exactly one place -- `derive_completeness`
at the client chokepoint -- which folds the terminal status, the data shape, AND
the wallet signal into one `CompletenessState`
(`COMPLETE` / `PARTIAL_WALLET` / `FAILED` / `INDETERMINATE`). Every downstream
decision (a tool's primary rows, Amazon `ingest_allowed`, the CLI exit code) keys
on that state; nothing re-reads the raw provider status to judge completeness.
`COMPLETE` is the only ingestible state, and the derivation is **fail-closed**:
any (status, wallet, data) combination not explicitly mapped -- a wallet signal
under *any* status including `completed`, a novel status a provider might emit --
derives a non-ingestible state, so an unmapped shape defaults to withheld rather
than ingestible (within the bounded claim above). This is the decomposition that
closes the wallet-under-completed regression a distributed status check kept
missing: because the status, wallet, and shape axes are orthogonal, the *decision*
is centralized rather than the gate being asked to re-enumerate every axis.

**Wallet detection is marker-based AND balance-reconciled.** The wallet signal
fed to the derivation is the OR of two independent detectors, and the `audit`
container records which fired (`wallet_signals`: `marker`, `balance`, or both)
plus the post-harvest `balance_after`:

- *Marker* -- `_extract_wallet_marker` reads an anchored reason-field set
  (`error`, `message`, `notice`), merged from BOTH the `/results` payload and the
  job resource `GET /jobs/{id}`, on every terminal status (completed and failed
  alike). It is the only code that decides wallet exhaustion from provider prose,
  and a guard test pins both the field set and that exclusivity, so a marker in one
  of those fields on either source is seen by every path. A marker the provider
  places in a field *outside* the anchored set would be missed by this detector --
  the balance backstop is the net for that case, and a new reason field must be
  added to the set deliberately (which the guard test forces).
- *Balance backstop* -- after every terminal harvest, `GET /balance` is read once;
  if `usd_remaining` cannot cover one more row at the agent's per-row rate, the
  harvest is forced to a non-ingestible state **even with zero markers present**.
  This signal does not depend on provider wording or field placement -- it measures
  the phenomenon (an exhausted wallet) directly rather than parsing prose about it.
  It **fails closed** twice: a failing/unreadable `/balance` withholds rather than
  admits (so a broken balance endpoint does not wave a harvest through), and an
  agent with no calibrated per-row rate (any `run_agent` escape-hatch agent) is
  audit-only regardless of balance. False positives are conservative by
  construction -- a full harvest is withheld to `audit`, visible and re-runnable,
  not passed downstream.

### The class-closure gates (machine-checked invariants)

Two failure classes recurred across the review of this package (a bypass class:
a shipped surface reaching a guarded provider raw; and a silent-ingest class: a
non-complete or flagged harvest surfacing as ingestible). Each is closed by a
mechanical invariant that **quantifies over its class dimension** -- every
module, every field, every harvest-tool x status x wallet combination -- and, for
ingestibility specifically, by **centralizing the decision** into one fail-closed
derivation rather than asking a gate to re-enumerate orthogonal axes (a lesson
paid for when a `completed`-with-wallet-marker shape slipped a status-only
quantified gate). Adding a new module, public name, or harvest tool fails these
gates until it is deliberately classified:

- **Bypass gate** (`tests/test_boundary.py`): enumerates *every* `.py` module in
  the package and asserts each is classified (in-boundary vs console surface), so
  a new file fails until placed. Over that full set it asserts (via AST) that the
  ONLY module importing the raw client type / `run` is the enforcement layer
  (`tools.py`), and that no console surface calls a raw `.run` (the FastMCP
  transport's `mcp.run()` excepted). It also asserts the package/tools/submodule
  public attributes and CLI subcommands equal explicit allowlists and hold no raw
  client.
- **Derivation-table gate** (`tests/test_completeness.py`): pins the mapped
  `CompletenessState` for *every* cell of the full (status x wallet-signal x
  data-shape) cross product against a hand-authored oracle, including that
  `completed` + wallet + rows derives `PARTIAL_WALLET` (the falsification finding)
  and that every unmapped combination fails closed to `INDETERMINATE`. Because
  ingestibility is this one derivation, closing the wallet dimension here closes
  it for every consumer at once.
- **Extraction-anchor gate** (`tests/test_completeness.py`): pins the wallet
  reason-field enumeration (`error`/`message`/`notice`), asserts a marker is
  detected in each and ignored outside the set, and asserts (via AST over the
  client source) that the wallet predicate is called ONLY inside the extractor --
  so no code path can decide wallet exhaustion from a field the extractor doesn't
  read. The balance backstop then catches exhaustion even when no marker exists at
  all: `tests/test_status_matrix.py` drives a `completed` harvest with a healthy
  status, no marker, and a balance below one row's cost and asserts it derives
  `PARTIAL_WALLET` and is withheld, with `audit.wallet_signals` recording which
  detector fired.
- **Ingest gate** (`tests/test_status_matrix.py`): a property test that, for
  every screened / non-screened harvest tool and every non-ingestible condition
  (FLAGGED, wallet-partial under a *failed* or a *completed* status, unknown
  status), recursively walks the *entire* returned object and asserts zero
  row-shaped dicts are reachable outside `audit`, while `audit` preserves exactly
  the provider's rows. On `COMPLETE` + CLEAN it asserts
  `ingest_rows == audit.admitted_rows`.
- **Status-matrix gate** (`tests/test_status_matrix.py` + `test_cli_exit.py`):
  parametrizes over the `HARVEST_TOOLS` registry x terminal job status x
  wallet-marker, asserting which combinations may populate the primary rows field
  (only a `COMPLETE` harvest; for Amazon, only `COMPLETE` and CLEAN) and that real
  `cli.main` exit codes agree (0 only for a full ingestible result, including
  non-zero for a `completed`-with-wallet-marker harvest). A new harvest tool must
  join the registry and matrix or the suite fails.

The enforcement claim is bounded to what these gates check: a consumer following
the documentation (including an LLM agent reading the public surface) does not
reach a raw harvest, and does not ingest a non-CLEAN / non-COMPLETE pool by
reading the obvious field, for the channels these gates cover. As stated in the
bounded claim above, the silent-ingest class is not asserted provably closed;
the residual exposure fails conservative (cached ground truth, visible `audit`,
re-runnable). Deliberate use of underscore-private internals is out of scope.

Only `mcp_server.py` imports the MCP SDK. Both surfaces call the same
`MindcaseTools` methods, so behavior is identical whether a call arrives from
Claude Desktop or the command line.

Every tool returns **trimmed** rows for context economy (text, author, date,
permalink, engagement, plus depth/flags where relevant). The full raw response
is written to a cache dir keyed by job id, so ground truth survives without
transiting a model's context. The cache dir is `MINDCASE_CACHE_DIR` if set,
otherwise a user-writable location on both operating systems.

## WARNING: the docs are unreliable; the live schema is the only truth

Mindcase's own documentation diverges from runtime behavior. Testing
established that the live `GET /agents/{group}/{slug}` schema is the **only**
trustworthy schema source. When an agent behaves unexpectedly, fetch its live
schema (the `run_agent` tool does this automatically for any agent without a
dedicated tool). Do not trust the published docs or curl examples.

`run_agent` is an escape hatch for agents **without** a dedicated tool only. An
agent that has a dedicated tool (any reddit/*, youtube/*, amazon/*, instagram/*,
twitter/*) is refused with the correct tool named, so the hatch cannot be used
to bypass a tool's quirk shaping, input guards, or the mandatory Amazon
contamination screen.

## The quirks (all encoded in the tools)

Each quirk below was established through real-world harvest testing and is
encoded in the shipped code (the "Where encoded" column names the module or
function).

| # | Quirk | Where encoded |
|---|---|---|
| 1 | Base URL is `https://api.mindcase.co/api/v1`; the documented `/v1/data` pattern 404s. | `_client.BASE_URL` |
| 2 | Run body MUST wrap params under a `params` key; a flat body is accepted then silently fails at the worker. | `_client.run` |
| 3 | The verbatim failure reason is on `GET /jobs/{id}`, not `GET /jobs/{id}/results`. | `_client._verbatim_error` |
| 4 | `amazon/products.keywords` is a string, not an array; run one job per keyword. | `tools.amazon_search_products` |
| 5 | `amazon/reviews.urls` is a single ASIN/URL, not an array; one job per product. | `tools.amazon_reviews_screened` |
| 6 | Amazon prefix dedup passes while templated 4-5 star bursts sail through; a contamination screen is required. | `screens.amazon_contamination_screen` |
| 7 | `youtube/search` and `youtube/video-transcript` agents do not exist; use `youtube/videos`; no transcript is available anywhere. | `tools.youtube_search_videos` |
| 8 | `youtube/videos` has no sort param (relevance only); reproduce popular order post-hoc. | `screens.sort_videos_by_engagement` |
| 9 | `youtube/comments` has no order or creator-exclude param, but rows carry `authorIsOwner`; sort and filter post-hoc. | `screens.sort_comments_by_likes`, `filter_creator_comments` |
| 10 | `youtube/comments` effective ceiling is ~100 rows per video regardless of maxResults. | `tools.youtube_video_comments` (documented in output) |
| 11 | `twitter/tweets` keyword search runs via a multi-token string in `inputs`; a single token is read as a handle, so force it with ` lang:en`; never send `query`. | `tools._twitter_inputs` |
| 12 | `instagram/posts` hashtag mode is inconsistent; fall back to query mode on zero rows. | `tools.instagram_posts` |
| 13 | `reddit/posts` `timeRange` only applies under `sortBy: top`. | `tools.reddit_search_posts` (documented) |
| 14 | Selecting highest-comment Reddit threads lands on price/megathread traffic; rank by experiential likelihood first. | `screens.reddit_thread_screen` |
| 15 | Facebook organic brand-page harvest is excluded (VOICE 0.0% on 4 rows; handle resolution unreliable). | no Facebook tool is exposed |

## The screens

- **`amazon_contamination_screen`**: flags templated / incentivized review
  bursts. The verdict (`CLEAN` / `FLAGGED`) is driven by two signals that
  cleanly separate organic from templated voice in the ground-truth sample data:
  burst concentration (share of 4-5 star rows in one calendar month) and
  template near-duplication (overlap-coefficient clusters among high-star
  texts). Off-topic token overlap and image-attachment ratio are reported as
  corroborating context but never flip the verdict alone, because off-topic
  overlap over-flags terse and non-English organic reviews. Rows are partitioned
  into `admitted_rows` and `quarantined_rows` (admitted + quarantined always
  equals the total, so nothing is silently dropped). Quarantine is driven only
  by the reliable triggers, and only when they fire (near-duplicate cluster,
  burst-month membership); each quarantined row carries its reasons. off_topic
  and has_image are per-row annotations surfaced in `_flags` and the counts, but
  they never quarantine a row, so a CLEAN pool loses no organic voice. A caller
  wanting zero cross-product contamination can drop admitted rows whose
  `_flags["off_topic"]` is true. A `FLAGGED` pool is excluded from avatar
  weighting with its reasons logged. The partition contract is owned by a
  `ScreenResult` dataclass whose `counts` are derived (admitted + quarantined ==
  total by construction), so no surface can rebuild or truncate them. Burst may
  drive quarantine only when the high-star pool has at least `MIN_BURST_SUPPORT`
  rows (default 8); a sparse pool trivially reaches full concentration, so below
  that support burst demotes to a pool-level note rather than quarantining.
- **`reddit_thread_screen`**: ranks candidate threads by experiential
  likelihood before comment pulls, so results-journey and side-effects threads
  outrank deal / price / megathread traffic. Comment count is only a small
  volume tiebreaker.
- **YouTube post-hoc helpers**: reproduce the sort and creator-exclude controls
  the API lacks.

## Install

There are two ways to run it.

**With `uvx` (recommended — no manual install).**
[uv](https://docs.astral.sh/uv/) fetches and runs the server on demand:

```bash
uvx ca-mindcase-mcp        # starts the stdio MCP server
```

**With `pip`.**

```bash
pip install ca-mindcase-mcp
```

This installs `requests` and the official `mcp` Python SDK and creates two
console scripts: `ca-mindcase` (CLI) and `ca-mindcase-mcp` (MCP server). The
import package is `mindcase` (`from mindcase import MindcaseTools`).

### API key

The API key is resolved in this order (works on Windows, macOS, and Linux):

1. `MINDCASE_API_KEY` environment variable.
2. `~/.secrets/mindcase.env` (a `MINDCASE_API_KEY=...` line), if present.

The key is never logged, echoed, or written to the cache, and it must be
`mk_live_`-prefixed. **Never commit it.**

## Register with Claude Desktop

Add the server to `claude_desktop_config.json`
(`%APPDATA%\Claude\claude_desktop_config.json` on Windows,
`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS). The
one-block `uvx` config needs no absolute paths — put the key in the `env` block:

```json
{
  "mcpServers": {
    "mindcase": {
      "command": "uvx",
      "args": ["ca-mindcase-mcp"],
      "env": {
        "MINDCASE_API_KEY": "mk_live_your_key_here"
      }
    }
  }
}
```

This requires `uv` on your `PATH` (install it from
<https://docs.astral.sh/uv/>). On Windows, if `uvx` is not found, use its
absolute path as `command` (e.g.
`C:\\Users\\<you>\\.local\\bin\\uvx.exe`). If you installed with `pip` instead,
set `"command": "ca-mindcase-mcp"` and drop `args`. Restart Claude Desktop after
editing the config. The server speaks stdio only; it opens no network listener.

## CLI usage

```bash
ca-mindcase balance
ca-mindcase reddit-posts --subreddit <subreddit> --keyword "product results"
ca-mindcase reddit-comments --post-url "https://www.reddit.com/r/<sub>/comments/xxxx/slug/"
ca-mindcase youtube-videos --query "product review"
ca-mindcase youtube-comments --video-url "https://www.youtube.com/watch?v=XXXX"
ca-mindcase amazon-products --keywords "product category" "alternative"
ca-mindcase amazon-reviews --asin B0XXXXXXXX
ca-mindcase instagram-posts --term <hashtag>
ca-mindcase twitter-search --query "product name"
ca-mindcase twitter-replies --tweet-url "https://x.com/handle/status/12345"
ca-mindcase run-agent --agent google/search --params '{"query":"..."}'
```

Add `--out result.json` to write JSON to a file instead of stdout, and
`--cache-dir PATH` to override where raw responses are cached.

## Smoke test (post-install verification)

`smoke` runs one cheap live call per platform group (a balance read and a single
Reddit post). It is the only code path that touches the live API outside
production use.

```bash
ca-mindcase smoke
```

It prints a per-check report and exits non-zero if any check fails.

## Tests

The package is covered by a fixture-based suite (no live API calls) whose
boundary and completeness invariants are described in the threat model above: a
bypass gate, a derivation-table gate, and ingest / status-matrix gates that
quantify over every module, every public name, and every harvest-tool × status ×
wallet combination. The suite and its fixtures ship with the project source, not
in the installable wheel.
