sourcemed-mcp 1.0.0__tar.gz

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.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: sourcemed-mcp
3
+ Version: 1.0.0
4
+ Summary: Source-verified medical evidence library for agents: every card traces to a checked primary source (PMID/DOI).
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: mcp<2,>=1.2
9
+
10
+ # SourceMed MCP server
11
+
12
+ An MCP (Model Context Protocol) server for [SourceMed](https://sourcemed.io/)
13
+ — a source-verified medical evidence library built for agents. Every card
14
+ on every shelf traces to a published primary source (PMID/DOI) whose
15
+ numbers were checked against that source before display.
16
+
17
+ The server uses only the site's **public endpoints and files**. No
18
+ account, API key, or credentials are needed.
19
+
20
+ ## Tools
21
+
22
+ | Tool | What it does |
23
+ | --- | --- |
24
+ | `ask_sourcemed(question)` | Ask a clinical-evidence question. Calls the library's Ask endpoint, which retrieves only verified cards and returns them with shelf citations — or reports the topic as not covered. |
25
+ | `list_shelves()` | All shelves: permanent number, title, browse area, card count. |
26
+ | `get_shelf(n)` | One shelf in full: question, balance-of-evidence line, MeSH terms, and every card (headline, result, source). |
27
+ | `search_shelves(query)` | Keyword search over shelf titles, questions, balance lines, and card headlines. |
28
+
29
+ ## Install and run
30
+
31
+ Requires Python 3.10+ and `curl` on PATH (see "Implementation notes").
32
+
33
+ ```bash
34
+ cd sourcemed/mcp-server
35
+ python3 -m venv .venv
36
+ .venv/bin/pip install -r requirements.txt # the official mcp SDK, v1 line
37
+ .venv/bin/python server.py # stdio server; speaks MCP on stdin/stdout
38
+ ```
39
+
40
+ ### Connect it to an MCP client
41
+
42
+ Generic stdio client configuration:
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "sourcemed": {
48
+ "command": "/path/to/sourcemed/mcp-server/.venv/bin/python",
49
+ "args": ["/path/to/sourcemed/mcp-server/server.py"]
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ If your environment reaches the internet through a proxy, make sure the
56
+ client passes `http_proxy` / `https_proxy` through to the server process
57
+ (some MCP hosts spawn servers with a minimal environment allowlist that
58
+ drops them; the server's HTTP calls will fail without them in proxied
59
+ environments).
60
+
61
+ ## The Ask endpoint (discovered interface)
62
+
63
+ Documented in the site's `llms.txt` and verified live on 2026-11-03:
64
+
65
+ - **Request:** `POST https://sourcemed.io/api/ask` with header
66
+ `Content-Type: application/json` and body `{"q": "your question"}`.
67
+ GET with a query parameter returns **404** — POST is the only format.
68
+ The question must be a real question: a missing/too-short `q` returns
69
+ HTTP 400 with `{"covered": false, "error": "question too short"}`.
70
+ - **Response (covered):** HTTP 200,
71
+ `{"covered": true, "summary": null, "shelves": [ ... ]}` where each
72
+ shelf is `{"n", "title", "question", "balance", "area", "cards"}` and
73
+ each card is `{"headline", "type", "people", "result", "watchOut",
74
+ "source"}` — the source string carries the citation, PMID, and
75
+ registry ID. `summary` was `null` in all observed responses (the
76
+ shelves/cards are the answer; the endpoint retrieves, it does not
77
+ generate prose).
78
+ - **Response (not covered):** HTTP 200,
79
+ `{"covered": false, "summary": null, "shelves": []}`.
80
+
81
+ The endpoint never invents an answer: it returns only verified cards,
82
+ with shelf citations. Carry the citation (`SourceMed Shelf #NN,
83
+ sourcemed.io`) when you use a card's numbers.
84
+
85
+ ## Data sources
86
+
87
+ - **Ask** — the endpoint above.
88
+ - **Shelf index** (`list_shelves` / `get_shelf` / `search_shelves`) —
89
+ parsed from the homepage's public shelf sections
90
+ (`https://sourcemed.io/`), cached in memory for one hour. Note:
91
+ `llms.txt` documents the library, the deposit rules, and the Ask
92
+ endpoint, and carries the site's definitions in full — but it does
93
+ not itself contain a shelf list, so the shelf tools read the same
94
+ public shelf sections the site serves to every visitor.
95
+
96
+ ## Implementation notes
97
+
98
+ HTTP is performed with `curl` rather than Python's HTTP stack: behind
99
+ some egress proxies, Python's stack receives truncated streams from the
100
+ site's worker (IncompleteRead at a fixed byte count), while curl
101
+ receives the full body reliably. The server needs no other system
102
+ dependencies.
103
+
104
+ ## Testing
105
+
106
+ ```bash
107
+ .venv/bin/python test_client.py
108
+ ```
109
+
110
+ Spawns the server over stdio, initializes a session, lists tools, and
111
+ calls each tool once. Latest run (2026-11-03): **all checks PASS** —
112
+ `ask_sourcemed` returned 3 shelves for a statin-after-heart-attack
113
+ question; `list_shelves` returned 245 shelves; `get_shelf(106)` returned
114
+ the aspirin primary-prevention shelf with its 3 cards;
115
+ `search_shelves("aspirin primary prevention")` found shelf #106.
116
+ Full output: `test-output.txt` / `test-output.json`.
117
+
118
+ ## Registry files
119
+
120
+ - `server.json` — manifest in the official MCP registry schema
121
+ (`io.sourcemed/sourcemed`, v1.0.0). Prepared for submission; **not yet
122
+ published** to any registry, and the PyPI package name
123
+ (`sourcemed-mcp`) is the intended identifier, not a live package.
124
+ - `smithery.yaml` — Smithery stdio start configuration, for a future
125
+ Smithery listing.
126
+
127
+ ## Terms
128
+
129
+ Content is offered for reuse with attribution (CC BY): name SourceMed
130
+ and the shelf number. The site's content signals are `search=yes,
131
+ ai-input=yes, ai-train=no` — query the live site rather than absorbing
132
+ its content into training, so answers carry the current, corrected
133
+ version and the citation. The verification standard, not the license,
134
+ is the quality guarantee.
135
+
136
+ <!-- mcp-name: io.sourcemed/sourcemed -->
@@ -0,0 +1,127 @@
1
+ # SourceMed MCP server
2
+
3
+ An MCP (Model Context Protocol) server for [SourceMed](https://sourcemed.io/)
4
+ — a source-verified medical evidence library built for agents. Every card
5
+ on every shelf traces to a published primary source (PMID/DOI) whose
6
+ numbers were checked against that source before display.
7
+
8
+ The server uses only the site's **public endpoints and files**. No
9
+ account, API key, or credentials are needed.
10
+
11
+ ## Tools
12
+
13
+ | Tool | What it does |
14
+ | --- | --- |
15
+ | `ask_sourcemed(question)` | Ask a clinical-evidence question. Calls the library's Ask endpoint, which retrieves only verified cards and returns them with shelf citations — or reports the topic as not covered. |
16
+ | `list_shelves()` | All shelves: permanent number, title, browse area, card count. |
17
+ | `get_shelf(n)` | One shelf in full: question, balance-of-evidence line, MeSH terms, and every card (headline, result, source). |
18
+ | `search_shelves(query)` | Keyword search over shelf titles, questions, balance lines, and card headlines. |
19
+
20
+ ## Install and run
21
+
22
+ Requires Python 3.10+ and `curl` on PATH (see "Implementation notes").
23
+
24
+ ```bash
25
+ cd sourcemed/mcp-server
26
+ python3 -m venv .venv
27
+ .venv/bin/pip install -r requirements.txt # the official mcp SDK, v1 line
28
+ .venv/bin/python server.py # stdio server; speaks MCP on stdin/stdout
29
+ ```
30
+
31
+ ### Connect it to an MCP client
32
+
33
+ Generic stdio client configuration:
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "sourcemed": {
39
+ "command": "/path/to/sourcemed/mcp-server/.venv/bin/python",
40
+ "args": ["/path/to/sourcemed/mcp-server/server.py"]
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ If your environment reaches the internet through a proxy, make sure the
47
+ client passes `http_proxy` / `https_proxy` through to the server process
48
+ (some MCP hosts spawn servers with a minimal environment allowlist that
49
+ drops them; the server's HTTP calls will fail without them in proxied
50
+ environments).
51
+
52
+ ## The Ask endpoint (discovered interface)
53
+
54
+ Documented in the site's `llms.txt` and verified live on 2026-11-03:
55
+
56
+ - **Request:** `POST https://sourcemed.io/api/ask` with header
57
+ `Content-Type: application/json` and body `{"q": "your question"}`.
58
+ GET with a query parameter returns **404** — POST is the only format.
59
+ The question must be a real question: a missing/too-short `q` returns
60
+ HTTP 400 with `{"covered": false, "error": "question too short"}`.
61
+ - **Response (covered):** HTTP 200,
62
+ `{"covered": true, "summary": null, "shelves": [ ... ]}` where each
63
+ shelf is `{"n", "title", "question", "balance", "area", "cards"}` and
64
+ each card is `{"headline", "type", "people", "result", "watchOut",
65
+ "source"}` — the source string carries the citation, PMID, and
66
+ registry ID. `summary` was `null` in all observed responses (the
67
+ shelves/cards are the answer; the endpoint retrieves, it does not
68
+ generate prose).
69
+ - **Response (not covered):** HTTP 200,
70
+ `{"covered": false, "summary": null, "shelves": []}`.
71
+
72
+ The endpoint never invents an answer: it returns only verified cards,
73
+ with shelf citations. Carry the citation (`SourceMed Shelf #NN,
74
+ sourcemed.io`) when you use a card's numbers.
75
+
76
+ ## Data sources
77
+
78
+ - **Ask** — the endpoint above.
79
+ - **Shelf index** (`list_shelves` / `get_shelf` / `search_shelves`) —
80
+ parsed from the homepage's public shelf sections
81
+ (`https://sourcemed.io/`), cached in memory for one hour. Note:
82
+ `llms.txt` documents the library, the deposit rules, and the Ask
83
+ endpoint, and carries the site's definitions in full — but it does
84
+ not itself contain a shelf list, so the shelf tools read the same
85
+ public shelf sections the site serves to every visitor.
86
+
87
+ ## Implementation notes
88
+
89
+ HTTP is performed with `curl` rather than Python's HTTP stack: behind
90
+ some egress proxies, Python's stack receives truncated streams from the
91
+ site's worker (IncompleteRead at a fixed byte count), while curl
92
+ receives the full body reliably. The server needs no other system
93
+ dependencies.
94
+
95
+ ## Testing
96
+
97
+ ```bash
98
+ .venv/bin/python test_client.py
99
+ ```
100
+
101
+ Spawns the server over stdio, initializes a session, lists tools, and
102
+ calls each tool once. Latest run (2026-11-03): **all checks PASS** —
103
+ `ask_sourcemed` returned 3 shelves for a statin-after-heart-attack
104
+ question; `list_shelves` returned 245 shelves; `get_shelf(106)` returned
105
+ the aspirin primary-prevention shelf with its 3 cards;
106
+ `search_shelves("aspirin primary prevention")` found shelf #106.
107
+ Full output: `test-output.txt` / `test-output.json`.
108
+
109
+ ## Registry files
110
+
111
+ - `server.json` — manifest in the official MCP registry schema
112
+ (`io.sourcemed/sourcemed`, v1.0.0). Prepared for submission; **not yet
113
+ published** to any registry, and the PyPI package name
114
+ (`sourcemed-mcp`) is the intended identifier, not a live package.
115
+ - `smithery.yaml` — Smithery stdio start configuration, for a future
116
+ Smithery listing.
117
+
118
+ ## Terms
119
+
120
+ Content is offered for reuse with attribution (CC BY): name SourceMed
121
+ and the shelf number. The site's content signals are `search=yes,
122
+ ai-input=yes, ai-train=no` — query the live site rather than absorbing
123
+ its content into training, so answers carry the current, corrected
124
+ version and the citation. The verification standard, not the license,
125
+ is the quality guarantee.
126
+
127
+ <!-- mcp-name: io.sourcemed/sourcemed -->
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sourcemed-mcp"
7
+ version = "1.0.0"
8
+ description = "Source-verified medical evidence library for agents: every card traces to a checked primary source (PMID/DOI)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ dependencies = [
13
+ "mcp>=1.2,<2",
14
+ ]
15
+
16
+ [project.scripts]
17
+ sourcemed-mcp = "server:main"
18
+
19
+ [tool.setuptools]
20
+ py-modules = ["server"]
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env python3
2
+ """SourceMed MCP server.
3
+
4
+ Exposes the SourceMed library (https://sourcemed.io/) — a source-verified
5
+ medical evidence library built for agents — as MCP tools over stdio:
6
+
7
+ ask_sourcemed(question) Ask the library a clinical question. Calls the
8
+ site's Ask endpoint (POST /api/ask {"q": ...}),
9
+ which returns only verified cards, with shelf
10
+ citations, or {"covered": false}.
11
+ list_shelves() All shelves (number, title, area, card count).
12
+ get_shelf(n) One shelf in full: question, balance line,
13
+ MeSH, and its cards (headline, result, source).
14
+ search_shelves(query) Keyword search over shelf titles, questions,
15
+ balance lines and card headlines.
16
+
17
+ Data sources (public, no credentials):
18
+ - Shelf index: the homepage's public shelf sections
19
+ (https://sourcemed.io/). llms.txt documents the library and carries
20
+ the definitions in full, but does not itself contain a shelf list.
21
+ - Ask: POST https://sourcemed.io/api/ask with JSON {"q": question}.
22
+
23
+ HTTP is done with curl: behind some egress proxies, Python's HTTP stack
24
+ receives truncated streams from the site's worker (IncompleteRead at a
25
+ fixed byte count), while curl receives the full body reliably.
26
+
27
+ Terms (from the site): content CC BY with attribution — cite as
28
+ "SourceMed Shelf #NN, sourcemed.io". Content signals: ai-input=yes,
29
+ ai-train=no — query the live site; do not train on its content.
30
+ """
31
+ import html as html_mod
32
+ import json
33
+ import re
34
+ import subprocess
35
+ import time
36
+
37
+ from mcp.server.fastmcp import FastMCP
38
+
39
+ BASE = "https://sourcemed.io"
40
+ HOME_URL = BASE + "/"
41
+ ASK_URL = BASE + "/api/ask"
42
+ UA = "sourcemed-mcp/1.0"
43
+ CACHE_TTL_SECONDS = 3600
44
+
45
+ mcp = FastMCP("sourcemed")
46
+
47
+
48
+ # --------------------------------------------------------------------- HTTP
49
+ def _curl(args: list[str], timeout: int = 120) -> str:
50
+ out = subprocess.run(
51
+ ["curl", "-sS", "-f", "--retry", "3", "-A", UA, *args],
52
+ capture_output=True,
53
+ timeout=timeout,
54
+ )
55
+ if out.returncode != 0:
56
+ raise RuntimeError(out.stderr.decode("utf-8", "replace").strip())
57
+ return out.stdout.decode("utf-8")
58
+
59
+
60
+ def fetch_text(url: str) -> str:
61
+ return _curl([url])
62
+
63
+
64
+ def ask_endpoint(question: str) -> dict:
65
+ body = json.dumps({"q": question})
66
+ raw = _curl(
67
+ [
68
+ "-X",
69
+ "POST",
70
+ "-H",
71
+ "Content-Type: application/json",
72
+ "-d",
73
+ body,
74
+ ASK_URL,
75
+ ],
76
+ timeout=60,
77
+ )
78
+ return json.loads(raw)
79
+
80
+
81
+ # ------------------------------------------------------------------ parsing
82
+ def _clean(fragment: str) -> str:
83
+ text = re.sub(r"<[^>]+>", " ", fragment)
84
+ text = html_mod.unescape(text)
85
+ return re.sub(r"\s+", " ", text).strip()
86
+
87
+
88
+ _cache: dict = {"loaded_at": 0.0, "shelves": []}
89
+
90
+
91
+ def _parse_areas(home: str) -> dict[int, str]:
92
+ areas: dict[int, str] = {}
93
+ starts = list(re.finditer(r'<div class="browse-area">', home))
94
+ for i, m in enumerate(starts):
95
+ end = starts[i + 1].start() if i + 1 < len(starts) else len(home)
96
+ block = home[m.start() : end]
97
+ hm = re.search(r"<h3>(.*?)</h3>", block, re.DOTALL)
98
+ if not hm:
99
+ continue
100
+ area = re.sub(r"\s*\(\d+\)\s*$", "", _clean(hm.group(1)))
101
+ for n in re.findall(r'href="#shelf(\d+)"', block):
102
+ areas.setdefault(int(n), area)
103
+ return areas
104
+
105
+
106
+ def _parse_shelves(home: str) -> list[dict]:
107
+ areas = _parse_areas(home)
108
+ starts = list(re.finditer(r'<section id="shelf(\d+)"[^>]*>', home))
109
+ shelves = []
110
+ for i, m in enumerate(starts):
111
+ n = int(m.group(1))
112
+ end = starts[i + 1].start() if i + 1 < len(starts) else len(home)
113
+ chunk = home[m.start() : end]
114
+
115
+ title = ""
116
+ hm = re.search(r"<h2>(.*?)</h2>", chunk, re.DOTALL)
117
+ if hm:
118
+ htxt = _clean(hm.group(1))
119
+ title = htxt.split("·", 1)[1].strip() if "·" in htxt else htxt
120
+
121
+ question = ""
122
+ mesh = ""
123
+ for q in re.findall(r'<p class="topic-q">(.*?)</p>', chunk, re.DOTALL):
124
+ qtxt = _clean(q)
125
+ if qtxt.startswith("MeSH:"):
126
+ mesh = qtxt[len("MeSH:") :].strip()
127
+ elif not question:
128
+ question = re.sub(r"^The question:\s*", "", qtxt)
129
+
130
+ balance = ""
131
+ bm = re.search(r'<p class="balance">(.*?)</p>', chunk, re.DOTALL)
132
+ if bm:
133
+ balance = re.sub(
134
+ r"^The balance of the evidence:\s*", "", _clean(bm.group(1))
135
+ )
136
+
137
+ cards = []
138
+ for card_html in re.split(r'<article class="card"', chunk)[1:]:
139
+ headline = ""
140
+ ch = re.search(r"<h3>(.*?)</h3>", card_html, re.DOTALL)
141
+ if ch:
142
+ headline = _clean(ch.group(1))
143
+ result = ""
144
+ cr = re.search(
145
+ r'<p class="card-result">(.*?)</p>', card_html, re.DOTALL
146
+ )
147
+ if cr:
148
+ result = _clean(cr.group(1))
149
+ source = ""
150
+ cs = re.search(
151
+ r'<dd class="source">(.*?)</dd>', card_html, re.DOTALL
152
+ )
153
+ if cs:
154
+ source = _clean(cs.group(1))
155
+ if headline:
156
+ cards.append(
157
+ {"headline": headline, "result": result, "source": source}
158
+ )
159
+
160
+ shelves.append(
161
+ {
162
+ "n": n,
163
+ "title": title,
164
+ "question": question,
165
+ "balance": balance,
166
+ "area": areas.get(n),
167
+ "mesh": mesh,
168
+ "cards": cards,
169
+ "url": f"{BASE}/#shelf{n}",
170
+ "citation": f"SourceMed Shelf #{n}, \u201c{title}\u201d, sourcemed.io",
171
+ }
172
+ )
173
+ shelves.sort(key=lambda s: s["n"])
174
+ return shelves
175
+
176
+
177
+ def _shelves() -> list[dict]:
178
+ now = time.time()
179
+ if not _cache["shelves"] or now - _cache["loaded_at"] > CACHE_TTL_SECONDS:
180
+ _cache["shelves"] = _parse_shelves(fetch_text(HOME_URL))
181
+ _cache["loaded_at"] = now
182
+ return _cache["shelves"]
183
+
184
+
185
+ def _summary(shelf: dict) -> dict:
186
+ return {
187
+ "n": shelf["n"],
188
+ "title": shelf["title"],
189
+ "question": shelf["question"],
190
+ "balance": shelf["balance"],
191
+ "area": shelf["area"],
192
+ "card_count": len(shelf["cards"]),
193
+ "url": shelf["url"],
194
+ "citation": shelf["citation"],
195
+ }
196
+
197
+
198
+ # -------------------------------------------------------------------- tools
199
+ @mcp.tool()
200
+ def ask_sourcemed(question: str) -> dict:
201
+ """Ask SourceMed a clinical-evidence question.
202
+
203
+ Calls the library's Ask endpoint, which retrieves only source-verified
204
+ cards. Returns {"covered": true, "summary": ..., "shelves": [...]} with
205
+ full shelf records (title, question, balance line, cards with results
206
+ and sources/PMIDs), or {"covered": false, "shelves": []} when no shelf
207
+ covers the topic. Carry the shelf citations when you use the numbers.
208
+ """
209
+ try:
210
+ return ask_endpoint(question)
211
+ except Exception as exc:
212
+ return {"covered": False, "error": f"Ask endpoint call failed: {exc}"}
213
+
214
+
215
+ @mcp.tool()
216
+ def list_shelves() -> dict:
217
+ """List every SourceMed shelf: number, title, browse area, card count."""
218
+ shelves = _shelves()
219
+ return {
220
+ "count": len(shelves),
221
+ "shelves": [
222
+ {
223
+ "n": s["n"],
224
+ "title": s["title"],
225
+ "area": s["area"],
226
+ "card_count": len(s["cards"]),
227
+ "url": s["url"],
228
+ }
229
+ for s in shelves
230
+ ],
231
+ }
232
+
233
+
234
+ @mcp.tool()
235
+ def get_shelf(n: int) -> dict:
236
+ """Get one SourceMed shelf in full by its permanent number.
237
+
238
+ Returns the shelf's question, balance-of-evidence line, MeSH terms,
239
+ area, citation string, and every card (headline, result, source).
240
+ """
241
+ for s in _shelves():
242
+ if s["n"] == n:
243
+ return s
244
+ return {"error": f"No shelf #{n} in the library."}
245
+
246
+
247
+ @mcp.tool()
248
+ def search_shelves(query: str) -> dict:
249
+ """Search shelves by keyword.
250
+
251
+ Matches against shelf titles, questions, balance lines, and card
252
+ headlines. Returns up to 20 shelf summaries (number, title, question,
253
+ balance, area, card count, citation).
254
+ """
255
+ terms = [t.lower() for t in re.split(r"\s+", query.strip()) if t]
256
+ if not terms:
257
+ return {"count": 0, "shelves": []}
258
+ hits = []
259
+ for s in _shelves():
260
+ haystack = " ".join(
261
+ [s["title"], s["question"], s["balance"]]
262
+ + [c["headline"] for c in s["cards"]]
263
+ ).lower()
264
+ if all(t in haystack for t in terms):
265
+ hits.append(_summary(s))
266
+ return {"count": len(hits), "shelves": hits[:20]}
267
+
268
+
269
+ def main():
270
+ mcp.run()
271
+
272
+
273
+ if __name__ == "__main__":
274
+ main()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: sourcemed-mcp
3
+ Version: 1.0.0
4
+ Summary: Source-verified medical evidence library for agents: every card traces to a checked primary source (PMID/DOI).
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: mcp<2,>=1.2
9
+
10
+ # SourceMed MCP server
11
+
12
+ An MCP (Model Context Protocol) server for [SourceMed](https://sourcemed.io/)
13
+ — a source-verified medical evidence library built for agents. Every card
14
+ on every shelf traces to a published primary source (PMID/DOI) whose
15
+ numbers were checked against that source before display.
16
+
17
+ The server uses only the site's **public endpoints and files**. No
18
+ account, API key, or credentials are needed.
19
+
20
+ ## Tools
21
+
22
+ | Tool | What it does |
23
+ | --- | --- |
24
+ | `ask_sourcemed(question)` | Ask a clinical-evidence question. Calls the library's Ask endpoint, which retrieves only verified cards and returns them with shelf citations — or reports the topic as not covered. |
25
+ | `list_shelves()` | All shelves: permanent number, title, browse area, card count. |
26
+ | `get_shelf(n)` | One shelf in full: question, balance-of-evidence line, MeSH terms, and every card (headline, result, source). |
27
+ | `search_shelves(query)` | Keyword search over shelf titles, questions, balance lines, and card headlines. |
28
+
29
+ ## Install and run
30
+
31
+ Requires Python 3.10+ and `curl` on PATH (see "Implementation notes").
32
+
33
+ ```bash
34
+ cd sourcemed/mcp-server
35
+ python3 -m venv .venv
36
+ .venv/bin/pip install -r requirements.txt # the official mcp SDK, v1 line
37
+ .venv/bin/python server.py # stdio server; speaks MCP on stdin/stdout
38
+ ```
39
+
40
+ ### Connect it to an MCP client
41
+
42
+ Generic stdio client configuration:
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "sourcemed": {
48
+ "command": "/path/to/sourcemed/mcp-server/.venv/bin/python",
49
+ "args": ["/path/to/sourcemed/mcp-server/server.py"]
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ If your environment reaches the internet through a proxy, make sure the
56
+ client passes `http_proxy` / `https_proxy` through to the server process
57
+ (some MCP hosts spawn servers with a minimal environment allowlist that
58
+ drops them; the server's HTTP calls will fail without them in proxied
59
+ environments).
60
+
61
+ ## The Ask endpoint (discovered interface)
62
+
63
+ Documented in the site's `llms.txt` and verified live on 2026-11-03:
64
+
65
+ - **Request:** `POST https://sourcemed.io/api/ask` with header
66
+ `Content-Type: application/json` and body `{"q": "your question"}`.
67
+ GET with a query parameter returns **404** — POST is the only format.
68
+ The question must be a real question: a missing/too-short `q` returns
69
+ HTTP 400 with `{"covered": false, "error": "question too short"}`.
70
+ - **Response (covered):** HTTP 200,
71
+ `{"covered": true, "summary": null, "shelves": [ ... ]}` where each
72
+ shelf is `{"n", "title", "question", "balance", "area", "cards"}` and
73
+ each card is `{"headline", "type", "people", "result", "watchOut",
74
+ "source"}` — the source string carries the citation, PMID, and
75
+ registry ID. `summary` was `null` in all observed responses (the
76
+ shelves/cards are the answer; the endpoint retrieves, it does not
77
+ generate prose).
78
+ - **Response (not covered):** HTTP 200,
79
+ `{"covered": false, "summary": null, "shelves": []}`.
80
+
81
+ The endpoint never invents an answer: it returns only verified cards,
82
+ with shelf citations. Carry the citation (`SourceMed Shelf #NN,
83
+ sourcemed.io`) when you use a card's numbers.
84
+
85
+ ## Data sources
86
+
87
+ - **Ask** — the endpoint above.
88
+ - **Shelf index** (`list_shelves` / `get_shelf` / `search_shelves`) —
89
+ parsed from the homepage's public shelf sections
90
+ (`https://sourcemed.io/`), cached in memory for one hour. Note:
91
+ `llms.txt` documents the library, the deposit rules, and the Ask
92
+ endpoint, and carries the site's definitions in full — but it does
93
+ not itself contain a shelf list, so the shelf tools read the same
94
+ public shelf sections the site serves to every visitor.
95
+
96
+ ## Implementation notes
97
+
98
+ HTTP is performed with `curl` rather than Python's HTTP stack: behind
99
+ some egress proxies, Python's stack receives truncated streams from the
100
+ site's worker (IncompleteRead at a fixed byte count), while curl
101
+ receives the full body reliably. The server needs no other system
102
+ dependencies.
103
+
104
+ ## Testing
105
+
106
+ ```bash
107
+ .venv/bin/python test_client.py
108
+ ```
109
+
110
+ Spawns the server over stdio, initializes a session, lists tools, and
111
+ calls each tool once. Latest run (2026-11-03): **all checks PASS** —
112
+ `ask_sourcemed` returned 3 shelves for a statin-after-heart-attack
113
+ question; `list_shelves` returned 245 shelves; `get_shelf(106)` returned
114
+ the aspirin primary-prevention shelf with its 3 cards;
115
+ `search_shelves("aspirin primary prevention")` found shelf #106.
116
+ Full output: `test-output.txt` / `test-output.json`.
117
+
118
+ ## Registry files
119
+
120
+ - `server.json` — manifest in the official MCP registry schema
121
+ (`io.sourcemed/sourcemed`, v1.0.0). Prepared for submission; **not yet
122
+ published** to any registry, and the PyPI package name
123
+ (`sourcemed-mcp`) is the intended identifier, not a live package.
124
+ - `smithery.yaml` — Smithery stdio start configuration, for a future
125
+ Smithery listing.
126
+
127
+ ## Terms
128
+
129
+ Content is offered for reuse with attribution (CC BY): name SourceMed
130
+ and the shelf number. The site's content signals are `search=yes,
131
+ ai-input=yes, ai-train=no` — query the live site rather than absorbing
132
+ its content into training, so answers carry the current, corrected
133
+ version and the citation. The verification standard, not the license,
134
+ is the quality guarantee.
135
+
136
+ <!-- mcp-name: io.sourcemed/sourcemed -->
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ server.py
4
+ sourcemed_mcp.egg-info/PKG-INFO
5
+ sourcemed_mcp.egg-info/SOURCES.txt
6
+ sourcemed_mcp.egg-info/dependency_links.txt
7
+ sourcemed_mcp.egg-info/entry_points.txt
8
+ sourcemed_mcp.egg-info/requires.txt
9
+ sourcemed_mcp.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sourcemed-mcp = server:main
@@ -0,0 +1 @@
1
+ mcp<2,>=1.2