Developers and AI agents
Every public record in Life of Research is available to software without an account or API key: a JSON API for applications, OAI-PMH for harvesters, a Model Context Protocol server for AI assistants, and feeds for crawlers. Metadata is free to reuse for any purpose, including text and data mining.
| Interface | Endpoint | Best for |
|---|---|---|
| JSON API | /api/records | Search and fetch records from your own code |
| MCP server | /mcp | Letting an AI assistant search, read and cite records |
| OAI-PMH 2.0 | /oai2 | Bulk metadata harvesting by aggregators |
| Feeds and descriptions | /feed.xml, /sitemap.xml, /api/openapi.json | Change monitoring, crawling and client generation |
JSON API
The JSON API returns published records as JSON with open CORS (Access-Control-Allow-Origin: *), so it works from a browser as well as from a server. There is no authentication.
| Request | Returns |
|---|---|
GET /api/records | Published records, newest deposit first, 20 per page: {"total", "page", "per_page", "records": [...]} |
GET /api/records?q=words&page=n | Search titles, abstracts, keywords and author names |
GET /api/records/id | One record plus its files, each with a download_url; 404 if it does not exist or was withdrawn |
Each record carries id, identifier (such as LOR-00042), url, title, abstract, authors (name, affiliation, ORCID), type, license, keywords, publication_date, doi, version, views and downloads. The full schema is in the OpenAPI 3.1 description.
curl
curl -s 'https://lifeofresearch.com/api/records?q=climate&page=1'
curl -s 'https://lifeofresearch.com/api/records/111'Python (standard library only)
import json
import urllib.parse
import urllib.request
BASE = "https://lifeofresearch.com"
HEADERS = {"User-Agent": "my-project/1.0 (mailto:you@example.org)"}
def get(path, **params):
url = BASE + path + ("?" + urllib.parse.urlencode(params) if params else "")
with urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS), timeout=30) as r:
return json.load(r)
page = get("/api/records", q="climate", page=1)
print(page["total"], "matching records")
for rec in page["records"]:
print(rec["identifier"], rec["title"], rec["url"])
record = get("/api/records/111")
for f in record["files"]:
print(f["name"], f["download_url"])MCP server for AI assistants
The Model Context Protocol server at https://lifeofresearch.com/mcp lets an AI assistant search the repository, read records and produce citations itself. It is read-only, needs no account, and uses the Streamable HTTP transport with JSON-RPC 2.0.
- Protocol versions: 2026-07-28 (stateless, per-request
_meta,server/discover), and theinitializehandshake of 2025-11-25, 2025-06-18, 2025-03-26 for older clients. - Server card:
/mcp/server-carddescribes the endpoint before connecting. It follows the draft server-card proposal (SEP-2127), an experimental extension that may still change. - Sessions: none.
GETandDELETEon/mcpreturn 405; every request stands alone.
Connect a client
Most MCP clients only need the URL: add a remote server of type Streamable HTTP (sometimes labelled “HTTP”) pointing at https://lifeofresearch.com/mcp. With Claude Code, for example:
claude mcp add --transport http life-of-research https://lifeofresearch.com/mcpTo explore the tools interactively, run the MCP Inspector (npx @modelcontextprotocol/inspector) and connect it to the same URL.
Tools
| Tool | What it does | Arguments |
|---|---|---|
search_records |
Search published records in the Life of Research open-access repository. The query matches titles, abstracts, keywords and author names. Filter by record type and publication year. Results are newest deposit first. | query, type, year_from, year_to, limit, offset |
get_record |
Full metadata for one published record: title, authors with affiliations and ORCID iDs, abstract, keywords, dates, license, DOI, journal details, files with download links, view and download counts, and an APA citation. | id (required) |
get_citation |
A ready-to-use citation for a published record. Use style for formatted text (apa, mla, chicago, harvard, ieee, vancouver) or format for a reference-manager export (bibtex, ris, csl-json). Defaults to APA. | id (required), style, format |
list_recent |
The most recently deposited published records, newest first, optionally of one type. | limit, type |
list_subjects |
The most common subject keywords across published records, with record counts, plus the number of records of each type. Useful for seeing what the repository covers before searching. | limit |
Every tool returns both structuredContent (matching its outputSchema) and the same JSON as text. Invalid arguments come back as a tool result with isError: true and a message saying what to fix; an unknown tool name is a JSON-RPC -32602 error.
Raw requests (protocol 2026-07-28)
Each request carries its protocol version and client capabilities in params._meta, mirrored in the MCP-Protocol-Version, Mcp-Method and (for tools/call) Mcp-Name headers. Mismatched headers are rejected with 400 and error -32020; an unsupported version with -32022, which lists the versions this server accepts.
curl -s https://lifeofresearch.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0"}}}}'curl -s https://lifeofresearch.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: search_records' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_records","arguments":{"query":"open access","limit":3},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0"}}}}'Older clients (initialize handshake)
curl -s https://lifeofresearch.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'Python
import json
import urllib.request
URL = "https://lifeofresearch.com/mcp"
VERSION = "2026-07-28"
def mcp(method, params=None, request_id=1):
params = dict(params or {})
params["_meta"] = {
"io.modelcontextprotocol/protocolVersion": VERSION,
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {"name": "my-script", "version": "1.0"},
}
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": VERSION,
"Mcp-Method": method,
}
if method == "tools/call":
headers["Mcp-Name"] = params["name"]
body = json.dumps({"jsonrpc": "2.0", "id": request_id, "method": method, "params": params}).encode()
with urllib.request.urlopen(urllib.request.Request(URL, body, headers), timeout=30) as r:
return json.load(r)["result"]
hits = mcp("tools/call", {"name": "search_records", "arguments": {"query": "open access", "limit": 3}})
for rec in hits["structuredContent"]["records"]:
cite = mcp("tools/call", {"name": "get_citation", "arguments": {"id": rec["identifier"], "format": "bibtex"}}, 2)
print(cite["structuredContent"]["citation"])OAI-PMH for harvesters
The OAI-PMH 2.0 endpoint at https://lifeofresearch.com/oai2 (also /oai) is how academic aggregators collect metadata in bulk. GET and POST behave identically, as the protocol requires.
- Verbs: Identify, ListMetadataFormats, ListSets, ListIdentifiers, ListRecords, GetRecord.
- Formats:
oai_dc(Dublin Core) andoai_datacite(DataCite metadata). - Sets:
type:article,type:datasetand one per record type, plusopenairefor the scholarly types. - Identifiers:
oai:lifeofresearch.com:id. Withdrawn records are reported as deleted (transient). - Paging: 100 records per response, continued with
resumptionToken;fromanduntiltake UTC dates or date-times.
curl -s 'https://lifeofresearch.com/oai2?verb=Identify'
curl -s 'https://lifeofresearch.com/oai2?verb=ListRecords&metadataPrefix=oai_dc&set=type:dataset'
curl -s 'https://lifeofresearch.com/oai2?verb=GetRecord&metadataPrefix=oai_datacite&identifier=oai:lifeofresearch.com:111'Python harvest loop
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
BASE = "https://lifeofresearch.com/oai2"
NS = {"oai": "http://www.openarchives.org/OAI/2.0/", "dc": "http://purl.org/dc/elements/1.1/"}
params = {"verb": "ListRecords", "metadataPrefix": "oai_dc"}
while True:
with urllib.request.urlopen(BASE + "?" + urllib.parse.urlencode(params), timeout=60) as r:
root = ET.parse(r).getroot()
for rec in root.iterfind(".//oai:record", NS):
ident = rec.findtext("oai:header/oai:identifier", namespaces=NS)
title = rec.findtext(".//dc:title", namespaces=NS)
print(ident, title)
token = root.findtext(".//oai:resumptionToken", namespaces=NS)
if not token:
break
params = {"verb": "ListRecords", "resumptionToken": token}Feeds, descriptions and discovery files
/feed.xml— Atom feed of the 50 most recent records./sitemap.xml— every indexable page, with last-modified dates for records./api/openapi.json— OpenAPI 3.1 description of the public HTTP endpoints./.well-known/api-catalog— API catalog (RFC 9727) linking each API to its description and documentation./stats.json— the repository statistics as JSON./llms.txt— a short plain-text map of the site for language models./.well-known/security.txt— where to report a security problem (RFC 9116).
Record pages also carry machine-readable metadata: Highwire Press citation_* and Dublin Core meta tags, schema.org JSON-LD, and Signposting Link headers (cite-as, author, license, item, describedby) pointing to the JSON and OAI-PMH descriptions of the same record.
Rate limits and etiquette
| Interface | Limit |
|---|---|
MCP server (/mcp) | 120 requests per minute per IP address. Above that the server answers 429 with a Retry-After header. |
| JSON API, OAI-PMH | No fixed limit today. Please keep to about one request per second and avoid fetching the same page twice in a short time. |
Sitemap, Atom feed, llms.txt, statistics, OpenAPI | No fixed limit. These send an ETag: repeat the request with If-None-Match and an unchanged file costs a 304 Not Modified with no body. |
Send a descriptive User-Agent with a contact address, so we can reach you before blocking anything. For a full copy of the metadata, harvest OAI-PMH rather than paging through search results.
Reusing the data
Record metadata (titles, authors, abstracts, keywords, dates and identifiers) may be reused for any purpose, including indexing and text and data mining. The files attached to each record are covered by the license the depositor chose, shown on the record page and in the API's license field. When you display a record, link to its permanent URL or DOI.
Questions, bugs or a feature you need: contact@lifeofresearch.com.