Tools¶
All research tools are prefixed research_ and grouped by provider (research_arxiv_*, research_europepmc_*), with identifier resolution exposed as a single grouping-level tool, research_resolve_identifier, that isn't tied to one provider. See the Software Requirements Specification — specifically Architecture and Interface specification — for the full design rationale and exact wire-level schemas; this page is a practical, per-tool reference.
Errors¶
A tool failure surfaces as an MCP ToolError — a plain error response carrying a human-readable message (Error calling tool '<name>': <detail>), not a structured field a caller can branch on programmatically; there is no {"error": "<code>", ...} envelope. The categories below name which condition produced a given failure, for reference — they aren't literal response fields. See Interface specification → Conventions for the exception type each one maps to.
| Category | Meaning |
|---|---|
not_found |
Identifier not recognised by the provider, or the requested format hasn't been fetched yet. |
format_unavailable |
The identifier is valid, but doesn't offer the requested format. |
unsupported_provider |
A DOI resolved to a domain outside the v1 provider allowlist (arXiv, Europe PMC). |
invalid_request |
Caller-supplied arguments fail validation before any outbound call or storage read (e.g. an arXiv search exceeding arXiv's own result-count bounds, an unsupported format passed to research_resolve_identifier, page passed to research_arxiv_parse_full_text with format="html", identifier passed to research_search_fetched without provider, or malformed FTS5 query syntax in research_search_fetched/research_notes_search). Also covers research_notes_search's other structured-filter validation — canonical_identifier without provider, author_filter="named" without author_name (or vice versa), and an unparseable date_from/date_to. |
rate_limited |
The provider's outbound queue exhausted its backoff budget after repeated 429s from the source. Discovery has no such queue — a 429 from OpenAlex surfaces immediately instead of being retried. |
provider_unavailable |
A timeout, connection failure, or 5xx from the source — surfaced immediately, never retried. |
file_too_large |
research_localfile_fetch_full_text's decoded content exceeds PRIORIS_MCP_LOCAL_FILE_MAX_SIZE_BYTES. Also covers the chunked-upload path: a single chunk over PRIORIS_MCP_LOCAL_FILE_UPLOAD_MAX_CHUNK_BYTES in research_localfile_upload_chunk, or a reassembled total over PRIORIS_MCP_LOCAL_FILE_MAX_SIZE_BYTES in research_localfile_upload_chunk/research_localfile_finalize_upload. |
configuration_error |
A required server-side configuration value is missing, checked before any outbound call — currently research_discovery/research://openalex/work-types when PRIORIS_MCP_OPENALEX_API_KEY isn't set (OpenAlex has required a key on every request since 2026-02-13). |
arXiv tools¶
| Tool | Description | Key inputs | Notes |
|---|---|---|---|
research_arxiv_search |
Search arXiv by keyword/query. | query, max_results (default 10), start (default 0), sort_by, sort_order |
Validates max_results (≤2000) and start + max_results (≤30000) before calling arXiv. |
research_arxiv_list_top_n |
List the N most recently submitted items matching one or more arXiv subject categories. | include_categories (list, e.g. ["cs.CL"]), n, exclude_categories (list, optional) |
"Top N" means most recent, not most cited/viewed. include_categories is AND-combined, exclude_categories is ANDNOT-combined; both are deduplicated. Empty include_categories fails with invalid_request. |
research_arxiv_fetch_metadata |
Fetch metadata for one or more arXiv identifiers in a single call. | arxiv_ids (list) |
Unrecognised IDs are reported in not_found, not a failure. |
research_arxiv_fetch_full_text |
Fetch (or return the already-persisted) full text for an arXiv item. | arxiv_id, format (pdf|html) |
Unversioned IDs resolve to the current version first. html isn't available for every paper (arXiv's HTML rendering is a comparatively recent rollout). |
research_arxiv_parse_full_text |
Convert already-fetched arXiv full text into one page of Markdown. | arxiv_id, format, offset (default 0), limit (default PRIORIS_MCP_MAX_INLINE_CHARS), page (optional, 1-indexed, pdf only) |
Never triggers a fetch itself — fetch first, or this fails with not_found. Returns offset/limit/total_length/has_more alongside markdown so a caller can page through content longer than the limit; see Caching and rate limiting below. When page is given, offset becomes relative to that page's start rather than to the whole document. total_pages/page_range are always present in the response but null for format="html", which has no page concept. Passing page with format="html" fails with invalid_request. |
All arXiv tools share a single outbound request queue, serialised to arXiv's documented limit of one request per 3 seconds.
Europe PMC tools¶
| Tool | Description | Key inputs | Notes |
|---|---|---|---|
research_europepmc_search |
Search Europe PMC by keyword/query. | query, page_size (default 25), cursor_mark (default *) |
Paginate by passing the previous response's next_cursor_mark back in as cursor_mark. |
research_europepmc_fetch_metadata |
Fetch metadata for one or more Europe PMC identifiers in a single call. | identifiers (list — bare PMCID or {source}:{id}) |
Unrecognised identifiers are reported in not_found, not a failure. |
research_europepmc_fetch_full_text |
Fetch (or return the already-persisted) JATS XML full text for a Europe PMC item. | identifier |
No format parameter — Europe PMC's only directly-servable full-text format is XML. Fails with format_unavailable if Europe PMC doesn't host full text for that item itself. |
research_europepmc_parse_full_text |
Convert already-fetched Europe PMC XML full text into one page of Markdown. | identifier, offset (default 0), limit (default PRIORIS_MCP_MAX_INLINE_CHARS) |
Never triggers a fetch itself — fetch first, or this fails with not_found. Returns offset/limit/total_length/has_more alongside markdown, same as arXiv's. |
There is no research_europepmc_list_top_n — Europe PMC has no single classification field equivalent to arXiv's subject categories.
Europe PMC publishes no numeric rate limit; the provider self-imposes the same one-request-per-3-seconds policy as arXiv, through its own separate queue.
Discovery tool¶
v3 — see Discovery. Not a per-provider tool: research_discovery sits in front of OpenAlex's /works search.semantic parameter, ranking external, not-yet-fetched candidates by embedding similarity rather than keyword overlap.
| Tool | Description | Key inputs | Notes |
|---|---|---|---|
research_discovery |
Discover external research candidates not already in the local corpus, ranked by OpenAlex's embedding-based search.semantic. |
query (free text, ≤2000 chars), max_results (1-50, default PRIORIS_MCP_DISCOVERY_MAX_RESULTS), page (default 1, 1-indexed), from_year/to_year (optional, inclusive publication_year bounds), open_access_only (default false) |
total is capped at 50 — search.semantic's own hard per-query ceiling, not a PriorisMCP-imposed limit. page/per_page/total/has_more reflect OpenAlex's own pre-exclusion counts; a returned page can carry fewer hits than requested once candidates already present in local storage are filtered out of the response. Each hit includes a fetch_route (known_provider|oa_link|manual_upload) describing how to actually retrieve full text for it — see Fetch ladder. |
Authenticated via PRIORIS_MCP_OPENALEX_API_KEY, sent as OpenAlex's own api_key query parameter, not the deprecated mailto polite-pool parameter. OpenAlex has required this key on every request since 2026-02-13; if it isn't configured, the tool fails with configuration_error rather than making an unauthenticated call. See research://openalex/work-types in Resources for the reference vocabulary behind a hit's own work_type field — it isn't a filter research_discovery itself accepts.
Identifier resolution¶
| Tool | Description | Key inputs | Notes |
|---|---|---|---|
research_resolve_identifier |
Resolve an identifier of unknown provider — an arXiv ID, a Europe PMC identifier, or a DOI — to its owning provider, canonical identifier, and a fetchable URL. | identifier, format |
Self-identifying schemes (arXiv IDs, Europe PMC identifiers) route directly, with no network round-trip. A DOI resolves via doi.org/Crossref first; if the redirect lands outside the arXiv/Europe PMC domain allowlist, this fails with unsupported_provider rather than following it. |
This is the one capability exposed at the grouping level rather than per-provider — see Architecture → Identifier routing.
Local filesystem tools¶
| Tool | Description | Key inputs | Notes |
|---|---|---|---|
research_localfile_fetch_full_text |
Validate and persist caller-sent PDF bytes, in a single call. | content_base64, filename (optional) |
Rejects invalid base64, or a payload whose (encoded or decoded) size exceeds PRIORIS_MCP_LOCAL_FILE_MAX_SIZE_BYTES, with file_too_large/invalid_request. Validates content as a PDF by its magic bytes, not filename's extension. Re-fetching unchanged content reuses the same server-assigned id; changed content gets a new one. Deprecation candidate now that the chunked-upload flow below exists for files at risk of hitting transport/relay size ceilings — kept unchanged as the small-file path for now, no forced migration. |
research_localfile_parse_full_text |
Convert an already-fetched local PDF's full text into one page of Markdown. | id, offset (default 0), limit (default PRIORIS_MCP_MAX_INLINE_CHARS), page (optional, 1-indexed) |
id is the caller-facing identifier research_localfile_fetch_full_text/research_localfile_finalize_upload returned. Never triggers a fetch — fails with not_found if id isn't recognised. page is always valid here (this source is PDF-only), unlike arXiv's format-gated version — response includes total_pages/page_range alongside markdown/offset/limit/total_length/has_more. |
research_localfile_begin_upload |
Start a new chunked upload session for a large local PDF. | filename (optional) |
Returns session_id and max_chunk_bytes. Fails invalid_request at PRIORIS_MCP_LOCAL_FILE_UPLOAD_MAX_CONCURRENT_SESSIONS open sessions. |
research_localfile_upload_chunk |
Upload one chunk to an in-progress upload session. | session_id, index, chunk_base64 |
Chunks must arrive in strict sequential order (no total_chunks argument). Fails not_found on an unknown/expired session, invalid_request on an out-of-order index, file_too_large on an oversized chunk or cumulative total. |
research_localfile_finalize_upload |
Reassemble a session's chunks and persist them, same validation as research_localfile_fetch_full_text. |
session_id |
Same output shape as research_localfile_fetch_full_text. Removes the session whether it succeeds or fails. |
Deliberately narrower than the arXiv/Europe PMC tool sets — no search, listing, metadata, or identifier resolution for this source (see Architecture → Local filesystem source). None of the five tools is subject to rate limiting — there's no outbound network request to throttle.
Storage management tools¶
| Tool | Description | Key inputs | Notes |
|---|---|---|---|
research_list_fetched |
Enumerate persisted (provider, identifier, format, artefact) catalogue entries, read from catalogue.sqlite. |
provider (optional), format (optional), offset (default 0), limit (default 50) |
Never triggers a fetch or parse. Omitting both filters lists every persisted entry across all three sources. artefact is document or markdown — the fetched source and its parsed Markdown are listed as separate entries, each with its own fetched_at_or_parsed_at/size_bytes. Returns offset/limit/total/has_more alongside entries for paging (total is the full unfiltered-by-page count matching provider/format; has_more = offset + len(entries) < total). |
research_delete_fetched |
Remove one or more persisted artefacts. | entries (list of {provider, identifier, format, artefact}, artefact is document, markdown, or all) |
Tolerates entries no longer present — reports them in not_found rather than failing the whole call. Does not cascade between artefacts: deleting document leaves markdown in place and vice versa; artefact="all" removes both (plus the format directory and, if it was the last format, the document-hash directory itself). Deleting markdown or all also removes that document from the search index. identifier must match the canonical form actually in storage — use research_list_fetched first if unsure. Never touches a local filesystem source's original file. |
research_search_fetched |
Search previously-persisted chunks (or leaves, for documents with none) — full-text, semantic-vector, or both. | query (FTS5 syntax for mode="fts"; free text otherwise), provider/identifier (optional, identifier requires provider)/format (optional), mode ("fts" (default) | "vector" | "hybrid", or any other registered mechanism name), offset (default 0, per mechanism), limit (default PRIORIS_MCP_VECTOR_SEARCH_DEFAULT_LIMIT, per mechanism) |
mode="fts" runs against the global SQLite FTS5 index (search.sqlite3), ranked by bm25() (most relevant first). mode="vector" runs a cosine-similarity KNN search over a separate embedding index instead — unthresholded, so it always returns up to limit nearest matches regardless of how dissimilar they are; its score is a cosine distance (0 = identical, larger = less similar — lower is better, the opposite of bm25()). mode="hybrid" runs both, returned as separate fts/vector result sets (each independently paged via its own total/has_more), not fused into one ranked list. Never triggers a fetch or parse — searching before anything is persisted just returns no results. Every response also carries index_status, mapping each mechanism name to "ready"/"stale"/"not_built"/(vector only) "building", populated only when provider/identifier/format are all given. identifier without provider fails with invalid_request, as does malformed FTS5 query syntax under mode="fts". Each FTS/vector match's offset is in the document's own coordinate space and can be passed straight to the matching parse_full_text tool's offset to fetch surrounding context. |
Grouping-level, like research_resolve_identifier — not split per provider, since none of the three tools validates anything provider-specific (see Architecture → list_fetched/delete_fetched). See Vector search for the full semantic-search design and Interface specification → research_search_fetched for exact wire-level fields.
Vector index reconciliation¶
Corpus-wide vector reindexing after an embedding-model change (PRIORIS_MCP_EMBEDDING_MODEL) runs automatically at server startup — there is no tool to trigger it — see Vector search → Reconciliation runs automatically at server startup and ADR-00030. Its live progress is readable via the research://vector-index/rebuild-status resource: {"documents": {"total": <int>, "pending": <int>, "succeeded": <int>, "failed": <int>, "cancelled": <int>, "active": <bool>}, "notes": {...}}, where total/pending/succeeded/failed/cancelled count only the documents/notes the most recent reconciliation run decided needed rebuilding (a corpus already fully ready under the configured model reports all zeros; total == pending + succeeded + failed + cancelled always holds), and active (pending > 0) is a convenience field so a caller doesn't have to derive it. A nonzero failed with active=false is a terminal state — no in-process retry runs; the item stays failed until the next server restart re-derives and re-schedules it — see ADR-00031. This state is process-local and in-memory, not a durable job log — it resets to a fresh count on every server restart, the same "derived transiently, self-healing on restart" property index_status's own building value already has (see Vector search → Index status is per-document/note).
Notes tools¶
| Tool | Description | Key inputs | Notes |
|---|---|---|---|
research_notes_create |
Create a new user-authored note against a document, or against a bare identifier. | provider, identifier, format (optional — omit for a note predating any fetch), author_name (optional, null means self), metadata (optional, caller-owned opaque dict), anchors (list of unresolved positional hints), tags (list), text (free-form Markdown, the note itself) |
identifier is canonicalised before storing — resolved through the owning provider (arXiv/Europe PMC) when format is given, used as-is for localfile or when format is omitted. |
research_notes_read |
Read a single note by id. | note_id |
Fails with not_found if the id isn't recognised. |
research_notes_update |
Partially edit an existing note. | note_id, text, anchors, tags, metadata (all optional) |
Fields left as null/omitted are unchanged. Fails with not_found if the id isn't recognised. |
research_notes_delete |
Delete a note by id. | note_id |
Returns false, not an error, if the note is already absent. |
research_notes_search |
Search/list notes; structured filters plus optional full-text or semantic-vector search over note text. | provider, canonical_identifier, format, date_from/date_to (ISO 8601), keyword (FTS5 syntax for mode="fts"; free text otherwise), author_filter (any|mine|named), author_name, tags_all/tags_any/tags_exclude, mode ("fts" (default) | "vector" | "hybrid"), offset/limit (paged, newest first) |
No filters at all returns everything, paged. canonical_identifier requires provider; author_filter="named" requires author_name and vice versa; both fail with invalid_request. date_from/date_to are validated as ISO 8601 and normalised to UTC before comparison — an unparseable value fails with invalid_request. Malformed FTS5 syntax in keyword under mode="fts"/"hybrid" also fails with invalid_request, same as research_search_fetched. mode="vector" requires keyword (embedded as free text, not FTS5 syntax) and runs an unthresholded cosine-similarity KNN search scoped by any structural filters given; its matches carry a score — cosine distance, 0 = identical, lower is better — instead of FTS5 relevance, and no snippet/offset (only note_id/text_preview/score). mode="hybrid" returns both fts/vector result sets independently, each with its own paging. The response's index_status.vector (populated only when a vector search actually ran) is the worst-case status — priority order "building" > "not_built" > "stale" > "ready" — aggregated across every note id in the caller's structural-filter scope (the whole corpus when unfiltered), not just the notes in vector.matches this page; an empty scope reports "not_built". |
Notes are persisted separately from fetched-document storage — one user-authored note against a document, or against a bare identifier predating any fetch — see Notes storage. A note's exportable file representation is available as a resource; see Resources.
Caching and rate limiting¶
research_*_search, research_*_list_top_n, research_*_fetch_metadata, and research_discovery responses are covered by the server's response-caching middleware (PRIORIS_MCP_RESPONSE_CACHE_TTL, see Configuration). fetch_full_text and parse_full_text are backed by persistent storage instead (see Resources) — a repeat call returns the already-persisted content (served_from_storage: true) without a second network fetch or parse.
Response shapes are still evolving alongside the SRS — prefer the Interface specification as the source of truth for exact wire-level fields.
research_localfile_fetch_full_text/research_localfile_parse_full_text, research_localfile_begin_upload/research_localfile_upload_chunk/research_localfile_finalize_upload, and research_list_fetched/research_delete_fetched/research_search_fetched are excluded from the response cache entirely — the local file source re-hashes file content on every call by design, the chunked-upload tools mutate in-memory session state that must never be served stale, and list/delete/search must reflect live storage state. The five research_notes_* tools are excluded for the same reason as list/delete/search: notes are mutable (create/update/delete), so a cached tool response could return stale or deleted data. The notes://{note_id}/export resource is likewise never served from the response cache, for the same reason — see Resources.