@cyanheads/pubmed-mcp-server 2.0.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +65 -12
  2. package/dist/index.js +1313 -794
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -135501,7 +135501,7 @@ config(en_default());
135501
135501
  // package.json
135502
135502
  var package_default = {
135503
135503
  name: "@cyanheads/pubmed-mcp-server",
135504
- version: "2.0.1",
135504
+ version: "2.1.1",
135505
135505
  mcpName: "io.github.cyanheads/pubmed-mcp-server",
135506
135506
  description: "MCP server for PubMed/NCBI E-utilities integration. Search articles, fetch metadata, generate citations, explore MeSH terms, and discover related research.",
135507
135507
  main: "dist/index.js",
@@ -139088,7 +139088,16 @@ var NCBI_ARRAY_JPATHS = new Set([
139088
139088
  "DescriptorRecordSet.DescriptorRecord",
139089
139089
  "ConceptList.Concept",
139090
139090
  "TermList.Term",
139091
- "TreeNumberList.TreeNumber"
139091
+ "TreeNumberList.TreeNumber",
139092
+ "pmc-articleset.article",
139093
+ "article-meta.article-id",
139094
+ "article-meta.pub-date",
139095
+ "contrib-group.contrib",
139096
+ "kwd-group.kwd",
139097
+ "body.sec",
139098
+ "sec.sec",
139099
+ "sec.p",
139100
+ "ref-list.ref"
139092
139101
  ]);
139093
139102
  var ERROR_PATHS = [
139094
139103
  "eLinkResult.ERROR",
@@ -139117,8 +139126,8 @@ function extractTextValues(source, prefix = "") {
139117
139126
  const items = Array.isArray(source) ? source : [source];
139118
139127
  const messages = [];
139119
139128
  for (const item of items) {
139120
- if (typeof item === "string") {
139121
- messages.push(`${prefix}${item}`);
139129
+ if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
139130
+ messages.push(`${prefix}${String(item)}`);
139122
139131
  } else if (item && typeof item["#text"] === "string") {
139123
139132
  messages.push(`${prefix}${item["#text"]}`);
139124
139133
  }
@@ -139178,7 +139187,7 @@ class NcbiResponseHandler {
139178
139187
  });
139179
139188
  }
139180
139189
  const parsedXml = this.xmlParser.parse(responseText);
139181
- const hasError = resolvePath(parsedXml, "eLinkResult.ERROR") !== undefined || resolvePath(parsedXml, "eSummaryResult.ERROR") !== undefined || resolvePath(parsedXml, "PubmedArticleSet.ErrorList") !== undefined || resolvePath(parsedXml, "ERROR") !== undefined;
139190
+ const hasError = ERROR_PATHS.some((path) => resolvePath(parsedXml, path) !== undefined);
139182
139191
  if (hasError) {
139183
139192
  const errorMessages = this.extractNcbiErrorMessages(parsedXml);
139184
139193
  logger.error("NCBI API returned an error in XML response.", {
@@ -150769,809 +150778,1318 @@ class TaskManager {
150769
150778
  }
150770
150779
  }
150771
150780
 
150772
- // src/services/ncbi/formatting/citation-formatter.ts
150773
- function getYear(article) {
150774
- return article.journalInfo?.publicationDate?.year ?? "n.d.";
150775
- }
150776
- function splitPages(pages) {
150777
- if (!pages)
150778
- return {};
150779
- const parts = pages.split(/[-\u2013\u2014]/).map((p) => p.trim());
150780
- if (parts.length >= 2) {
150781
- const start2 = parts[0];
150782
- const end = parts[1];
150783
- return {
150784
- ...start2 !== undefined && { start: start2 },
150785
- ...end !== undefined && { end }
150786
- };
150787
- }
150788
- const start = parts[0];
150789
- return start !== undefined ? { start } : {};
150790
- }
150791
- function escapeBibtex(text) {
150792
- return text.replace(/\\/g, "\\textbackslash{}").replace(/&/g, "\\&").replace(/%/g, "\\%").replace(/\$/g, "\\$").replace(/#/g, "\\#").replace(/_/g, "\\_").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/~/g, "\\textasciitilde{}").replace(/\^/g, "\\textasciicircum{}");
150793
- }
150794
- function formatAuthorApa(author) {
150795
- if (author.collectiveName)
150796
- return author.collectiveName;
150797
- const last = author.lastName ?? "";
150798
- const initials = author.initials ?? author.firstName?.split(/[\s-]+/).map((part) => `${part[0]}.`).join(" ");
150799
- if (!last)
150800
- return initials ?? "";
150801
- if (!initials)
150802
- return last;
150803
- const formatted = initials.replace(/[^A-Za-z]/g, "").split("").map((c) => `${c}.`).join(" ");
150804
- return `${last}, ${formatted}`;
150805
- }
150806
- function formatAuthorsApa(authors) {
150807
- const formatted = authors.map(formatAuthorApa);
150808
- if (formatted.length === 0)
150781
+ // src/services/ncbi/parsing/pmc-article-parser.ts
150782
+ function extractTextContent(node) {
150783
+ if (node === undefined || node === null)
150809
150784
  return "";
150810
- if (formatted.length === 1)
150811
- return formatted[0] ?? "";
150812
- if (formatted.length === 2)
150813
- return `${formatted[0]}, & ${formatted[1]}`;
150814
- if (formatted.length <= 20) {
150815
- const allButLast = formatted.slice(0, -1).join(", ");
150816
- return `${allButLast}, & ${formatted.at(-1)}`;
150785
+ if (typeof node === "string")
150786
+ return node;
150787
+ if (typeof node === "number" || typeof node === "boolean")
150788
+ return String(node);
150789
+ if (Array.isArray(node)) {
150790
+ return node.map(extractTextContent).filter(Boolean).join(" ");
150817
150791
  }
150818
- const first19 = formatted.slice(0, 19).join(", ");
150819
- return `${first19}, ... ${formatted.at(-1)}`;
150820
- }
150821
- function formatAuthorMla(author, isFirst) {
150822
- if (author.collectiveName)
150823
- return author.collectiveName;
150824
- const last = author.lastName ?? "";
150825
- const first = author.firstName ?? "";
150826
- if (!last && !first)
150827
- return "";
150828
- if (!first)
150829
- return last;
150830
- if (!last)
150831
- return first;
150832
- return isFirst ? `${last}, ${first}` : `${first} ${last}`;
150833
- }
150834
- function formatAuthorsMla(authors) {
150835
- const first = authors[0];
150836
- if (!first)
150837
- return "";
150838
- if (authors.length === 1)
150839
- return formatAuthorMla(first, true);
150840
- if (authors.length === 2) {
150841
- const second = authors[1];
150842
- return second ? `${formatAuthorMla(first, true)}, and ${formatAuthorMla(second, false)}` : formatAuthorMla(first, true);
150792
+ if (typeof node === "object") {
150793
+ const obj = node;
150794
+ const parts = [];
150795
+ if (obj["#text"] !== undefined) {
150796
+ const text = typeof obj["#text"] === "string" ? obj["#text"] : String(obj["#text"]);
150797
+ if (text)
150798
+ parts.push(text);
150799
+ }
150800
+ for (const key of Object.keys(obj)) {
150801
+ if (key === "#text" || key.startsWith("@_"))
150802
+ continue;
150803
+ const childText = extractTextContent(obj[key]);
150804
+ if (childText)
150805
+ parts.push(childText);
150806
+ }
150807
+ return parts.join(" ").replace(/\s+/g, " ").trim();
150843
150808
  }
150844
- return `${formatAuthorMla(first, true)}, et al.`;
150845
- }
150846
- function formatAuthorBibtex(author) {
150847
- if (author.collectiveName)
150848
- return `{${escapeBibtex(author.collectiveName)}}`;
150849
- const last = author.lastName ? escapeBibtex(author.lastName) : "";
150850
- const first = author.firstName ? escapeBibtex(author.firstName) : "";
150851
- if (!last && !first)
150852
- return "";
150853
- if (!first)
150854
- return `{${last}}`;
150855
- if (!last)
150856
- return first;
150857
- return `{${last}}, ${first}`;
150809
+ return "";
150858
150810
  }
150859
- function formatApa(article) {
150860
- const parts = [];
150861
- const authorStr = article.authors?.length ? formatAuthorsApa(article.authors) : "";
150862
- if (authorStr) {
150863
- parts.push(authorStr);
150864
- }
150865
- const year = getYear(article);
150866
- parts.push(`(${year}).`);
150867
- if (article.title) {
150868
- const title = article.title.replace(/\.\s*$/, "");
150869
- parts.push(`${title}.`);
150811
+ function extractArticleId(articleIds, pubIdType) {
150812
+ for (const id of ensureArray(articleIds)) {
150813
+ if (getAttribute(id, "pub-id-type") === pubIdType) {
150814
+ const val = id["#text"];
150815
+ if (val !== undefined)
150816
+ return String(val);
150817
+ }
150870
150818
  }
150871
- const journal = article.journalInfo;
150872
- if (journal?.title) {
150873
- let journalPart = `*${journal.title}*`;
150874
- if (journal.volume) {
150875
- journalPart += `, *${journal.volume}*`;
150876
- if (journal.issue) {
150877
- journalPart += `(${journal.issue})`;
150819
+ return;
150820
+ }
150821
+ function extractJatsAuthors(contribGroups) {
150822
+ if (!contribGroups)
150823
+ return [];
150824
+ const authors = [];
150825
+ for (const group of ensureArray(contribGroups)) {
150826
+ for (const contrib of ensureArray(group.contrib)) {
150827
+ if (contrib["@_contrib-type"] && contrib["@_contrib-type"] !== "author")
150828
+ continue;
150829
+ const collectiveName = extractTextContent(contrib.collab);
150830
+ if (collectiveName) {
150831
+ authors.push({ collectiveName });
150832
+ continue;
150833
+ }
150834
+ if (contrib.name) {
150835
+ const lastName = extractTextContent(contrib.name.surname) || undefined;
150836
+ const givenNames = extractTextContent(contrib.name["given-names"]) || undefined;
150837
+ authors.push({
150838
+ ...lastName && { lastName },
150839
+ ...givenNames && { givenNames }
150840
+ });
150878
150841
  }
150879
150842
  }
150880
- if (journal.pages) {
150881
- journalPart += `, ${journal.pages}`;
150882
- }
150883
- journalPart += ".";
150884
- parts.push(journalPart);
150885
- }
150886
- if (article.doi) {
150887
- parts.push(`https://doi.org/${article.doi}`);
150888
150843
  }
150889
- return parts.join(" ");
150844
+ return authors;
150890
150845
  }
150891
- function formatMla(article) {
150892
- const parts = [];
150893
- const authorStr = article.authors?.length ? formatAuthorsMla(article.authors) : "";
150894
- if (authorStr) {
150895
- parts.push(authorStr.endsWith(".") ? authorStr : `${authorStr}.`);
150896
- }
150897
- if (article.title) {
150898
- const title = article.title.replace(/\.\s*$/, "");
150899
- parts.push(`"${title}."`);
150846
+ function extractAffiliations(affs) {
150847
+ if (!affs)
150848
+ return [];
150849
+ const result = [];
150850
+ for (const aff of ensureArray(affs)) {
150851
+ const text = extractTextContent(aff);
150852
+ if (text)
150853
+ result.push(text);
150900
150854
  }
150901
- const journal = article.journalInfo;
150902
- if (journal?.title) {
150903
- const detailParts = [];
150904
- detailParts.push(`*${journal.title}*`);
150905
- if (journal.volume) {
150906
- detailParts.push(`vol. ${journal.volume}`);
150907
- }
150908
- if (journal.issue) {
150909
- detailParts.push(`no. ${journal.issue}`);
150855
+ return result;
150856
+ }
150857
+ function extractJournal(journalMeta, articleMeta) {
150858
+ if (!journalMeta)
150859
+ return;
150860
+ const title = extractTextContent(journalMeta["journal-title-group"]?.["journal-title"]) || extractTextContent(journalMeta["journal-title"]) || undefined;
150861
+ const issns = ensureArray(journalMeta.issn);
150862
+ const issn = issns.length > 0 ? getText(issns[0]) || undefined : undefined;
150863
+ const volume = articleMeta?.volume ? extractTextContent(articleMeta.volume) || undefined : undefined;
150864
+ const issue2 = articleMeta?.issue ? extractTextContent(articleMeta.issue) || undefined : undefined;
150865
+ const fpage = articleMeta?.fpage ? extractTextContent(articleMeta.fpage) : undefined;
150866
+ const lpage = articleMeta?.lpage ? extractTextContent(articleMeta.lpage) : undefined;
150867
+ const pages = fpage && lpage ? `${fpage}-${lpage}` : fpage || undefined;
150868
+ if (!title && !issn && !volume && !issue2 && !pages)
150869
+ return;
150870
+ return {
150871
+ ...title && { title },
150872
+ ...issn && { issn },
150873
+ ...volume && { volume },
150874
+ ...issue2 && { issue: issue2 },
150875
+ ...pages && { pages }
150876
+ };
150877
+ }
150878
+ function extractPubDate(pubDates) {
150879
+ if (!pubDates)
150880
+ return;
150881
+ const dates = ensureArray(pubDates);
150882
+ const preferred = dates.find((d) => getAttribute(d, "pub-type") === "epub") ?? dates.find((d) => getAttribute(d, "pub-type") === "ppub") ?? dates.find((d) => getAttribute(d, "date-type") === "pub") ?? dates[0];
150883
+ if (!preferred)
150884
+ return;
150885
+ const year = extractTextContent(preferred.year) || undefined;
150886
+ const month = extractTextContent(preferred.month) || undefined;
150887
+ const day = extractTextContent(preferred.day) || undefined;
150888
+ if (!year)
150889
+ return;
150890
+ return {
150891
+ year,
150892
+ ...month && { month },
150893
+ ...day && { day }
150894
+ };
150895
+ }
150896
+ function extractAbstract(abstractNode) {
150897
+ if (!abstractNode)
150898
+ return;
150899
+ if (typeof abstractNode === "object" && !Array.isArray(abstractNode)) {
150900
+ const obj = abstractNode;
150901
+ if (obj.sec) {
150902
+ const sections = ensureArray(obj.sec);
150903
+ const parts = [];
150904
+ for (const sec of sections) {
150905
+ const secObj = sec;
150906
+ const title = extractTextContent(secObj.title);
150907
+ const text2 = extractTextContent(secObj.p);
150908
+ if (title && text2) {
150909
+ parts.push(`${title}: ${text2}`);
150910
+ } else if (text2) {
150911
+ parts.push(text2);
150912
+ }
150913
+ }
150914
+ const result = parts.join(`
150915
+
150916
+ `).trim();
150917
+ return result || undefined;
150910
150918
  }
150911
- const year = getYear(article);
150912
- if (year !== "n.d.") {
150913
- detailParts.push(year);
150919
+ if (obj.p) {
150920
+ const text2 = extractTextContent(obj.p);
150921
+ return text2 || undefined;
150914
150922
  }
150915
- if (journal.pages) {
150916
- detailParts.push(`pp. ${journal.pages}`);
150923
+ }
150924
+ const text = extractTextContent(abstractNode);
150925
+ return text || undefined;
150926
+ }
150927
+ function extractKeywords(kwdGroups) {
150928
+ if (!kwdGroups)
150929
+ return [];
150930
+ const keywords = [];
150931
+ for (const group of ensureArray(kwdGroups)) {
150932
+ for (const kwd of ensureArray(group.kwd)) {
150933
+ const text = extractTextContent(kwd);
150934
+ if (text)
150935
+ keywords.push(text);
150917
150936
  }
150918
- parts.push(`${detailParts.join(", ")}.`);
150919
150937
  }
150920
- if (article.doi) {
150921
- parts.push(`https://doi.org/${article.doi}.`);
150938
+ return keywords;
150939
+ }
150940
+ function extractBodySections(body) {
150941
+ if (!body)
150942
+ return [];
150943
+ if (!body.sec) {
150944
+ if (body.p) {
150945
+ const text = extractTextContent(body.p);
150946
+ return text ? [{ text }] : [];
150947
+ }
150948
+ return [];
150922
150949
  }
150923
- return parts.join(" ");
150950
+ return ensureArray(body.sec).map(extractSection).filter((s) => s !== null);
150924
150951
  }
150925
- function formatBibtex(article) {
150926
- const key = `pmid${article.pmid}`;
150927
- const fields = [];
150928
- if (article.authors?.length) {
150929
- const authorStr = article.authors.map(formatAuthorBibtex).filter(Boolean).join(" and ");
150930
- if (authorStr)
150931
- fields.push(["author", authorStr]);
150952
+ function extractSection(sec) {
150953
+ const title = extractTextContent(sec.title) || undefined;
150954
+ const label = extractTextContent(sec.label) || undefined;
150955
+ const paragraphs = ensureArray(sec.p);
150956
+ const textParts = [];
150957
+ for (const p of paragraphs) {
150958
+ const text2 = extractTextContent(p);
150959
+ if (text2)
150960
+ textParts.push(text2);
150932
150961
  }
150933
- if (article.title) {
150934
- fields.push(["title", `{${escapeBibtex(article.title)}}`]);
150962
+ const subsections = sec.sec ? ensureArray(sec.sec).map(extractSection).filter((s) => s !== null) : undefined;
150963
+ const text = textParts.join(`
150964
+
150965
+ `);
150966
+ if (!text && (!subsections || subsections.length === 0))
150967
+ return null;
150968
+ return {
150969
+ ...title && { title },
150970
+ ...label && { label },
150971
+ text,
150972
+ ...subsections && subsections.length > 0 && { subsections }
150973
+ };
150974
+ }
150975
+ function extractReferences(back) {
150976
+ if (!back?.["ref-list"]?.ref)
150977
+ return [];
150978
+ const refs = ensureArray(back["ref-list"].ref);
150979
+ const results = [];
150980
+ for (const ref of refs) {
150981
+ const citationNode = ref["mixed-citation"] ?? ref["element-citation"];
150982
+ const citation = extractTextContent(citationNode);
150983
+ if (!citation)
150984
+ continue;
150985
+ const label = ref.label ? extractTextContent(ref.label) || undefined : undefined;
150986
+ results.push({
150987
+ ...ref["@_id"] && { id: ref["@_id"] },
150988
+ ...label && { label },
150989
+ citation
150990
+ });
150991
+ }
150992
+ return results;
150993
+ }
150994
+ function parsePmcArticle(xmlArticle) {
150995
+ const front = xmlArticle.front;
150996
+ const articleMeta = front?.["article-meta"];
150997
+ const journalMeta = front?.["journal-meta"];
150998
+ const articleIds = articleMeta?.["article-id"];
150999
+ const pmcId = extractArticleId(articleIds, "pmcid") ?? extractArticleId(articleIds, "pmc-uid") ?? "";
151000
+ const pmid = extractArticleId(articleIds, "pmid");
151001
+ const doi = extractArticleId(articleIds, "doi");
151002
+ const title = articleMeta?.["title-group"] ? extractTextContent(articleMeta["title-group"]["article-title"]) || undefined : undefined;
151003
+ const authors = extractJatsAuthors(articleMeta?.["contrib-group"]);
151004
+ const affiliations = extractAffiliations(articleMeta?.aff);
151005
+ const journal = extractJournal(journalMeta, articleMeta);
151006
+ const publicationDate = extractPubDate(articleMeta?.["pub-date"]);
151007
+ const abstract = extractAbstract(articleMeta?.abstract);
151008
+ const keywords = extractKeywords(articleMeta?.["kwd-group"]);
151009
+ const sections = extractBodySections(xmlArticle.body);
151010
+ const references = extractReferences(xmlArticle.back);
151011
+ const normalizedPmcId = !pmcId ? "" : pmcId.startsWith("PMC") ? pmcId : `PMC${pmcId}`;
151012
+ return {
151013
+ pmcId: normalizedPmcId,
151014
+ ...pmid && { pmid },
151015
+ ...doi && { doi },
151016
+ ...title && { title },
151017
+ ...authors.length > 0 && { authors },
151018
+ ...affiliations.length > 0 && { affiliations },
151019
+ ...journal && { journal },
151020
+ ...publicationDate && { publicationDate },
151021
+ ...abstract && { abstract },
151022
+ ...keywords.length > 0 && { keywords },
151023
+ sections,
151024
+ ...references.length > 0 && { references },
151025
+ ...xmlArticle["@_article-type"] && { articleType: xmlArticle["@_article-type"] },
151026
+ pmcUrl: `https://www.ncbi.nlm.nih.gov/pmc/articles/${normalizedPmcId}/`,
151027
+ ...pmid && { pubmedUrl: `https://pubmed.ncbi.nlm.nih.gov/${pmid}/` }
151028
+ };
151029
+ }
151030
+
151031
+ // src/utils/formatting/markdownBuilder.ts
151032
+ class MarkdownBuilder {
151033
+ sections = [];
151034
+ h1(text, emoji3) {
151035
+ const prefix = emoji3 ? `${emoji3} ` : "";
151036
+ this.sections.push(`# ${prefix}${text}
151037
+
151038
+ `);
151039
+ return this;
150935
151040
  }
150936
- const journal = article.journalInfo;
150937
- if (journal?.title) {
150938
- fields.push(["journal", escapeBibtex(journal.title)]);
151041
+ h2(text, emoji3) {
151042
+ const prefix = emoji3 ? `${emoji3} ` : "";
151043
+ this.sections.push(`## ${prefix}${text}
151044
+
151045
+ `);
151046
+ return this;
150939
151047
  }
150940
- const year = getYear(article);
150941
- if (year !== "n.d.") {
150942
- fields.push(["year", year]);
151048
+ h3(text, emoji3) {
151049
+ const prefix = emoji3 ? `${emoji3} ` : "";
151050
+ this.sections.push(`### ${prefix}${text}
151051
+
151052
+ `);
151053
+ return this;
150943
151054
  }
150944
- if (journal?.volume) {
150945
- fields.push(["volume", escapeBibtex(journal.volume)]);
151055
+ h4(text, emoji3) {
151056
+ const prefix = emoji3 ? `${emoji3} ` : "";
151057
+ this.sections.push(`#### ${prefix}${text}
151058
+
151059
+ `);
151060
+ return this;
150946
151061
  }
150947
- if (journal?.issue) {
150948
- fields.push(["number", escapeBibtex(journal.issue)]);
151062
+ keyValue(key, value) {
151063
+ const displayValue = value === null ? "null" : String(value);
151064
+ this.sections.push(`**${key}:** ${displayValue}
151065
+ `);
151066
+ return this;
150949
151067
  }
150950
- if (journal?.pages) {
150951
- fields.push(["pages", escapeBibtex(journal.pages)]);
151068
+ keyValuePlain(key, value) {
151069
+ const displayValue = value === null ? "null" : String(value);
151070
+ this.sections.push(`${key}: ${displayValue}
151071
+ `);
151072
+ return this;
150952
151073
  }
150953
- if (article.doi) {
150954
- fields.push(["doi", article.doi]);
151074
+ list(items, ordered = false) {
151075
+ if (items.length === 0)
151076
+ return this;
151077
+ const marker = ordered ? (i) => `${i + 1}.` : () => "-";
151078
+ this.sections.push(`${items.map((item, i) => `${marker(i)} ${item}`).join(`
151079
+ `)}
151080
+
151081
+ `);
151082
+ return this;
150955
151083
  }
150956
- fields.push(["pmid", article.pmid]);
150957
- const maxKeyLen = Math.max(...fields.map(([k]) => k.length));
150958
- const fieldLines = fields.map(([k, v]) => ` ${k.padEnd(maxKeyLen)} = {${v}}`).join(`,
151084
+ codeBlock(content, language = "") {
151085
+ this.sections.push(`\`\`\`${language}
151086
+ ${content}
151087
+ \`\`\`
151088
+
150959
151089
  `);
150960
- return `@article{${key},
150961
- ${fieldLines}
150962
- }`;
150963
- }
150964
- function formatRis(article) {
150965
- const lines = [];
150966
- const tag = (code, value) => {
150967
- if (value)
150968
- lines.push(`${code} - ${value}`);
150969
- };
150970
- lines.push("TY - JOUR");
150971
- if (article.authors?.length) {
150972
- for (const author of article.authors) {
150973
- if (author.collectiveName) {
150974
- tag("AU", author.collectiveName);
150975
- } else {
150976
- const last = author.lastName ?? "";
150977
- const first = author.firstName ?? "";
150978
- if (last || first) {
150979
- tag("AU", first ? `${last}, ${first}` : last);
150980
- }
150981
- }
150982
- }
151090
+ return this;
150983
151091
  }
150984
- tag("TI", article.title);
150985
- const journal = article.journalInfo;
150986
- if (journal?.title) {
150987
- tag("JF", journal.title);
151092
+ inlineCode(code) {
151093
+ this.sections.push(`\`${code}\``);
151094
+ return this;
150988
151095
  }
150989
- if (journal?.isoAbbreviation) {
150990
- tag("JO", journal.isoAbbreviation);
151096
+ paragraph(text) {
151097
+ this.sections.push(`${text}
151098
+
151099
+ `);
151100
+ return this;
150991
151101
  }
150992
- const year = getYear(article);
150993
- if (year !== "n.d.") {
150994
- tag("PY", year);
151102
+ blockquote(text) {
151103
+ const lines = text.split(`
151104
+ `);
151105
+ const quoted = lines.map((line) => `> ${line}`).join(`
151106
+ `);
151107
+ this.sections.push(`${quoted}
151108
+
151109
+ `);
151110
+ return this;
150995
151111
  }
150996
- tag("VL", journal?.volume);
150997
- tag("IS", journal?.issue);
150998
- if (journal?.pages) {
150999
- const { start, end } = splitPages(journal.pages);
151000
- tag("SP", start);
151001
- tag("EP", end);
151112
+ hr() {
151113
+ this.sections.push(`---
151114
+
151115
+ `);
151116
+ return this;
151002
151117
  }
151003
- tag("DO", article.doi);
151004
- tag("AN", article.pmid);
151005
- lines.push(`UR - https://pubmed.ncbi.nlm.nih.gov/${article.pmid}/`);
151006
- if (article.keywords?.length) {
151007
- for (const kw of article.keywords) {
151008
- tag("KW", kw);
151118
+ link(text, url2) {
151119
+ this.sections.push(`[${text}](${url2})`);
151120
+ return this;
151121
+ }
151122
+ table(headers, rows) {
151123
+ if (headers.length === 0 || rows.length === 0)
151124
+ return this;
151125
+ this.sections.push(`| ${headers.join(" | ")} |
151126
+ `);
151127
+ this.sections.push(`| ${headers.map(() => "---").join(" | ")} |
151128
+ `);
151129
+ rows.forEach((row) => {
151130
+ this.sections.push(`| ${row.join(" | ")} |
151131
+ `);
151132
+ });
151133
+ this.sections.push(`
151134
+ `);
151135
+ return this;
151136
+ }
151137
+ section(title, levelOrContent, content) {
151138
+ let level;
151139
+ let callback;
151140
+ if (typeof levelOrContent === "function") {
151141
+ level = 2;
151142
+ callback = levelOrContent;
151143
+ } else {
151144
+ level = levelOrContent;
151145
+ callback = content ?? (() => {});
151146
+ }
151147
+ switch (level) {
151148
+ case 2:
151149
+ this.h2(title);
151150
+ break;
151151
+ case 3:
151152
+ this.h3(title);
151153
+ break;
151154
+ case 4:
151155
+ this.h4(title);
151156
+ break;
151009
151157
  }
151158
+ callback();
151159
+ return this;
151010
151160
  }
151011
- tag("AB", article.abstractText);
151012
- lines.push("ER - ");
151013
- return lines.join(`
151161
+ details(summary, details) {
151162
+ this.sections.push(`<details>
151163
+ <summary>${summary}</summary>
151164
+
151014
151165
  `);
151015
- }
151016
- function formatCitation(article, style) {
151017
- switch (style) {
151018
- case "apa":
151019
- return formatApa(article);
151020
- case "mla":
151021
- return formatMla(article);
151022
- case "bibtex":
151023
- return formatBibtex(article);
151024
- case "ris":
151025
- return formatRis(article);
151166
+ this.sections.push(`${details}
151167
+
151168
+ `);
151169
+ this.sections.push(`</details>
151170
+
151171
+ `);
151172
+ return this;
151026
151173
  }
151027
- }
151028
- function formatCitations(article, styles) {
151029
- const result = {};
151030
- for (const style of styles) {
151031
- result[style] = formatCitation(article, style);
151174
+ alert(type, content) {
151175
+ const typeUpper = type.toUpperCase();
151176
+ const lines = content.split(`
151177
+ `);
151178
+ this.sections.push(`> [!${typeUpper}]
151179
+ `);
151180
+ lines.forEach((line) => {
151181
+ this.sections.push(`> ${line}
151182
+ `);
151183
+ });
151184
+ this.sections.push(`
151185
+ `);
151186
+ return this;
151032
151187
  }
151033
- return result;
151034
- }
151188
+ taskList(items) {
151189
+ if (items.length === 0)
151190
+ return this;
151191
+ this.sections.push(`${items.map((item) => `- [${item.checked ? "x" : " "}] ${item.text}`).join(`
151192
+ `)}
151035
151193
 
151036
- // src/services/ncbi/parsing/article-parser.ts
151037
- function extractAuthors(authorListXml) {
151038
- if (!authorListXml)
151039
- return { authors: [], affiliations: [] };
151040
- const affiliationMap = new Map;
151041
- const affiliationList = [];
151042
- function getAffiliationIndex(text) {
151043
- const existing = affiliationMap.get(text);
151044
- if (existing !== undefined)
151045
- return existing;
151046
- const idx = affiliationList.length;
151047
- affiliationList.push(text);
151048
- affiliationMap.set(text, idx);
151049
- return idx;
151194
+ `);
151195
+ return this;
151050
151196
  }
151051
- const xmlAuthors = ensureArray(authorListXml.Author);
151052
- const authors = xmlAuthors.map((auth) => {
151053
- const collectiveName = getText(auth.CollectiveName);
151054
- if (collectiveName) {
151055
- return { collectiveName };
151197
+ image(altText, url2, title) {
151198
+ const titlePart = title ? ` "${title}"` : "";
151199
+ this.sections.push(`![${altText}](${url2}${titlePart})
151200
+
151201
+ `);
151202
+ return this;
151203
+ }
151204
+ strikethrough(text) {
151205
+ this.sections.push(`~~${text}~~`);
151206
+ return this;
151207
+ }
151208
+ diff(changes) {
151209
+ const lines = [];
151210
+ if (changes.context) {
151211
+ lines.push(...changes.context.map((line) => ` ${line}`));
151056
151212
  }
151057
- const authorAffiliationInfos = ensureArray(auth.AffiliationInfo);
151058
- const indices = [];
151059
- for (const info of authorAffiliationInfos) {
151060
- const text = getText(info?.Affiliation);
151061
- if (text)
151062
- indices.push(getAffiliationIndex(text));
151213
+ if (changes.deletions) {
151214
+ lines.push(...changes.deletions.map((line) => `- ${line}`));
151063
151215
  }
151064
- let orcid;
151065
- const identifiers = ensureArray(auth.Identifier);
151066
- for (const id of identifiers) {
151067
- if (getAttribute(id, "Source") === "ORCID") {
151068
- const val = getText(id);
151069
- if (val) {
151070
- orcid = val;
151071
- break;
151072
- }
151073
- }
151216
+ if (changes.additions) {
151217
+ lines.push(...changes.additions.map((line) => `+ ${line}`));
151074
151218
  }
151075
- return {
151076
- lastName: getText(auth.LastName),
151077
- firstName: getText(auth.ForeName),
151078
- initials: getText(auth.Initials),
151079
- ...indices.length > 0 && { affiliationIndices: indices },
151080
- ...orcid && { orcid }
151081
- };
151082
- });
151083
- return { authors, affiliations: affiliationList };
151084
- }
151085
- function extractJournalInfo(journalXml, articleXml) {
151086
- if (!journalXml)
151087
- return;
151088
- const pubDate = journalXml.JournalIssue?.PubDate;
151089
- const year = getText(pubDate?.Year, getText(pubDate?.MedlineDate, "").match(/\d{4}/)?.[0]);
151090
- const issnElement = journalXml.ISSN;
151091
- const issnValue = getText(issnElement);
151092
- const issnType = getAttribute(issnElement, "IssnType");
151093
- const issn = issnType === "Electronic" ? undefined : issnValue || undefined;
151094
- const eIssn = issnType === "Electronic" ? issnValue || undefined : undefined;
151095
- return {
151096
- title: getText(journalXml.Title),
151097
- isoAbbreviation: getText(journalXml.ISOAbbreviation),
151098
- ...issn && { issn },
151099
- ...eIssn && { eIssn },
151100
- volume: getText(journalXml.JournalIssue?.Volume),
151101
- issue: getText(journalXml.JournalIssue?.Issue),
151102
- pages: getText(articleXml?.Pagination?.MedlinePgn),
151103
- publicationDate: {
151104
- ...year && { year },
151105
- ...getText(pubDate?.Month) && { month: getText(pubDate?.Month) },
151106
- ...getText(pubDate?.Day) && { day: getText(pubDate?.Day) },
151107
- ...getText(pubDate?.MedlineDate) && { medlineDate: getText(pubDate?.MedlineDate) }
151219
+ if (lines.length > 0) {
151220
+ this.codeBlock(lines.join(`
151221
+ `), "diff");
151108
151222
  }
151109
- };
151223
+ return this;
151224
+ }
151225
+ badge(label, message, color = "blue") {
151226
+ const encodedLabel = encodeURIComponent(label);
151227
+ const encodedMessage = encodeURIComponent(message);
151228
+ const url2 = `https://img.shields.io/badge/${encodedLabel}-${encodedMessage}-${color}`;
151229
+ this.sections.push(`![${label}: ${message}](${url2})`);
151230
+ return this;
151231
+ }
151232
+ bold(text) {
151233
+ this.sections.push(`**${text}**`);
151234
+ return this;
151235
+ }
151236
+ italic(text) {
151237
+ this.sections.push(`*${text}*`);
151238
+ return this;
151239
+ }
151240
+ boldItalic(text) {
151241
+ this.sections.push(`***${text}***`);
151242
+ return this;
151243
+ }
151244
+ raw(markdown) {
151245
+ this.sections.push(markdown);
151246
+ return this;
151247
+ }
151248
+ blankLine() {
151249
+ this.sections.push(`
151250
+ `);
151251
+ return this;
151252
+ }
151253
+ text(text) {
151254
+ this.sections.push(text);
151255
+ return this;
151256
+ }
151257
+ when(condition, content) {
151258
+ if (condition) {
151259
+ content();
151260
+ }
151261
+ return this;
151262
+ }
151263
+ build() {
151264
+ return this.sections.join("").trim();
151265
+ }
151266
+ reset() {
151267
+ this.sections = [];
151268
+ return this;
151269
+ }
151110
151270
  }
151111
- function extractMeshTerms(meshHeadingListXml) {
151112
- if (!meshHeadingListXml)
151113
- return [];
151114
- const meshHeadings = ensureArray(meshHeadingListXml.MeshHeading);
151115
- return meshHeadings.map((mh) => {
151116
- const isMajorDescriptor = getAttribute(mh.DescriptorName, "MajorTopicYN") === "Y";
151117
- const isMajorRoot = getAttribute(mh, "MajorTopicYN") === "Y";
151118
- const descriptorUi = getAttribute(mh.DescriptorName, "UI");
151119
- const rawQualifiers = ensureArray(mh.QualifierName);
151120
- const qualifiers = rawQualifiers.flatMap((q) => {
151121
- const name = getText(q);
151122
- if (!name)
151123
- return [];
151124
- const ui = getAttribute(q, "UI");
151125
- return {
151126
- qualifierName: name,
151127
- ...ui && { qualifierUi: ui },
151128
- isMajorTopic: getAttribute(q, "MajorTopicYN") === "Y"
151129
- };
151130
- });
151131
- const isMajorAnyQualifier = qualifiers.some((q) => q.isMajorTopic);
151132
- return {
151133
- descriptorName: getText(mh.DescriptorName),
151134
- ...descriptorUi && { descriptorUi },
151135
- ...qualifiers.length > 0 && { qualifiers },
151136
- isMajorTopic: isMajorRoot || isMajorDescriptor || isMajorAnyQualifier
151137
- };
151138
- });
151271
+ function markdown() {
151272
+ return new MarkdownBuilder;
151139
151273
  }
151140
- function extractGrants(grantListXml) {
151141
- if (!grantListXml)
151142
- return [];
151143
- const grants = ensureArray(grantListXml.Grant);
151144
- return grants.map((g) => {
151145
- const grantId = getText(g.GrantID);
151146
- const acronym = getText(g.Acronym);
151147
- const agency = getText(g.Agency);
151148
- const country = getText(g.Country);
151149
- return {
151150
- ...grantId && { grantId },
151151
- ...acronym && { acronym },
151152
- ...agency && { agency },
151153
- ...country && { country }
151154
- };
151155
- });
151274
+
151275
+ // src/mcp-server/tools/definitions/pmc-fetch.tool.ts
151276
+ var ncbi2 = () => container.resolve(NcbiServiceToken);
151277
+ var TOOL_NAME = "pmc_fetch";
151278
+ var TOOL_TITLE = "PMC Full-Text Fetch";
151279
+ var TOOL_DESCRIPTION = "Fetch full-text articles from PubMed Central (PMC). Returns complete article body text, sections, and references for open-access articles. Accepts PMC IDs directly or PubMed IDs (auto-resolved via ELink). Only articles available in PMC will return full text.";
151280
+ var TOOL_ANNOTATIONS = {
151281
+ idempotentHint: true,
151282
+ openWorldHint: true,
151283
+ readOnlyHint: true
151284
+ };
151285
+ var InputSchema = exports_external.object({
151286
+ includeReferences: exports_external.boolean().default(false).describe("Include reference list from back matter"),
151287
+ maxSections: exports_external.number().int().min(1).max(50).optional().describe("Maximum number of top-level body sections to return"),
151288
+ pmcids: exports_external.array(exports_external.string()).min(1).max(10).optional().describe('PMC IDs to fetch (e.g., ["PMC9575052"] or ["9575052"]). Provide pmcids or pmids, not both.'),
151289
+ pmids: exports_external.array(exports_external.string().regex(/^\d+$/)).min(1).max(10).optional().describe("PubMed IDs to resolve to PMC articles via ELink. Only articles available in PMC will be returned."),
151290
+ sections: exports_external.array(exports_external.string()).optional().describe('Filter to specific sections by title (case-insensitive match, e.g., ["methods", "results"])')
151291
+ }).refine((data) => data.pmcids ?? data.pmids, {
151292
+ message: "Either pmcids or pmids must be provided"
151293
+ }).refine((data) => !(data.pmcids && data.pmids), {
151294
+ message: "Provide pmcids or pmids, not both"
151295
+ });
151296
+ var SectionSchema = exports_external.object({
151297
+ label: exports_external.string().optional().describe('Section label (e.g., "1", "2.1")'),
151298
+ subsections: exports_external.lazy(() => exports_external.array(SectionSchema)).optional().describe("Nested subsections"),
151299
+ text: exports_external.string().describe("Section body text"),
151300
+ title: exports_external.string().optional().describe("Section heading")
151301
+ });
151302
+ var ArticleSchema = exports_external.object({
151303
+ abstract: exports_external.string().optional().describe("Article abstract"),
151304
+ affiliations: exports_external.array(exports_external.string()).optional().describe("Author affiliations"),
151305
+ articleType: exports_external.string().optional().describe('Article type (e.g., "research-article")'),
151306
+ authors: exports_external.array(exports_external.object({
151307
+ collectiveName: exports_external.string().optional(),
151308
+ givenNames: exports_external.string().optional(),
151309
+ lastName: exports_external.string().optional()
151310
+ })).optional().describe("Author list"),
151311
+ doi: exports_external.string().optional().describe("Digital Object Identifier"),
151312
+ journal: exports_external.any().optional().describe("Journal information"),
151313
+ keywords: exports_external.array(exports_external.string()).optional().describe("Article keywords"),
151314
+ pmcId: exports_external.string().describe("PubMed Central ID"),
151315
+ pmcUrl: exports_external.string().describe("PMC article URL"),
151316
+ pmid: exports_external.string().optional().describe("PubMed ID"),
151317
+ pubmedUrl: exports_external.string().optional().describe("PubMed article URL"),
151318
+ publicationDate: exports_external.any().optional().describe("Publication date"),
151319
+ references: exports_external.array(exports_external.object({ citation: exports_external.string(), id: exports_external.string().optional(), label: exports_external.string().optional() })).optional().describe("Reference list"),
151320
+ sections: exports_external.array(SectionSchema).describe("Article body sections with full text"),
151321
+ title: exports_external.string().optional().describe("Article title")
151322
+ });
151323
+ var OutputSchema2 = exports_external.object({
151324
+ articles: exports_external.array(ArticleSchema).describe("Parsed full-text articles"),
151325
+ totalReturned: exports_external.number().describe("Number of articles returned"),
151326
+ unavailablePmids: exports_external.array(exports_external.string()).optional().describe("PMIDs that could not be resolved to PMC articles")
151327
+ });
151328
+ function normalizePmcId(id) {
151329
+ return id.replace(/^PMC/i, "");
151156
151330
  }
151157
- function extractDoi(articleXml, pubmedDataArticleIdList) {
151158
- if (!articleXml)
151159
- return;
151160
- const eLocationIDs = ensureArray(articleXml.ELocationID);
151161
- for (const eloc of eLocationIDs) {
151162
- if (getAttribute(eloc, "EIdType") === "doi" && getAttribute(eloc, "ValidYN") === "Y") {
151163
- const doi = getText(eloc);
151164
- if (doi)
151165
- return doi;
151166
- }
151167
- }
151168
- for (const eloc of eLocationIDs) {
151169
- if (getAttribute(eloc, "EIdType") === "doi") {
151170
- const doi = getText(eloc);
151171
- if (doi)
151172
- return doi;
151173
- }
151331
+ function extractLinkId(field) {
151332
+ if (field === undefined || field === null)
151333
+ return "";
151334
+ if (typeof field === "object") {
151335
+ return field["#text"] !== undefined ? String(field["#text"]) : "";
151174
151336
  }
151175
- const articleIds = ensureArray(articleXml.ArticleIdList?.ArticleId);
151176
- for (const aid of articleIds) {
151177
- if (getAttribute(aid, "IdType") === "doi") {
151178
- const doi = getText(aid);
151179
- if (doi)
151180
- return doi;
151337
+ return String(field);
151338
+ }
151339
+ async function resolvePmidsToPmcIds(pmids, appContext) {
151340
+ const eLinkResult = await ncbi2().eLink({
151341
+ cmd: "neighbor",
151342
+ db: "pmc",
151343
+ dbfrom: "pubmed",
151344
+ id: pmids.join(","),
151345
+ linkname: "pubmed_pmc",
151346
+ retmode: "xml"
151347
+ }, appContext);
151348
+ const resolved = new Map;
151349
+ const eLinkResults = ensureArray(eLinkResult?.eLinkResult);
151350
+ for (const result of eLinkResults) {
151351
+ if (result?.ERROR) {
151352
+ logger.warning("ELink error during PMID resolution", {
151353
+ ...appContext,
151354
+ error: result.ERROR
151355
+ });
151356
+ continue;
151181
151357
  }
151182
- }
151183
- if (pubmedDataArticleIdList) {
151184
- const pubmedDataIds = ensureArray(pubmedDataArticleIdList.ArticleId);
151185
- for (const aid of pubmedDataIds) {
151186
- if (getAttribute(aid, "IdType") === "doi") {
151187
- const doi = getText(aid);
151188
- if (doi)
151189
- return doi;
151358
+ const linkSet = result?.LinkSet;
151359
+ if (!linkSet?.LinkSetDb)
151360
+ continue;
151361
+ const linkSetDbArray = ensureArray(linkSet.LinkSetDb);
151362
+ const pmcLinkSet = linkSetDbArray.find((db) => db.LinkName === "pubmed_pmc") ?? linkSetDbArray[0];
151363
+ if (pmcLinkSet?.Link) {
151364
+ const sourcePmid = extractLinkId(linkSet.IdList?.Id);
151365
+ const links = ensureArray(pmcLinkSet.Link);
151366
+ for (const link of links) {
151367
+ const pmcId = extractLinkId(link.Id);
151368
+ if (pmcId && sourcePmid) {
151369
+ resolved.set(sourcePmid, pmcId);
151370
+ }
151190
151371
  }
151191
151372
  }
151192
151373
  }
151193
- return;
151374
+ const unavailable = pmids.filter((pmid) => !resolved.has(pmid));
151375
+ return { resolved, unavailable };
151194
151376
  }
151195
- function extractPmcId(articleXml, pubmedDataArticleIdList) {
151196
- const articleIds = ensureArray(articleXml?.ArticleIdList?.ArticleId);
151197
- for (const aid of articleIds) {
151198
- if (getAttribute(aid, "IdType") === "pmc") {
151199
- const val = getText(aid);
151200
- if (val)
151201
- return val;
151377
+ function filterSections(sections, sectionFilter) {
151378
+ const lowerFilter = sectionFilter.map((s) => s.toLowerCase());
151379
+ return sections.filter((s) => {
151380
+ if (!s.title)
151381
+ return false;
151382
+ return lowerFilter.some((f) => s.title?.toLowerCase().includes(f));
151383
+ });
151384
+ }
151385
+ async function logic(input, appContext, _sdkContext) {
151386
+ logger.info("Executing pmc_fetch tool", {
151387
+ ...appContext,
151388
+ hasPmcids: !!input.pmcids,
151389
+ hasPmids: !!input.pmids,
151390
+ idCount: (input.pmcids ?? input.pmids)?.length
151391
+ });
151392
+ let pmcIds;
151393
+ let unavailablePmids;
151394
+ if (input.pmids) {
151395
+ const resolution = await resolvePmidsToPmcIds(input.pmids, appContext);
151396
+ if (resolution.resolved.size === 0) {
151397
+ logger.notice("No PMC articles found for provided PMIDs", {
151398
+ ...appContext,
151399
+ pmids: input.pmids
151400
+ });
151401
+ return {
151402
+ articles: [],
151403
+ totalReturned: 0,
151404
+ unavailablePmids: input.pmids
151405
+ };
151202
151406
  }
151203
- }
151204
- if (pubmedDataArticleIdList) {
151205
- const pubmedDataIds = ensureArray(pubmedDataArticleIdList.ArticleId);
151206
- for (const aid of pubmedDataIds) {
151207
- if (getAttribute(aid, "IdType") === "pmc") {
151208
- const val = getText(aid);
151209
- if (val)
151210
- return val;
151211
- }
151407
+ pmcIds = [...resolution.resolved.values()];
151408
+ if (resolution.unavailable.length > 0) {
151409
+ unavailablePmids = resolution.unavailable;
151410
+ logger.debug("Some PMIDs not available in PMC", {
151411
+ ...appContext,
151412
+ unavailable: resolution.unavailable
151413
+ });
151212
151414
  }
151415
+ } else {
151416
+ pmcIds = (input.pmcids ?? []).map(normalizePmcId);
151417
+ }
151418
+ const xmlData = await ncbi2().eFetch({ db: "pmc", id: pmcIds.join(","), retmode: "xml" }, appContext, { retmode: "xml", usePost: pmcIds.length > 5 });
151419
+ if (!xmlData || !("pmc-articleset" in xmlData)) {
151420
+ throw new McpError(-32603 /* InternalError */, "Invalid PMC EFetch response: missing pmc-articleset", { requestId: appContext.requestId });
151421
+ }
151422
+ const articleSet = xmlData["pmc-articleset"];
151423
+ if (!articleSet?.article) {
151424
+ return { articles: [], totalReturned: 0, ...unavailablePmids && { unavailablePmids } };
151425
+ }
151426
+ const xmlArticles = ensureArray(articleSet.article);
151427
+ let articles = xmlArticles.map((xmlArticle) => parsePmcArticle(xmlArticle));
151428
+ if (input.sections && input.sections.length > 0) {
151429
+ const sectionFilter = input.sections;
151430
+ articles = articles.map((article) => ({
151431
+ ...article,
151432
+ sections: filterSections(article.sections, sectionFilter)
151433
+ }));
151213
151434
  }
151214
- return;
151215
- }
151216
- function extractPublicationTypes(publicationTypeListXml) {
151217
- if (!publicationTypeListXml)
151218
- return [];
151219
- const pubTypes = ensureArray(publicationTypeListXml.PublicationType);
151220
- return pubTypes.map((pt) => getText(pt)).filter(Boolean);
151435
+ if (input.maxSections !== undefined) {
151436
+ articles = articles.map((article) => ({
151437
+ ...article,
151438
+ sections: article.sections.slice(0, input.maxSections)
151439
+ }));
151440
+ }
151441
+ if (!input.includeReferences) {
151442
+ articles = articles.map((article) => {
151443
+ const { references: _refs, ...rest } = article;
151444
+ return rest;
151445
+ });
151446
+ }
151447
+ logger.notice("pmc_fetch completed", {
151448
+ ...appContext,
151449
+ requested: pmcIds.length,
151450
+ returned: articles.length
151451
+ });
151452
+ return {
151453
+ articles,
151454
+ totalReturned: articles.length,
151455
+ ...unavailablePmids && { unavailablePmids }
151456
+ };
151221
151457
  }
151222
- function extractKeywords(keywordListsXml) {
151223
- if (!keywordListsXml)
151224
- return [];
151225
- const lists = ensureArray(keywordListsXml);
151226
- const allKeywords = [];
151227
- for (const list of lists) {
151228
- for (const kw of ensureArray(list.Keyword)) {
151229
- const keywordText = getText(kw);
151230
- if (keywordText) {
151231
- allKeywords.push(keywordText);
151232
- }
151458
+ function formatSection(md, section, depth) {
151459
+ const headingLevel = Math.min(depth, 6);
151460
+ const prefix = "#".repeat(headingLevel);
151461
+ if (section.title) {
151462
+ md.text(`${prefix} ${section.title}
151463
+
151464
+ `);
151465
+ }
151466
+ if (section.text) {
151467
+ md.paragraph(section.text);
151468
+ }
151469
+ if (section.subsections) {
151470
+ for (const sub of section.subsections) {
151471
+ formatSection(md, sub, depth + 1);
151233
151472
  }
151234
151473
  }
151235
- return allKeywords;
151236
151474
  }
151237
- function extractAbstractText(abstractXml) {
151238
- if (!abstractXml || !abstractXml.AbstractText)
151239
- return;
151240
- const abstractTexts = ensureArray(abstractXml.AbstractText);
151241
- if (abstractTexts.length === 0)
151242
- return;
151243
- const processedTexts = abstractTexts.map((at) => {
151244
- if (typeof at === "string") {
151245
- return at;
151475
+ function responseFormatter(result) {
151476
+ const md = markdown().h2("PMC Full-Text Articles").keyValue("Articles Returned", result.totalReturned);
151477
+ if (result.unavailablePmids?.length) {
151478
+ md.keyValue("Unavailable PMIDs", result.unavailablePmids.join(", "));
151479
+ }
151480
+ for (const article of result.articles) {
151481
+ md.h3(article.title ?? article.pmcId);
151482
+ md.keyValue("PMCID", article.pmcId);
151483
+ if (article.pmid)
151484
+ md.keyValue("PMID", article.pmid);
151485
+ if (article.doi)
151486
+ md.keyValue("DOI", article.doi);
151487
+ md.keyValue("PMC", article.pmcUrl);
151488
+ if (article.pubmedUrl)
151489
+ md.keyValue("PubMed", article.pubmedUrl);
151490
+ if (article.authors?.length) {
151491
+ const authorStr = article.authors.slice(0, 5).map((a) => a.collectiveName ?? `${a.lastName ?? ""} ${a.givenNames ?? ""}`.trim()).join(", ");
151492
+ md.keyValue("Authors", article.authors.length > 5 ? `${authorStr}, et al.` : authorStr);
151246
151493
  }
151247
- const sectionText = getText(at);
151248
- const label = getAttribute(at, "Label");
151249
- if (label && sectionText) {
151250
- return `${label.trim()}: ${sectionText.trim()}`;
151494
+ if (article.journal) {
151495
+ const parts = [article.journal.title, article.journal.volume, article.journal.pages].filter(Boolean);
151496
+ if (parts.length > 0)
151497
+ md.keyValue("Journal", parts.join(", "));
151251
151498
  }
151252
- return sectionText.trim();
151253
- }).filter(Boolean);
151254
- if (processedTexts.length === 0)
151255
- return;
151256
- return processedTexts.join(`
151257
-
151258
- `).trim() || undefined;
151499
+ if (article.abstract) {
151500
+ md.h4("Abstract").paragraph(article.abstract);
151501
+ }
151502
+ if (article.sections.length > 0) {
151503
+ for (const section of article.sections) {
151504
+ formatSection(md, section, 4);
151505
+ }
151506
+ }
151507
+ if (article.references?.length) {
151508
+ md.h4("References");
151509
+ const refItems = article.references.map((r) => `${r.label ? `[${r.label}] ` : ""}${r.citation}`);
151510
+ md.list(refItems);
151511
+ }
151512
+ }
151513
+ return [{ type: "text", text: md.build() }];
151259
151514
  }
151260
- function extractPmid(medlineCitationXml) {
151261
- if (!medlineCitationXml || !medlineCitationXml.PMID)
151262
- return;
151263
- return getText(medlineCitationXml.PMID);
151515
+ var pmcFetchTool = {
151516
+ annotations: TOOL_ANNOTATIONS,
151517
+ description: TOOL_DESCRIPTION,
151518
+ inputSchema: InputSchema,
151519
+ logic: withToolAuth(["tool:pmc_fetch:read"], logic),
151520
+ name: TOOL_NAME,
151521
+ outputSchema: OutputSchema2,
151522
+ responseFormatter,
151523
+ title: TOOL_TITLE
151524
+ };
151525
+
151526
+ // src/services/ncbi/formatting/citation-formatter.ts
151527
+ function getYear(article) {
151528
+ return article.journalInfo?.publicationDate?.year ?? "n.d.";
151264
151529
  }
151265
- function extractArticleDates(articleXml) {
151266
- if (!articleXml || !articleXml.ArticleDate)
151267
- return [];
151268
- const articleDatesXml = ensureArray(articleXml.ArticleDate);
151269
- return articleDatesXml.map((ad) => ({
151270
- dateType: getAttribute(ad, "DateType"),
151271
- year: getText(ad.Year),
151272
- month: getText(ad.Month),
151273
- day: getText(ad.Day)
151274
- }));
151530
+ function splitPages(pages) {
151531
+ if (!pages)
151532
+ return {};
151533
+ const parts = pages.split(/[-\u2013\u2014]/).map((p) => p.trim());
151534
+ const [start, end] = parts;
151535
+ if (start && end)
151536
+ return { start, end };
151537
+ return start ? { start } : {};
151275
151538
  }
151276
- function parseFullArticle(xmlArticle, options = {}) {
151277
- const medlineCitation = xmlArticle.MedlineCitation;
151278
- const article = medlineCitation?.Article;
151279
- const { includeMesh = true, includeGrants = false } = options;
151280
- const abstractText = extractAbstractText(article?.Abstract);
151281
- const journalInfo = extractJournalInfo(article?.Journal, article);
151282
- const pubmedDataArticleIdList = xmlArticle.PubmedData?.ArticleIdList;
151283
- const doi = extractDoi(article, pubmedDataArticleIdList);
151284
- const pmcId = extractPmcId(article, pubmedDataArticleIdList);
151285
- const { authors, affiliations } = extractAuthors(article?.AuthorList);
151286
- return {
151287
- pmid: extractPmid(medlineCitation) ?? "",
151288
- title: getText(article?.ArticleTitle),
151289
- ...abstractText !== undefined && { abstractText },
151290
- ...affiliations.length > 0 && { affiliations },
151291
- authors,
151292
- ...journalInfo !== undefined && { journalInfo },
151293
- publicationTypes: extractPublicationTypes(article?.PublicationTypeList),
151294
- keywords: extractKeywords(medlineCitation?.KeywordList ?? article?.KeywordList),
151295
- ...includeMesh && { meshTerms: extractMeshTerms(medlineCitation?.MeshHeadingList) },
151296
- ...includeGrants && { grantList: extractGrants(article?.GrantList) },
151297
- ...doi !== undefined && { doi },
151298
- ...pmcId !== undefined && { pmcId },
151299
- articleDates: extractArticleDates(article)
151300
- };
151539
+ function escapeBibtex(text) {
151540
+ return text.replace(/[\\&%$#_{}~^]/g, (ch) => {
151541
+ switch (ch) {
151542
+ case "\\":
151543
+ return "\\textbackslash{}";
151544
+ case "~":
151545
+ return "\\textasciitilde{}";
151546
+ case "^":
151547
+ return "\\textasciicircum{}";
151548
+ default:
151549
+ return `\\${ch}`;
151550
+ }
151551
+ });
151301
151552
  }
151302
-
151303
- // src/utils/formatting/markdownBuilder.ts
151304
- class MarkdownBuilder {
151305
- sections = [];
151306
- h1(text, emoji3) {
151307
- const prefix = emoji3 ? `${emoji3} ` : "";
151308
- this.sections.push(`# ${prefix}${text}
151309
-
151310
- `);
151311
- return this;
151553
+ function formatAuthorApa(author) {
151554
+ if (author.collectiveName)
151555
+ return author.collectiveName;
151556
+ const last = author.lastName ?? "";
151557
+ const initials = author.initials ?? author.firstName?.split(/[\s-]+/).filter(Boolean).map((part) => `${part[0]}.`).join(" ");
151558
+ if (!initials)
151559
+ return last;
151560
+ const formatted = initials.replace(/[^A-Za-z]/g, "").split("").map((c) => `${c}.`).join(" ");
151561
+ if (!last)
151562
+ return formatted;
151563
+ return `${last}, ${formatted}`;
151564
+ }
151565
+ function formatAuthorsApa(authors) {
151566
+ const formatted = authors.map(formatAuthorApa);
151567
+ if (formatted.length === 0)
151568
+ return "";
151569
+ if (formatted.length === 1)
151570
+ return formatted[0] ?? "";
151571
+ if (formatted.length === 2)
151572
+ return `${formatted[0]}, & ${formatted[1]}`;
151573
+ if (formatted.length <= 20) {
151574
+ const allButLast = formatted.slice(0, -1).join(", ");
151575
+ return `${allButLast}, & ${formatted.at(-1)}`;
151312
151576
  }
151313
- h2(text, emoji3) {
151314
- const prefix = emoji3 ? `${emoji3} ` : "";
151315
- this.sections.push(`## ${prefix}${text}
151316
-
151317
- `);
151318
- return this;
151577
+ const first19 = formatted.slice(0, 19).join(", ");
151578
+ return `${first19}, ... ${formatted.at(-1)}`;
151579
+ }
151580
+ function formatAuthorMla(author, isFirst) {
151581
+ if (author.collectiveName)
151582
+ return author.collectiveName;
151583
+ const last = author.lastName ?? "";
151584
+ const first = author.firstName ?? "";
151585
+ if (!last && !first)
151586
+ return "";
151587
+ if (!first)
151588
+ return last;
151589
+ if (!last)
151590
+ return first;
151591
+ return isFirst ? `${last}, ${first}` : `${first} ${last}`;
151592
+ }
151593
+ function formatAuthorsMla(authors) {
151594
+ const first = authors[0];
151595
+ if (!first)
151596
+ return "";
151597
+ if (authors.length === 1)
151598
+ return formatAuthorMla(first, true);
151599
+ if (authors.length === 2) {
151600
+ const second = authors[1];
151601
+ return second ? `${formatAuthorMla(first, true)}, and ${formatAuthorMla(second, false)}` : formatAuthorMla(first, true);
151319
151602
  }
151320
- h3(text, emoji3) {
151321
- const prefix = emoji3 ? `${emoji3} ` : "";
151322
- this.sections.push(`### ${prefix}${text}
151323
-
151324
- `);
151325
- return this;
151603
+ return `${formatAuthorMla(first, true)}, et al.`;
151604
+ }
151605
+ function formatAuthorBibtex(author) {
151606
+ if (author.collectiveName)
151607
+ return `{${escapeBibtex(author.collectiveName)}}`;
151608
+ const last = author.lastName ? escapeBibtex(author.lastName) : "";
151609
+ const first = author.firstName ? escapeBibtex(author.firstName) : "";
151610
+ if (!last && !first)
151611
+ return "";
151612
+ if (!first)
151613
+ return `{${last}}`;
151614
+ if (!last)
151615
+ return first;
151616
+ return `{${last}}, ${first}`;
151617
+ }
151618
+ function formatApa(article) {
151619
+ const parts = [];
151620
+ const authorStr = article.authors?.length ? formatAuthorsApa(article.authors) : "";
151621
+ if (authorStr) {
151622
+ parts.push(authorStr);
151326
151623
  }
151327
- h4(text, emoji3) {
151328
- const prefix = emoji3 ? `${emoji3} ` : "";
151329
- this.sections.push(`#### ${prefix}${text}
151330
-
151331
- `);
151332
- return this;
151624
+ const year = getYear(article);
151625
+ parts.push(`(${year}).`);
151626
+ if (article.title) {
151627
+ const title = article.title.replace(/\.\s*$/, "");
151628
+ parts.push(`${title}.`);
151333
151629
  }
151334
- keyValue(key, value) {
151335
- const displayValue = value === null ? "null" : String(value);
151336
- this.sections.push(`**${key}:** ${displayValue}
151337
- `);
151338
- return this;
151630
+ const journal = article.journalInfo;
151631
+ if (journal?.title) {
151632
+ let journalPart = `*${journal.title}*`;
151633
+ if (journal.volume) {
151634
+ journalPart += `, *${journal.volume}*`;
151635
+ if (journal.issue) {
151636
+ journalPart += `(${journal.issue})`;
151637
+ }
151638
+ }
151639
+ if (journal.pages) {
151640
+ journalPart += `, ${journal.pages}`;
151641
+ }
151642
+ journalPart += ".";
151643
+ parts.push(journalPart);
151339
151644
  }
151340
- keyValuePlain(key, value) {
151341
- const displayValue = value === null ? "null" : String(value);
151342
- this.sections.push(`${key}: ${displayValue}
151343
- `);
151344
- return this;
151645
+ if (article.doi) {
151646
+ parts.push(`https://doi.org/${article.doi}`);
151345
151647
  }
151346
- list(items, ordered = false) {
151347
- if (items.length === 0)
151348
- return this;
151349
- const marker = ordered ? (i) => `${i + 1}.` : () => "-";
151350
- this.sections.push(`${items.map((item, i) => `${marker(i)} ${item}`).join(`
151351
- `)}
151352
-
151353
- `);
151354
- return this;
151648
+ return parts.join(" ");
151649
+ }
151650
+ function formatMla(article) {
151651
+ const parts = [];
151652
+ const authorStr = article.authors?.length ? formatAuthorsMla(article.authors) : "";
151653
+ if (authorStr) {
151654
+ parts.push(authorStr.endsWith(".") ? authorStr : `${authorStr}.`);
151355
151655
  }
151356
- codeBlock(content, language = "") {
151357
- this.sections.push(`\`\`\`${language}
151358
- ${content}
151359
- \`\`\`
151360
-
151361
- `);
151362
- return this;
151656
+ if (article.title) {
151657
+ const title = article.title.replace(/\.\s*$/, "");
151658
+ parts.push(`"${title}."`);
151363
151659
  }
151364
- inlineCode(code) {
151365
- this.sections.push(`\`${code}\``);
151366
- return this;
151660
+ const journal = article.journalInfo;
151661
+ if (journal?.title) {
151662
+ const detailParts = [];
151663
+ detailParts.push(`*${journal.title}*`);
151664
+ if (journal.volume) {
151665
+ detailParts.push(`vol. ${journal.volume}`);
151666
+ }
151667
+ if (journal.issue) {
151668
+ detailParts.push(`no. ${journal.issue}`);
151669
+ }
151670
+ const year = getYear(article);
151671
+ if (year !== "n.d.") {
151672
+ detailParts.push(year);
151673
+ }
151674
+ if (journal.pages) {
151675
+ detailParts.push(`pp. ${journal.pages}`);
151676
+ }
151677
+ parts.push(`${detailParts.join(", ")}.`);
151367
151678
  }
151368
- paragraph(text) {
151369
- this.sections.push(`${text}
151370
-
151371
- `);
151372
- return this;
151679
+ if (article.doi) {
151680
+ parts.push(`https://doi.org/${article.doi}.`);
151373
151681
  }
151374
- blockquote(text) {
151375
- const lines = text.split(`
151376
- `);
151377
- const quoted = lines.map((line) => `> ${line}`).join(`
151378
- `);
151379
- this.sections.push(`${quoted}
151380
-
151381
- `);
151382
- return this;
151682
+ return parts.join(" ");
151683
+ }
151684
+ function formatBibtex(article) {
151685
+ const key = `pmid${article.pmid}`;
151686
+ const fields = [];
151687
+ if (article.authors?.length) {
151688
+ const authorStr = article.authors.map(formatAuthorBibtex).filter(Boolean).join(" and ");
151689
+ if (authorStr)
151690
+ fields.push(["author", authorStr]);
151383
151691
  }
151384
- hr() {
151385
- this.sections.push(`---
151386
-
151387
- `);
151388
- return this;
151692
+ if (article.title) {
151693
+ fields.push(["title", `{${escapeBibtex(article.title)}}`]);
151389
151694
  }
151390
- link(text, url2) {
151391
- this.sections.push(`[${text}](${url2})`);
151392
- return this;
151695
+ const journal = article.journalInfo;
151696
+ if (journal?.title) {
151697
+ fields.push(["journal", escapeBibtex(journal.title)]);
151393
151698
  }
151394
- table(headers, rows) {
151395
- if (headers.length === 0 || rows.length === 0)
151396
- return this;
151397
- this.sections.push(`| ${headers.join(" | ")} |
151398
- `);
151399
- this.sections.push(`| ${headers.map(() => "---").join(" | ")} |
151400
- `);
151401
- rows.forEach((row) => {
151402
- this.sections.push(`| ${row.join(" | ")} |
151403
- `);
151404
- });
151405
- this.sections.push(`
151406
- `);
151407
- return this;
151699
+ const year = getYear(article);
151700
+ if (year !== "n.d.") {
151701
+ fields.push(["year", year]);
151408
151702
  }
151409
- section(title, levelOrContent, content) {
151410
- let level;
151411
- let callback;
151412
- if (typeof levelOrContent === "function") {
151413
- level = 2;
151414
- callback = levelOrContent;
151415
- } else {
151416
- level = levelOrContent;
151417
- callback = content ?? (() => {});
151418
- }
151419
- switch (level) {
151420
- case 2:
151421
- this.h2(title);
151422
- break;
151423
- case 3:
151424
- this.h3(title);
151425
- break;
151426
- case 4:
151427
- this.h4(title);
151428
- break;
151703
+ if (journal?.volume) {
151704
+ fields.push(["volume", escapeBibtex(journal.volume)]);
151705
+ }
151706
+ if (journal?.issue) {
151707
+ fields.push(["number", escapeBibtex(journal.issue)]);
151708
+ }
151709
+ if (journal?.pages) {
151710
+ fields.push(["pages", escapeBibtex(journal.pages)]);
151711
+ }
151712
+ if (article.doi) {
151713
+ fields.push(["doi", article.doi]);
151714
+ }
151715
+ fields.push(["pmid", article.pmid]);
151716
+ const maxKeyLen = Math.max(...fields.map(([k]) => k.length));
151717
+ const fieldLines = fields.map(([k, v]) => ` ${k.padEnd(maxKeyLen)} = {${v}}`).join(`,
151718
+ `);
151719
+ return `@article{${key},
151720
+ ${fieldLines}
151721
+ }`;
151722
+ }
151723
+ function formatRis(article) {
151724
+ const lines = [];
151725
+ const tag = (code, value) => {
151726
+ if (value)
151727
+ lines.push(`${code} - ${value}`);
151728
+ };
151729
+ lines.push("TY - JOUR");
151730
+ if (article.authors?.length) {
151731
+ for (const author of article.authors) {
151732
+ if (author.collectiveName) {
151733
+ tag("AU", author.collectiveName);
151734
+ } else {
151735
+ const last = author.lastName ?? "";
151736
+ const first = author.firstName ?? "";
151737
+ if (last || first) {
151738
+ tag("AU", first ? `${last}, ${first}` : last);
151739
+ }
151740
+ }
151429
151741
  }
151430
- callback();
151431
- return this;
151432
151742
  }
151433
- details(summary, details) {
151434
- this.sections.push(`<details>
151435
- <summary>${summary}</summary>
151436
-
151437
- `);
151438
- this.sections.push(`${details}
151439
-
151440
- `);
151441
- this.sections.push(`</details>
151442
-
151443
- `);
151444
- return this;
151743
+ tag("TI", article.title);
151744
+ const journal = article.journalInfo;
151745
+ if (journal?.title) {
151746
+ tag("JF", journal.title);
151747
+ }
151748
+ if (journal?.isoAbbreviation) {
151749
+ tag("JO", journal.isoAbbreviation);
151750
+ }
151751
+ const year = getYear(article);
151752
+ if (year !== "n.d.") {
151753
+ tag("PY", year);
151754
+ }
151755
+ tag("VL", journal?.volume);
151756
+ tag("IS", journal?.issue);
151757
+ if (journal?.pages) {
151758
+ const { start, end } = splitPages(journal.pages);
151759
+ tag("SP", start);
151760
+ tag("EP", end);
151445
151761
  }
151446
- alert(type, content) {
151447
- const typeUpper = type.toUpperCase();
151448
- const lines = content.split(`
151449
- `);
151450
- this.sections.push(`> [!${typeUpper}]
151451
- `);
151452
- lines.forEach((line) => {
151453
- this.sections.push(`> ${line}
151454
- `);
151455
- });
151456
- this.sections.push(`
151457
- `);
151458
- return this;
151762
+ tag("DO", article.doi);
151763
+ tag("AN", article.pmid);
151764
+ lines.push(`UR - https://pubmed.ncbi.nlm.nih.gov/${article.pmid}/`);
151765
+ if (article.keywords?.length) {
151766
+ for (const kw of article.keywords) {
151767
+ tag("KW", kw);
151768
+ }
151459
151769
  }
151460
- taskList(items) {
151461
- if (items.length === 0)
151462
- return this;
151463
- this.sections.push(`${items.map((item) => `- [${item.checked ? "x" : " "}] ${item.text}`).join(`
151464
- `)}
151465
-
151770
+ tag("AB", article.abstractText);
151771
+ lines.push("ER - ");
151772
+ return lines.join(`
151466
151773
  `);
151467
- return this;
151774
+ }
151775
+ function formatCitation(article, style) {
151776
+ switch (style) {
151777
+ case "apa":
151778
+ return formatApa(article);
151779
+ case "mla":
151780
+ return formatMla(article);
151781
+ case "bibtex":
151782
+ return formatBibtex(article);
151783
+ case "ris":
151784
+ return formatRis(article);
151468
151785
  }
151469
- image(altText, url2, title) {
151470
- const titlePart = title ? ` "${title}"` : "";
151471
- this.sections.push(`![${altText}](${url2}${titlePart})
151472
-
151473
- `);
151474
- return this;
151786
+ }
151787
+ function formatCitations(article, styles) {
151788
+ const result = {};
151789
+ for (const style of styles) {
151790
+ result[style] = formatCitation(article, style);
151475
151791
  }
151476
- strikethrough(text) {
151477
- this.sections.push(`~~${text}~~`);
151478
- return this;
151792
+ return result;
151793
+ }
151794
+
151795
+ // src/services/ncbi/parsing/article-parser.ts
151796
+ function extractAuthors(authorListXml) {
151797
+ if (!authorListXml)
151798
+ return { authors: [], affiliations: [] };
151799
+ const affiliationMap = new Map;
151800
+ const affiliationList = [];
151801
+ function getAffiliationIndex(text) {
151802
+ const existing = affiliationMap.get(text);
151803
+ if (existing !== undefined)
151804
+ return existing;
151805
+ const idx = affiliationList.length;
151806
+ affiliationList.push(text);
151807
+ affiliationMap.set(text, idx);
151808
+ return idx;
151479
151809
  }
151480
- diff(changes) {
151481
- const lines = [];
151482
- if (changes.context) {
151483
- lines.push(...changes.context.map((line) => ` ${line}`));
151810
+ const xmlAuthors = ensureArray(authorListXml.Author);
151811
+ const authors = xmlAuthors.map((auth) => {
151812
+ const collectiveName = getText(auth.CollectiveName);
151813
+ if (collectiveName) {
151814
+ return { collectiveName };
151484
151815
  }
151485
- if (changes.deletions) {
151486
- lines.push(...changes.deletions.map((line) => `- ${line}`));
151816
+ const authorAffiliationInfos = ensureArray(auth.AffiliationInfo);
151817
+ const indices = [];
151818
+ for (const info of authorAffiliationInfos) {
151819
+ const text = getText(info?.Affiliation);
151820
+ if (text)
151821
+ indices.push(getAffiliationIndex(text));
151487
151822
  }
151488
- if (changes.additions) {
151489
- lines.push(...changes.additions.map((line) => `+ ${line}`));
151823
+ let orcid;
151824
+ const identifiers = ensureArray(auth.Identifier);
151825
+ for (const id of identifiers) {
151826
+ if (getAttribute(id, "Source") === "ORCID") {
151827
+ const val = getText(id);
151828
+ if (val) {
151829
+ orcid = val;
151830
+ break;
151831
+ }
151832
+ }
151490
151833
  }
151491
- if (lines.length > 0) {
151492
- this.codeBlock(lines.join(`
151493
- `), "diff");
151834
+ return {
151835
+ lastName: getText(auth.LastName),
151836
+ firstName: getText(auth.ForeName),
151837
+ initials: getText(auth.Initials),
151838
+ ...indices.length > 0 && { affiliationIndices: indices },
151839
+ ...orcid && { orcid }
151840
+ };
151841
+ });
151842
+ return { authors, affiliations: affiliationList };
151843
+ }
151844
+ function extractJournalInfo(journalXml, articleXml) {
151845
+ if (!journalXml)
151846
+ return;
151847
+ const pubDate = journalXml.JournalIssue?.PubDate;
151848
+ const year = getText(pubDate?.Year, getText(pubDate?.MedlineDate, "").match(/\d{4}/)?.[0]);
151849
+ const issnElement = journalXml.ISSN;
151850
+ const issnValue = getText(issnElement);
151851
+ const issnType = getAttribute(issnElement, "IssnType");
151852
+ const issn = issnType === "Electronic" ? undefined : issnValue || undefined;
151853
+ const eIssn = issnType === "Electronic" ? issnValue || undefined : undefined;
151854
+ const month = getText(pubDate?.Month);
151855
+ const day = getText(pubDate?.Day);
151856
+ const medlineDate = getText(pubDate?.MedlineDate);
151857
+ return {
151858
+ title: getText(journalXml.Title),
151859
+ isoAbbreviation: getText(journalXml.ISOAbbreviation),
151860
+ ...issn && { issn },
151861
+ ...eIssn && { eIssn },
151862
+ volume: getText(journalXml.JournalIssue?.Volume),
151863
+ issue: getText(journalXml.JournalIssue?.Issue),
151864
+ pages: getText(articleXml?.Pagination?.MedlinePgn),
151865
+ publicationDate: {
151866
+ ...year && { year },
151867
+ ...month && { month },
151868
+ ...day && { day },
151869
+ ...medlineDate && { medlineDate }
151870
+ }
151871
+ };
151872
+ }
151873
+ function extractMeshTerms(meshHeadingListXml) {
151874
+ if (!meshHeadingListXml)
151875
+ return [];
151876
+ const meshHeadings = ensureArray(meshHeadingListXml.MeshHeading);
151877
+ return meshHeadings.map((mh) => {
151878
+ const isMajorDescriptor = getAttribute(mh.DescriptorName, "MajorTopicYN") === "Y";
151879
+ const isMajorRoot = getAttribute(mh, "MajorTopicYN") === "Y";
151880
+ const descriptorUi = getAttribute(mh.DescriptorName, "UI");
151881
+ const rawQualifiers = ensureArray(mh.QualifierName);
151882
+ const qualifiers = rawQualifiers.flatMap((q) => {
151883
+ const name = getText(q);
151884
+ if (!name)
151885
+ return [];
151886
+ const ui = getAttribute(q, "UI");
151887
+ return {
151888
+ qualifierName: name,
151889
+ ...ui && { qualifierUi: ui },
151890
+ isMajorTopic: getAttribute(q, "MajorTopicYN") === "Y"
151891
+ };
151892
+ });
151893
+ const isMajorAnyQualifier = qualifiers.some((q) => q.isMajorTopic);
151894
+ return {
151895
+ descriptorName: getText(mh.DescriptorName),
151896
+ ...descriptorUi && { descriptorUi },
151897
+ ...qualifiers.length > 0 && { qualifiers },
151898
+ isMajorTopic: isMajorRoot || isMajorDescriptor || isMajorAnyQualifier
151899
+ };
151900
+ });
151901
+ }
151902
+ function extractGrants(grantListXml) {
151903
+ if (!grantListXml)
151904
+ return [];
151905
+ const grants = ensureArray(grantListXml.Grant);
151906
+ return grants.map((g) => {
151907
+ const grantId = getText(g.GrantID);
151908
+ const acronym = getText(g.Acronym);
151909
+ const agency = getText(g.Agency);
151910
+ const country = getText(g.Country);
151911
+ return {
151912
+ ...grantId && { grantId },
151913
+ ...acronym && { acronym },
151914
+ ...agency && { agency },
151915
+ ...country && { country }
151916
+ };
151917
+ });
151918
+ }
151919
+ function extractDoi(articleXml, pubmedDataArticleIdList) {
151920
+ if (!articleXml)
151921
+ return;
151922
+ const eLocationIDs = ensureArray(articleXml.ELocationID);
151923
+ for (const eloc of eLocationIDs) {
151924
+ if (getAttribute(eloc, "EIdType") === "doi" && getAttribute(eloc, "ValidYN") === "Y") {
151925
+ const doi = getText(eloc);
151926
+ if (doi)
151927
+ return doi;
151494
151928
  }
151495
- return this;
151496
- }
151497
- badge(label, message, color = "blue") {
151498
- const encodedLabel = encodeURIComponent(label);
151499
- const encodedMessage = encodeURIComponent(message);
151500
- const url2 = `https://img.shields.io/badge/${encodedLabel}-${encodedMessage}-${color}`;
151501
- this.sections.push(`![${label}: ${message}](${url2})`);
151502
- return this;
151503
- }
151504
- bold(text) {
151505
- this.sections.push(`**${text}**`);
151506
- return this;
151507
- }
151508
- italic(text) {
151509
- this.sections.push(`*${text}*`);
151510
- return this;
151511
- }
151512
- boldItalic(text) {
151513
- this.sections.push(`***${text}***`);
151514
- return this;
151515
151929
  }
151516
- raw(markdown) {
151517
- this.sections.push(markdown);
151518
- return this;
151930
+ for (const eloc of eLocationIDs) {
151931
+ if (getAttribute(eloc, "EIdType") === "doi") {
151932
+ const doi = getText(eloc);
151933
+ if (doi)
151934
+ return doi;
151935
+ }
151519
151936
  }
151520
- blankLine() {
151521
- this.sections.push(`
151522
- `);
151523
- return this;
151937
+ const articleIds = ensureArray(articleXml.ArticleIdList?.ArticleId);
151938
+ for (const aid of articleIds) {
151939
+ if (getAttribute(aid, "IdType") === "doi") {
151940
+ const doi = getText(aid);
151941
+ if (doi)
151942
+ return doi;
151943
+ }
151524
151944
  }
151525
- text(text) {
151526
- this.sections.push(text);
151527
- return this;
151945
+ if (pubmedDataArticleIdList) {
151946
+ const pubmedDataIds = ensureArray(pubmedDataArticleIdList.ArticleId);
151947
+ for (const aid of pubmedDataIds) {
151948
+ if (getAttribute(aid, "IdType") === "doi") {
151949
+ const doi = getText(aid);
151950
+ if (doi)
151951
+ return doi;
151952
+ }
151953
+ }
151528
151954
  }
151529
- when(condition, content) {
151530
- if (condition) {
151531
- content();
151955
+ return;
151956
+ }
151957
+ function extractPmcId(articleXml, pubmedDataArticleIdList) {
151958
+ const articleIds = ensureArray(articleXml?.ArticleIdList?.ArticleId);
151959
+ for (const aid of articleIds) {
151960
+ if (getAttribute(aid, "IdType") === "pmc") {
151961
+ const val = getText(aid);
151962
+ if (val)
151963
+ return val;
151532
151964
  }
151533
- return this;
151534
151965
  }
151535
- build() {
151536
- return this.sections.join("").trim();
151966
+ if (pubmedDataArticleIdList) {
151967
+ const pubmedDataIds = ensureArray(pubmedDataArticleIdList.ArticleId);
151968
+ for (const aid of pubmedDataIds) {
151969
+ if (getAttribute(aid, "IdType") === "pmc") {
151970
+ const val = getText(aid);
151971
+ if (val)
151972
+ return val;
151973
+ }
151974
+ }
151537
151975
  }
151538
- reset() {
151539
- this.sections = [];
151540
- return this;
151976
+ return;
151977
+ }
151978
+ function extractPublicationTypes(publicationTypeListXml) {
151979
+ if (!publicationTypeListXml)
151980
+ return [];
151981
+ const pubTypes = ensureArray(publicationTypeListXml.PublicationType);
151982
+ return pubTypes.map((pt) => getText(pt)).filter(Boolean);
151983
+ }
151984
+ function extractKeywords2(keywordListsXml) {
151985
+ if (!keywordListsXml)
151986
+ return [];
151987
+ const lists = ensureArray(keywordListsXml);
151988
+ const allKeywords = [];
151989
+ for (const list of lists) {
151990
+ for (const kw of ensureArray(list.Keyword)) {
151991
+ const keywordText = getText(kw);
151992
+ if (keywordText) {
151993
+ allKeywords.push(keywordText);
151994
+ }
151995
+ }
151541
151996
  }
151997
+ return allKeywords;
151542
151998
  }
151543
- function markdown() {
151544
- return new MarkdownBuilder;
151999
+ function extractAbstractText(abstractXml) {
152000
+ if (!abstractXml || !abstractXml.AbstractText)
152001
+ return;
152002
+ const abstractTexts = ensureArray(abstractXml.AbstractText);
152003
+ if (abstractTexts.length === 0)
152004
+ return;
152005
+ const processedTexts = abstractTexts.map((at) => {
152006
+ if (typeof at === "string") {
152007
+ return at;
152008
+ }
152009
+ const sectionText = getText(at);
152010
+ const label = getAttribute(at, "Label");
152011
+ if (label && sectionText) {
152012
+ return `${label.trim()}: ${sectionText.trim()}`;
152013
+ }
152014
+ return sectionText.trim();
152015
+ }).filter(Boolean);
152016
+ if (processedTexts.length === 0)
152017
+ return;
152018
+ return processedTexts.join(`
152019
+
152020
+ `).trim() || undefined;
152021
+ }
152022
+ function extractPmid(medlineCitationXml) {
152023
+ if (!medlineCitationXml || !medlineCitationXml.PMID)
152024
+ return;
152025
+ return getText(medlineCitationXml.PMID);
152026
+ }
152027
+ function extractArticleDates(articleXml) {
152028
+ if (!articleXml || !articleXml.ArticleDate)
152029
+ return [];
152030
+ const articleDatesXml = ensureArray(articleXml.ArticleDate);
152031
+ return articleDatesXml.map((ad) => ({
152032
+ dateType: getAttribute(ad, "DateType"),
152033
+ year: getText(ad.Year),
152034
+ month: getText(ad.Month),
152035
+ day: getText(ad.Day)
152036
+ }));
152037
+ }
152038
+ function parseFullArticle(xmlArticle, options = {}) {
152039
+ const medlineCitation = xmlArticle.MedlineCitation;
152040
+ const article = medlineCitation?.Article;
152041
+ const { includeMesh = true, includeGrants = false } = options;
152042
+ const abstractText = extractAbstractText(article?.Abstract);
152043
+ const journalInfo = extractJournalInfo(article?.Journal, article);
152044
+ const pubmedDataArticleIdList = xmlArticle.PubmedData?.ArticleIdList;
152045
+ const doi = extractDoi(article, pubmedDataArticleIdList);
152046
+ const pmcId = extractPmcId(article, pubmedDataArticleIdList);
152047
+ const { authors, affiliations } = extractAuthors(article?.AuthorList);
152048
+ return {
152049
+ pmid: extractPmid(medlineCitation) ?? "",
152050
+ title: getText(article?.ArticleTitle),
152051
+ ...abstractText !== undefined && { abstractText },
152052
+ ...affiliations.length > 0 && { affiliations },
152053
+ authors,
152054
+ ...journalInfo !== undefined && { journalInfo },
152055
+ publicationTypes: extractPublicationTypes(article?.PublicationTypeList),
152056
+ keywords: extractKeywords2(medlineCitation?.KeywordList ?? article?.KeywordList),
152057
+ ...includeMesh && { meshTerms: extractMeshTerms(medlineCitation?.MeshHeadingList) },
152058
+ ...includeGrants && { grantList: extractGrants(article?.GrantList) },
152059
+ ...doi !== undefined && { doi },
152060
+ ...pmcId !== undefined && { pmcId },
152061
+ articleDates: extractArticleDates(article)
152062
+ };
151545
152063
  }
151546
152064
 
151547
152065
  // src/mcp-server/tools/definitions/pubmed-cite.tool.ts
151548
- var ncbi2 = () => container.resolve(NcbiServiceToken);
151549
- var TOOL_NAME = "pubmed_cite";
151550
- var TOOL_TITLE = "PubMed Citations";
151551
- var TOOL_DESCRIPTION = "Get formatted citations for PubMed articles in APA, MLA, BibTeX, or RIS format.";
151552
- var TOOL_ANNOTATIONS = {
152066
+ var ncbi3 = () => container.resolve(NcbiServiceToken);
152067
+ var TOOL_NAME2 = "pubmed_cite";
152068
+ var TOOL_TITLE2 = "PubMed Citations";
152069
+ var TOOL_DESCRIPTION2 = "Get formatted citations for PubMed articles in APA, MLA, BibTeX, or RIS format.";
152070
+ var TOOL_ANNOTATIONS2 = {
151553
152071
  readOnlyHint: true,
151554
152072
  idempotentHint: true,
151555
152073
  openWorldHint: true
151556
152074
  };
151557
- var InputSchema = exports_external.object({
152075
+ var InputSchema2 = exports_external.object({
151558
152076
  pmids: exports_external.array(exports_external.string().regex(/^\d+$/)).min(1).max(50).describe("PubMed IDs to cite"),
151559
152077
  styles: exports_external.array(exports_external.enum(["apa", "mla", "bibtex", "ris"])).default(["apa"]).describe("Citation styles to generate")
151560
152078
  });
151561
- var OutputSchema2 = exports_external.object({
152079
+ var OutputSchema3 = exports_external.object({
151562
152080
  citations: exports_external.array(exports_external.object({
151563
152081
  pmid: exports_external.string(),
151564
152082
  title: exports_external.string().optional(),
151565
152083
  citations: exports_external.record(exports_external.string(), exports_external.string())
151566
152084
  })).describe("Citations per article")
151567
152085
  });
151568
- async function logic(input, appContext, _sdkContext) {
152086
+ async function logic2(input, appContext, _sdkContext) {
151569
152087
  logger.debug("Fetching articles for citation generation", {
151570
152088
  ...appContext,
151571
152089
  pmids: input.pmids,
151572
152090
  styles: input.styles
151573
152091
  });
151574
- const raw = await ncbi2().eFetch({ db: "pubmed", id: input.pmids.join(",") }, appContext);
152092
+ const raw = await ncbi3().eFetch({ db: "pubmed", id: input.pmids.join(",") }, appContext);
151575
152093
  const xmlArticles = ensureArray(raw?.PubmedArticleSet?.PubmedArticle);
151576
152094
  if (xmlArticles.length === 0) {
151577
152095
  throw new McpError(-32001 /* NotFound */, `No articles found for PMIDs: ${input.pmids.join(", ")}`, { requestId: appContext.requestId });
@@ -151590,7 +152108,7 @@ async function logic(input, appContext, _sdkContext) {
151590
152108
  });
151591
152109
  return { citations };
151592
152110
  }
151593
- function responseFormatter(result) {
152111
+ function responseFormatter2(result) {
151594
152112
  const md = markdown();
151595
152113
  md.text(`# PubMed Citations
151596
152114
  `);
@@ -151617,32 +152135,32 @@ ${citation}
151617
152135
  return [{ type: "text", text: md.build() }];
151618
152136
  }
151619
152137
  var pubmedCiteTool = {
151620
- name: TOOL_NAME,
151621
- title: TOOL_TITLE,
151622
- description: TOOL_DESCRIPTION,
151623
- annotations: TOOL_ANNOTATIONS,
151624
- inputSchema: InputSchema,
151625
- outputSchema: OutputSchema2,
151626
- logic: withToolAuth(["tool:pubmed_cite:read"], logic),
151627
- responseFormatter
152138
+ name: TOOL_NAME2,
152139
+ title: TOOL_TITLE2,
152140
+ description: TOOL_DESCRIPTION2,
152141
+ annotations: TOOL_ANNOTATIONS2,
152142
+ inputSchema: InputSchema2,
152143
+ outputSchema: OutputSchema3,
152144
+ logic: withToolAuth(["tool:pubmed_cite:read"], logic2),
152145
+ responseFormatter: responseFormatter2
151628
152146
  };
151629
152147
 
151630
152148
  // src/mcp-server/tools/definitions/pubmed-fetch.tool.ts
151631
- var ncbi3 = () => container.resolve(NcbiServiceToken);
151632
- var TOOL_NAME2 = "pubmed_fetch";
151633
- var TOOL_TITLE2 = "PubMed Fetch";
151634
- var TOOL_DESCRIPTION2 = "Fetch full article metadata by PubMed IDs. Returns detailed article information including abstract, authors, journal, MeSH terms.";
151635
- var TOOL_ANNOTATIONS2 = {
152149
+ var ncbi4 = () => container.resolve(NcbiServiceToken);
152150
+ var TOOL_NAME3 = "pubmed_fetch";
152151
+ var TOOL_TITLE3 = "PubMed Fetch";
152152
+ var TOOL_DESCRIPTION3 = "Fetch full article metadata by PubMed IDs. Returns detailed article information including abstract, authors, journal, MeSH terms.";
152153
+ var TOOL_ANNOTATIONS3 = {
151636
152154
  readOnlyHint: true,
151637
152155
  idempotentHint: true,
151638
152156
  openWorldHint: true
151639
152157
  };
151640
- var InputSchema2 = exports_external.object({
152158
+ var InputSchema3 = exports_external.object({
151641
152159
  pmids: exports_external.array(exports_external.string().regex(/^\d+$/)).min(1).max(200).describe("PubMed IDs to fetch"),
151642
152160
  includeMesh: exports_external.boolean().default(true).describe("Include MeSH terms"),
151643
152161
  includeGrants: exports_external.boolean().default(false).describe("Include grant information")
151644
152162
  });
151645
- var ArticleSchema = exports_external.object({
152163
+ var ArticleSchema2 = exports_external.object({
151646
152164
  pmid: exports_external.string().optional().describe("PubMed ID"),
151647
152165
  title: exports_external.string().optional().describe("Article title"),
151648
152166
  abstractText: exports_external.string().optional().describe("Abstract text"),
@@ -151657,16 +152175,16 @@ var ArticleSchema = exports_external.object({
151657
152175
  meshTerms: exports_external.array(exports_external.any()).optional().describe("MeSH terms"),
151658
152176
  grantList: exports_external.array(exports_external.any()).optional().describe("Grant information")
151659
152177
  });
151660
- var OutputSchema3 = exports_external.object({
151661
- articles: exports_external.array(ArticleSchema).describe("Parsed articles"),
152178
+ var OutputSchema4 = exports_external.object({
152179
+ articles: exports_external.array(ArticleSchema2).describe("Parsed articles"),
151662
152180
  totalReturned: exports_external.number().describe("Number of articles returned")
151663
152181
  });
151664
- async function logic2(input, appContext, _sdkContext) {
152182
+ async function logic3(input, appContext, _sdkContext) {
151665
152183
  logger.info("Executing pubmed_fetch tool", {
151666
152184
  ...appContext,
151667
152185
  pmidCount: input.pmids.length
151668
152186
  });
151669
- const xmlData = await ncbi3().eFetch({ db: "pubmed", id: input.pmids.join(","), retmode: "xml" }, appContext, { retmode: "xml", usePost: input.pmids.length > 200 });
152187
+ const xmlData = await ncbi4().eFetch({ db: "pubmed", id: input.pmids.join(","), retmode: "xml" }, appContext, { retmode: "xml", usePost: input.pmids.length > 200 });
151670
152188
  if (!xmlData || !("PubmedArticleSet" in xmlData)) {
151671
152189
  throw new McpError(-32603 /* InternalError */, "Invalid EFetch response from NCBI: missing PubmedArticleSet", { requestId: appContext.requestId });
151672
152190
  }
@@ -151694,7 +152212,7 @@ async function logic2(input, appContext, _sdkContext) {
151694
152212
  });
151695
152213
  return { articles, totalReturned: articles.length };
151696
152214
  }
151697
- function responseFormatter2(result) {
152215
+ function responseFormatter3(result) {
151698
152216
  const md = markdown().h2("PubMed Articles").keyValue("Articles Returned", result.totalReturned);
151699
152217
  for (const article of result.articles) {
151700
152218
  md.h3(article.title ?? article.pmid ?? "Unknown");
@@ -151729,27 +152247,27 @@ function responseFormatter2(result) {
151729
152247
  return [{ type: "text", text: md.build() }];
151730
152248
  }
151731
152249
  var pubmedFetchTool = {
151732
- name: TOOL_NAME2,
151733
- title: TOOL_TITLE2,
151734
- description: TOOL_DESCRIPTION2,
151735
- annotations: TOOL_ANNOTATIONS2,
151736
- inputSchema: InputSchema2,
151737
- outputSchema: OutputSchema3,
151738
- logic: withToolAuth(["tool:pubmed_fetch:read"], logic2),
151739
- responseFormatter: responseFormatter2
152250
+ name: TOOL_NAME3,
152251
+ title: TOOL_TITLE3,
152252
+ description: TOOL_DESCRIPTION3,
152253
+ annotations: TOOL_ANNOTATIONS3,
152254
+ inputSchema: InputSchema3,
152255
+ outputSchema: OutputSchema4,
152256
+ logic: withToolAuth(["tool:pubmed_fetch:read"], logic3),
152257
+ responseFormatter: responseFormatter3
151740
152258
  };
151741
152259
 
151742
152260
  // src/mcp-server/tools/definitions/pubmed-mesh-lookup.tool.ts
151743
- var ncbi4 = () => container.resolve(NcbiServiceToken);
151744
- var TOOL_NAME3 = "pubmed_mesh_lookup";
151745
- var TOOL_TITLE3 = "MeSH Term Lookup";
151746
- var TOOL_DESCRIPTION3 = "Search and explore MeSH (Medical Subject Headings) vocabulary. Essential for building precise PubMed queries.";
151747
- var TOOL_ANNOTATIONS3 = {
152261
+ var ncbi5 = () => container.resolve(NcbiServiceToken);
152262
+ var TOOL_NAME4 = "pubmed_mesh_lookup";
152263
+ var TOOL_TITLE4 = "MeSH Term Lookup";
152264
+ var TOOL_DESCRIPTION4 = "Search and explore MeSH (Medical Subject Headings) vocabulary. Essential for building precise PubMed queries.";
152265
+ var TOOL_ANNOTATIONS4 = {
151748
152266
  readOnlyHint: true,
151749
152267
  idempotentHint: true,
151750
152268
  openWorldHint: true
151751
152269
  };
151752
- var InputSchema3 = exports_external.object({
152270
+ var InputSchema4 = exports_external.object({
151753
152271
  term: exports_external.string().min(1).describe("MeSH term to look up"),
151754
152272
  maxResults: exports_external.number().int().min(1).max(50).default(10).describe("Maximum results"),
151755
152273
  includeDetails: exports_external.boolean().default(true).describe("Fetch full MeSH records (scope notes, tree numbers, entry terms)")
@@ -151761,7 +152279,7 @@ var MeshResultItem = exports_external.object({
151761
152279
  scopeNote: exports_external.string().optional().describe("Scope note describing the descriptor"),
151762
152280
  entryTerms: exports_external.array(exports_external.string()).optional().describe("Synonyms / entry terms")
151763
152281
  });
151764
- var OutputSchema4 = exports_external.object({
152282
+ var OutputSchema5 = exports_external.object({
151765
152283
  term: exports_external.string().describe("Original search term"),
151766
152284
  results: exports_external.array(MeshResultItem).describe("Matching MeSH records")
151767
152285
  });
@@ -151831,8 +152349,8 @@ async function meshLookupLogic(input, context, _sdkContext) {
151831
152349
  const { term, maxResults, includeDetails } = input;
151832
152350
  logger.debug("MeSH lookup started.", { ...context, term, maxResults, includeDetails });
151833
152351
  const hasFieldTag = /\[.+\]/.test(term);
151834
- const broadSearch = ncbi4().eSearch({ db: "mesh", term, retmax: maxResults }, context);
151835
- const exactSearch = hasFieldTag ? undefined : ncbi4().eSearch({ db: "mesh", term: `${term}[MH]`, retmax: 1 }, context);
152352
+ const broadSearch = ncbi5().eSearch({ db: "mesh", term, retmax: maxResults }, context);
152353
+ const exactSearch = hasFieldTag ? undefined : ncbi5().eSearch({ db: "mesh", term: `${term}[MH]`, retmax: 1 }, context);
151836
152354
  const [broadResult, exactResult] = await Promise.all([broadSearch, exactSearch]);
151837
152355
  const seen = new Set;
151838
152356
  const ids = [];
@@ -151847,7 +152365,7 @@ async function meshLookupLogic(input, context, _sdkContext) {
151847
152365
  logger.debug("No MeSH results found.", { ...context, term });
151848
152366
  return { term, results: [] };
151849
152367
  }
151850
- const summaryData = await ncbi4().eSummary({ db: "mesh", id: ids.join(",") }, context);
152368
+ const summaryData = await ncbi5().eSummary({ db: "mesh", id: ids.join(",") }, context);
151851
152369
  const results = parseSummaryRecords(summaryData, ids, includeDetails);
151852
152370
  const termLower = term.toLowerCase();
151853
152371
  results.sort((a, b) => {
@@ -151894,12 +152412,12 @@ function formatResponse(result) {
151894
152412
  return [{ type: "text", text: md.build() }];
151895
152413
  }
151896
152414
  var pubmedMeshLookupTool = {
151897
- name: TOOL_NAME3,
151898
- title: TOOL_TITLE3,
151899
- description: TOOL_DESCRIPTION3,
151900
- annotations: TOOL_ANNOTATIONS3,
151901
- inputSchema: InputSchema3,
151902
- outputSchema: OutputSchema4,
152415
+ name: TOOL_NAME4,
152416
+ title: TOOL_TITLE4,
152417
+ description: TOOL_DESCRIPTION4,
152418
+ annotations: TOOL_ANNOTATIONS4,
152419
+ inputSchema: InputSchema4,
152420
+ outputSchema: OutputSchema5,
151903
152421
  logic: withToolAuth(["tool:pubmed_mesh_lookup:read"], meshLookupLogic),
151904
152422
  responseFormatter: formatResponse
151905
152423
  };
@@ -155051,21 +155569,21 @@ async function extractBriefSummaries(eSummaryResult, context) {
155051
155569
  }
155052
155570
 
155053
155571
  // src/mcp-server/tools/definitions/pubmed-related.tool.ts
155054
- var ncbi5 = () => container.resolve(NcbiServiceToken);
155055
- var TOOL_NAME4 = "pubmed_related";
155056
- var TOOL_TITLE4 = "PubMed Related Articles";
155057
- var TOOL_DESCRIPTION4 = "Find articles related to a source article — similar content, citing articles, or references.";
155058
- var TOOL_ANNOTATIONS4 = {
155572
+ var ncbi6 = () => container.resolve(NcbiServiceToken);
155573
+ var TOOL_NAME5 = "pubmed_related";
155574
+ var TOOL_TITLE5 = "PubMed Related Articles";
155575
+ var TOOL_DESCRIPTION5 = "Find articles related to a source article — similar content, citing articles, or references.";
155576
+ var TOOL_ANNOTATIONS5 = {
155059
155577
  readOnlyHint: true,
155060
155578
  idempotentHint: true,
155061
155579
  openWorldHint: true
155062
155580
  };
155063
- var InputSchema4 = exports_external.object({
155581
+ var InputSchema5 = exports_external.object({
155064
155582
  pmid: exports_external.string().regex(/^\d+$/).describe("Source PubMed ID"),
155065
155583
  relationship: exports_external.enum(["similar", "cited_by", "references"]).default("similar").describe("Type of relationship"),
155066
155584
  maxResults: exports_external.number().int().min(1).max(50).default(10).describe("Maximum related articles")
155067
155585
  });
155068
- var OutputSchema5 = exports_external.object({
155586
+ var OutputSchema6 = exports_external.object({
155069
155587
  sourcePmid: exports_external.string().describe("Source PubMed ID"),
155070
155588
  relationship: exports_external.string().describe("Relationship type used"),
155071
155589
  articles: exports_external.array(exports_external.object({
@@ -155084,7 +155602,7 @@ function extractValue(field) {
155084
155602
  }
155085
155603
  return String(field);
155086
155604
  }
155087
- async function logic3(input, appContext, _sdkContext) {
155605
+ async function logic4(input, appContext, _sdkContext) {
155088
155606
  logger.debug("Finding related articles", {
155089
155607
  ...appContext,
155090
155608
  pmid: input.pmid,
@@ -155109,7 +155627,7 @@ async function logic3(input, appContext, _sdkContext) {
155109
155627
  eLinkParams.linkname = "pubmed_pubmed_refs";
155110
155628
  break;
155111
155629
  }
155112
- const eLinkResult = await ncbi5().eLink(eLinkParams, appContext);
155630
+ const eLinkResult = await ncbi6().eLink(eLinkParams, appContext);
155113
155631
  logger.debug("Raw ELink response received", {
155114
155632
  ...appContext,
155115
155633
  hasResult: !!eLinkResult?.eLinkResult
@@ -155151,7 +155669,7 @@ async function logic3(input, appContext, _sdkContext) {
155151
155669
  }
155152
155670
  const pmidsToEnrich = foundPmids.slice(0, input.maxResults);
155153
155671
  const pmidIds = pmidsToEnrich.map((p) => p.pmid);
155154
- const summaryResult = await ncbi5().eSummary({ db: "pubmed", id: pmidIds.join(",") }, appContext);
155672
+ const summaryResult = await ncbi6().eSummary({ db: "pubmed", id: pmidIds.join(",") }, appContext);
155155
155673
  const briefSummaries = await extractBriefSummaries(summaryResult, appContext);
155156
155674
  const summaryMap = new Map(briefSummaries.map((bs) => [bs.pmid, bs]));
155157
155675
  const articles = pmidsToEnrich.map((p) => {
@@ -155175,7 +155693,7 @@ async function logic3(input, appContext, _sdkContext) {
155175
155693
  totalFound
155176
155694
  };
155177
155695
  }
155178
- function responseFormatter3(result) {
155696
+ function responseFormatter4(result) {
155179
155697
  const md = markdown();
155180
155698
  md.text(`# Related Articles for PMID ${result.sourcePmid}
155181
155699
  `);
@@ -155198,27 +155716,27 @@ function responseFormatter3(result) {
155198
155716
  return [{ type: "text", text: md.build() }];
155199
155717
  }
155200
155718
  var pubmedRelatedTool = {
155201
- name: TOOL_NAME4,
155202
- title: TOOL_TITLE4,
155203
- description: TOOL_DESCRIPTION4,
155204
- annotations: TOOL_ANNOTATIONS4,
155205
- inputSchema: InputSchema4,
155206
- outputSchema: OutputSchema5,
155207
- logic: withToolAuth(["tool:pubmed_related:read"], logic3),
155208
- responseFormatter: responseFormatter3
155719
+ name: TOOL_NAME5,
155720
+ title: TOOL_TITLE5,
155721
+ description: TOOL_DESCRIPTION5,
155722
+ annotations: TOOL_ANNOTATIONS5,
155723
+ inputSchema: InputSchema5,
155724
+ outputSchema: OutputSchema6,
155725
+ logic: withToolAuth(["tool:pubmed_related:read"], logic4),
155726
+ responseFormatter: responseFormatter4
155209
155727
  };
155210
155728
 
155211
155729
  // src/mcp-server/tools/definitions/pubmed-search.tool.ts
155212
- var ncbi6 = () => container.resolve(NcbiServiceToken);
155213
- var TOOL_NAME5 = "pubmed_search";
155214
- var TOOL_TITLE5 = "PubMed Search";
155215
- var TOOL_DESCRIPTION5 = "Search PubMed with full query syntax, filters, and date ranges. Returns PMIDs and optional brief summaries. " + "Supports field-specific filters (author, journal, MeSH terms), common filters (language, species, free full text), " + "and pagination via offset for paging through large result sets.";
155216
- var TOOL_ANNOTATIONS5 = {
155730
+ var ncbi7 = () => container.resolve(NcbiServiceToken);
155731
+ var TOOL_NAME6 = "pubmed_search";
155732
+ var TOOL_TITLE6 = "PubMed Search";
155733
+ var TOOL_DESCRIPTION6 = "Search PubMed with full query syntax, filters, and date ranges. Returns PMIDs and optional brief summaries. " + "Supports field-specific filters (author, journal, MeSH terms), common filters (language, species, free full text), " + "and pagination via offset for paging through large result sets.";
155734
+ var TOOL_ANNOTATIONS6 = {
155217
155735
  readOnlyHint: true,
155218
155736
  idempotentHint: true,
155219
155737
  openWorldHint: true
155220
155738
  };
155221
- var InputSchema5 = exports_external.object({
155739
+ var InputSchema6 = exports_external.object({
155222
155740
  query: exports_external.string().min(1).describe("PubMed search query (supports full NCBI syntax)"),
155223
155741
  maxResults: exports_external.number().int().min(1).max(1000).default(20).describe("Maximum results to return"),
155224
155742
  offset: exports_external.number().int().min(0).default(0).describe("Result offset for pagination (0-based). Use with maxResults to page through results"),
@@ -155238,7 +155756,7 @@ var InputSchema5 = exports_external.object({
155238
155756
  species: exports_external.enum(["humans", "animals"]).optional().describe("Filter by species"),
155239
155757
  summaryCount: exports_external.number().int().min(0).max(50).default(0).describe("Fetch brief summaries for top N results (0 = PMIDs only)")
155240
155758
  });
155241
- var OutputSchema6 = exports_external.object({
155759
+ var OutputSchema7 = exports_external.object({
155242
155760
  query: exports_external.string().describe("Original query"),
155243
155761
  totalFound: exports_external.number().describe("Total matching articles"),
155244
155762
  offset: exports_external.number().describe("Result offset used"),
@@ -155256,7 +155774,7 @@ var OutputSchema6 = exports_external.object({
155256
155774
  })).optional().describe("Brief summaries"),
155257
155775
  searchUrl: exports_external.string().describe("PubMed search URL")
155258
155776
  });
155259
- async function logic4(input, appContext, _sdkContext) {
155777
+ async function logic5(input, appContext, _sdkContext) {
155260
155778
  logger.info("Executing pubmed_search tool", { ...appContext, query: input.query });
155261
155779
  let effectiveQuery = sanitization.sanitizeString(input.query, { context: "text" });
155262
155780
  if (input.dateRange) {
@@ -155291,7 +155809,7 @@ async function logic4(input, appContext, _sdkContext) {
155291
155809
  if (input.species) {
155292
155810
  effectiveQuery += ` AND ${input.species}[MeSH Terms]`;
155293
155811
  }
155294
- const esResult = await ncbi6().eSearch({
155812
+ const esResult = await ncbi7().eSearch({
155295
155813
  db: "pubmed",
155296
155814
  term: effectiveQuery,
155297
155815
  retmax: input.maxResults,
@@ -155315,7 +155833,7 @@ async function logic4(input, appContext, _sdkContext) {
155315
155833
  } else {
155316
155834
  eSummaryParams.id = pmids.slice(0, input.summaryCount).join(",");
155317
155835
  }
155318
- const eSummaryResult = await ncbi6().eSummary(eSummaryParams, appContext);
155836
+ const eSummaryResult = await ncbi7().eSummary(eSummaryParams, appContext);
155319
155837
  if (eSummaryResult) {
155320
155838
  const briefSummaries = await extractBriefSummaries(eSummaryResult, appContext);
155321
155839
  summaries = briefSummaries.map((s) => ({
@@ -155340,7 +155858,7 @@ async function logic4(input, appContext, _sdkContext) {
155340
155858
  });
155341
155859
  return { query: input.query, totalFound, offset: input.offset, pmids, summaries, searchUrl };
155342
155860
  }
155343
- function responseFormatter4(result) {
155861
+ function responseFormatter5(result) {
155344
155862
  const md = markdown().h2("PubMed Search Results").keyValue("Query", result.query).keyValue("Total Found", result.totalFound).keyValue("Offset", result.offset).keyValue("PMIDs Returned", result.pmids.length).keyValue("Search URL", result.searchUrl);
155345
155863
  if (result.pmids.length > 0) {
155346
155864
  md.h3("PMIDs").paragraph(result.pmids.join(", "));
@@ -155367,37 +155885,37 @@ function responseFormatter4(result) {
155367
155885
  return [{ type: "text", text: md.build() }];
155368
155886
  }
155369
155887
  var pubmedSearchTool = {
155370
- name: TOOL_NAME5,
155371
- title: TOOL_TITLE5,
155372
- description: TOOL_DESCRIPTION5,
155373
- annotations: TOOL_ANNOTATIONS5,
155374
- inputSchema: InputSchema5,
155375
- outputSchema: OutputSchema6,
155376
- logic: withToolAuth(["tool:pubmed_search:read"], logic4),
155377
- responseFormatter: responseFormatter4
155888
+ name: TOOL_NAME6,
155889
+ title: TOOL_TITLE6,
155890
+ description: TOOL_DESCRIPTION6,
155891
+ annotations: TOOL_ANNOTATIONS6,
155892
+ inputSchema: InputSchema6,
155893
+ outputSchema: OutputSchema7,
155894
+ logic: withToolAuth(["tool:pubmed_search:read"], logic5),
155895
+ responseFormatter: responseFormatter5
155378
155896
  };
155379
155897
 
155380
155898
  // src/mcp-server/tools/definitions/pubmed-spell.tool.ts
155381
- var ncbi7 = () => container.resolve(NcbiServiceToken);
155382
- var TOOL_NAME6 = "pubmed_spell";
155383
- var TOOL_TITLE6 = "PubMed Spell Check";
155384
- var TOOL_DESCRIPTION6 = "Spell-check a query and get NCBI's suggested correction. Useful for refining search queries.";
155385
- var TOOL_ANNOTATIONS6 = {
155899
+ var ncbi8 = () => container.resolve(NcbiServiceToken);
155900
+ var TOOL_NAME7 = "pubmed_spell";
155901
+ var TOOL_TITLE7 = "PubMed Spell Check";
155902
+ var TOOL_DESCRIPTION7 = "Spell-check a query and get NCBI's suggested correction. Useful for refining search queries.";
155903
+ var TOOL_ANNOTATIONS7 = {
155386
155904
  readOnlyHint: true,
155387
155905
  idempotentHint: true,
155388
155906
  openWorldHint: true
155389
155907
  };
155390
- var InputSchema6 = exports_external.object({
155908
+ var InputSchema7 = exports_external.object({
155391
155909
  query: exports_external.string().min(2).describe("Query to spell-check")
155392
155910
  });
155393
- var OutputSchema7 = exports_external.object({
155911
+ var OutputSchema8 = exports_external.object({
155394
155912
  original: exports_external.string().describe("Original query"),
155395
155913
  corrected: exports_external.string().describe("Corrected query (same as original if no suggestion)"),
155396
155914
  hasSuggestion: exports_external.boolean().describe("Whether NCBI suggested a correction")
155397
155915
  });
155398
- async function logic5(input, appContext, _sdkContext) {
155916
+ async function logic6(input, appContext, _sdkContext) {
155399
155917
  logger.info("Executing pubmed_spell tool", { ...appContext, query: input.query });
155400
- const result = await ncbi7().eSpell({ db: "pubmed", term: input.query }, appContext);
155918
+ const result = await ncbi8().eSpell({ db: "pubmed", term: input.query }, appContext);
155401
155919
  logger.notice("pubmed_spell completed", {
155402
155920
  ...appContext,
155403
155921
  hasSuggestion: result.hasSuggestion
@@ -155408,7 +155926,7 @@ async function logic5(input, appContext, _sdkContext) {
155408
155926
  hasSuggestion: result.hasSuggestion
155409
155927
  };
155410
155928
  }
155411
- function responseFormatter5(result) {
155929
+ function responseFormatter6(result) {
155412
155930
  const md = markdown().h2("PubMed Spell Check");
155413
155931
  if (result.hasSuggestion) {
155414
155932
  md.keyValue("Original", result.original).keyValue("Suggested", result.corrected);
@@ -155418,20 +155936,21 @@ function responseFormatter5(result) {
155418
155936
  return [{ type: "text", text: md.build() }];
155419
155937
  }
155420
155938
  var pubmedSpellTool = {
155421
- name: TOOL_NAME6,
155422
- title: TOOL_TITLE6,
155423
- description: TOOL_DESCRIPTION6,
155424
- annotations: TOOL_ANNOTATIONS6,
155425
- inputSchema: InputSchema6,
155426
- outputSchema: OutputSchema7,
155427
- logic: withToolAuth(["tool:pubmed_spell:read"], logic5),
155428
- responseFormatter: responseFormatter5
155939
+ name: TOOL_NAME7,
155940
+ title: TOOL_TITLE7,
155941
+ description: TOOL_DESCRIPTION7,
155942
+ annotations: TOOL_ANNOTATIONS7,
155943
+ inputSchema: InputSchema7,
155944
+ outputSchema: OutputSchema8,
155945
+ logic: withToolAuth(["tool:pubmed_spell:read"], logic6),
155946
+ responseFormatter: responseFormatter6
155429
155947
  };
155430
155948
 
155431
155949
  // src/mcp-server/tools/definitions/index.ts
155432
155950
  var allToolDefinitions = [
155433
155951
  pubmedSearchTool,
155434
155952
  pubmedFetchTool,
155953
+ pmcFetchTool,
155435
155954
  pubmedSpellTool,
155436
155955
  pubmedCiteTool,
155437
155956
  pubmedRelatedTool,
@@ -155633,8 +156152,8 @@ var defaultResponseFormatter2 = (result) => [
155633
156152
  function createMcpToolHandler({
155634
156153
  toolName,
155635
156154
  inputSchema,
155636
- logic: logic6,
155637
- responseFormatter: responseFormatter6 = defaultResponseFormatter2
156155
+ logic: logic7,
156156
+ responseFormatter: responseFormatter7 = defaultResponseFormatter2
155638
156157
  }) {
155639
156158
  return async (input, callContext) => {
155640
156159
  const sdkContext = callContext;
@@ -155649,10 +156168,10 @@ function createMcpToolHandler({
155649
156168
  });
155650
156169
  try {
155651
156170
  const validatedInput = inputSchema.parse(input);
155652
- const result = await measureToolExecution(() => logic6(validatedInput, appContext, sdkContext), { ...appContext, toolName }, validatedInput);
156171
+ const result = await measureToolExecution(() => logic7(validatedInput, appContext, sdkContext), { ...appContext, toolName }, validatedInput);
155653
156172
  return {
155654
156173
  structuredContent: result,
155655
- content: responseFormatter6(result)
156174
+ content: responseFormatter7(result)
155656
156175
  };
155657
156176
  } catch (error48) {
155658
156177
  const handled = ErrorHandler.handleError(error48, {