omniseek
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| OMNISEEK_CONTACT_EMAIL | No | Optional but recommended: set for a faster lane with Crossref, SEC, and Unpaywall. |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
This server publishes no instructions, or was last inspected before Glama recorded them.
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": false
} |
| prompts | {
"listChanged": false
} |
| resources | {
"subscribe": false,
"listChanged": false
} |
| experimental | {} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| omniseek_sourcesA | List all sources — call this to ROUTE before searching. Fully-qualified MCP name: mcp__omniseek__omniseek_sources (server name is BOUNDED ORIENT: a bare (no-arg) call does NOT dump every source's facets. It returns the routing
VOCABULARY (available_domains / available_regions with counts) + the capabilities verb index +
The no-arg (orient) call also returns
Returns: {"count": N, "backend_count": M, "backend_breakdown": {...}, and EITHER
|
| omniseek_searchA | Search the curated sources. The default for "best/latest on X". ONE verb, three shapes. Fully-qualified MCP name: mcp__omniseek__omniseek_search (server name is DISPATCH (deterministic):
• DEFAULT (raw=False): DEDUP + RANK across sources into ONE list. Cross-source duplicates merge
(same paper from arxiv + openalex + … → one entry, the others in metadata.also_in); ordered by
a relevance+recency+engagement blend (metadata._rank) you may re-sort — each doc's named signals
map (e.g. citations / upvotes / stars, each provenance-stamped) + its date are on the doc.
CROSS-LINGUAL + SEMANTIC (default on): also runs VECTOR recall over the local perception-memory
index, so a Chinese query surfaces relevant ENGLISH docs (and vice-versa) and paraphrases match
with no shared words — fused with the lexical + live results by the SAME transparent ranker (the
eye still only retrieves + scores mechanically; you judge). ROUTING (all shapes): sources=None = all non-explicit_only, deadline-bounded — slow ones drop and are listed in _meta.timed_out. explicit_only sources (browser/CDP + twitter_x) are excluded from the broad sweep → _meta.excluded_count (the size; the full name->reason map is in omniseek_sources) + _meta.excluded_relevant (the query-AWARE subset: walled/slow sources whose facets thematically match THIS query, each with a copy-paste sources=[...] re-run hint). Name them to include their (deeper, login-walled) coverage. SHARED WEB-SEARCH BACKEND: naming any search-index venue or nowcoder draws on ONE paced backend; when it is cooling the result carries _meta.web_search_backend {active, brave:{cooling_s}, ddg:{cooling_s}, last_error}: wait cooling_s, do not retry earlier. Prefer <=2 such venues per gather. TIME + STALENESS: FIRE-THEN-COLLECT IS NOT JUST FOR WALLED SOURCES: it is the BEST way to run an ORDINARY broad search, and it is both faster AND more complete than waiting. A plain broad call blocks ~16s for ~223 docs. Instead FIRE omniseek_search(query, wait_s=3) -> first results in ~3s, then COLLECT omniseek_search(query, staleness="cache_only") ~20s later -> ~296 docs. Measured over 3 quiesced reps (2026-07-25): 5x faster to first result AND ~33% MORE docs than the blocking call. It wins on both axes because sources the deadline would have cut keep running detached and warm the cache with no deadline over them, so the collect reads MORE than the 16s window could ever hold. The cache plateaus by ~+20s (no gain at +35s), so collecting later buys nothing. Same limit both calls. vs the open web: searches only OmniSeek's curated sources; pair with WebSearch for open-web breadth (orthogonal, often use BOTH). PER-DOC METADATA is LEAN by default: internal ranking/recall telemetry (recall_rrf / freshness_class /
relevance_hook / merge_basis / ...) is omitted (~25% of a ranked doc); the SIGNAL stays (_rank, also_in,
seen_before / first_seen_at, source-native signals). Returns (default): {"query", "count", "documents": [...], "_meta": {..., excluded_relevant, "deduped": {in, out}}, routing_hint? (TOP-LEVEL: the strongest excluded vertical/walled matches for THIS query, overlap-ranked, each with its param_hint — name one for its authoritative coverage; present only on a broad sweep with a strong match)}. (raw one-source drill): {"source", "query", "count", "documents": [...], "_meta": {"diagnostic": {...}} # only when empty/errored}. (raw buckets): {"query", "results": {source: [...]}, "total_count", "_meta": {searched, empty, timed_out, errored, excluded_count, excluded_relevant, truncated, progressive:{fast,slow,timed_out}, ...}}. An unknown staleness value is treated as cached_ok and a "note" is added to the return. |
| omniseek_field_skeletonA | Map a research field's shape — use WHEN you need its citation neighborhood (foundational core by citations vs frontier by date) to cluster yourself, from a topic or seed papers. Fully-qualified MCP name: mcp__omniseek__omniseek_field_skeleton (server name is A thin graph primitive, NO judgment: given YOU are the cartographer — do ALL the intelligence over this raw data:
• SEEDS: if the auto-seeds are off (e.g. a generic survey crept in), re-call with
Returns: {seeds, n_nodes, n_edges, edges:[[citer_id, cited_id]], nodes:[{id, title, year, date, cited_by, in_degree, concept, first_author, doi, url, is_seed, seed_ref_freq, seed_cite_freq}]} (sorted by in_degree as a default view only; seed_ref_freq/seed_cite_freq on non-seed nodes). _meta carries seed_titles + seed_note (auto-seed drift check), degraded, deadline_hit, partial. |
| omniseek_paper_recommendA | Use WHEN you have a paper and want more like it — semantically-similar papers (SPECTER embeddings) that keyword search and the citation graph miss, including very recent work. Uses Semantic Scholar's recommendation model (SPECTER embeddings + co-citation), so it surfaces conceptually-related work that omniseek_search (keyword) and omniseek_field_skeleton (citations) miss — including very recent papers the citation graph has not caught up to. Fully-qualified MCP name: mcp__omniseek__omniseek_paper_recommend (server name is Pass seed paper ids (arXiv ids / DOIs / S2 ids — a paper you found via omniseek_search or omniseek_field_skeleton). One seed = "more like this"; several = recommendations from that set. This is OmniSeek's "semantic search": it routes to S2's existing embeddings rather than building any. For an openalex omniseek_search result pass metadata.paper_id (or metadata.doi), NOT source_id — the OpenAlex W-id is a graph id the paper tools do not accept. Returns: {"seeds", "n", "papers": [{id, title, year, date, cited_by, first_author, doi, url}]} (ordered by S2 relevance; YOU re-judge). Citation neighborhood instead → omniseek_field_skeleton; keyword search → omniseek_search. |
| omniseek_paper_enrichA | Use WHEN you need ONE paper's open-access full-text PDF, retraction / integrity status, or citation count — signals omniseek_search / field_skeleton do NOT give cleanly. Keyless, mechanical: YOU decide when + on which papers. Fully-qualified MCP name: mcp__omniseek__omniseek_paper_enrich (server name is Pass DOIs and/or arXiv ids (e.g. "2306.08543", "10.1145/3292500.3330701"; use a node's
Returns: {"results": [{id, kind, doi, is_oa, pdf_url, oa_url, citation_count, integrity:{retracted, notices}}, ...]} (or {id, error} for an unrecognized id). |
| omniseek_resolve_identityA | Resolve a PERSON's name to candidate author ids — the shared front door for EVERY relationship layer (you must know WHICH person before you can map their connections). Fully-qualified MCP name: mcp__omniseek__omniseek_resolve_identity (server name is OmniSeek's other tools keyword-search PAPERS; this resolves an AUTHOR. It NEVER silently
picks — it returns ranked CANDIDATES so YOU disambiguate (the homonym trap: "Zhennan Shen"
is three different people in OpenAlex).
Use the returned id with omniseek_coauthors.
Returns: {query, source, candidates:[{id, source, name, works_count, cited_by,
institution, via_paper?}], ambiguous, note, likely_same_person?:[{source, ids, name,
merge_token, note}], degraded?:{openalex}}. |
| omniseek_coauthorsA | Use WHEN you want WHO a researcher collaborates with — advisor + closest collaborators by joint-paper count, or how a paper's author group is connected (WebSearch cannot build this). One LAYER, not the whole graph — co-authorship is one edge type; YOU overlay the others (advising, institution cohort, citation, code, social) and judge what each connection MEANS. Fully-qualified MCP name: mcp__omniseek__omniseek_coauthors (server name is Pass author NAMES and/or ids (from omniseek_resolve_identity). A brand-new arXiv paper is not in the graph yet, so this reconstructs from each author's PRIOR work: • N=1 -> that author's frequency-ranked coauthor neighborhood. The advisor + closest collaborators surface by joint-paper count (e.g. Yi R. Fung -> Heng Ji ~51x = her PhD advisor, no advisor field needed — YOU read that signal). • N>1 (e.g. a paper's whole author list) -> additionally the PAIRWISE prior joint-work edges among them (with the actual joint paper titles as evidence) + BRIDGE collaborators (people who co-authored with >=2 of the inputs but are not in the set). This is the "how is this author group actually connected" reconstruction. Each input may be a NAME, an id, or '+'-joined ids ("id1+id2") for ONE person SPLIT
across ids — their works are MERGED (OpenAlex/S2 routinely split a junior's recent papers;
merging recovers the complete network). Each becomes a node with
Returns: {source, n_authors, nodes:[{query, resolved, ambiguous, alternatives, works_seen,
top_coauthors:[{id,name,joint}], degraded?}], edges:[{a,b,joint_count,papers:[{title,year,id}]}],
bridges:[{id,name,shared_by,total_joint}], cooc:[{a,b,n}], degraded?}. (top_coauthors/bridges
carry a representative |
| omniseek_institution_cohortA | Use WHEN you need the people-ROSTER of a lab / department / university (who actively publishes there, optionally scoped to a field) — the "who's at this lab" question, orthogonal to co-authorship ("same lab, never co-authored" is still a tie, and the people-roster of a target lab is exactly the SG/Canada cohort question). Fully-qualified MCP name: mcp__omniseek__omniseek_institution_cohort (server name is Resolve the institution (+ optional FIELD) -> roster ranked by their output AT that
institution IN that field (so juniors with a few papers surface, not just senior profs).
IMPORTANT: without Returns: {institution:{id,name}, filters, n, people:[{id, name, works_at_institution_in_field}], note}. |
| omniseek_readA | Read text from any URL OR document FILE — OmniSeek's single "read this deep" verb. AUTO-ROUTES. Fully-qualified MCP name: mcp__omniseek__omniseek_read (server name is ROUTING: if URL BRANCH: fetch + normalize ONE URL. Tries each registered adapter until one claims it — a
specific article link (a Reddit post, an arXiv paper, a Bluesky post) as a normalized document.
arXiv is two-tier by design: an DOCUMENT BRANCH (pptx / docx / xlsx / pdf / txt / md / csv): read the FILE into readable, structured text — the document counterpart of omniseek_transcribe (speech). Free, keyless, cached. WHERE THE FILE LIVES:
|
| omniseek_transcribeA | Transcribe the SPOKEN content of a video / podcast / audio URL via local SenseVoice ASR (free, keyless, private, cached forever; chosen over Whisper after a real-audio benchmark — Whisper hallucinates on Chinese podcast intros). For the 干货-in-audio case where the substance is in the audio, not any text: bilibili videos (论文精读 / 方法论 / 读博 / 求职 talks), 小宇宙 podcasts, or any direct audio-file URL. (youtube already returns its captions via omniseek_read — no ASR needed; use that instead.) Fully-qualified MCP name: mcp__omniseek__omniseek_transcribe (server name is THE LONG-EPISODE PATTERN: do NOT transcribe a 2-3h episode whole (30k+ chars nobody reads).
Pull the chapter timestamps from the episode's shownotes (小宇宙 episode pages list them; use
omniseek_search(query, sources=["xiaoyuzhou"], raw=True, full=True) / omniseek_read first), judge WHICH chapter matters, then transcribe just
that slice: start="1:02:30", duration="12:00". Accepts seconds ("3750") or MM:SS / HH:MM:SS.
Slices are also fast to start — on direct/enclosure audio only the slice region is downloaded.
The flat Whole-item transcription remains right for short/dense items (a 10-min talk, a keynote clip); it is SLOW on first call for a long item, then cached forever. Reach for it deliberately on ONE item you've judged worth it, never as part of a broad sweep. language: "" auto-detects; set "zh" / "en" to skip detection and sharpen accuracy when you already know the language. diarize=True answers WHO said what (interviews / 对谈 / multi-host podcasts): speakers=N pins the diarization to N speakers (the KNOWN head-count: a 1-on-1 interview = 2, a solo talk = 1, a 3-host panel = 3). PASS IT whenever you know the count: cam++'s automatic estimate is unstable on short / noisy slices and will over- or under-split, so pinning N is what makes the turns track reality. Leave it 0 (auto) only when the count is genuinely unknown. Ignored unless diarize=True. Returns: {url, transcript, chars, audio_seconds, asr_seconds, source, title, cached, start_seconds?, duration_seconds?, segments?, speakers?} — or {url, error, transcript:""} if no audio resolved. |
| omniseek_viewA | SEE with your own vision, IN-BAND — document figures, loose image URLs, or video frames. ONE verb; kind="auto" picks the branch (or force it with kind=document|images|video). Fully-qualified MCP name: mcp__omniseek__omniseek_view (server name is ROUTING (kind="auto"): a document path/extension (.pdf/.pptx/.docx/.xlsx/…, as in omniseek_read) → DOCUMENT figures; a video URL (youtube/bilibili/douyin host or a .mp4/.webm/.mov suffix) → VIDEO frames; otherwise → loose IMAGE URLs (target may be a comma-separated URL list). The images come back as image content you can look at directly (no download/scp dance); OmniSeek only renders the pixels, what they MEAN is yours to read. WHICH PARAMS BELONG TO WHICH KIND:
• document: sections (comma-separated slide/page numbers to pull, "" = all), names (comma-separated
exact image names from the omniseek_read outline Returns image content blocks: document = [contact-sheet montage + legend] or [manifest + one block per figure]; images = [manifest + one block per URL that loaded]; video = [contact-sheet + timestamp legend]. Or an error / honest "nothing to show" note. |
| omniseek_curator_viewA | Use WHEN running the source-curation protocol (judge the admission queue or a source audit) — READ curator state: queue | packet | audit. Never mutates. Pick a view with Fully-qualified MCP name: mcp__omniseek__omniseek_curator_view (server name is • what="queue" -> the candidate-admission backlog (optionally filtered by
Unknown |
| omniseek_curator_actA | Use WHEN acting on the source-curation protocol — WRITE a source-lifecycle action (submit / probe / decide / admit / retire ...); every safety gate lives in the impl, unchanged. Pick
the action with Fully-qualified MCP name: mcp__omniseek__omniseek_curator_act (server name is • submit (name, urls, mode, domain, family; optional kind, regions, rationale, draft) -> add a CANDIDATE source to the admission backlog. mode ∈ STRUCTURE/UNWALL/TRANSCRIBE/RECALL/MONITOR. draft (foundry-grade) is a WORKING artifact ({"row", "fixture", "probe_summary"}) surfaced in the packet and preferred as stage_commit's ready-to-paste block. • probe (candidate_id) -> run the MECHANICAL evidence-gatherers, persist + return the packet. • wall_probe (candidate_id) -> P2 re-probe: RENDER the candidate in the network-isolated jail (egress only via the SSRF-pin proxy) so a source whose real content the plain-HTTP probe MISSED (client-rendered SPA / anti-bot / soft-login-wall) is measured on its REAL content. Eligible on a parked_p2 candidate OR an awaiting_verdict one YOU judge to be a client-rendered shell (WHEN to spend a render is your call, not an auto-gate). Surfaces content -> lands in awaiting_verdict on the rendered packet (parked_p2 revives, awaiting_verdict re-enriches); nothing -> stays put with the reason. Facts are render-derived (M7): the code never admits, only surfaces. • decide (candidate_id, decision, reasons; baseline_ref required to admit) -> record the admit/watch/reject verdict. MECHANICALLY REFUSES an admit on hard red-line / incomplete evidence / empty baseline_ref / no packet. admit -> owner_review; watch -> watching; reject -> rejected. • apply_live (candidate_id) -> ONE-TAP LIVE ADMIT (rss-safe subclass only): a REVERSIBLE overlay row + live re-register, NO git. Non-auto families are refused (use stage_commit). • rollback_live (name, family) -> full revert of a live-applied overlay row (unregister + drop). • stage_commit (candidate_id) -> ONE-TAP STAGED COMMIT for the NON-auto subclass: prepares the git commit text (does NOT apply); the operator does the git add / commit / deploy by hand. When the candidate has a foundry draft, the draft row IS the ready-to-paste block (+ a provenance line). • retire_live (name; confirm) -> ONE-TAP PRUNE (needs an existing PRUNE verdict): confirm=False previews; confirm=True writes a reversible runtime explicit_only override + stages the git commit. • rollback_retire (name) -> drop the runtime retire override so the source rejoins the fan-out. • source_verdict (name, verdict, rationale; prune_class, coverage_impact) -> record KEEP / WATCH / PRUNE for an EXISTING source. MECHANICALLY REFUSES a PRUNE the source's safety flags forbid. Unknown |
| omniseek_gatherA | Run N independent read-only eye tools IN PARALLEL, returning results in one response. Fully-qualified MCP name: mcp__omniseek__omniseek_gather (server name is The agent decides WHAT to call (judgment). OmniSeek executes them (mechanical). Each call runs independently; one failure does not affect others. Calls that depend on a prior call's result belong in a SEPARATE gather (the agent reads this batch first, then decides the next batch).
Returns: {results: [{index, tool, status, result|error|hint}, ...],
elapsed_s, completed, warming, failed, total}
On an errored call whose failure is a call-signature mismatch (a wrong / missing argument), |
| omniseek_graphA | Use WHEN you want HOW two entities connect, or what OmniSeek already knows AROUND a paper / author / entity; read-only, budgeted projections of its accumulated evidence graph with typed edges (ONE graph). Fully-qualified MCP name: mcp__omniseek__omniseek_graph (server name is Everything OmniSeek perceives is a statement with provenance ("X relates to Y, per Z"); the evidence graph accumulates those typed edges in ONE store surfaced through N indexes. It stores FACTS + labeled CANDIDATES, never verdicts: mechanical world edges (tier M: cites, authored, coauthored, affiliated, published_in, about, observed, exact-id same_as) and alignment CANDIDATES (tier A: title-fingerprint / fuzzy-name same_as, name-match authored, string mentions, signal conflicts). Judgment (claims, gaps, identity rulings) is tier J and is STRUCTURALLY excluded from OmniSeek's store — the views project structure, YOU judge it. ONE STABLE VERB:
• view="find", args={"label_query": ..., "kind"?: ...} -> the ENTRY POINT. A node id is minted
by the backend that knows it, so a NAME ("Siva Reddy") is not a node until you resolve it: find
does the mechanical token/substring match over node labels and returns candidate ids + kinds.
Every other view takes an
COLD START (set the expectation or the first stats reads as failure): documents and same-work edges are LIVE FROM DAY ONE (derived over recall's docs — the wall is born pre-populated by construction). Document THIN rows (title + url only, from NON-indexed sources) now accumulate from EVERY search (stats.node_kinds.document_thin), so the perception history is complete, not just the ~40 enumerable sources. Entity kinds (work / person / institution / venue / topic) still fill in as the P2/P3 write taps ship and calls happen; emptiness of those kinds early is CORRECT, not broken. BUDGETS (the no-silent-caps discipline): depth is clamped to <=2, max_nodes caps the node
count, and any capped result stamps FAIL-OPEN: a graph failure returns an error dict, never an exception — the graph is memory, it must NEVER break search or recall. |
| omniseek_sensorA | Use WHEN you want to MONITOR a query over time and be told only what's NEW — standing queries with novelty detection. ONE verb; Fully-qualified MCP name: mcp__omniseek__omniseek_sensor (server name is The agent decides WHAT to monitor (judgment); the sensor diffs mechanically (a (source, source_id) fingerprint diff against baseline). Each action's REQUIRED args: • action="create" (query; optional sources, schedule, notify) -> register a standing query that detects NEW results over time. Sensors run on their schedule automatically in the live service (hourly | daily | weekly; unknown = daily); use action="run" to trigger one manually. Returns the created sensor with its id. notify=True means the scheduler alerts when a scheduled run finds new results; optional notify_if=[keywords] narrows that alert to ONLY new results whose title/content match (notify_if_match="any" default, or "all"), so a broad standing query alerts on the sliver you care about instead of every new item. Optional detect_absence=True ALSO alerts when a tracked STABLE-source item DISAPPEARS (e.g. a page_watch policy page that goes dark / 404s); scoped to stable sources so a churny query sensor is unaffected. • action="list" -> all registered sensors with last-run stats {id, query, sources, schedule, last_run_at, last_new_count, total_runs, baseline_size}. • action="delete" (sensor_id) -> delete a sensor by id. Returns {deleted: true/false}. • action="run" (sensor_id) -> manually trigger one sensor NOW (the manual path beside the automatic scheduler): runs its query, diffs against baseline, updates state, returns a summary with new_count + new_titles. Tests a sensor on demand without waiting for its schedule. Unknown action, or a missing required arg, returns {"error": ...}. |
| omniseek_rulingA | Use WHEN two graph nodes ARE (or are NOT) the same person / entity and you want views to collapse them — record / list / retract same_as | not_same_as rulings (the one judgment channel the graph's working policy applies). Fully-qualified MCP name: mcp__omniseek__omniseek_ruling (server name is OmniSeek never MAKES a ruling; it STORES yours as declarative state and APPLIES it at read time
(the sensors.json precedent: judgment persisted as config OmniSeek executes mechanically). A ruling
says "these two graph nodes ARE / are NOT the same entity"; omniseek_graph's
This is a SEPARATE tool from omniseek_graph (not an omniseek_graph action) because omniseek_graph is batchable in omniseek_gather ONLY because it is read-only; folding a write into it would let the gather whitelist write. Unknown action -> {"error": ...}. |
| omniseek_statementA | Use WHEN you've concluded a DIRECTED, decision-relevant relation OmniSeek does NOT already store mechanically (X acquired_by Y, paper P refutes claim Q, path R requires gate S) and want the graph to carry it forward — record / list / retract typed relation statements (the general sibling of omniseek_ruling; identity types belong to omniseek_ruling). Fully-qualified MCP name: mcp__omniseek__omniseek_statement (server name is OmniSeek never MAKES a statement; it STORES yours as declarative state and PROJECTS it at read time
(the rulings / sensors.json precedent: judgment persisted as config OmniSeek applies mechanically).
A statement is a DIRECTED, typed relation between two graph node ids: "openai --acquired_by-->
someone", "paper X --refutes--> claim Y". It surfaces in omniseek_graph's neighborhood / between / since
under WHAT EARNS A STATEMENT (the value gate — all three must hold, else it is noise that BURIES the edges carrying a real decision; the graph's value is inverse to its noise density):
MEMORY-vs-GRAPH boundary: prose understanding (a lesson, a conclusion, context, confidence, scope) is
the ATOM — it lives in YOUR own notes / memory, or in this statement's Endpoints may be ANY node id, even ones no tap minted (
Like omniseek_ruling this is a SEPARATE tool from omniseek_graph (omniseek_graph stays read-only, hence batchable in omniseek_gather). Unknown action -> {"error": ...}. |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
| investigate | Parameterized investigation recipe. shape picks the WAVE 1/2 pattern: person (researcher/advisor/practitioner), lab (research group/institution), field (topic/landscape), product (tool/company/service), chase (walled-source depth-pursuit). Unknown shape falls back to the person recipe with a note. |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
TDQS
Scored across 18 tools
Each tool targets a clearly distinct resource or action: search, paper metadata enrichment, field citation graphs, author resolution, coauthorship, institution rosters, document/audio/image reading, source curation, parallel gathering, evidence graph views, sensors, and graph rulings/statements. Potential overlaps like omniseek_read vs omniseek_view or omniseek_ruling vs omniseek_statement are explicitly separated and cross-referenced, so routing between them is reliable.
All tools share the omniseek_ prefix and use consistent lowercase snake_case, which makes the surface feel uniform and predictable. However, the suffix pattern is not fully consistent: some are single verbs (search, read, view), some are nouns (sources, graph, sensor), and some are object-verb compounds (paper_enrich, resolve_identity).
Eighteen tools is slightly above the typical sweet spot, but the server's purpose is genuinely broad: search, deep reading, media understanding, paper analytics, person/institution graphs, source curation, persistent memory, and monitoring. Each tool appears to earn its place, and the administrative pairs (curator_view/curator_act, ruling/statement) add cohesion rather than padding.
The tool surface covers the full research lifecycle: discover (search, sources), consume (read, transcribe, view), analyze (paper_enrich, field_skeleton, paper_recommend, coauthors, institution_cohort), persist (ruling, statement, sensor), and administer (curator, gather). Tools are explicitly designed with handoffs between them, such as resolve_identity to coauthors, read outlines to view figures, and fire-then-collect search, with no obvious dead ends.