Undermind Search (Stage 1)
Takes the Undermind brief that Stage 0 extracts and returns the papers
Undermind's deep search finds, enriched and saved as <stem>.json + <stem>.bib
for dedup and screening.
The stage is agent-driven end to end. There is no subprocess driver: the host
agent calls the Undermind MCP tools, saves what they return, and one script
normalizes it.
- The agent —
launch_deep_search → poll inspect_deep_searches → page the
results.
scripts/undermind_harvest.py — copies the paged tool results out of the
session transcript byte-for-byte into OUT/undermind_page_<k>.txt (Claude Code
~/.claude/projects/<slug>/<session>.jsonl, following its <persisted-output>
pointers; Codex ~/.codex/sessions/**/rollout-*.jsonl).
scripts/undermind_ingest.py — parses those files (or any RIS/BibTeX
export), fills missing DOIs (Crossref) and abstracts/journals (OpenAlex), and
writes the pipeline JSON. Importable, and runnable standalone.
Prerequisite: the Undermind MCP server
Undermind's official MCP endpoint is https://mcp.undermind.ai/mcp (OAuth,
your own Undermind account).
Call get_orientation once per conversation before the other tools.
The flow
brief ─► create_workspace (or reuse) ─► launch_deep_search(goal=brief)
─► poll inspect_deep_searches(status_only) every ~60 s (2–5 min)
─► inspect_deep_searches(papers_only, detail_level="full", limit=50, offset=0/50/…)
─► undermind_harvest.py --latest --match <name> --out-dir OUT (pages → OUT/undermind_page_<k>.txt)
─► undermind_ingest.py --input OUT/undermind_page_*.txt -o OUT/stage1_undermind.json
Step by step, with OUT the run's output directory:
list_workspaces → reuse a workspace for this manuscript, or
create_workspace(title=<document stem>). Keep the workspace_id.
launch_deep_search(workspace_id, goal=<contents of OUT/undermind_brief.txt>, name=<short label>). The tool returns immediately. Launch it first, then
start the other channels while it runs. The brief goes in verbatim: the
goal is meant to be a self-contained paragraph, which is exactly what
Stage 0 writes (no keyword lists, no journal names). The MCP search asks no
clarifying questions; the plan's undermind_clarifications field is
informational only (a holdover from the browser flow).
- Poll
inspect_deep_searches(names=[<name>], status_only=true) from a
background Monitor, not a foreground wait. A rate_limited result means
wait and retry; Undermind runs only a few deep searches concurrently.
- On completion, page the papers:
inspect_deep_searches(names=[<name>], papers_only=true, detail_level="full", limit=50, offset=0), then offset=50, 100, … until a page comes back
short. Do not retype or summarize anything.
- Harvest the pages from the transcript, then ingest:
python undermind-search/scripts/undermind_harvest.py --latest --match "<name>" --out-dir OUT
python undermind-search/scripts/undermind_ingest.py --input OUT/undermind_page_*.txt \
-o OUT/stage1_undermind.json
The harvester scans the newest transcript under the Claude Code projects dir
(CLAUDE_CONFIG_DIR or ~/.claude) and ~/.codex/sessions, keeps every
distinct tool result that contains [Key] Title (Year) blocks and whose
header (the search-name line above the paper list) contains the --match
text, and writes them as undermind_page_01.txt, _02, …; without
--match, when the transcript holds several searches, only the latest
search's pages are kept and the others are named. Pages whose
showing a-b of n ranges leave a gap raise a WARNING naming the missing
offsets.
(--transcript PATH to name the file; --min-records N to drop short
fragments). Falling back to saving a page by hand as .txt or as the
{"result": "..."} JSON also works: the ingest reads both.
Optional add-ons before ingest, for the same workspace: search_papers with
search_type="citations" / "references" seeded on the top cite keys widens
coverage; save its output as another page file and ingest it together.
Input formats the ingest accepts
Auto-detected from content:
| Input |
Notes |
| Raw MCP tool text |
[Key] Title (Year) blocks from inspect_deep_searches, get_paper_info, or search_papers at any detail level. Journal, Date:, By …, DOI:, Link:, PDF, and the abstract paragraph are read; trailer legends are ignored. |
{"result": "…"} JSON |
The tool result saved as JSON. |
| JSON array |
[{"title", "authors" (string or list), "year", "journal", "doi", "url", "abstract"}]; only title is required. |
.ris / .bib |
The web app's Export, or any reference manager. |
Several --input files are concatenated and deduplicated by cite key, DOI, or
normalized title, so paginated dumps just go in together.
CLI
undermind_harvest.py:
| Flag |
Default |
Description |
--latest |
(default) |
Scan the most recently modified transcript under the Claude Code / Codex session dirs |
--transcript PATH |
— |
Scan this transcript instead |
--match TEXT |
— |
Keep only pages whose header (search-name line) contains this text; omitted → the latest search in the transcript |
--min-records N |
1 |
Skip pages with fewer paper blocks |
--out-dir DIR |
. |
Where undermind_page_<k>.txt is written |
--selftest |
off |
Run the harvester self-check |
undermind_ingest.py:
| Flag |
Default |
Description |
--input, -i PATH… |
— |
One or more input files (globs expanded internally; PowerShell-safe) |
-o, --output PATH |
stage1_undermind.json |
Output JSON (a .bib sibling is written) |
--sibling {bibtex,ris,none} |
bibtex |
Reference-manager copy beside the JSON |
--source TAG |
undermind |
source value stamped on each record |
--no-enrich |
off |
Skip Crossref/OpenAlex enrichment |
--selftest |
off |
Run the parser self-check |
Graceful degradation
If the MCP tools are unavailable (server not added, not authenticated, or a
Codex/Claude session without MCP), the agent prints UNDERMIND_DEFERRED, writes
[] to stage1_undermind.json, and continues; the ingest does the same when no
input file exists or nothing parses. The brief is always available in
undermind_brief.txt for a manual run in the web app (Export → RIS, then ingest
the file). The autonomous orchestrator.py never runs this stage: it has no MCP
client.
Output schema
Same as the other stages, with source: "undermind":
{"title": "...", "authors": "A, B", "year": "2024",
"doi": "https://doi.org/10.x/y", "abstract": "...", "journal": "...",
"url": "...", "source": "undermind", "verified": true,
"citations": 0, "open_access": false}
Troubleshooting
- Only
mcp__undermind__authenticate is available — the session predates
the login. Call it, approve in the browser, and the tools appear.
claude mcp list says "Needs authentication" although login worked — a
stale mcp-needs-auth-cache.json in the Claude config dir; harmless to delete.
rate_limited — Undermind caps concurrent deep searches; wait and retry.
- The harvester finds no pages — the tool results have not been flushed to
the transcript yet, or the newest transcript belongs to another session; wait
a moment and retry, or pass
--transcript with the session file. --match
is case-insensitive and is tested against the page header (the search-name
line), never against paper text.
- A page parses to zero records — the tool output format drifted. Run
--selftest, compare the saved page with the header pattern in
parse_mcp_text, and adjust; the JSON-array input is the format-independent
fallback.
1---2name: undermind-search3description: Stage 1 of the lit review pipeline: run an Undermind deep search from the natural-language brief produced by Stage 0 through the Undermind MCP server (agent-driven; no browser, no stored credentials), then parse + enrich the returned papers into the pipeline schema. Only use this skill when explicitly requested. Do NOT auto-trigger on general literature review or paper search requests.4---56# Undermind Search (Stage 1)78Takes the **Undermind brief** that Stage 0 extracts and returns the papers9Undermind's deep search finds, enriched and saved as `<stem>.json` + `<stem>.bib`10for dedup and screening.1112The stage is agent-driven end to end. There is no subprocess driver: the host13agent calls the Undermind MCP tools, saves what they return, and one script14normalizes it.1516- **The agent** — `launch_deep_search` → poll `inspect_deep_searches` → page the17 results.18- **`scripts/undermind_harvest.py`** — copies the paged tool results out of the19 session transcript byte-for-byte into `OUT/undermind_page_<k>.txt` (Claude Code20 `~/.claude/projects/<slug>/<session>.jsonl`, following its `<persisted-output>`21 pointers; Codex `~/.codex/sessions/**/rollout-*.jsonl`).22- **`scripts/undermind_ingest.py`** — parses those files (or any RIS/BibTeX23 export), fills missing DOIs (Crossref) and abstracts/journals (OpenAlex), and24 writes the pipeline JSON. Importable, and runnable standalone.2526## Prerequisite: the Undermind MCP server2728Undermind's official MCP endpoint is `https://mcp.undermind.ai/mcp` (OAuth,29your own Undermind account).3031- **Claude Code:** `claude mcp add --transport http undermind https://mcp.undermind.ai/mcp`32 then `claude mcp login undermind` in an interactive terminal. A session that33 was already open when you logged in only exposes `mcp__undermind__authenticate`;34 call it once, approve in the browser, and the full tool set swaps in.35- **Codex:** in `~/.codex/config.toml`:36 ```toml37 [mcp_servers.undermind]38 url = "https://mcp.undermind.ai/mcp"39 [mcp_servers.undermind.oauth]40 client_id = "codex"41 ```42 Codex completes the OAuth flow on first use.4344Call `get_orientation` once per conversation before the other tools.4546## The flow4748```49brief ─► create_workspace (or reuse) ─► launch_deep_search(goal=brief)50 ─► poll inspect_deep_searches(status_only) every ~60 s (2–5 min)51 ─► inspect_deep_searches(papers_only, detail_level="full", limit=50, offset=0/50/…)52 ─► undermind_harvest.py --latest --match <name> --out-dir OUT (pages → OUT/undermind_page_<k>.txt)53 ─► undermind_ingest.py --input OUT/undermind_page_*.txt -o OUT/stage1_undermind.json54```5556Step by step, with `OUT` the run's output directory:57581. `list_workspaces` → reuse a workspace for this manuscript, or59 `create_workspace(title=<document stem>)`. Keep the `workspace_id`.602. `launch_deep_search(workspace_id, goal=<contents of OUT/undermind_brief.txt>,61 name=<short label>)`. The tool returns immediately. Launch it **first**, then62 start the other channels while it runs. The brief goes in verbatim: the63 `goal` is meant to be a self-contained paragraph, which is exactly what64 Stage 0 writes (no keyword lists, no journal names). The MCP search asks no65 clarifying questions; the plan's `undermind_clarifications` field is66 informational only (a holdover from the browser flow).673. Poll `inspect_deep_searches(names=[<name>], status_only=true)` from a68 background Monitor, not a foreground wait. A `rate_limited` result means69 wait and retry; Undermind runs only a few deep searches concurrently.704. On completion, page the papers:71 `inspect_deep_searches(names=[<name>], papers_only=true, detail_level="full",72 limit=50, offset=0)`, then `offset=50`, `100`, … until a page comes back73 short. Do not retype or summarize anything.745. Harvest the pages from the transcript, then ingest:75 ```bash76 python undermind-search/scripts/undermind_harvest.py --latest --match "<name>" --out-dir OUT77 python undermind-search/scripts/undermind_ingest.py --input OUT/undermind_page_*.txt \78 -o OUT/stage1_undermind.json79 ```80 The harvester scans the newest transcript under the Claude Code projects dir81 (`CLAUDE_CONFIG_DIR` or `~/.claude`) and `~/.codex/sessions`, keeps every82 distinct tool result that contains `[Key] Title (Year)` blocks and whose83 header (the search-name line above the paper list) contains the `--match`84 text, and writes them as `undermind_page_01.txt`, `_02`, …; without85 `--match`, when the transcript holds several searches, only the latest86 search's pages are kept and the others are named. Pages whose87 `showing a-b of n` ranges leave a gap raise a WARNING naming the missing88 offsets.89 (`--transcript PATH` to name the file; `--min-records N` to drop short90 fragments). Falling back to saving a page by hand as `.txt` or as the91 `{"result": "..."}` JSON also works: the ingest reads both.9293Optional add-ons before ingest, for the same workspace: `search_papers` with94`search_type="citations"` / `"references"` seeded on the top cite keys widens95coverage; save its output as another page file and ingest it together.9697## Input formats the ingest accepts9899Auto-detected from content:100101| Input | Notes |102|-------|-------|103| Raw MCP tool text | `[Key] Title (Year)` blocks from `inspect_deep_searches`, `get_paper_info`, or `search_papers` at any detail level. Journal, `Date:`, `By …`, `DOI:`, `Link:`, `PDF`, and the abstract paragraph are read; trailer legends are ignored. |104| `{"result": "…"}` JSON | The tool result saved as JSON. |105| JSON array | `[{"title", "authors" (string or list), "year", "journal", "doi", "url", "abstract"}]`; only `title` is required. |106| `.ris` / `.bib` | The web app's Export, or any reference manager. |107108Several `--input` files are concatenated and deduplicated by cite key, DOI, or109normalized title, so paginated dumps just go in together.110111## CLI112113`undermind_harvest.py`:114115| Flag | Default | Description |116|------|---------|-------------|117| `--latest` | (default) | Scan the most recently modified transcript under the Claude Code / Codex session dirs |118| `--transcript PATH` | — | Scan this transcript instead |119| `--match TEXT` | — | Keep only pages whose header (search-name line) contains this text; omitted → the latest search in the transcript |120| `--min-records N` | `1` | Skip pages with fewer paper blocks |121| `--out-dir DIR` | `.` | Where `undermind_page_<k>.txt` is written |122| `--selftest` | off | Run the harvester self-check |123124`undermind_ingest.py`:125126| Flag | Default | Description |127|------|---------|-------------|128| `--input, -i PATH…` | — | One or more input files (globs expanded internally; PowerShell-safe) |129| `-o, --output PATH` | `stage1_undermind.json` | Output JSON (a `.bib` sibling is written) |130| `--sibling {bibtex,ris,none}` | `bibtex` | Reference-manager copy beside the JSON |131| `--source TAG` | `undermind` | `source` value stamped on each record |132| `--no-enrich` | off | Skip Crossref/OpenAlex enrichment |133| `--selftest` | off | Run the parser self-check |134135## Graceful degradation136137If the MCP tools are unavailable (server not added, not authenticated, or a138Codex/Claude session without MCP), the agent prints `UNDERMIND_DEFERRED`, writes139`[]` to `stage1_undermind.json`, and continues; the ingest does the same when no140input file exists or nothing parses. The brief is always available in141`undermind_brief.txt` for a manual run in the web app (Export → RIS, then ingest142the file). The autonomous `orchestrator.py` never runs this stage: it has no MCP143client.144145## Output schema146147Same as the other stages, with `source: "undermind"`:148149```json150{"title": "...", "authors": "A, B", "year": "2024",151 "doi": "https://doi.org/10.x/y", "abstract": "...", "journal": "...",152 "url": "...", "source": "undermind", "verified": true,153 "citations": 0, "open_access": false}154```155156## Troubleshooting157158- **Only `mcp__undermind__authenticate` is available** — the session predates159 the login. Call it, approve in the browser, and the tools appear.160- **`claude mcp list` says "Needs authentication" although login worked** — a161 stale `mcp-needs-auth-cache.json` in the Claude config dir; harmless to delete.162- **`rate_limited`** — Undermind caps concurrent deep searches; wait and retry.163- **The harvester finds no pages** — the tool results have not been flushed to164 the transcript yet, or the newest transcript belongs to another session; wait165 a moment and retry, or pass `--transcript` with the session file. `--match`166 is case-insensitive and is tested against the page header (the search-name167 line), never against paper text.168- **A page parses to zero records** — the tool output format drifted. Run169 `--selftest`, compare the saved page with the header pattern in170 `parse_mcp_text`, and adjust; the JSON-array input is the format-independent171 fallback.