npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@minicod/mcp-server

v0.6.1

Published

MCP server for Minicod — query academic papers, authors, journals (with peer-review comments & submission guidelines), Chinese universities (高校/学院), and OpenAlex entities (sources, institutions, topics, funders) from Semantic Scholar, OpenAlex, PubMed, an

Readme

@minicod/mcp-server

MCP server for Minicod — query academic papers, authors, journals (with real peer-review comments & submission guidelines), Chinese universities (高校/学院), and OpenAlex entities (sources, institutions, topics, funders) from Semantic Scholar, OpenAlex, PubMed, and Minicod's curated databases, through any MCP-compatible client (Claude Desktop, Cursor, Cline, Continue, etc.).

🔑 An API key is required. Sign up and generate one at https://www.minicod.com/ → Dashboard → API Keys. The key has the form sk-... (same format as OpenAI keys) and is used both for the REST API and this MCP server. Calls that pass local MCP validation reach the Minicod API and use the same quota/refund rules as direct REST calls.

Quick start

  1. Get an API key at https://www.minicod.com/ (free signup; pay-per-call after the included quota).
  2. Add the snippet for your client below.
  3. Restart the client. You should see 87 minicod tools available to the model (the same number npm run test:e2e asserts).

Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "minicod": {
      "command": "npx",
      "args": ["-y", "@minicod/mcp-server"],
      "env": {
        "MINICOD_API_KEY": "sk-your-key-here"
      }
    }
  }
}

Cursor

Add to ~/.cursor/mcp.json (or per-project .cursor/mcp.json):

{
  "mcpServers": {
    "minicod": {
      "command": "npx",
      "args": ["-y", "@minicod/mcp-server"],
      "env": { "MINICOD_API_KEY": "sk-your-key-here" }
    }
  }
}

Cline / Continue / other stdio clients

Same shape — point to npx -y @minicod/mcp-server and pass MINICOD_API_KEY as an environment variable.

Configuration

| Env var | Required | Default | Notes | |---|---|---|---| | MINICOD_API_KEY | yes | — | Your sk-... API key from https://www.minicod.com/ | | MINICOD_API_BASE_URL | no | https://www.minicod.com/v1 | Override for self-hosted or staging | | MINICOD_TIMEOUT_MS | no | 60000 | Per-request timeout. The default allows for the backend's worst-case upstream retry path; lower it only if your client has a shorter budget |

If MINICOD_API_KEY is missing, the server exits immediately with an instructional error message — the MCP client will surface that error in its logs.

The 87 tools

Tools are source-explicit where multiple sources exist: paper and author tools take a source argument. Results are never merged across sources, and every returned item carries a source field so you can always tell where data came from. For cross-source coverage, call the tool multiple times in parallel.

⚠ The source value on a returned row is NOT the word you passed in. The argument takes the long name; the rows carry the three-letter route code:

| source argument | source on every returned row | |---|---| | semantic | "stc" | | openalex | "olx" | | pubmed | "pmd" |

So row.source === "openalex" is always false — join, de-duplicate and label by the three-letter code, and translate it back before showing it to a user. Paper rows, author rows and embedded authors[] entries all carry it. OpenAlex entity records (sources / institutions / topics / publishers / funders / concepts / awards / vocabularies) carry no source key at all: there is only one source they can come from. (This mapping was absent from the whole package until 2026-08-21, which made source === "openalex" a predictable first mistake.)

Seven groups: Papers (12), Authors (6), Journals (14, curated DB incl. review comments + submission files), Universities (5, 全国高校 + 学院/院系), OpenAlex entities (41), PubMed-only (6), Discovery (3, snippets + recommendations).

Papers (12)

| Tool | What it does | Sources | |---|---|---| | search_papers | Keyword search with structured filters (year/date range, open access, min citations, venue, subjects, types, sort). On source: openalex it also takes six passthrough arguments — raw_filter, raw_sort, cursor, sample, seed, response_fields — see below | semantic, openalex, pubmed | | get_paper | Fetch one paper by its native source ID | semantic, openalex, pubmed | | get_paper_by_doi | Resolve a DOI to a paper | semantic, openalex, pubmed | | get_paper_authors | List authors of a paper | semantic, openalex, pubmed | | get_paper_citations | Papers that cite this one. publication_date_or_year became a real filter on openalex on 2026-08-20 (it used to be accepted and dropped, returning the unfiltered list) and is now a 400 on pubmed, where ELink has no date input at all | semantic, openalex, pubmed | | get_paper_references | The paper's bibliography. No date filter — publication_date_or_year was removed 2026-08 because no data source's references endpoint accepts it | semantic, openalex, pubmed | | get_papers_batch | Hydrate many paper IDs in ONE call (cheaper than N get_paper). ⚠ Unresolvable ids differ by source since 2026-08-21: on semantic the array is POSITIONAL — data.length === ids.length and data[i] is null when ids[i] did not resolve (matching Semantic Scholar's own batch semantics; it used to be a filtered short array with no marker, so zipping against the input went off-by-N). On openalex / pubmed it is not positional — unknown ids produce no row, so match back by id | semantic, openalex, pubmed | | search_papers_bulk | Token-cursor deep pagination for large harvests. On semantic it genuinely lifts the 1,000-hit ceiling; on pubmed (added 2026-08) it is a stable server-side result set but still stops at PubMed's first 9,999 records — no pagination mode gets past that, partition by date instead. Rows come back under data, not items — the only list tool here that does. types / subjects / authors / date_type were added 2026-08-20; both routes had always bound them | semantic, pubmed | | match_paper | Title → single best-match paper (reference-string resolution) | semantic | | autocomplete_papers | Title type-ahead. ⚠ The two sources return structurally incompatible payloads: semantic gives a bare ARRAY of {id, title, authors_year} with a bare hex id; openalex gives {items, total} whose rows key the title under display_name and whose id is a full URL. Reading items on the first, or title on the second, yields undefined | semantic, openalex | | get_related_works | "More like this" for a work. Fixed 2026-08-21: limit means what it says again. The backend now resolves the seed's whole related_works list, drops ids OpenAlex has merged/deleted, re-sorts the survivors into the seed's own order, and only then takes limit — so limit: 1 really is the single most-related work. It used to truncate before resolving, which made limit: 1 return zero rows on a paper with 8 related works. total equals the rows actually returned | openalex | | group_works | Bucketed counts over the whole OpenAlex corpus (322,044,938 works, measured 2026-08-21) — analytics by year / institution / topic / OA / funder | openalex |

Source guide:

  • semantic — Semantic Scholar. Best citation graph, broad coverage. Only source for bulk / match / snippets. sort on keyword search is a 400 — the upstream relevance endpoint has no sort parameter, so since 2026-08-19 asking for citations / date there is rejected with a pointer to search_papers_bulk instead of being silently ignored. Only the first 1,000 hits are reachable and going past them is an error, not an empty page: offset + limit must be ≤ 1000 (the SUM), and a straddling request such as offset: 980, limit: 100 is a 400 naming the deepest usable offset — it used to be served with limit silently cut to 1000 - offset. venue may be a comma-separated list here. Three arguments that used to be accepted-and-dropped are now 400s on this source: authors on search_papers_bulk / match_paper (upstream has no author parameter there, and folding the names into the query took a measured 46 hits to 0), the fields name papers, and an unrecognised fields_of_study on search_snippets. ⚠ Corrected 2026-08-21 — this list used to name four fields values. contexts / intents / contextsWithIntent now work on get_paper_citations / get_paper_references (they land in citation_contexts / citation_intents, and citation_contexts / citation_intents / is_influential are accepted as aliases). They stay out of the default field set — ~2KB a row — so you get them only by naming them, and their absence in a default response means "not requested", not "no context exists".
  • openalex — OpenAlex. Richest metadata, full open-access coverage (322,044,938 works, measured 2026-08-21 — earlier revisions of this file said ~250M). Only source for related-works / group analytics. Honors every filter and all three sort values. venue is one name, not a list (, | ! < > are replaced with spaces), and since 2026-08-19 the name is resolved to OpenAlex source ids before filtering — one extra upstream call, cached 6h; a name that resolves to nothing returns 200 with an empty page and total: 0 (that is "no such journal", not an error). Before that fix, any openalex search carrying a venue failed with a 400.
  • pubmed — PubMed/NCBI. Biomedical only. Honors query, limit, offset, year_from/year_to, date_from/date_to, venue (a single journal name), and sort — relevance, date, author and journal, the four NCBI ESearch sort schemas; sort: "citations" is rejected with a 400, it is not silently downgraded. (author / journal are PubMed-only and are a 400 on the other two sources.) A one-sided range is genuinely open-ended: date_from/year_from alone reaches ahead-of-print and future-issue-dated records, date_to/year_to alone reaches the earliest indexed records. Since 2026-08-19 authors, types and open_access also take effect here (all three used to be dropped): authors becomes "{name}"[Author] AND-ed; types maps to [pt] but only article/review/editorial/letter/news/dataset/preprint/case-report/clinical-trial/meta-analysis have a counterpart — asking for book, book-chapter, thesis, software, conference or study here is a 400 since 2026-08-20, where it used to be skipped silently (which returned the whole unfiltered corpus); open_access maps to free full text[sb], an approximation (free-to-read is not open-access), so do not report those results as licence-verified OA. The default sort is now genuinely relevance-ranked (every request sends sort=relevance explicitly; before that an omitted sort fell back to Entrez's newest-indexed-first default, which had zero overlap with relevance). min_citations and subjects are 400s on this source since 2026-08-20 — PubMed indexes neither citation counts nor the Minicod subject vocabulary (MeSH is a different taxonomy), and both used to be dropped silently, returning unfiltered results. fields now genuinely projects here (changed 2026-08-21, breaking): it used to be accepted and ignored, and every route returned the complete object; the paper-returning /v1/pmd/* routes now trim the rows to the keys you name. Nothing is forwarded to NCBI — ESummary/EFetch have no projection parameter, and never having one was never a reason we could not trim the mapped rows ourselves, which is what the backend does. Two consequences: the vocabulary is Minicod paper JSON keys (title, year, citation_count), not Semantic Scholar names, so a fields string is not portable from source: semantic; and an unknown key is a 400 listing every legal key, raised before the NCBI call. The one pmd route that rejects fields is get_paper_authors — it returns authors, so there is no paper to project — and get_author_papers never declared the key at all (both are refused locally by this server). PubMed rows carry no is_open_access and no citation_count key at all (changed 2026-08-21 — see the response-model section below); pdf_url is a best-effort PMC deep link populated whenever a PMCID exists — a PMCID does not imply open access (NIHMS author manuscripts and embargoed records have one too), so it is not an open-access indicator, and it must not be derived from an OA claim or used to make one. For "can I read this" use get_pubmed_fulltext_links. No bulk/match/snippets, no author entity.

tldr is Semantic-Scholar-only and endpoint-limited. The upstream carries the one-sentence AI summary only on its full-record endpoints, so it can appear in search_papers, get_paper, get_paper_by_doi, get_papers_batch and match_paper (with source: semantic) — and never in get_paper_citations, get_paper_references, get_author_papers, search_papers_bulk, recommend_paper or recommend_from_pool. text_availability (fulltext / abstract / none) follows exactly the same rule. To add either to papers gathered from the reduced-record tools, re-fetch the ids through get_papers_batch; naming tldr or textAvailability in fields on one of those tools is silently ignored, not an error. bibtex is available on every Semantic Scholar paper result.

OpenAlex-only passthrough arguments on search_papers (added 2026-08, source: openalex only). The cross-source arguments are the intersection of what three upstreams can express; these six reach the rest of the OpenAlex /works API:

| Argument | What it unlocks | |---|---| | raw_filter | Any of the ~200 native filter fields the structured arguments cannot name — cites, cited_by, related_to, title.search, abstract.search, doi, pmid, keywords.id, sustainable_development_goals.id, awards.*, authorships.institutions.ror, language, … AND-merged after whatever the structured arguments produced (they do not override each other). An invalid field returns a 400 whose body enumerates every legal one | | raw_sort | Explicit direction and multi-key ordering (cited_by_count:desc,publication_date:asc). Mutually exclusive with sort | | cursor | Deep pagination past OpenAlex's 10,000-result offset ceiling. Send '*', then feed back next_cursor. Mutually exclusive with offset | | sample + seed | Random sampling for unbiased estimates. seed makes the draw reproducible but is undocumented upstream. Sampling does not combine with deep paging: next_cursor is always null and paging past the sample size returns an empty 200 | | response_fields | The projection that actually shrinks the response on OpenAlex, listing Minicod paper keys. Unlike fields — which on OpenAlex only widens the upstream select — this drops keys from the payload. It is OpenAlex-only: on semantic and pubmed use fields, which shrinks the row on both (on pubmed since 2026-08-21, taking these same Minicod keys) |

These are strictly OpenAlex-only: the semantic / pubmed endpoints accept the JSON key and never act on it, so the tool refuses to send them there rather than return a result that looks filtered and is not.

OpenAlex full-text availability is a separate pair of fields, has_content ({pdf, grobid_xml}) and content_urls, present only on source: openalex papers. content_urls addresses the OpenAlex Content API — it needs its own credentials and OpenAlex bills per fetch, so it is an availability signal, not a free download link like pdf_url.

Paging and batch ceilings. Exceeding an OpenAlex ceiling, a batch id cap or the PubMed offset window is a 400, and the tools reject those locally before the call. The one exception is the limit ceiling on semantic / pubmed keyword search, which clamps silently instead. On OpenAlex, offset must additionally be a multiple of limit (a 400 since 2026-08-20 — upstream pages by page number, so a stray offset used to silently re-serve the previous page while the envelope echoed a different number).

| Constraint | On excess | Where it applies | |---|---|---| | limit ≤ 100 | 400 | every OpenAlex search — papers, authors, and the six entity searches | | limit clamped to 100 | silent | semantic and pubmed paper/author search — the API reduces an oversized value instead of rejecting it. The tool schemas stop at 100, so you only meet this clamp when calling the REST API directly | | page × page-size ≤ 10,000 | 400 | OpenAlex offset paging on papers/authors (limit counts as 20 when omitted) and page paging on the entity tools (limit counts as 25 when omitted). get_paper_references is exempt — it slices a list held in memory and has no depth ceiling | | first 1,000 hits only | 400 | Semantic Scholar keyword search — offset + limit must be ≤ 1000 and the backend rejects a deeper request (since 2026-08-19; it used to return an empty page with total: 0). search_papers refuses it locally too rather than spending the call; switch to search_papers_bulk | | first 9,999 records only | 400 | PubMed search — offset must be ≤ 9998; partition by date to reach the rest | | 500 ids / call | 400 | get_papers_batch with source: semantic | | 1,000 ids / call | 400 | get_authors_batch (both sources), get_papers_batch with source: openalex or pubmed, and every batch_openalex_* tool |

The 500 / 1,000 semantic caps are upstream's. The 1,000 on the OpenAlex and PubMed batches is Minicod's own fan-out ceiling: those backends split the list into upstream chunks of 100, so 1,000 ids is at most 10 upstream calls inside the one metered request.

Response-model changes, 2026-08-21 (BREAKING). Two things happened at once: a batch of keys that no upstream actually provides stopped being emitted as plausible-looking zeros, and a batch of keys upstream does provide started being emitted.

Removed / now omitempty — a missing key means "this source does not provide it", which is not the same statement as 0 / "" / false:

| Key | Change | |---|---| | authors[].url | Deleted on all three sources, on embedded authorships and on author-entity rows alike. It pointed at an on-site author page that has never existed — a dead link on every row. Use source_url (the upstream author page) | | authors[].paper_count / citation_count / h_index | Omitted on embedded authorships (they were hard-coded 0 on 100% of rows — measured olx 0/177, stc 0/157, pmd 0/63). Still returned, real zeros included, by search_authors / get_author / get_authors_batch | | authors[].id / orcid / affiliations | Omitted when empty rather than sent as "" / null. On pubmed id is therefore always absent — NCBI has no author entity | | paper.abstract / paper.fields_of_study | Omitted when empty. This is most visible on the nine pubmed list routes, which run on esummary and never carry an abstract: they used to send "" / null, which reads as "this paper has no abstract". Fetch the PMID through get_paper for the real one | | paper.year / publication_date / doi / venue / pdf_url / url / authors | Omitted when empty. year: 0 is not a year, an empty pdf_url is not a paywall, and "authors": null is not "this paper has no authors" — it is a pubmed reference row that is only an unparsed NLM citation string | | paper.citation_count / reference_count / is_open_access | Now three-state pointers. Key absent = this source does not carry the value (or fields left it out); 0 / false = the source explicitly said so, and that is a real answer you must report as such. Always present on openalex (fixed upstream selection, genuine 0 / false included) and on semantic under default fields | | paper.citation_count on pubmed | Removed outright — and the old number was wrong, not merely fake. It was esummary's pmcrefcount, which measurement shows is the paper's REFERENCE count: PMID 32109013 (Guan WJ, NEJM 2020) was reported as 24 citations when pubmed_pubmed_citedin says 15,452, and 24 is exactly its bibliography length. PubMed publishes no citation count, so the key is gone. Resolve the DOI on openalex / semantic if you need a number, and say where it came from | | paper.reference_count on pubmed | Now the real quantity on every pmd route: list routes read pmcrefcount, get_paper counts efetch's <ReferenceList>, and the two now agree (they used to disagree — 0 on the list, a real count on the detail page). Absent, not 0, when PMC has no parsed bibliography for the record | | paper.is_open_access on pubmed | Removed outright. The constant false was contradicted by the pmc_id / pdf_url in the same row (10 of 20 rows on a measured crispr search). A PMCID is not an OA licence — the authoritative test is a db=pmc efetch we do not make. Use get_pubmed_fulltext_links (has_free_full_text) for "can I read this" | | snippet.paper_id / paper_title / section | Omitted when empty. section is absent on every snippet_kind: "title" snippet (upstream reports no section for a title fragment); an absent paper_id must not be passed on to get_paper |

Added — all omitempty, so they are simply absent on sources that lack them:

| Key | Where | |---|---| | paper.topics[] / paper.primary_topic / paper.keywords[] | openalex paper rows. Topic ids are bare (T11438 → get_openalex_topic), keyword ids are bare slugs (crispr → batch_openalex_keywords). This is what makes "what is this paper about" a $0.0001 question instead of a $0.01 classify_text_topics call | | authors[].author_position / is_corresponding / countries[] / institutions[] | openalex embedded authorships. institutions[].ror is a BARE identifier (04xs57h96) and feeds get_openalex_institution directly. is_corresponding is a real boolean here — OpenAlex states it on every authorship, so false means "explicitly not corresponding" | | authors[].raw_author_name / raw_affiliation_strings[] / raw_orcid / raw_affiliations[] | openalex embedded authorships (2026-09-29): the raw byline layer of upstream authorships[], verbatim. name / id stay the disambiguated author entity and can disagree with it (on W7204054862: raw_author_name: "null", name: "Zimo Tian"). raw_affiliations is upstream affiliations renamed; its institution_ids are bare | | author.summary_stats / counts_by_year[] / topics[] / topic_share[] / institutions[] / affiliation_history[] / name_alternatives[] | openalex author-entity rows (get_author and friends) | | authors[].affiliations / orcid / external_ids / homepage / source_url / institutions[] | semantic paper rows. homepage and source_url are different links and this row omitted homepage entirely until 2026-08-21: homepage is the site the author declared themselves (S2 authors.homepage, e.g. https://noamshazeer.com), source_url is the Semantic Scholar author page. Answer "where is their personal site" from homepage; both are omitempty, so most authorships carry neither. ⚠ Except get_paper_citations / get_paper_references, where Semantic Scholar rejects every authors.<sub-field> with a 400 — those two stay at id + name, an upstream ceiling rather than a gap here | | publication_venue.issn / alternate_issns[] / alternate_urls[] | semantic paper rows | | snippet authors[] / open_access_info / ref_mentions[] | search_snippets. ref_mentions[]{start, end, corpus_id} is the full list of citation mentions in a passage (measured: one passage with 4); the older single-valued matched_paper_corpus_id is kept for compatibility and names only the first, and is absent when upstream resolved none of the mentions | | autocomplete short_id / filter_key | All 10 autocomplete_* tools. filter_key is upstream's own answer to "which filter key does this id go into" (keywords.id, not the guessable keywords) | | paper.influential_citation_count: 0 | semantic. A genuine 0 now survives — it used to be erased by omitempty, so "S2 says zero influential citations" and "this source has no such measure" looked identical | | paper.is_influential: false | semantic get_paper_citations / get_paper_references, in the DEFAULT response. false = S2 judged this edge not influential (the ordinary case, and a real answer); it used to vanish with the key | | paper.citation_contexts[]{text, intents?} / paper.citation_intents[] | semantic get_paper_citations / get_paper_references, only when named in fields (they were a hard 400 until 2026-08-21). Prefer citation_contexts — it pairs each sentence with its own intent labels, while the flat intents list cannot be aligned to a sentence. An empty intents list means "unlabelled", not background | | retrieval_version | search_snippets envelope (measured "pa1-v1"). The upstream retrieval/chunking model version behind this answer — snippet boundaries and ranking shift when it changes, so record it when comparing or caching results across days |

⚠ snippet.matched_paper_corpus_id deliberately stayed a string rather than becoming an array — old callers keep working, and new ones should read ref_mentions[] instead.

publication_types[] is a different vocabulary from the types argument. Every paper row from all three sources carries it (it is not new — it was simply undocumented anywhere in this package until 2026-08-21), but it holds the upstream's own labels, not the Minicod enum you filter with:

| Source | Shape of publication_types[] | Example row (filtered with types: ['review']) | |---|---|---| | semantic | Semantic Scholar CamelCase, usually several | ["Review", "JournalArticle"] | | openalex | the OpenAlex slug, normally exactly one — the only source where a value can coincide with the argument | ["review"] | | pubmed | NLM publication types, spaced and title-cased, mixed in with non-document labels | ["Journal Article", "Research Support, Non-U.S. Gov't", "Review"] |

Research Support, … on a PubMed row is a funding tag, not a document type. A correct types: ['review'] filter therefore still yields rows whose publication_types never contains the literal string review on semantic / pubmed — do not read that as "the filter was ignored", do not re-filter rows against the argument, and report a paper's type by reading the row rather than echoing what you asked for.

Envelope fields declared but not currently emitted. The paper- and author-search envelopes carry an optional sources_failed (sources that were queried and failed, i.e. the result is partial and a retry may help), and the author envelope additionally carries optional has_more / next_offset for upstreams that report a next page but no total. Every tool here pins exactly one source, and no endpoint populates the paging pair yet, so all three are absent from every response today — treat them as forward-compatible optional fields rather than something to branch on.

Authors (6)

| Tool | What it does | Sources | |---|---|---| | search_authors | Find authors by name; h-index, citations, ORCID, affiliations | openalex, semantic | | get_author | Fetch one author profile by ID | openalex, semantic | | get_author_papers | Papers by an author (optional year/date filter on openalex / semantic). On pubmed (added 2026-08) the argument is the author's name in NLM form (Doudna JA), not an id — see below | openalex, semantic, pubmed | | get_authors_batch | Many author IDs in one call. Same positional contract as get_papers_batch: on semantic data[i] corresponds to ids[i] and is null when unresolvable (changed 2026-08-21); on openalex unknown ids simply produce no row | openalex, semantic | | autocomplete_authors | Name type-ahead | openalex | | group_authors | Bucketed author aggregation | openalex |

source defaults to openalex (backward-compatible). Author IDs are per-source — pass the same source you searched with.

PubMed has no author entity, so search_authors / get_author / get_authors_batch / autocomplete_authors / group_authors genuinely cannot support it — there is nothing to address. But "list this person's papers" is expressible upstream (ESearch Name[Author]), so get_author_papers alone accepts source: 'pubmed' and takes the name in author_id. Two consequences worth passing to the user: there is no disambiguation (everyone sharing the name is merged into one list), and publication_date_or_year / fields do not exist on that route — the tool rejects them locally instead of letting them be silently dropped. That is a gap in this one route's request contract, not a source-wide rule: since 2026-08-21 every paper-returning pmd route honours fields. For a year-filtered PubMed author list use search_papers with source: 'pubmed' and query: "Surname Initials[Author]".

Journals (14, Minicod curated database)

| Tool | What it does | |---|---| | find_journals | The one journal query. Name / ISSN / category / DOAJ keyword / publisher / country / type / open access / JCR·SCImago·中科院分区·新锐分区 quartiles / CCF rank / 预警 level / IF·SJR·h-index·works ranges / review-rating thresholds — in any combination, with sorting, cursor paging and an optional view (metrics are in the default) | | batch_journals | Resolve a known list in one billed call: exactly one of ids (≤200) / issns (≤100) / names (≤100, 20 in token mode). Always returns a per-input lookup; resolve_ids defaults to follow | | get_journal | One journal by numeric id. include: ['submission_detail'] appends the structured author guideline (~18 keys); include: ['sources'] appends the current unified-entity per-source detail tree; include: ['issn_details'] appends canonical ISSN relationships {issn,type,unified_id,valid_checksum} without the large sources tree. The backend reapplies view / fields after its internal full-row fetch, so include never expands the unified public fields | | compare_journals | Side-by-side comparison of 2–10 ids. Unknown/deleted ids are skipped, merged ids use the survivor row, and redirects records merged targets or deleted null values | | autocomplete_journals | Name-prefix type-ahead. Its deliberately slim rows are exactly {id, name, issn_l?, coverage:{source_count}}; it is the one journal tool where coverage.sources is absent because upstream suggest does not return a source list | | get_journal_timeline | Year-by-year trajectory per source (IF / SJR / quartile evolution) | | get_journal_comments | Real submission reports — journal-level aggregates (accept / reject / desk-reject / revision rates, handling days, difficulty / speed / quality ratings, status_counts, tag_counts) + a paginated report list | | export_journals | One offset-paged CSV file (page, up to 5,000 rows) under the same filters/sort as find_journals, plus optional top-level ids/ISSNs/names selector. It only pages within the 100,000-row offset window; use cursor search beyond it. X-Export-Scope is returned as export_scope | | journal_changes | Cursor-paged feed of retired ids (merged → successor, deleted → new_id: null). since is inclusive, so consumers must tolerate replay | | journals_by_publisher | One offset-paginated page for ONE publisher, matched exactly (cursor is a 400) | | list_journal_vocabulary | The four live filter vocabularies: categories / keywords / publishers / countries (countries is unpaged). The first three use offset paging (cursor is a 400); category level may be used across sources, while a supplied source / level pair is whitelisted. Use response totals or get_journal_meta({kind: 'stats'}) for current sizes | | get_journal_meta | kind: 'meta' (freshness + live source coverage) / 'filters' (legal filter values) / 'stats' (current corpus statistics) | | get_journal_submission_guideline | Guideline as Markdown. Requires a top-tier membership key — non-members get a billing-free 403 | | get_journal_submission_latex | LaTeX template (.zip, base64). Requires a top-tier membership key |

The journal API was redrawn for upstream v4.0.0 (2026-08-21) and thirteen tools were removed, not deprecated. search_journals, list_journals, journals_by_category, journals_by_keyword, get_journals_by_ids, batch_journals_by_issn, list_categories, list_journal_keywords, list_publishers, list_countries, suggest_journals, get_journal_filters and get_journal_stats are gone, together with the /v1/jnl/search and /v1/jnl/by* routes behind them. There is no compatibility layer and no deprecation window: upstream answers the old endpoints with 410 endpoint_gone, so a shim would only have produced convincing errors later.

| Removed tool | Use instead | |---|---| | search_journals, list_journals | find_journals | | journals_by_category | find_journals({category, category_source, level, sort_by: 'rank'}) | | journals_by_keyword | find_journals({keyword, sort_by: 'source_count'}) | | get_journals_by_ids | batch_journals({ids: […]}) | | batch_journals_by_issn | batch_journals({issns: […]}) | | get_journal({lookup_by: 'issn'/'name'/'names'}) | find_journals({issn}) / find_journals({name, name_match}) / batch_journals({names}) | | suggest_journals | autocomplete_journals | | list_categories, list_journal_keywords, list_publishers, list_countries | list_journal_vocabulary({vocabulary}) | | get_journal_filters, get_journal_stats | get_journal_meta({kind: 'filters' / 'stats'}) |

A journal row is now two levels deep: purpose bucket → per-source sub-object. Flat identity at the top (id, name, issn_l, issns, type, publisher, country, is_oa, coverage, plus submission and comments summaries), then rankings.{jcr,scimago,fqb,xr,ccf,jufo,cwts}, metrics.openalex, oa.doaj, warning.gjqk, and — at view: 'full', which is what get_journal returns by default — identifiers.* and the rest of provenance.*. A source that does not list the journal contributes no key at all, so Object.keys(rankings) is literally "which rankings cover this journal". The old flat 94-column row, in which a typical response carried 51 nulls and "we checked, nothing there" was indistinguishable from "this source has never heard of it", is gone.

Four honesty rules follow from that, and every one of them has bitten somebody:

  • A missing key means unknown — never render it as 0, false or "".

  • coverage.sources[] is the existence test, not the presence of a rankings.<src> key. A journal JCR indexes but did not score this year still returns rankings.jcr without an impact_factor. The deliberate exception is autocomplete_journals: its slim type-ahead rows only carry coverage.source_count, so the absence of coverage.sources there says nothing about source membership. Resolve the selected id with get_journal, batch_journals or find_journals before applying this rule.

  • Genuine 0 / false are sent and are real: rankings.jufo.level_fi: "0" is a real JUFO level, metrics.openalex.works_count: 0, oa.doaj.has_apc: false, comments.count: 0.

  • You do not need to pass view — the defaults already match the upstream journal service's own defaults. Collection routes (find_journals, batch_journals, journals_by_publisher) default to view: 'standard', which covers every column the upstream list endpoint returns by default (rankings, impact factor, quartiles, SJR, h-index, works counts, 预警 level); get_journal defaults to view: 'full', matching the upstream detail endpoint. view: 'compact' is an opt-in payload saving (~0.6 KB/row against the default's ~1.5, measured at page_size: 200) that drops rankings and warning entirely and thins metrics.openalex to country and oa.doaj to in_doaj — so on a compact response the absence is your own projection, and reporting "this journal has no impact factor" off one is wrong. fields: ['rankings.jcr.impact_factor', …] takes exact dot paths (prefixes expand) and rejects unknown ones by name. view + fields together is a 400.

    Upstream full now accepts only 0|1|false|true; values such as 2 are a 400. Lists still default to full=0, detail to full=1; full=1 plus fields still silently swallows the projection, and full selector pages have stricter dynamic page-size ceilings. The MCP surface therefore exposes view / fields, not full.

Composite values are split for you. rankings.fqb gives quartile (1–4), rank_in_category, category_size and the original raw ("1 [1/118]"); rankings.xr.quartile is the number with 区 stripped. When the source string holds no number (未分区) only raw is sent — no invented quartile. Impact factor and SJR are numbers; if the upstream cell held something non-numeric you get impact_factor_raw / sjr_raw instead of a silent 0.

ISSN fields and exact lookup share one canonical identity projection (2026-08 integrity rules). issn_l, issns.print / issns.electronic, full-view provenance.all_issns, opt-in issn_details, exact find_journals({issn}), batch_journals({issns}), and CSV ISSN filtering all use the same accepted set. It contains only ISO 3297 checksum-valid, non-placeholder ISSNs that are not isolated by a strong cross-entity conflict. 0000-0000 / 0000-000X never appear. If several unified entities strongly claim one ISSN, the number is isolated from every conflicting entity and exact lookup matches neither side. A fully shaped but checksum-invalid, placeholder, or conflicted query therefore returns no row; that does not prove no raw source ever declared it. get_journal({id, include:['issn_details']}) exposes the accepted relationships as {issn,type,unified_id,valid_checksum}; valid_checksum is currently always true and unified_id equals the returned journal id. When this include is explicit, issn_details is always present ([] means no accepted relationship); the key is absent only when it was not requested. include:['sources'] is the current unified-entity detail tree, not a complete audit log of raw declarations, so never fill an ISSN missing from the canonical projection back in from that tree.

⚠ Journal ids are not reused, but they can retire through merges or deletions. In one historical two-month snapshot, multiple clean-up rounds retired a material share of the then-current corpus. get_journal on a merged id answers 200 with top-level redirected_from: {id, reason: 'merged'} and the surviving journal's row (persist the new id); a deleted id answers 410 journal_deleted, deliberately distinct from 404 journal_not_found. batch_journals follows merges by default and flags them in lookup. To keep a stored set of ids fresh, consume journal_changes; probing ids one at a time cannot discover anything you are not already asking about.

journal_changes.since means built_at >= since, not “strictly after.” Follow page.next_cursor until page.has_more is false before advancing your checkpoint. On the next poll, reuse the greatest completed built_at; rows sharing that timestamp can replay, so apply each change idempotently (for example, deduplicate the complete {old_id,new_id,reason,built_at} event). Never advance a checkpoint from a partially consumed page.

Collections all answer {items, page} (batch_journals adds lookup), and page always carries both total and has_more, so the key set never changes shape. The specified default is count_mode: 'none', although name/selector paths may dynamically choose exact; always branch on the echoed page.count_mode. In none mode total is null — never read that as "zero results". Offset paging is capped at 100,000 rows ((page-1)*page_size beyond it is a 400 offset_too_deep); walk large sets by passing the previous page.next_cursor back verbatim as cursor. page.applied_sort reports the sort that actually ran, and page.truncated_reason: 'token_candidates_capped' is the one truncation that makes total meaningless.

Not every route has a cursor, and the ones that do never lie about it. find_journals, get_journal_comments and journal_changes take a real cursor; the dimension listings — list_journal_vocabulary (categories / keywords / publishers) and journals_by_publisher — are offset only, so cursor there is declared purely to be rejected with a 400 pointing at page / page_size. Until 2026-08-22 those four routes accepted a cursor, threw it away, and still answered page.mode: 'cursor' with has_more: true and next_cursor: null — a while loop written off page.mode either never left page 1 or never terminated, HTTP 200 throughout. Both halves are fixed: page.mode can no longer name a mode the route does not have, and on a cursor page has_more is now true if and only if next_cursor is non-null, so while (page.has_more) { cursor = page.next_cursor } always ends. countries is unpaged entirely and takes no parameters at all.

Per-input answers live in lookup.items[] — one entry per input in input order, {input, status, id, row_index}. row_index != null is the only honest answer to "did this input get a row"; status is not orthogonal (an input can be redirected and still be filtered out). matched_count is recomputed as "inputs that produced a row" because upstream's own number counts resolved ids and over-reports.

Enum spellings are not what you would guess, and a wrong value is a 400 that lists the legal ones (upstream itself answers 200 + zero rows, which reads exactly like "this category has no journals"). Returned canonical values are 'A类'/'B类'/'C类' for ccf_rank and '1'–'4' for fqb_quartile; the convenient aliases A/B/C and Q1–Q4 are accepted and normalised. JCR / SCImago use 'Q1'–'Q4' canonically, gjqk_warning_level is '高'/'中'/'低'/'未分级'. country is an upstream region code, not ISO 3166-1 — its live vocabulary includes MARC-21 codes such as CAU, ENK, NYU. Discover everything with get_journal_meta({kind: 'filters'}) and list_journal_vocabulary.

Two traps worth stating outright. keyword is an exact DOAJ subject-term filter, not free-text search; use name + name_match: 'token' for journal-name text search. And find_journals({publisher}) is a prefix match, so it can return more rows than the publisher directory's exact count. Use journals_by_publisher whenever the result must agree with that directory; obtain current totals from the response rather than copying a measured count into client code or documentation.

is_oa is a genuine three-state filter: true selects OA, false selects explicitly non-OA, and omission means no OA filter. export_journals returns one offset page named journals-page-{page}-size-{page_size}.csv; top-level page / page_size paginate it, top-level ids / issns / names are mutually exclusive optional selectors; id exports default resolve_ids: 'follow' (explicit off / meta remain available), and filter.sort_by / filter.sort_dir are supported. Nested paging/cursor/count/view/fields remain rejected. Its top-level fields goes through the same dot-path whitelist as find_journals.

sort_dir requires sort_by and sending it alone is a 400: upstream silently ignores a direction with no key. When omitted, numeric scores/counts default descending while ids/text default ascending; export without sort_by uses source_count DESC, id ASC. list_journal_vocabulary's source / level are checked against the same live taxonomy whitelist that backs find_journals({category_source, level}) — a misspelt source is a 400 listing ccf / doaj / fqb / jcr / jufo / openalex / scimago / wikidata / xr, not a 200 with zero rows that reads as "this source has no categories". For the categories vocabulary, level alone deliberately filters across sources; when source is present, the pair is checked for compatibility.

Retired arguments are declared only so they can be rejected. limit, full, after_id, sep, lookup_by, after, the old search mode / source / batch selectors, page on journal_changes, and cursor on the offset-only dimension listings are all gone. They are still listed in the affected tools' schemas and raise an error when passed, because an undeclared argument is silently dropped at the MCP boundary — measured: journal_changes({page: 2}) used to lose page before the request was built and answer with page 1, HTTP 200, no warning. limit in particular was the v4.0.0 casualty upstream (suggest?limit=3 returned 10 rows, export?limit=3 returned 1,000), so it is now page_size everywhere and an error under its old name.

The journal database merges multiple upstream sources into one record per journal. Current sources include Crossref, OpenAlex, DOAJ, Wikidata, SCImago, JCR, 中科院分区表 (FQB), 中科院新锐分区 (XR), CCF, 国际期刊预警 (GJQK), JUFO, CWTS, submission guidelines/templates, and peer-review comments. get_journal_meta({kind: 'meta'}) returns the authoritative list live, so do not hard-code this paragraph.

Review comments themselves are aggregated from several communities (Peeref, SciRev, LetPub, PeipuSci, MedSci); each individual review carries its own source_name / source_url, so attribute to those rather than to a fixed list.

Universities (5, Minicod 全国高校 database)

| Tool | What it does | |---|---| | search_universities | Search Chinese universities by keyword + filters: province / city, education level, ownership (公办/民办), discipline type, 985 / 211 / 双一流 flag, offers-graduate, has-thesis | | get_university | One university by Ministry-of-Education school_code. Set include_colleges: true to also load 学院/院系 (departments + majors), thesis resources, rankings, campuses, contacts | | get_universities_batch | Fetch up to 100 universities by code list | | get_university_filters | Available facet values (province / city / level / ownership / discipline) | | get_university_stats | Corpus counts (by level, 985 / 211 / 双一流, graduate, thesis) |

Tri-state flags (is_985 etc.): null means not collected, not false.

OpenAlex entities (41)

Fine-grained tools for OpenAlex's non-paper entities. Each entity has search / get / batch / autocomplete, plus group (aggregation) where supported. These are a different dataset from the curated Minicod DBs: use search_openalex_sources for OpenAlex-scale bibliometrics (ISSN-L, OA, APC, works counts) vs find_journals for quartiles / impact-factor / review data; use search_openalex_institutions (global, ROR-keyed) vs search_universities (curated Chinese 高校).

| Entity | Tool prefix | Ops | Notes | |---|---|---|---| | Sources (journals/venues) | *_openalex_sources | search / get / batch / autocomplete / group | get resolves an ISSN directly since 2026-08-19 (id='0028-0836' or 'issn:0028-0836'), alongside the S… id. summary_stats (h_index / i10_index / two_year_mean_citedness), counts_by_year, apc_usd and societies are now returned | | Institutions | *_openalex_institutions | search / get / batch / autocomplete / group | get resolves a ROR directly since 2026-08-19 (bare, ror:-prefixed, or the full ror.org URL), alongside the I… id. lineage, repositories, roles, display_name_acronyms, display_name_alternatives, summary_stats and counts_by_year are now returned; international is mapped correctly but upstream returns {} for most institutions | | Topics | *_openalex_topics | search / get / batch / autocomplete / group | current ~4,500-topic taxonomy | | Publishers | *_openalex_publishers | search / get / batch / autocomplete / group | group added 2026-08 (it was always available upstream). No sources_count field — count via search_openalex_sources + filter='host_organization.id:…'. summary_stats / counts_by_year added 2026-08-19 | | Funders | *_openalex_funders | search / get / batch / autocomplete / group | group added 2026-08. The field is awards_count, not grants_count; pair with the works filter awards.funder_id — corrected 2026-08-20, grants.funder is rejected upstream with 400 "grants.funder is not a valid field". summary_stats / counts_by_year added 2026-08-19 | | Concepts | *_openalex_concepts | search / get / batch / autocomplete / group | DEPRECATED by OpenAlex — prefer topics. get resolves a Wikidata QID directly since 2026-08-19 ('Q11660' or 'wikidata:Q11660'). summary_stats / counts_by_year are mapped but upstream returns null for both on frozen concepts, so expect them absent | | Awards (grants) | *_openalex_awards | search / get / batch / group | New 2026-08. ~16.6M funded projects (G…). group added 2026-08-21 — group_by: 'funder.id' / 'funding_type' / 'start_year' answers "how is grant money distributed" in one call instead of paging 16.6M rows. (Two earlier notes here were wrong and are recorded so nobody re-derives them: the first said group and autocomplete were both upstream 404s; the second said group was ours to register but had not been. group_by is a query parameter on the /awards list endpoint, never a separate path.) No autocomplete — that one really is an upstream 404 (/autocomplete/awards) — and id='random' 404s too, so the backend rejects it with an explanatory 400. Rows DO carry primary_topic / topics (an earlier note claimed they did not). display_name is empty on many rows (funders often deposit only a scheme code — fall back to funder_award_id + funder.display_name), and funded_outputs can run to hundreds of work ids, so pass select on any multi-row search |

Plus one cross-entity tool: autocomplete_openalex completes over all entity types at once (each suggestion carries its own entity_type); set entity_type to narrow it, omit it for a genuine cross-entity lookup.

Plus five vocabulary tools covering the six controlled vocabularies upstream exposes — keywords (~65k), sdgs (17), domains (4), fields (26), subfields (252), work-types (25). They are folded into list_openalex_vocabulary / get_openalex_vocabulary_entry / autocomplete_openalex_vocabulary / group_openalex_vocabulary with an entity argument, plus the entity-specific batch_openalex_keywords, rather than spread over 30-odd separate tools: to a caller they are all the same operation — look up a legal value, then use it as a works filter — and four of the six fit in a single page, so there is no per-entity search semantics to preserve. Autocomplete exists for keywords and subfields only (the other four 404 upstream), and id='random' 404s on all six — those two really are upstream limitations.

group and batch landed 2026-08-21 and are no longer gaps. group_openalex_vocabulary covers all six (upstream group_by works on every one of them; entity: 'subfields', group_by: 'field.id' is the useful case, while domains/sdgs/work-types/fields are small enough to just list). batch_openalex_keywords covers keywords alone — with 65,004 entries it is the only vocabulary where "one search is enough" was ever false; the other five top out at 252 rows. It accepts the bare slug, keywords/<slug> and the full openalex.org URL interchangeably, and drops unknown slugs silently, so match rows back by id. Earlier revisions of this paragraph blamed both omissions on upstream; that was wrong in both cases and is recorded here so it is not re-derived.

Read works_api_url off the row you picked rather than reconstructing the filter — the id form differs per vocabulary (bare slugs for keywords/work-types, numbers for domains/fields/subfields, a full metadata.un.org URL for SDGs).

select on the six big entities is merged with the mapping layer's required set since 2026-08-20, so it can only widen: the row keeps every key it would have had anyway. That closed a data-fabrication bug — those models have no omitempty, so an unselected field used to serialise as its zero value (type: "", cited_by_count: 0), indistinguishable from the real thing. On awards and the six vocabularies select is still passed verbatim and genuinely shrinks the row (their models are fully omitempty), so that is where it is a real payload lever.

Paging on these: limit ≤ 100 and page × limit ≤ 10,000, both enforced with a 400. Since 2026-08-19 every entity here accepts cursor and returns next_cursor, so cursor paging round-trips: send cursor: '*' first, then feed back each response's next_cursor (absent = last page). This is the only way past the 10,000 ceiling, which concepts (65k), funders (45k) and publishers (10k) all exceed. cursor and page are mutually exclusive — sending both is a 400 raised locally, before any upstream call. (Previously cursor was forwarded on sources/institutions only, silently dropped on the other four, and no entity search returned a cursor at all.) batch_* takes at most 1,000 ids; the backend chunks the list at 100 per upstream call.

On the group_* tools limit is the bucket page size, 1–200 (raised from 100 on 2026-08-20: group_by is a projection and upstream serves 200 buckets a page, but the backend used to clamp it with the list row ceiling of 100, so asking for 200 returned fewer buckets than omitting limit entirely). Omitting it still returns the upstream's full default page of up to 200 buckets, so set it only when you deliberately want fewer. groups_count counts the buckets in the page you received, not the number of distinct values that exist — nothing in the response reports that total. Group responses carry next_cursor, so use cursor to page buckets — including on group_authors, which gained a cursor argument on 2026-08-20 (this line used to say it was the one group tool without one).

PubMed-only (6)

Capabilities with no Semantic Scholar / OpenAlex counterpart. Everything that does have a counterpart lives in the paper / author / discovery tools behind source: 'pubmed' instead, so a tool appearing here means PubMed is the only implementation. NCBI E-utilities is not usage-priced — these are the cheapest tools in the server upstream-wise (the real limit is a request rate, 3/s without an NCBI key and 10/s with one) — but each is still one metered Minicod call, so batch where a batch endpoint exists.

| Tool | What it does | |---|---| | pubmed_spellcheck | ESpell sentence-level spelling correction. Not autocomplete — E-utilities has no prefix endpoint at all. corrected: false with no corrected_query is the normal "spelling is fine" answer, not an error. search_papers already attaches the same suggestion on a zero-hit PubMed search, so reach for this only when you want to correct before searching | | pubmed_citation_match | ECitMatch: resolve up to 100 structured citations (journal / year / volume / first page / first author) to PMIDs in one call. Not match_paper — that takes one free-text title. Results come back in input order. An all-empty citation is rejected locally because NCBI silently discards the rest of the batch after one | | get_pubmed_fulltext_links | PMC deposit status + publisher/LinkOut URLs. in_pmc: true is not open access (NIHMS manuscripts and embargoed records have a PMCID too) — read has_free_full_text / links[].is_free to answer "can I read it now?" | | get_pubmed_reviews | ELink pubmed_pubmed_reviews — related review articles only, unlike recommend_paper which returns related work of any type. Seed paper removed from results. Takes fields (new 2026-08-21) to trim the rows | | get_pubmed_pmc_citations | ELink pubmed_pmc_refs — citations found in PMC full text, a wider measurement than get_paper_citations on this source (8 vs 7 on PMID 34577065). Neither is a complete citation count. A few rows carry a PMC… id with no PMID and cannot be re-fetched with get_paper. Takes fields (new 2026-08-21) to trim the rows | | export_pubmed_records | EFetch passthrough: MEDLINE / abstract / uilist / XML, up to 200 ids. For citation-manager exports only — the response is a raw text blob (~2–4 KB per MEDLINE record) that will dominate a context window. Use get_papers_batch for anything you intend to read or reason over. An unknown format is rejected locally because upstream answers it with HTTP 200 and a zero-byte body |

Discovery (3)

| Tool | What it does | Sources | |---|---|---| | search_snippets | Retrieve citable text passages with their source paper, section, score, snippet_kind and snippet_offset — ideal for grounded / RAG answers. Not pageable, and the response has NO total key at all (it was removed in 2026-08 — it only ever echoed the page size, which invited callers to report a page count as a hit count). The body is {data}, plus limit only when you sent one; count the rows in data yourself. Per-TOKEN match offsets are not returned; fields here takes dotted snippet.* paths, not paper field names. An unrecognised fields_of_study is a 400 since 2026-08-20 | semantic | | recommend_paper | "More like this" for a single paper | semantic, pubmed | | recommend_from_pool | Recommend from a pool of seed papers. Negative steering (negative_paper_ids) is semantic-only; on pubmed it is a 400, not a silent drop — NCBI has no exclusion input | semantic, pubmed |

Recommendation results are full metadata records (year, authors, abstract, venue, citations, DOI, PDF url, bibtex) but never carry tldr — see the note in the Papers section.

Search best practices

The upstream APIs treat the query (or keyword) field as keyword AND-matching, not natural-language parsing. Cramming year / venue / author / quartile into the query string will return empty or wrong results. Always decompose the user's request into structured fields.

Papers — decomposition rules

| User says | Use this field, NOT query | |---|---| | "in 2023" / "from 2020 to 2023" | year_from, year_to | | Specific date range | date_from, date_to (YYYY-MM-DD) | | "in Nature" / "at NeurIPS" | venue | | "highly cited" / "≥1000 citations" | min_citations | | "open access only" | open_access: true | | Field of study ("medicine", "CS") | subjects: ["medicine"] | | Document type ("review", "article") | types: ["review"] | | An author's name | Call search_authors first → get_author_papers |

types is a closed enum — anything else is rejected with a 400 listing the valid values: article, book, book-chapter, case-report, clinical-trial, conference, dataset, editorial, letter, meta-analysis, news, preprint, review, software, study, thesis.

Since 2026-08-20 a value that is valid globally but has no counterpart on the source you chose is also a 400, and so is a mixed list where only some values map. It used to be dropped silently, which returned the entire unfiltered corpus with HTTP 200 (measured: ['case-report'] on openalex gave 367,880 hits — the whole corpus — against 1,529 for ['review']). Per-source filterable sets:

| Source | Filterable types | |---|---| | semantic | article, book, book-chapter, case-report, clinical-trial, conference, dataset, editorial, letter, meta-analysis, news, review, study | | openalex | article, book, book-chapter, conference, dataset, editorial, letter, preprint, review, software, thesis (conference selects genuine conference papers, never folded into article) | | pubmed | article, case-report, clinical-trial, dataset, editorial, letter, meta-analysis, news, preprint, review (mapped to [pt]) |

So preprint / thesis / software need openalex, study needs semantic, and book / book-chapter / conference are unavailable on pubmed.

subjects accepts: art, agricultural-and-food-sciences, biology, business, chemistry, computer-science, economics, education, engineering, environmental-science, geography, geology, history, law, linguistics, materials-science, mathematics, medicine, philosophy, physics, political-science, psychology, sociology. An unrecognized subject is a 400 listing this set — corrected 2026-08-20, it used to be dropped silently, which returned unfiltered results. Spelling is lenient (underscores, spaces, mixed case and the Semantic Scholar native form Computer Science all normalise), but the value itself must be in the list. subjects is semantic / openalex only: any value is a 400 on pubmed, which indexes MeSH rather than this vocabulary.

❌ query: "attention is all you need 2017 NeurIPS Vaswani highly cited" ✅ query: "attention mechanism transformer", year_from: 2017, year_to: 2017, venue: "NeurIPS", min_citations: 1000

Journals — decomposition rules

| User says | Use this field, NOT a free-text string | |---|---| | journal name, possibly misspelt | name + name_match: "token" | | "Q1" journals | jcr_quartile: "Q1" / scimago_quartile: "Q1" / fqb_quartile: "1" | | "中科院一区" | fqb_quartile: "1" (canonical; alias "Q1" is also accepted) | | "CCF A" | ccf_rank: "A类" (canonical; alias "A" is also accepted) | | "IF > 30" | min_impact_factor: 30 | | SJR / h-index thresholds | min_sjr / max_sjr / min_h_index / max_h_index | | ISSN known | issn (one) or batch_journals({issns}) (many); exact lookup uses the checksum-valid, non-placeholder, non-conflict-isolated canonical projection described above | | publisher / country | publisher (prefix) or journals_by_publisher (exact) / country (region code) | | "open access" / "non-OA" | is_oa: true / is_oa: false | | a subject area | category (+ category_source), from list_journal_vocabulary({vocabulary: "categories"}) | | a DOAJ subject term | keyword, from list_journal_vocabulary({vocabulary: "keywords"}) | | anything about metrics | nothing to add — the default view: "standard" already carries them |

❌ name: "Nature Q1 IF>30 oncology UK" ✅ category: "oncology", jcr_quartile: "Q1", min_impact_factor: 30, country: "GB"

⚠ type: "journal" matches only part of the corpus. Other records include ebook platforms, conferences, book series and repositories, and records with no type (which the filter also excludes). Pass it when the user means journals; leave it off when a missing type would be an unacceptable omission. Query get_journal_meta({kind: "stats"}) when you need current corpus counts.

Billing

Every admitted tool call hits the Minicod public API and consumes one call from your plan's daily/monthly quota, then from purchased call packages after the plan quota is exhausted. Local MCP validation errors never reach the API and are not billed; server/upstream failures are refunded under the same rules as direct REST calls. Manage keys, usage, and packages at https://www.minicod.com/.

What this server does not cover

87 tools cover all 107 scholar routes (/v1/{stc,olx,pmd}/*), all 5 university routes and 19 of the 20 journal routes. Three public, billed endpoints have no tool on purpose, so you will not find one by looking harder:

| Endpoint | Why there is no tool | What to do instead | |---|---|---| | POST /v1/today/recent | "今天的科研界" is an editorial feed keyed on the calendar date, not a lookup a model steers with arguments | Call the REST endpoint directly | | POST /v1/jnl/meta/health | An upstream liveness probe for operators. A model cannot act on the answer, and it would spend a metered call to learn that the next call would have failed anyway | If every *_journal* tool returns an upstream error at once, that is the same signal | | POST /v1/chat/completions | An LLM endpoint. Inside an MCP client you already have a model — routing through another one adds cost and a second hallucination surface | Use the search tools and reason locally |

Reporting a problem: request_id

Every Minicod response — success or failure — carries an X-Request-ID header, and the backend logs that same id on every line it writes for the call. It is the only thing that lets support find your exact request.

This client returns failed calls as structured JSON, preserving the HTTP status, API code, details, request id, and Retry-After value instead of requiring callers to parse prose:

{
  "error": {
    "type": "minicod_api_error",
    "status": 503,
    "code": 503,
    "message": "Upstream service unavailable (...)...",
    "details": { "upstream_error": "auth_unavailable" },
    "request_id": "02083911-85ff-4c30-a1c7-0288ddf8231c",
    "retry_after": "5"
  }
}

Quote that id in any report to [email protected]. The header is also honoured inbound: send your own X-Request-ID on a REST call and the backend will log and echo yours i