evipedia-mcp-dev 0.1.19 → 0.1.20

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 (2) hide show
  1. package/dist/index.js +42 -15
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
6
  // src/tools.ts
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { readFileSync } from "fs";
9
+ import lunr from "lunr";
9
10
  import { z } from "zod";
10
11
  var pkg = (() => {
11
12
  try {
@@ -42,27 +43,53 @@ Typical workflow:
42
43
  3. Contribute \u2014 if the user wants an intervention reviewed that isn't in the catalogue, 'suggest_intervention(...)' submits it to the evipedia team. Only call this when the user explicitly asks to propose one; it sends real data to the team.
43
44
 
44
45
  'get_version()' reports the running build. Notes: an intervention may have multiple reviews for different goals (distinguished by canonical topic, e.g. "Botox for Skin Rejuvenation"). These are evidence reviews for information, not personalized medical advice \u2014 present conclusions as evidence summaries, not prescriptions.`;
46
+ var lunrCache = null;
47
+ function buildLunrIndex(searchIndex) {
48
+ if (lunrCache && lunrCache.key === searchIndex) return lunrCache.idx;
49
+ const idx = lunr(function() {
50
+ this.pipeline.remove(lunr.stemmer);
51
+ this.searchPipeline.remove(lunr.stemmer);
52
+ this.ref("url");
53
+ this.field("short_topic", { boost: 15 });
54
+ this.field("alternate_names", { boost: 10 });
55
+ this.field("ep_keywords", { boost: 8 });
56
+ this.field("ep_category", { boost: 3 });
57
+ for (const doc of searchIndex) this.add(doc);
58
+ });
59
+ lunrCache = { key: searchIndex, idx };
60
+ return idx;
61
+ }
62
+ function runLunrQuery(idx, query) {
63
+ const terms = query.toLowerCase().split(/[\s\-]+/).filter(Boolean);
64
+ if (terms.length === 0) return [];
65
+ try {
66
+ return idx.query((q) => {
67
+ for (const t of terms) {
68
+ q.term(t, { boost: 100 });
69
+ q.term(t, { boost: 10, wildcard: lunr.Query.wildcard.TRAILING });
70
+ q.term(t, { boost: 1, editDistance: 1 });
71
+ }
72
+ });
73
+ } catch {
74
+ return [];
75
+ }
76
+ }
45
77
  async function searchReviews(query, limit = 20) {
46
78
  const [searchIndex, reviewsIndex] = await Promise.all([
47
79
  fetchCached(`${BASE_URL}/search.json`),
48
80
  fetchCached(`${BASE_URL}/reviews.json`)
49
81
  ]);
50
82
  const bySlug = new Map(reviewsIndex.map((r) => [toSlug(r.permalink).toLowerCase(), r]));
51
- const STOPWORDS = /* @__PURE__ */ new Set(["for", "the", "a", "an", "and", "or", "of", "to", "in", "on", "with", "vs", "versus"]);
52
- const tokens = Array.from(new Set(
53
- query.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOPWORDS.has(t))
54
- ));
55
- if (tokens.length === 0) return [];
56
- const scored = searchIndex.map((e) => {
57
- const hay = [e.short_topic, e.alternate_names, e.ep_keywords, e.ep_category].map((v) => (v ?? "").toLowerCase()).join(" ");
58
- return { e, score: tokens.reduce((n, t) => n + (hay.includes(t) ? 1 : 0), 0) };
59
- });
60
- const best = scored.reduce((m, s) => Math.max(m, s.score), 0);
61
- if (best === 0) return [];
62
- const matches = scored.filter((s) => s.score === best).map((s) => s.e);
63
- return matches.slice(0, limit).map((e) => {
64
- const r = bySlug.get(toSlug(e.url).toLowerCase());
65
- return r ? { name: r.canonical_name, url: r.permalink, conclusion: r.er_conclusion, category: r.category } : { name: e.short_topic, url: e.url, category: e.ep_category };
83
+ const byUrl = new Map(searchIndex.map((e) => [e.url, e]));
84
+ const idx = buildLunrIndex(searchIndex);
85
+ const results = runLunrQuery(idx, query);
86
+ return results.slice(0, limit).map((res) => {
87
+ const review = bySlug.get(toSlug(res.ref).toLowerCase());
88
+ if (review) {
89
+ return { name: review.canonical_name, url: review.permalink, conclusion: review.er_conclusion, category: review.category };
90
+ }
91
+ const e = byUrl.get(res.ref);
92
+ return { name: e?.short_topic ?? res.ref, url: e?.url ?? res.ref, category: e?.ep_category };
66
93
  });
67
94
  }
68
95
  function createServer() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evipedia-mcp-dev",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "Internal development build of evipedia-mcp. Not for public use.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@modelcontextprotocol/sdk": "^1.0.0",
17
+ "lunr": "^2.3.9",
17
18
  "zod": "^3.0.0"
18
19
  },
19
20
  "license": "MIT",