get_related_works
Explore the relationship graph for a DOI by retrieving citations, references, versions, and parts. Specify relation type and page size to filter results.
Instructions
Explore the relationship graph for a DOI — citations, references, versions, and parts.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| doi | Yes | ||
| relation_type | No | all | |
| page_size | No |
Implementation Reference
- src/tools/get-related-works.ts:38-213 (handler)Main handler: registers the 'get_related_works' tool on the MCP server. The async callback fetches the base DOI record, then resolves citations (via DataCite /citations endpoint), references (via /references endpoint with fallback to relatedIdentifiers), versions, parts/part-of by filtering relatedIdentifiers by relation type. Results are deduplicated by DOI, formatted via formatDoiSummary, and returned as JSON.
export function registerTool(server: McpServer): void { server.tool( "get_related_works", "Explore the relationship graph for a DOI — citations, references, versions, and parts.", RelatedWorksSchema.shape, async (params) => { const input = RelatedWorksSchema.parse(params); const doi = normalizeDoi(input.doi); try { // Fetch the base record for its relatedIdentifiers const record = await getCached<DoiRecord>( doiCache, doi, () => dataciteClient .get<DoiResponse>(`/dois/${encodeURIComponent(doi)}`, { detail: true }) .then((r) => r.data) ); const relatedIdentifiers = record.attributes.relatedIdentifiers ?? []; const rt = input.relation_type; let works: DoiRecord[] = []; let total = 0; if (rt === "citations" || rt === "all") { // Fetch works that cite this DOI via DataCite events / relatedIdentifiers try { const citationResp = await getCached<SearchResponse>( searchCache, `citations:${doi}:${input.page_size}`, () => dataciteClient.get<SearchResponse>(`/dois/${encodeURIComponent(doi)}/citations`, { "page[size]": input.page_size, detail: true, }) ); const citationWorks = citationResp.data ?? []; works = [...works, ...citationWorks]; total += citationResp.meta?.total ?? citationWorks.length; } catch { // Citations endpoint may not exist; skip gracefully } } if (rt === "references" || rt === "all") { try { const refResp = await getCached<SearchResponse>( searchCache, `references:${doi}:${input.page_size}`, () => dataciteClient.get<SearchResponse>(`/dois/${encodeURIComponent(doi)}/references`, { "page[size]": input.page_size, detail: true, }) ); const refWorks = refResp.data ?? []; works = [...works, ...refWorks]; total += refResp.meta?.total ?? refWorks.length; } catch { // Fallback: filter relatedIdentifiers const refDois = relatedIdentifiers .filter((ri) => REFERENCE_RELATION_TYPES.includes(ri.relationType)) .slice(0, input.page_size) .map((ri) => ri.relatedIdentifier); for (const relDoi of refDois) { try { const relRecord = await getCached<DoiRecord>( doiCache, normalizeDoi(relDoi), () => dataciteClient .get<DoiResponse>(`/dois/${encodeURIComponent(normalizeDoi(relDoi))}`, { detail: true, }) .then((r) => r.data) ); works.push(relRecord); } catch { // skip individual failures } } total += refDois.length; } } if (rt === "versions" || rt === "all") { const versionDois = relatedIdentifiers .filter((ri) => VERSION_RELATION_TYPES.includes(ri.relationType)) .slice(0, input.page_size) .map((ri) => ri.relatedIdentifier); for (const relDoi of versionDois) { try { const relRecord = await getCached<DoiRecord>( doiCache, normalizeDoi(relDoi), () => dataciteClient .get<DoiResponse>(`/dois/${encodeURIComponent(normalizeDoi(relDoi))}`, { detail: true, }) .then((r) => r.data) ); works.push(relRecord); } catch { // skip } } total += versionDois.length; } if (rt === "parts" || rt === "part-of" || rt === "all") { const partDois = relatedIdentifiers .filter((ri) => PART_RELATION_TYPES.includes(ri.relationType)) .slice(0, input.page_size) .map((ri) => ri.relatedIdentifier); for (const relDoi of partDois) { try { const relRecord = await getCached<DoiRecord>( doiCache, normalizeDoi(relDoi), () => dataciteClient .get<DoiResponse>(`/dois/${encodeURIComponent(normalizeDoi(relDoi))}`, { detail: true, }) .then((r) => r.data) ); works.push(relRecord); } catch { // skip } } total += partDois.length; } // Deduplicate by DOI const seen = new Set<string>(); const unique = works.filter((w) => { const id = w.attributes?.doi ?? w.id; if (seen.has(id)) return false; seen.add(id); return true; }); return { content: [ { type: "text" as const, text: JSON.stringify( { doi, relation_type: input.relation_type, works: unique.map(formatDoiSummary), total, }, null, 2 ), }, ], }; } catch (err) { if (err instanceof DataCiteError && err.statusCode === 404) { throw notFound(doi); } const msg = err instanceof Error ? err.message : String(err); throw apiError(msg); } } ); } - src/tools/get-related-works.ts:15-21 (schema)RelatedWorksSchema: Zod schema defining input validation — doi (required string), relation_type (enum: citations, references, versions, parts, part-of, all; default 'all'), and page_size (int, 1-50, default 10).
const RelatedWorksSchema = z.object({ doi: z.string().min(1), relation_type: z .enum(["citations", "references", "versions", "parts", "part-of", "all"]) .default("all"), page_size: z.number().int().min(1).max(50).default(10), }); - src/tools/index.ts:6-17 (registration)Registration entry point: imports registerTool as registerGetRelatedWorks from './get-related-works.js' and calls it inside registerAllTools at line 17.
import { registerTool as registerGetRelatedWorks } from "./get-related-works.js"; import { registerTool as registerSearchByPerson } from "./search-by-person.js"; import { registerTool as registerListRepositories } from "./list-repositories.js"; import { registerTool as registerGetRepository } from "./get-repository.js"; import { registerTool as registerGetDoiSchemaXml } from "./get-doi-schema-xml.js"; export function registerAllTools(server: McpServer): void { registerSearchDois(server); registerGetDoi(server); registerFormatCitation(server); registerGetDoiMetrics(server); registerGetRelatedWorks(server); - src/utils/formatters.ts:43-62 (helper)formatDoiSummary helper: transforms a DoiRecord into a plain object with doi, title, creators (first 3), year, resource_type, publisher, abstract_snippet (truncated to 300 chars), view/download/citation counts.
export function formatDoiSummary(record: DoiRecord): object { const a = record.attributes; const title = a.titles?.[0]?.title ?? "(no title)"; const creators = (a.creators ?? []).slice(0, 3).map(formatCreator); const firstDesc = a.descriptions?.[0]?.description ?? ""; const abstract_snippet = firstDesc.length > 300 ? firstDesc.slice(0, 300) + "…" : firstDesc; return { doi: a.doi ?? record.id, title, creators, year: a.publicationYear, resource_type: a.types?.resourceTypeGeneral ?? a.resourceTypeGeneral, publisher: a.publisher, abstract_snippet: abstract_snippet || undefined, view_count: a.viewCount, download_count: a.downloadCount, citation_count: a.citationCount, }; } - src/cache/index.ts:22-110 (helper)getCached helper: generic LRU cache wrapper used heavily by the handler to cache DataCite API responses for DOI records, citations, references, and other search results.
export async function getCached<T extends {}>( cache: LRUCache<string, T>, key: string, fetcher: () => Promise<T> ): Promise<T> { const hit = cache.get(key); if (hit !== undefined) return hit; const value = await fetcher(); cache.set(key, value); return value; }