Skip to main content

Unofficial, non-affiliated semantic voice/review harvest tools over the Mindcase API (shared core + MCP server and CLI surfaces)

Project description

ca-mindcase-mcp — semantic Mindcase harvest tools

ca-mindcase-mcp is an unofficial, non-affiliated third-party client for the Mindcase 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:

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 fetches and runs the server on demand:

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

With pip.

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:

{
  "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

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.

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ca_mindcase_mcp-1.0.0.tar.gz (39.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ca_mindcase_mcp-1.0.0-py3-none-any.whl (42.1 kB view details)

Uploaded Python 3

File details

Details for the file ca_mindcase_mcp-1.0.0.tar.gz.

File metadata

  • Download URL: ca_mindcase_mcp-1.0.0.tar.gz
  • Upload date:
  • Size: 39.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for ca_mindcase_mcp-1.0.0.tar.gz
Algorithm Hash digest
SHA256 8cbd6e3fda3007df974a6d48b66234849e6e38f7e475f99437f60abe72c015af
MD5 ee93f2525bc10fdfdcc6422aeda6f2eb
BLAKE2b-256 139911591e4a0648eb18e3b273ffc8477038dfe6549bf6a0bd170ff40955c426

See more details on using hashes here.

File details

Details for the file ca_mindcase_mcp-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for ca_mindcase_mcp-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dd75e8ada9ceca9fc6605683dd0f5be749c0bd10dece68bd408ffeff31a827ba
MD5 68b0e4040bc60e6c9829e52a7df6f97c
BLAKE2b-256 579b6f9280fabb6b73c5c5cb0beb4a1ed43c663bc432a060fed93d73be26b788

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page