@cyanheads/pubmed-mcp-server 2.0.1 → 2.1.0

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 +1301 -790
  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.0",
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",
@@ -150769,809 +150778,1310 @@ 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.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(`
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() }];
151514
+ }
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
+ };
151257
151525
 
151258
- `).trim() || undefined;
151526
+ // src/services/ncbi/formatting/citation-formatter.ts
151527
+ function getYear(article) {
151528
+ return article.journalInfo?.publicationDate?.year ?? "n.d.";
151259
151529
  }
151260
- function extractPmid(medlineCitationXml) {
151261
- if (!medlineCitationXml || !medlineCitationXml.PMID)
151262
- return;
151263
- return getText(medlineCitationXml.PMID);
151530
+ function splitPages(pages) {
151531
+ if (!pages)
151532
+ return {};
151533
+ const parts = pages.split(/[-\u2013\u2014]/).map((p) => p.trim());
151534
+ if (parts.length >= 2) {
151535
+ const start2 = parts[0];
151536
+ const end = parts[1];
151537
+ return {
151538
+ ...start2 !== undefined && { start: start2 },
151539
+ ...end !== undefined && { end }
151540
+ };
151541
+ }
151542
+ const start = parts[0];
151543
+ return start !== undefined ? { start } : {};
151264
151544
  }
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
- }));
151545
+ function escapeBibtex(text) {
151546
+ 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{}");
151275
151547
  }
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
- };
151548
+ function formatAuthorApa(author) {
151549
+ if (author.collectiveName)
151550
+ return author.collectiveName;
151551
+ const last = author.lastName ?? "";
151552
+ const initials = author.initials ?? author.firstName?.split(/[\s-]+/).map((part) => `${part[0]}.`).join(" ");
151553
+ if (!last)
151554
+ return initials ?? "";
151555
+ if (!initials)
151556
+ return last;
151557
+ const formatted = initials.replace(/[^A-Za-z]/g, "").split("").map((c) => `${c}.`).join(" ");
151558
+ return `${last}, ${formatted}`;
151301
151559
  }
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;
151560
+ function formatAuthorsApa(authors) {
151561
+ const formatted = authors.map(formatAuthorApa);
151562
+ if (formatted.length === 0)
151563
+ return "";
151564
+ if (formatted.length === 1)
151565
+ return formatted[0] ?? "";
151566
+ if (formatted.length === 2)
151567
+ return `${formatted[0]}, & ${formatted[1]}`;
151568
+ if (formatted.length <= 20) {
151569
+ const allButLast = formatted.slice(0, -1).join(", ");
151570
+ return `${allButLast}, & ${formatted.at(-1)}`;
151312
151571
  }
151313
- h2(text, emoji3) {
151314
- const prefix = emoji3 ? `${emoji3} ` : "";
151315
- this.sections.push(`## ${prefix}${text}
151316
-
151317
- `);
151318
- return this;
151572
+ const first19 = formatted.slice(0, 19).join(", ");
151573
+ return `${first19}, ... ${formatted.at(-1)}`;
151574
+ }
151575
+ function formatAuthorMla(author, isFirst) {
151576
+ if (author.collectiveName)
151577
+ return author.collectiveName;
151578
+ const last = author.lastName ?? "";
151579
+ const first = author.firstName ?? "";
151580
+ if (!last && !first)
151581
+ return "";
151582
+ if (!first)
151583
+ return last;
151584
+ if (!last)
151585
+ return first;
151586
+ return isFirst ? `${last}, ${first}` : `${first} ${last}`;
151587
+ }
151588
+ function formatAuthorsMla(authors) {
151589
+ const first = authors[0];
151590
+ if (!first)
151591
+ return "";
151592
+ if (authors.length === 1)
151593
+ return formatAuthorMla(first, true);
151594
+ if (authors.length === 2) {
151595
+ const second = authors[1];
151596
+ return second ? `${formatAuthorMla(first, true)}, and ${formatAuthorMla(second, false)}` : formatAuthorMla(first, true);
151319
151597
  }
151320
- h3(text, emoji3) {
151321
- const prefix = emoji3 ? `${emoji3} ` : "";
151322
- this.sections.push(`### ${prefix}${text}
151323
-
151324
- `);
151325
- return this;
151598
+ return `${formatAuthorMla(first, true)}, et al.`;
151599
+ }
151600
+ function formatAuthorBibtex(author) {
151601
+ if (author.collectiveName)
151602
+ return `{${escapeBibtex(author.collectiveName)}}`;
151603
+ const last = author.lastName ? escapeBibtex(author.lastName) : "";
151604
+ const first = author.firstName ? escapeBibtex(author.firstName) : "";
151605
+ if (!last && !first)
151606
+ return "";
151607
+ if (!first)
151608
+ return `{${last}}`;
151609
+ if (!last)
151610
+ return first;
151611
+ return `{${last}}, ${first}`;
151612
+ }
151613
+ function formatApa(article) {
151614
+ const parts = [];
151615
+ const authorStr = article.authors?.length ? formatAuthorsApa(article.authors) : "";
151616
+ if (authorStr) {
151617
+ parts.push(authorStr);
151326
151618
  }
151327
- h4(text, emoji3) {
151328
- const prefix = emoji3 ? `${emoji3} ` : "";
151329
- this.sections.push(`#### ${prefix}${text}
151330
-
151331
- `);
151332
- return this;
151619
+ const year = getYear(article);
151620
+ parts.push(`(${year}).`);
151621
+ if (article.title) {
151622
+ const title = article.title.replace(/\.\s*$/, "");
151623
+ parts.push(`${title}.`);
151333
151624
  }
151334
- keyValue(key, value) {
151335
- const displayValue = value === null ? "null" : String(value);
151336
- this.sections.push(`**${key}:** ${displayValue}
151337
- `);
151338
- return this;
151625
+ const journal = article.journalInfo;
151626
+ if (journal?.title) {
151627
+ let journalPart = `*${journal.title}*`;
151628
+ if (journal.volume) {
151629
+ journalPart += `, *${journal.volume}*`;
151630
+ if (journal.issue) {
151631
+ journalPart += `(${journal.issue})`;
151632
+ }
151633
+ }
151634
+ if (journal.pages) {
151635
+ journalPart += `, ${journal.pages}`;
151636
+ }
151637
+ journalPart += ".";
151638
+ parts.push(journalPart);
151339
151639
  }
151340
- keyValuePlain(key, value) {
151341
- const displayValue = value === null ? "null" : String(value);
151342
- this.sections.push(`${key}: ${displayValue}
151343
- `);
151344
- return this;
151640
+ if (article.doi) {
151641
+ parts.push(`https://doi.org/${article.doi}`);
151345
151642
  }
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;
151643
+ return parts.join(" ");
151644
+ }
151645
+ function formatMla(article) {
151646
+ const parts = [];
151647
+ const authorStr = article.authors?.length ? formatAuthorsMla(article.authors) : "";
151648
+ if (authorStr) {
151649
+ parts.push(authorStr.endsWith(".") ? authorStr : `${authorStr}.`);
151355
151650
  }
151356
- codeBlock(content, language = "") {
151357
- this.sections.push(`\`\`\`${language}
151358
- ${content}
151359
- \`\`\`
151360
-
151361
- `);
151362
- return this;
151651
+ if (article.title) {
151652
+ const title = article.title.replace(/\.\s*$/, "");
151653
+ parts.push(`"${title}."`);
151363
151654
  }
151364
- inlineCode(code) {
151365
- this.sections.push(`\`${code}\``);
151366
- return this;
151655
+ const journal = article.journalInfo;
151656
+ if (journal?.title) {
151657
+ const detailParts = [];
151658
+ detailParts.push(`*${journal.title}*`);
151659
+ if (journal.volume) {
151660
+ detailParts.push(`vol. ${journal.volume}`);
151661
+ }
151662
+ if (journal.issue) {
151663
+ detailParts.push(`no. ${journal.issue}`);
151664
+ }
151665
+ const year = getYear(article);
151666
+ if (year !== "n.d.") {
151667
+ detailParts.push(year);
151668
+ }
151669
+ if (journal.pages) {
151670
+ detailParts.push(`pp. ${journal.pages}`);
151671
+ }
151672
+ parts.push(`${detailParts.join(", ")}.`);
151367
151673
  }
151368
- paragraph(text) {
151369
- this.sections.push(`${text}
151370
-
151371
- `);
151372
- return this;
151674
+ if (article.doi) {
151675
+ parts.push(`https://doi.org/${article.doi}.`);
151373
151676
  }
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;
151677
+ return parts.join(" ");
151678
+ }
151679
+ function formatBibtex(article) {
151680
+ const key = `pmid${article.pmid}`;
151681
+ const fields = [];
151682
+ if (article.authors?.length) {
151683
+ const authorStr = article.authors.map(formatAuthorBibtex).filter(Boolean).join(" and ");
151684
+ if (authorStr)
151685
+ fields.push(["author", authorStr]);
151383
151686
  }
151384
- hr() {
151385
- this.sections.push(`---
151386
-
151387
- `);
151388
- return this;
151687
+ if (article.title) {
151688
+ fields.push(["title", `{${escapeBibtex(article.title)}}`]);
151389
151689
  }
151390
- link(text, url2) {
151391
- this.sections.push(`[${text}](${url2})`);
151392
- return this;
151690
+ const journal = article.journalInfo;
151691
+ if (journal?.title) {
151692
+ fields.push(["journal", escapeBibtex(journal.title)]);
151393
151693
  }
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;
151694
+ const year = getYear(article);
151695
+ if (year !== "n.d.") {
151696
+ fields.push(["year", year]);
151408
151697
  }
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;
151429
- }
151430
- callback();
151431
- return this;
151698
+ if (journal?.volume) {
151699
+ fields.push(["volume", escapeBibtex(journal.volume)]);
151432
151700
  }
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;
151701
+ if (journal?.issue) {
151702
+ fields.push(["number", escapeBibtex(journal.issue)]);
151445
151703
  }
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;
151704
+ if (journal?.pages) {
151705
+ fields.push(["pages", escapeBibtex(journal.pages)]);
151459
151706
  }
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
-
151466
- `);
151467
- return this;
151707
+ if (article.doi) {
151708
+ fields.push(["doi", article.doi]);
151468
151709
  }
151469
- image(altText, url2, title) {
151470
- const titlePart = title ? ` "${title}"` : "";
151471
- this.sections.push(`![${altText}](${url2}${titlePart})
151472
-
151710
+ fields.push(["pmid", article.pmid]);
151711
+ const maxKeyLen = Math.max(...fields.map(([k]) => k.length));
151712
+ const fieldLines = fields.map(([k, v]) => ` ${k.padEnd(maxKeyLen)} = {${v}}`).join(`,
151473
151713
  `);
151474
- return this;
151714
+ return `@article{${key},
151715
+ ${fieldLines}
151716
+ }`;
151717
+ }
151718
+ function formatRis(article) {
151719
+ const lines = [];
151720
+ const tag = (code, value) => {
151721
+ if (value)
151722
+ lines.push(`${code} - ${value}`);
151723
+ };
151724
+ lines.push("TY - JOUR");
151725
+ if (article.authors?.length) {
151726
+ for (const author of article.authors) {
151727
+ if (author.collectiveName) {
151728
+ tag("AU", author.collectiveName);
151729
+ } else {
151730
+ const last = author.lastName ?? "";
151731
+ const first = author.firstName ?? "";
151732
+ if (last || first) {
151733
+ tag("AU", first ? `${last}, ${first}` : last);
151734
+ }
151735
+ }
151736
+ }
151475
151737
  }
151476
- strikethrough(text) {
151477
- this.sections.push(`~~${text}~~`);
151478
- return this;
151738
+ tag("TI", article.title);
151739
+ const journal = article.journalInfo;
151740
+ if (journal?.title) {
151741
+ tag("JF", journal.title);
151479
151742
  }
151480
- diff(changes) {
151481
- const lines = [];
151482
- if (changes.context) {
151483
- lines.push(...changes.context.map((line) => ` ${line}`));
151484
- }
151485
- if (changes.deletions) {
151486
- lines.push(...changes.deletions.map((line) => `- ${line}`));
151487
- }
151488
- if (changes.additions) {
151489
- lines.push(...changes.additions.map((line) => `+ ${line}`));
151490
- }
151491
- if (lines.length > 0) {
151492
- this.codeBlock(lines.join(`
151493
- `), "diff");
151743
+ if (journal?.isoAbbreviation) {
151744
+ tag("JO", journal.isoAbbreviation);
151745
+ }
151746
+ const year = getYear(article);
151747
+ if (year !== "n.d.") {
151748
+ tag("PY", year);
151749
+ }
151750
+ tag("VL", journal?.volume);
151751
+ tag("IS", journal?.issue);
151752
+ if (journal?.pages) {
151753
+ const { start, end } = splitPages(journal.pages);
151754
+ tag("SP", start);
151755
+ tag("EP", end);
151756
+ }
151757
+ tag("DO", article.doi);
151758
+ tag("AN", article.pmid);
151759
+ lines.push(`UR - https://pubmed.ncbi.nlm.nih.gov/${article.pmid}/`);
151760
+ if (article.keywords?.length) {
151761
+ for (const kw of article.keywords) {
151762
+ tag("KW", kw);
151494
151763
  }
151495
- return this;
151496
151764
  }
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;
151765
+ tag("AB", article.abstractText);
151766
+ lines.push("ER - ");
151767
+ return lines.join(`
151768
+ `);
151769
+ }
151770
+ function formatCitation(article, style) {
151771
+ switch (style) {
151772
+ case "apa":
151773
+ return formatApa(article);
151774
+ case "mla":
151775
+ return formatMla(article);
151776
+ case "bibtex":
151777
+ return formatBibtex(article);
151778
+ case "ris":
151779
+ return formatRis(article);
151503
151780
  }
151504
- bold(text) {
151505
- this.sections.push(`**${text}**`);
151506
- return this;
151781
+ }
151782
+ function formatCitations(article, styles) {
151783
+ const result = {};
151784
+ for (const style of styles) {
151785
+ result[style] = formatCitation(article, style);
151507
151786
  }
151508
- italic(text) {
151509
- this.sections.push(`*${text}*`);
151510
- return this;
151787
+ return result;
151788
+ }
151789
+
151790
+ // src/services/ncbi/parsing/article-parser.ts
151791
+ function extractAuthors(authorListXml) {
151792
+ if (!authorListXml)
151793
+ return { authors: [], affiliations: [] };
151794
+ const affiliationMap = new Map;
151795
+ const affiliationList = [];
151796
+ function getAffiliationIndex(text) {
151797
+ const existing = affiliationMap.get(text);
151798
+ if (existing !== undefined)
151799
+ return existing;
151800
+ const idx = affiliationList.length;
151801
+ affiliationList.push(text);
151802
+ affiliationMap.set(text, idx);
151803
+ return idx;
151511
151804
  }
151512
- boldItalic(text) {
151513
- this.sections.push(`***${text}***`);
151514
- return this;
151805
+ const xmlAuthors = ensureArray(authorListXml.Author);
151806
+ const authors = xmlAuthors.map((auth) => {
151807
+ const collectiveName = getText(auth.CollectiveName);
151808
+ if (collectiveName) {
151809
+ return { collectiveName };
151810
+ }
151811
+ const authorAffiliationInfos = ensureArray(auth.AffiliationInfo);
151812
+ const indices = [];
151813
+ for (const info of authorAffiliationInfos) {
151814
+ const text = getText(info?.Affiliation);
151815
+ if (text)
151816
+ indices.push(getAffiliationIndex(text));
151817
+ }
151818
+ let orcid;
151819
+ const identifiers = ensureArray(auth.Identifier);
151820
+ for (const id of identifiers) {
151821
+ if (getAttribute(id, "Source") === "ORCID") {
151822
+ const val = getText(id);
151823
+ if (val) {
151824
+ orcid = val;
151825
+ break;
151826
+ }
151827
+ }
151828
+ }
151829
+ return {
151830
+ lastName: getText(auth.LastName),
151831
+ firstName: getText(auth.ForeName),
151832
+ initials: getText(auth.Initials),
151833
+ ...indices.length > 0 && { affiliationIndices: indices },
151834
+ ...orcid && { orcid }
151835
+ };
151836
+ });
151837
+ return { authors, affiliations: affiliationList };
151838
+ }
151839
+ function extractJournalInfo(journalXml, articleXml) {
151840
+ if (!journalXml)
151841
+ return;
151842
+ const pubDate = journalXml.JournalIssue?.PubDate;
151843
+ const year = getText(pubDate?.Year, getText(pubDate?.MedlineDate, "").match(/\d{4}/)?.[0]);
151844
+ const issnElement = journalXml.ISSN;
151845
+ const issnValue = getText(issnElement);
151846
+ const issnType = getAttribute(issnElement, "IssnType");
151847
+ const issn = issnType === "Electronic" ? undefined : issnValue || undefined;
151848
+ const eIssn = issnType === "Electronic" ? issnValue || undefined : undefined;
151849
+ return {
151850
+ title: getText(journalXml.Title),
151851
+ isoAbbreviation: getText(journalXml.ISOAbbreviation),
151852
+ ...issn && { issn },
151853
+ ...eIssn && { eIssn },
151854
+ volume: getText(journalXml.JournalIssue?.Volume),
151855
+ issue: getText(journalXml.JournalIssue?.Issue),
151856
+ pages: getText(articleXml?.Pagination?.MedlinePgn),
151857
+ publicationDate: {
151858
+ ...year && { year },
151859
+ ...getText(pubDate?.Month) && { month: getText(pubDate?.Month) },
151860
+ ...getText(pubDate?.Day) && { day: getText(pubDate?.Day) },
151861
+ ...getText(pubDate?.MedlineDate) && { medlineDate: getText(pubDate?.MedlineDate) }
151862
+ }
151863
+ };
151864
+ }
151865
+ function extractMeshTerms(meshHeadingListXml) {
151866
+ if (!meshHeadingListXml)
151867
+ return [];
151868
+ const meshHeadings = ensureArray(meshHeadingListXml.MeshHeading);
151869
+ return meshHeadings.map((mh) => {
151870
+ const isMajorDescriptor = getAttribute(mh.DescriptorName, "MajorTopicYN") === "Y";
151871
+ const isMajorRoot = getAttribute(mh, "MajorTopicYN") === "Y";
151872
+ const descriptorUi = getAttribute(mh.DescriptorName, "UI");
151873
+ const rawQualifiers = ensureArray(mh.QualifierName);
151874
+ const qualifiers = rawQualifiers.flatMap((q) => {
151875
+ const name = getText(q);
151876
+ if (!name)
151877
+ return [];
151878
+ const ui = getAttribute(q, "UI");
151879
+ return {
151880
+ qualifierName: name,
151881
+ ...ui && { qualifierUi: ui },
151882
+ isMajorTopic: getAttribute(q, "MajorTopicYN") === "Y"
151883
+ };
151884
+ });
151885
+ const isMajorAnyQualifier = qualifiers.some((q) => q.isMajorTopic);
151886
+ return {
151887
+ descriptorName: getText(mh.DescriptorName),
151888
+ ...descriptorUi && { descriptorUi },
151889
+ ...qualifiers.length > 0 && { qualifiers },
151890
+ isMajorTopic: isMajorRoot || isMajorDescriptor || isMajorAnyQualifier
151891
+ };
151892
+ });
151893
+ }
151894
+ function extractGrants(grantListXml) {
151895
+ if (!grantListXml)
151896
+ return [];
151897
+ const grants = ensureArray(grantListXml.Grant);
151898
+ return grants.map((g) => {
151899
+ const grantId = getText(g.GrantID);
151900
+ const acronym = getText(g.Acronym);
151901
+ const agency = getText(g.Agency);
151902
+ const country = getText(g.Country);
151903
+ return {
151904
+ ...grantId && { grantId },
151905
+ ...acronym && { acronym },
151906
+ ...agency && { agency },
151907
+ ...country && { country }
151908
+ };
151909
+ });
151910
+ }
151911
+ function extractDoi(articleXml, pubmedDataArticleIdList) {
151912
+ if (!articleXml)
151913
+ return;
151914
+ const eLocationIDs = ensureArray(articleXml.ELocationID);
151915
+ for (const eloc of eLocationIDs) {
151916
+ if (getAttribute(eloc, "EIdType") === "doi" && getAttribute(eloc, "ValidYN") === "Y") {
151917
+ const doi = getText(eloc);
151918
+ if (doi)
151919
+ return doi;
151920
+ }
151515
151921
  }
151516
- raw(markdown) {
151517
- this.sections.push(markdown);
151518
- return this;
151922
+ for (const eloc of eLocationIDs) {
151923
+ if (getAttribute(eloc, "EIdType") === "doi") {
151924
+ const doi = getText(eloc);
151925
+ if (doi)
151926
+ return doi;
151927
+ }
151519
151928
  }
151520
- blankLine() {
151521
- this.sections.push(`
151522
- `);
151523
- return this;
151929
+ const articleIds = ensureArray(articleXml.ArticleIdList?.ArticleId);
151930
+ for (const aid of articleIds) {
151931
+ if (getAttribute(aid, "IdType") === "doi") {
151932
+ const doi = getText(aid);
151933
+ if (doi)
151934
+ return doi;
151935
+ }
151524
151936
  }
151525
- text(text) {
151526
- this.sections.push(text);
151527
- return this;
151937
+ if (pubmedDataArticleIdList) {
151938
+ const pubmedDataIds = ensureArray(pubmedDataArticleIdList.ArticleId);
151939
+ for (const aid of pubmedDataIds) {
151940
+ if (getAttribute(aid, "IdType") === "doi") {
151941
+ const doi = getText(aid);
151942
+ if (doi)
151943
+ return doi;
151944
+ }
151945
+ }
151528
151946
  }
151529
- when(condition, content) {
151530
- if (condition) {
151531
- content();
151947
+ return;
151948
+ }
151949
+ function extractPmcId(articleXml, pubmedDataArticleIdList) {
151950
+ const articleIds = ensureArray(articleXml?.ArticleIdList?.ArticleId);
151951
+ for (const aid of articleIds) {
151952
+ if (getAttribute(aid, "IdType") === "pmc") {
151953
+ const val = getText(aid);
151954
+ if (val)
151955
+ return val;
151532
151956
  }
151533
- return this;
151534
151957
  }
151535
- build() {
151536
- return this.sections.join("").trim();
151958
+ if (pubmedDataArticleIdList) {
151959
+ const pubmedDataIds = ensureArray(pubmedDataArticleIdList.ArticleId);
151960
+ for (const aid of pubmedDataIds) {
151961
+ if (getAttribute(aid, "IdType") === "pmc") {
151962
+ const val = getText(aid);
151963
+ if (val)
151964
+ return val;
151965
+ }
151966
+ }
151537
151967
  }
151538
- reset() {
151539
- this.sections = [];
151540
- return this;
151968
+ return;
151969
+ }
151970
+ function extractPublicationTypes(publicationTypeListXml) {
151971
+ if (!publicationTypeListXml)
151972
+ return [];
151973
+ const pubTypes = ensureArray(publicationTypeListXml.PublicationType);
151974
+ return pubTypes.map((pt) => getText(pt)).filter(Boolean);
151975
+ }
151976
+ function extractKeywords2(keywordListsXml) {
151977
+ if (!keywordListsXml)
151978
+ return [];
151979
+ const lists = ensureArray(keywordListsXml);
151980
+ const allKeywords = [];
151981
+ for (const list of lists) {
151982
+ for (const kw of ensureArray(list.Keyword)) {
151983
+ const keywordText = getText(kw);
151984
+ if (keywordText) {
151985
+ allKeywords.push(keywordText);
151986
+ }
151987
+ }
151541
151988
  }
151989
+ return allKeywords;
151542
151990
  }
151543
- function markdown() {
151544
- return new MarkdownBuilder;
151991
+ function extractAbstractText(abstractXml) {
151992
+ if (!abstractXml || !abstractXml.AbstractText)
151993
+ return;
151994
+ const abstractTexts = ensureArray(abstractXml.AbstractText);
151995
+ if (abstractTexts.length === 0)
151996
+ return;
151997
+ const processedTexts = abstractTexts.map((at) => {
151998
+ if (typeof at === "string") {
151999
+ return at;
152000
+ }
152001
+ const sectionText = getText(at);
152002
+ const label = getAttribute(at, "Label");
152003
+ if (label && sectionText) {
152004
+ return `${label.trim()}: ${sectionText.trim()}`;
152005
+ }
152006
+ return sectionText.trim();
152007
+ }).filter(Boolean);
152008
+ if (processedTexts.length === 0)
152009
+ return;
152010
+ return processedTexts.join(`
152011
+
152012
+ `).trim() || undefined;
152013
+ }
152014
+ function extractPmid(medlineCitationXml) {
152015
+ if (!medlineCitationXml || !medlineCitationXml.PMID)
152016
+ return;
152017
+ return getText(medlineCitationXml.PMID);
152018
+ }
152019
+ function extractArticleDates(articleXml) {
152020
+ if (!articleXml || !articleXml.ArticleDate)
152021
+ return [];
152022
+ const articleDatesXml = ensureArray(articleXml.ArticleDate);
152023
+ return articleDatesXml.map((ad) => ({
152024
+ dateType: getAttribute(ad, "DateType"),
152025
+ year: getText(ad.Year),
152026
+ month: getText(ad.Month),
152027
+ day: getText(ad.Day)
152028
+ }));
152029
+ }
152030
+ function parseFullArticle(xmlArticle, options = {}) {
152031
+ const medlineCitation = xmlArticle.MedlineCitation;
152032
+ const article = medlineCitation?.Article;
152033
+ const { includeMesh = true, includeGrants = false } = options;
152034
+ const abstractText = extractAbstractText(article?.Abstract);
152035
+ const journalInfo = extractJournalInfo(article?.Journal, article);
152036
+ const pubmedDataArticleIdList = xmlArticle.PubmedData?.ArticleIdList;
152037
+ const doi = extractDoi(article, pubmedDataArticleIdList);
152038
+ const pmcId = extractPmcId(article, pubmedDataArticleIdList);
152039
+ const { authors, affiliations } = extractAuthors(article?.AuthorList);
152040
+ return {
152041
+ pmid: extractPmid(medlineCitation) ?? "",
152042
+ title: getText(article?.ArticleTitle),
152043
+ ...abstractText !== undefined && { abstractText },
152044
+ ...affiliations.length > 0 && { affiliations },
152045
+ authors,
152046
+ ...journalInfo !== undefined && { journalInfo },
152047
+ publicationTypes: extractPublicationTypes(article?.PublicationTypeList),
152048
+ keywords: extractKeywords2(medlineCitation?.KeywordList ?? article?.KeywordList),
152049
+ ...includeMesh && { meshTerms: extractMeshTerms(medlineCitation?.MeshHeadingList) },
152050
+ ...includeGrants && { grantList: extractGrants(article?.GrantList) },
152051
+ ...doi !== undefined && { doi },
152052
+ ...pmcId !== undefined && { pmcId },
152053
+ articleDates: extractArticleDates(article)
152054
+ };
151545
152055
  }
151546
152056
 
151547
152057
  // 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 = {
152058
+ var ncbi3 = () => container.resolve(NcbiServiceToken);
152059
+ var TOOL_NAME2 = "pubmed_cite";
152060
+ var TOOL_TITLE2 = "PubMed Citations";
152061
+ var TOOL_DESCRIPTION2 = "Get formatted citations for PubMed articles in APA, MLA, BibTeX, or RIS format.";
152062
+ var TOOL_ANNOTATIONS2 = {
151553
152063
  readOnlyHint: true,
151554
152064
  idempotentHint: true,
151555
152065
  openWorldHint: true
151556
152066
  };
151557
- var InputSchema = exports_external.object({
152067
+ var InputSchema2 = exports_external.object({
151558
152068
  pmids: exports_external.array(exports_external.string().regex(/^\d+$/)).min(1).max(50).describe("PubMed IDs to cite"),
151559
152069
  styles: exports_external.array(exports_external.enum(["apa", "mla", "bibtex", "ris"])).default(["apa"]).describe("Citation styles to generate")
151560
152070
  });
151561
- var OutputSchema2 = exports_external.object({
152071
+ var OutputSchema3 = exports_external.object({
151562
152072
  citations: exports_external.array(exports_external.object({
151563
152073
  pmid: exports_external.string(),
151564
152074
  title: exports_external.string().optional(),
151565
152075
  citations: exports_external.record(exports_external.string(), exports_external.string())
151566
152076
  })).describe("Citations per article")
151567
152077
  });
151568
- async function logic(input, appContext, _sdkContext) {
152078
+ async function logic2(input, appContext, _sdkContext) {
151569
152079
  logger.debug("Fetching articles for citation generation", {
151570
152080
  ...appContext,
151571
152081
  pmids: input.pmids,
151572
152082
  styles: input.styles
151573
152083
  });
151574
- const raw = await ncbi2().eFetch({ db: "pubmed", id: input.pmids.join(",") }, appContext);
152084
+ const raw = await ncbi3().eFetch({ db: "pubmed", id: input.pmids.join(",") }, appContext);
151575
152085
  const xmlArticles = ensureArray(raw?.PubmedArticleSet?.PubmedArticle);
151576
152086
  if (xmlArticles.length === 0) {
151577
152087
  throw new McpError(-32001 /* NotFound */, `No articles found for PMIDs: ${input.pmids.join(", ")}`, { requestId: appContext.requestId });
@@ -151590,7 +152100,7 @@ async function logic(input, appContext, _sdkContext) {
151590
152100
  });
151591
152101
  return { citations };
151592
152102
  }
151593
- function responseFormatter(result) {
152103
+ function responseFormatter2(result) {
151594
152104
  const md = markdown();
151595
152105
  md.text(`# PubMed Citations
151596
152106
  `);
@@ -151617,32 +152127,32 @@ ${citation}
151617
152127
  return [{ type: "text", text: md.build() }];
151618
152128
  }
151619
152129
  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
152130
+ name: TOOL_NAME2,
152131
+ title: TOOL_TITLE2,
152132
+ description: TOOL_DESCRIPTION2,
152133
+ annotations: TOOL_ANNOTATIONS2,
152134
+ inputSchema: InputSchema2,
152135
+ outputSchema: OutputSchema3,
152136
+ logic: withToolAuth(["tool:pubmed_cite:read"], logic2),
152137
+ responseFormatter: responseFormatter2
151628
152138
  };
151629
152139
 
151630
152140
  // 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 = {
152141
+ var ncbi4 = () => container.resolve(NcbiServiceToken);
152142
+ var TOOL_NAME3 = "pubmed_fetch";
152143
+ var TOOL_TITLE3 = "PubMed Fetch";
152144
+ var TOOL_DESCRIPTION3 = "Fetch full article metadata by PubMed IDs. Returns detailed article information including abstract, authors, journal, MeSH terms.";
152145
+ var TOOL_ANNOTATIONS3 = {
151636
152146
  readOnlyHint: true,
151637
152147
  idempotentHint: true,
151638
152148
  openWorldHint: true
151639
152149
  };
151640
- var InputSchema2 = exports_external.object({
152150
+ var InputSchema3 = exports_external.object({
151641
152151
  pmids: exports_external.array(exports_external.string().regex(/^\d+$/)).min(1).max(200).describe("PubMed IDs to fetch"),
151642
152152
  includeMesh: exports_external.boolean().default(true).describe("Include MeSH terms"),
151643
152153
  includeGrants: exports_external.boolean().default(false).describe("Include grant information")
151644
152154
  });
151645
- var ArticleSchema = exports_external.object({
152155
+ var ArticleSchema2 = exports_external.object({
151646
152156
  pmid: exports_external.string().optional().describe("PubMed ID"),
151647
152157
  title: exports_external.string().optional().describe("Article title"),
151648
152158
  abstractText: exports_external.string().optional().describe("Abstract text"),
@@ -151657,16 +152167,16 @@ var ArticleSchema = exports_external.object({
151657
152167
  meshTerms: exports_external.array(exports_external.any()).optional().describe("MeSH terms"),
151658
152168
  grantList: exports_external.array(exports_external.any()).optional().describe("Grant information")
151659
152169
  });
151660
- var OutputSchema3 = exports_external.object({
151661
- articles: exports_external.array(ArticleSchema).describe("Parsed articles"),
152170
+ var OutputSchema4 = exports_external.object({
152171
+ articles: exports_external.array(ArticleSchema2).describe("Parsed articles"),
151662
152172
  totalReturned: exports_external.number().describe("Number of articles returned")
151663
152173
  });
151664
- async function logic2(input, appContext, _sdkContext) {
152174
+ async function logic3(input, appContext, _sdkContext) {
151665
152175
  logger.info("Executing pubmed_fetch tool", {
151666
152176
  ...appContext,
151667
152177
  pmidCount: input.pmids.length
151668
152178
  });
151669
- const xmlData = await ncbi3().eFetch({ db: "pubmed", id: input.pmids.join(","), retmode: "xml" }, appContext, { retmode: "xml", usePost: input.pmids.length > 200 });
152179
+ const xmlData = await ncbi4().eFetch({ db: "pubmed", id: input.pmids.join(","), retmode: "xml" }, appContext, { retmode: "xml", usePost: input.pmids.length > 200 });
151670
152180
  if (!xmlData || !("PubmedArticleSet" in xmlData)) {
151671
152181
  throw new McpError(-32603 /* InternalError */, "Invalid EFetch response from NCBI: missing PubmedArticleSet", { requestId: appContext.requestId });
151672
152182
  }
@@ -151694,7 +152204,7 @@ async function logic2(input, appContext, _sdkContext) {
151694
152204
  });
151695
152205
  return { articles, totalReturned: articles.length };
151696
152206
  }
151697
- function responseFormatter2(result) {
152207
+ function responseFormatter3(result) {
151698
152208
  const md = markdown().h2("PubMed Articles").keyValue("Articles Returned", result.totalReturned);
151699
152209
  for (const article of result.articles) {
151700
152210
  md.h3(article.title ?? article.pmid ?? "Unknown");
@@ -151729,27 +152239,27 @@ function responseFormatter2(result) {
151729
152239
  return [{ type: "text", text: md.build() }];
151730
152240
  }
151731
152241
  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
152242
+ name: TOOL_NAME3,
152243
+ title: TOOL_TITLE3,
152244
+ description: TOOL_DESCRIPTION3,
152245
+ annotations: TOOL_ANNOTATIONS3,
152246
+ inputSchema: InputSchema3,
152247
+ outputSchema: OutputSchema4,
152248
+ logic: withToolAuth(["tool:pubmed_fetch:read"], logic3),
152249
+ responseFormatter: responseFormatter3
151740
152250
  };
151741
152251
 
151742
152252
  // 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 = {
152253
+ var ncbi5 = () => container.resolve(NcbiServiceToken);
152254
+ var TOOL_NAME4 = "pubmed_mesh_lookup";
152255
+ var TOOL_TITLE4 = "MeSH Term Lookup";
152256
+ var TOOL_DESCRIPTION4 = "Search and explore MeSH (Medical Subject Headings) vocabulary. Essential for building precise PubMed queries.";
152257
+ var TOOL_ANNOTATIONS4 = {
151748
152258
  readOnlyHint: true,
151749
152259
  idempotentHint: true,
151750
152260
  openWorldHint: true
151751
152261
  };
151752
- var InputSchema3 = exports_external.object({
152262
+ var InputSchema4 = exports_external.object({
151753
152263
  term: exports_external.string().min(1).describe("MeSH term to look up"),
151754
152264
  maxResults: exports_external.number().int().min(1).max(50).default(10).describe("Maximum results"),
151755
152265
  includeDetails: exports_external.boolean().default(true).describe("Fetch full MeSH records (scope notes, tree numbers, entry terms)")
@@ -151761,7 +152271,7 @@ var MeshResultItem = exports_external.object({
151761
152271
  scopeNote: exports_external.string().optional().describe("Scope note describing the descriptor"),
151762
152272
  entryTerms: exports_external.array(exports_external.string()).optional().describe("Synonyms / entry terms")
151763
152273
  });
151764
- var OutputSchema4 = exports_external.object({
152274
+ var OutputSchema5 = exports_external.object({
151765
152275
  term: exports_external.string().describe("Original search term"),
151766
152276
  results: exports_external.array(MeshResultItem).describe("Matching MeSH records")
151767
152277
  });
@@ -151831,8 +152341,8 @@ async function meshLookupLogic(input, context, _sdkContext) {
151831
152341
  const { term, maxResults, includeDetails } = input;
151832
152342
  logger.debug("MeSH lookup started.", { ...context, term, maxResults, includeDetails });
151833
152343
  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);
152344
+ const broadSearch = ncbi5().eSearch({ db: "mesh", term, retmax: maxResults }, context);
152345
+ const exactSearch = hasFieldTag ? undefined : ncbi5().eSearch({ db: "mesh", term: `${term}[MH]`, retmax: 1 }, context);
151836
152346
  const [broadResult, exactResult] = await Promise.all([broadSearch, exactSearch]);
151837
152347
  const seen = new Set;
151838
152348
  const ids = [];
@@ -151847,7 +152357,7 @@ async function meshLookupLogic(input, context, _sdkContext) {
151847
152357
  logger.debug("No MeSH results found.", { ...context, term });
151848
152358
  return { term, results: [] };
151849
152359
  }
151850
- const summaryData = await ncbi4().eSummary({ db: "mesh", id: ids.join(",") }, context);
152360
+ const summaryData = await ncbi5().eSummary({ db: "mesh", id: ids.join(",") }, context);
151851
152361
  const results = parseSummaryRecords(summaryData, ids, includeDetails);
151852
152362
  const termLower = term.toLowerCase();
151853
152363
  results.sort((a, b) => {
@@ -151894,12 +152404,12 @@ function formatResponse(result) {
151894
152404
  return [{ type: "text", text: md.build() }];
151895
152405
  }
151896
152406
  var pubmedMeshLookupTool = {
151897
- name: TOOL_NAME3,
151898
- title: TOOL_TITLE3,
151899
- description: TOOL_DESCRIPTION3,
151900
- annotations: TOOL_ANNOTATIONS3,
151901
- inputSchema: InputSchema3,
151902
- outputSchema: OutputSchema4,
152407
+ name: TOOL_NAME4,
152408
+ title: TOOL_TITLE4,
152409
+ description: TOOL_DESCRIPTION4,
152410
+ annotations: TOOL_ANNOTATIONS4,
152411
+ inputSchema: InputSchema4,
152412
+ outputSchema: OutputSchema5,
151903
152413
  logic: withToolAuth(["tool:pubmed_mesh_lookup:read"], meshLookupLogic),
151904
152414
  responseFormatter: formatResponse
151905
152415
  };
@@ -155051,21 +155561,21 @@ async function extractBriefSummaries(eSummaryResult, context) {
155051
155561
  }
155052
155562
 
155053
155563
  // 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 = {
155564
+ var ncbi6 = () => container.resolve(NcbiServiceToken);
155565
+ var TOOL_NAME5 = "pubmed_related";
155566
+ var TOOL_TITLE5 = "PubMed Related Articles";
155567
+ var TOOL_DESCRIPTION5 = "Find articles related to a source article — similar content, citing articles, or references.";
155568
+ var TOOL_ANNOTATIONS5 = {
155059
155569
  readOnlyHint: true,
155060
155570
  idempotentHint: true,
155061
155571
  openWorldHint: true
155062
155572
  };
155063
- var InputSchema4 = exports_external.object({
155573
+ var InputSchema5 = exports_external.object({
155064
155574
  pmid: exports_external.string().regex(/^\d+$/).describe("Source PubMed ID"),
155065
155575
  relationship: exports_external.enum(["similar", "cited_by", "references"]).default("similar").describe("Type of relationship"),
155066
155576
  maxResults: exports_external.number().int().min(1).max(50).default(10).describe("Maximum related articles")
155067
155577
  });
155068
- var OutputSchema5 = exports_external.object({
155578
+ var OutputSchema6 = exports_external.object({
155069
155579
  sourcePmid: exports_external.string().describe("Source PubMed ID"),
155070
155580
  relationship: exports_external.string().describe("Relationship type used"),
155071
155581
  articles: exports_external.array(exports_external.object({
@@ -155084,7 +155594,7 @@ function extractValue(field) {
155084
155594
  }
155085
155595
  return String(field);
155086
155596
  }
155087
- async function logic3(input, appContext, _sdkContext) {
155597
+ async function logic4(input, appContext, _sdkContext) {
155088
155598
  logger.debug("Finding related articles", {
155089
155599
  ...appContext,
155090
155600
  pmid: input.pmid,
@@ -155109,7 +155619,7 @@ async function logic3(input, appContext, _sdkContext) {
155109
155619
  eLinkParams.linkname = "pubmed_pubmed_refs";
155110
155620
  break;
155111
155621
  }
155112
- const eLinkResult = await ncbi5().eLink(eLinkParams, appContext);
155622
+ const eLinkResult = await ncbi6().eLink(eLinkParams, appContext);
155113
155623
  logger.debug("Raw ELink response received", {
155114
155624
  ...appContext,
155115
155625
  hasResult: !!eLinkResult?.eLinkResult
@@ -155151,7 +155661,7 @@ async function logic3(input, appContext, _sdkContext) {
155151
155661
  }
155152
155662
  const pmidsToEnrich = foundPmids.slice(0, input.maxResults);
155153
155663
  const pmidIds = pmidsToEnrich.map((p) => p.pmid);
155154
- const summaryResult = await ncbi5().eSummary({ db: "pubmed", id: pmidIds.join(",") }, appContext);
155664
+ const summaryResult = await ncbi6().eSummary({ db: "pubmed", id: pmidIds.join(",") }, appContext);
155155
155665
  const briefSummaries = await extractBriefSummaries(summaryResult, appContext);
155156
155666
  const summaryMap = new Map(briefSummaries.map((bs) => [bs.pmid, bs]));
155157
155667
  const articles = pmidsToEnrich.map((p) => {
@@ -155175,7 +155685,7 @@ async function logic3(input, appContext, _sdkContext) {
155175
155685
  totalFound
155176
155686
  };
155177
155687
  }
155178
- function responseFormatter3(result) {
155688
+ function responseFormatter4(result) {
155179
155689
  const md = markdown();
155180
155690
  md.text(`# Related Articles for PMID ${result.sourcePmid}
155181
155691
  `);
@@ -155198,27 +155708,27 @@ function responseFormatter3(result) {
155198
155708
  return [{ type: "text", text: md.build() }];
155199
155709
  }
155200
155710
  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
155711
+ name: TOOL_NAME5,
155712
+ title: TOOL_TITLE5,
155713
+ description: TOOL_DESCRIPTION5,
155714
+ annotations: TOOL_ANNOTATIONS5,
155715
+ inputSchema: InputSchema5,
155716
+ outputSchema: OutputSchema6,
155717
+ logic: withToolAuth(["tool:pubmed_related:read"], logic4),
155718
+ responseFormatter: responseFormatter4
155209
155719
  };
155210
155720
 
155211
155721
  // 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 = {
155722
+ var ncbi7 = () => container.resolve(NcbiServiceToken);
155723
+ var TOOL_NAME6 = "pubmed_search";
155724
+ var TOOL_TITLE6 = "PubMed Search";
155725
+ 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.";
155726
+ var TOOL_ANNOTATIONS6 = {
155217
155727
  readOnlyHint: true,
155218
155728
  idempotentHint: true,
155219
155729
  openWorldHint: true
155220
155730
  };
155221
- var InputSchema5 = exports_external.object({
155731
+ var InputSchema6 = exports_external.object({
155222
155732
  query: exports_external.string().min(1).describe("PubMed search query (supports full NCBI syntax)"),
155223
155733
  maxResults: exports_external.number().int().min(1).max(1000).default(20).describe("Maximum results to return"),
155224
155734
  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 +155748,7 @@ var InputSchema5 = exports_external.object({
155238
155748
  species: exports_external.enum(["humans", "animals"]).optional().describe("Filter by species"),
155239
155749
  summaryCount: exports_external.number().int().min(0).max(50).default(0).describe("Fetch brief summaries for top N results (0 = PMIDs only)")
155240
155750
  });
155241
- var OutputSchema6 = exports_external.object({
155751
+ var OutputSchema7 = exports_external.object({
155242
155752
  query: exports_external.string().describe("Original query"),
155243
155753
  totalFound: exports_external.number().describe("Total matching articles"),
155244
155754
  offset: exports_external.number().describe("Result offset used"),
@@ -155256,7 +155766,7 @@ var OutputSchema6 = exports_external.object({
155256
155766
  })).optional().describe("Brief summaries"),
155257
155767
  searchUrl: exports_external.string().describe("PubMed search URL")
155258
155768
  });
155259
- async function logic4(input, appContext, _sdkContext) {
155769
+ async function logic5(input, appContext, _sdkContext) {
155260
155770
  logger.info("Executing pubmed_search tool", { ...appContext, query: input.query });
155261
155771
  let effectiveQuery = sanitization.sanitizeString(input.query, { context: "text" });
155262
155772
  if (input.dateRange) {
@@ -155291,7 +155801,7 @@ async function logic4(input, appContext, _sdkContext) {
155291
155801
  if (input.species) {
155292
155802
  effectiveQuery += ` AND ${input.species}[MeSH Terms]`;
155293
155803
  }
155294
- const esResult = await ncbi6().eSearch({
155804
+ const esResult = await ncbi7().eSearch({
155295
155805
  db: "pubmed",
155296
155806
  term: effectiveQuery,
155297
155807
  retmax: input.maxResults,
@@ -155315,7 +155825,7 @@ async function logic4(input, appContext, _sdkContext) {
155315
155825
  } else {
155316
155826
  eSummaryParams.id = pmids.slice(0, input.summaryCount).join(",");
155317
155827
  }
155318
- const eSummaryResult = await ncbi6().eSummary(eSummaryParams, appContext);
155828
+ const eSummaryResult = await ncbi7().eSummary(eSummaryParams, appContext);
155319
155829
  if (eSummaryResult) {
155320
155830
  const briefSummaries = await extractBriefSummaries(eSummaryResult, appContext);
155321
155831
  summaries = briefSummaries.map((s) => ({
@@ -155340,7 +155850,7 @@ async function logic4(input, appContext, _sdkContext) {
155340
155850
  });
155341
155851
  return { query: input.query, totalFound, offset: input.offset, pmids, summaries, searchUrl };
155342
155852
  }
155343
- function responseFormatter4(result) {
155853
+ function responseFormatter5(result) {
155344
155854
  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
155855
  if (result.pmids.length > 0) {
155346
155856
  md.h3("PMIDs").paragraph(result.pmids.join(", "));
@@ -155367,37 +155877,37 @@ function responseFormatter4(result) {
155367
155877
  return [{ type: "text", text: md.build() }];
155368
155878
  }
155369
155879
  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
155880
+ name: TOOL_NAME6,
155881
+ title: TOOL_TITLE6,
155882
+ description: TOOL_DESCRIPTION6,
155883
+ annotations: TOOL_ANNOTATIONS6,
155884
+ inputSchema: InputSchema6,
155885
+ outputSchema: OutputSchema7,
155886
+ logic: withToolAuth(["tool:pubmed_search:read"], logic5),
155887
+ responseFormatter: responseFormatter5
155378
155888
  };
155379
155889
 
155380
155890
  // 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 = {
155891
+ var ncbi8 = () => container.resolve(NcbiServiceToken);
155892
+ var TOOL_NAME7 = "pubmed_spell";
155893
+ var TOOL_TITLE7 = "PubMed Spell Check";
155894
+ var TOOL_DESCRIPTION7 = "Spell-check a query and get NCBI's suggested correction. Useful for refining search queries.";
155895
+ var TOOL_ANNOTATIONS7 = {
155386
155896
  readOnlyHint: true,
155387
155897
  idempotentHint: true,
155388
155898
  openWorldHint: true
155389
155899
  };
155390
- var InputSchema6 = exports_external.object({
155900
+ var InputSchema7 = exports_external.object({
155391
155901
  query: exports_external.string().min(2).describe("Query to spell-check")
155392
155902
  });
155393
- var OutputSchema7 = exports_external.object({
155903
+ var OutputSchema8 = exports_external.object({
155394
155904
  original: exports_external.string().describe("Original query"),
155395
155905
  corrected: exports_external.string().describe("Corrected query (same as original if no suggestion)"),
155396
155906
  hasSuggestion: exports_external.boolean().describe("Whether NCBI suggested a correction")
155397
155907
  });
155398
- async function logic5(input, appContext, _sdkContext) {
155908
+ async function logic6(input, appContext, _sdkContext) {
155399
155909
  logger.info("Executing pubmed_spell tool", { ...appContext, query: input.query });
155400
- const result = await ncbi7().eSpell({ db: "pubmed", term: input.query }, appContext);
155910
+ const result = await ncbi8().eSpell({ db: "pubmed", term: input.query }, appContext);
155401
155911
  logger.notice("pubmed_spell completed", {
155402
155912
  ...appContext,
155403
155913
  hasSuggestion: result.hasSuggestion
@@ -155408,7 +155918,7 @@ async function logic5(input, appContext, _sdkContext) {
155408
155918
  hasSuggestion: result.hasSuggestion
155409
155919
  };
155410
155920
  }
155411
- function responseFormatter5(result) {
155921
+ function responseFormatter6(result) {
155412
155922
  const md = markdown().h2("PubMed Spell Check");
155413
155923
  if (result.hasSuggestion) {
155414
155924
  md.keyValue("Original", result.original).keyValue("Suggested", result.corrected);
@@ -155418,20 +155928,21 @@ function responseFormatter5(result) {
155418
155928
  return [{ type: "text", text: md.build() }];
155419
155929
  }
155420
155930
  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
155931
+ name: TOOL_NAME7,
155932
+ title: TOOL_TITLE7,
155933
+ description: TOOL_DESCRIPTION7,
155934
+ annotations: TOOL_ANNOTATIONS7,
155935
+ inputSchema: InputSchema7,
155936
+ outputSchema: OutputSchema8,
155937
+ logic: withToolAuth(["tool:pubmed_spell:read"], logic6),
155938
+ responseFormatter: responseFormatter6
155429
155939
  };
155430
155940
 
155431
155941
  // src/mcp-server/tools/definitions/index.ts
155432
155942
  var allToolDefinitions = [
155433
155943
  pubmedSearchTool,
155434
155944
  pubmedFetchTool,
155945
+ pmcFetchTool,
155435
155946
  pubmedSpellTool,
155436
155947
  pubmedCiteTool,
155437
155948
  pubmedRelatedTool,
@@ -155633,8 +156144,8 @@ var defaultResponseFormatter2 = (result) => [
155633
156144
  function createMcpToolHandler({
155634
156145
  toolName,
155635
156146
  inputSchema,
155636
- logic: logic6,
155637
- responseFormatter: responseFormatter6 = defaultResponseFormatter2
156147
+ logic: logic7,
156148
+ responseFormatter: responseFormatter7 = defaultResponseFormatter2
155638
156149
  }) {
155639
156150
  return async (input, callContext) => {
155640
156151
  const sdkContext = callContext;
@@ -155649,10 +156160,10 @@ function createMcpToolHandler({
155649
156160
  });
155650
156161
  try {
155651
156162
  const validatedInput = inputSchema.parse(input);
155652
- const result = await measureToolExecution(() => logic6(validatedInput, appContext, sdkContext), { ...appContext, toolName }, validatedInput);
156163
+ const result = await measureToolExecution(() => logic7(validatedInput, appContext, sdkContext), { ...appContext, toolName }, validatedInput);
155653
156164
  return {
155654
156165
  structuredContent: result,
155655
- content: responseFormatter6(result)
156166
+ content: responseFormatter7(result)
155656
156167
  };
155657
156168
  } catch (error48) {
155658
156169
  const handled = ErrorHandler.handleError(error48, {