<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sidney da S. P. Bissoli</title>
    <description>The latest articles on DEV Community by Sidney da S. P. Bissoli (@sidneybissoli).</description>
    <link>https://dev.to/sidneybissoli</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3926068%2F9bef71c6-0092-4d9b-95ec-40dd4833cd9d.jpeg</url>
      <title>DEV Community: Sidney da S. P. Bissoli</title>
      <link>https://dev.to/sidneybissoli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sidneybissoli"/>
    <language>en</language>
    <item>
      <title>Your MCP server changed. Its version didn't. Here's how to catch it.</title>
      <dc:creator>Sidney da S. P. Bissoli</dc:creator>
      <pubDate>Wed, 07 Oct 2026 13:38:50 +0000</pubDate>
      <link>https://dev.to/sidneybissoli/your-mcp-server-changed-its-version-didnt-heres-how-to-catch-it-3ai5</link>
      <guid>https://dev.to/sidneybissoli/your-mcp-server-changed-its-version-didnt-heres-how-to-catch-it-3ai5</guid>
      <description>&lt;p&gt;A version number is a promise: &lt;em&gt;nothing a client depends on changed without me saying so&lt;/em&gt;. For MCP servers, almost nothing checks that promise.&lt;/p&gt;

&lt;p&gt;The copy of a server in the MCP Registry carries a name, a version, the packages and the remotes. It carries no surface: no tools, no instructions, nothing about what answers without a token. So anyone comparing the registry with the live server can compare exactly one thing, the version. That comparison is only worth something if every surface change bumps the version.&lt;/p&gt;

&lt;p&gt;This post is about turning that "if" into a failing test, and then about what happened when I ran the test backwards over the whole history of my seven servers: 142 published versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this came from
&lt;/h2&gt;

&lt;p&gt;It started in the comments of my &lt;a href="https://dev.to/sidneybissoli/building-an-mcp-server-for-financial-data-lessons-learned-24gi"&gt;previous post&lt;/a&gt;, in a thread with &lt;a href="https://dev.to/yahhi"&gt;@yahhi&lt;/a&gt;. The registry listing and the live worklore server both said &lt;code&gt;0.1.0&lt;/code&gt;, so a version diff passed cleanly. But, &lt;a href="https://dev.to/yahhi/comment/3g5m4"&gt;as @yahhi found&lt;/a&gt;, the server had moved on: &lt;code&gt;tools/list&lt;/code&gt; now answered without a token, and the auth rules had moved into the instructions. The drift was one level deeper than the version.&lt;/p&gt;

&lt;p&gt;Next came &lt;a href="https://dev.to/yahhi/comment/3g607"&gt;a working check&lt;/a&gt;, and a replay of the surface from the day of the listing against the current code. The registry said &lt;code&gt;0.1.0&lt;/code&gt; for a server with four read-only tools. The server behind that number had six, two of which write on the user's behalf.&lt;/p&gt;

&lt;p&gt;I said I'd build the same thing for my servers. It's now live on all seven, and packaged as &lt;a href="https://www.npmjs.com/package/@sbissoli/mcp-surface" rel="noopener noreferrer"&gt;&lt;code&gt;@sbissoli/mcp-surface&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Lock what the client sees, not what the code says
&lt;/h2&gt;

&lt;p&gt;The lock file, &lt;code&gt;surface.lock.json&lt;/code&gt;, sits at the root of each server, next to &lt;code&gt;package.json&lt;/code&gt;. It has two sections. Each stores the version it was locked under and a sha256 of its content.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Declared surface&lt;/strong&gt;: the &lt;code&gt;initialize&lt;/code&gt; result (instructions and capabilities included, &lt;code&gt;serverInfo&lt;/code&gt; without the version), plus &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;resources/list&lt;/code&gt;, &lt;code&gt;resources/templates/list&lt;/code&gt; and &lt;code&gt;prompts/list&lt;/code&gt;. Keys are sorted and lists are ordered by name or URI, so the hash only moves when the surface does. A method the server doesn't serve is recorded as &lt;code&gt;null&lt;/code&gt;, not &lt;code&gt;[]&lt;/code&gt;: "no prompts" and "prompts/list isn't served" are different surfaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No-token behaviour&lt;/strong&gt;: &lt;em&gt;which methods answer without a credential&lt;/em&gt;, on every MCP route, with the API key unset and set. No listing shows this, and it's exactly what had drifted in that server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The declared surface is captured from the server factory in-process. The no-token section is measured at the HTTP edge, by calling the Worker's &lt;code&gt;fetch&lt;/code&gt; with and without the key.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Make the version bump the only way out
&lt;/h2&gt;

&lt;p&gt;The rule is short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;measured ≠ locked &lt;strong&gt;and&lt;/strong&gt; the version is the same → the test fails;&lt;/li&gt;
&lt;li&gt;the version changed → the test fails and asks you to re-lock.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The script that rewrites the lock follows the same rule: it refuses to lock a new surface under the old version. You can't "fix" a red test by regenerating the lock. You bump the version, re-lock, and commit the lock with the bump:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm version minor &lt;span class="nt"&gt;--no-git-tag-version&lt;/span&gt;
npm run surface:lock   &lt;span class="c"&gt;# build + run the surface tests in write mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deploy runs &lt;code&gt;npm test&lt;/code&gt; before &lt;code&gt;wrangler&lt;/code&gt;, and the npm publish runs it before &lt;code&gt;npm publish&lt;/code&gt;. So a surface change without a version change can't be deployed or published.&lt;/p&gt;

&lt;p&gt;One side effect, and it's on purpose: an SDK upgrade that changes &lt;code&gt;capabilities&lt;/code&gt; also turns the lock red. The client sees a different surface, so the version should say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Green tests prove the code; a post-deploy check proves what's served
&lt;/h2&gt;

&lt;p&gt;Tests run against the code in the repository. The client talks to whatever is actually deployed. Usually those are the same thing. Wrangler configs, build steps, environment variables and caches are where they stop being the same.&lt;/p&gt;

&lt;p&gt;So every deploy now ends with one more step that queries the live endpoint and compares it with the lock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx mcp-surface verificar https://&amp;lt;host&amp;gt;/mcp &lt;span class="nt"&gt;--tool&lt;/span&gt; &amp;lt;a tool that needs no network&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If what's being served isn't what was locked, the deploy job goes red after the fact, which is still much better than never.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The lock only protects you from now on, so replay the history
&lt;/h2&gt;

&lt;p&gt;A lock written today says nothing about the versions that came before it. For those there's a replay:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx mcp-surface replay &lt;span class="nt"&gt;--url&lt;/span&gt; https://&amp;lt;host&amp;gt;/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For every version published on npm, it installs the package in a temporary directory (&lt;code&gt;--ignore-scripts&lt;/code&gt;), starts it over stdio and captures the surface with the same normalisation the lock uses. Then it compares each version with the previous one, lists every removal that didn't come with a major bump, and checks the live endpoint against the surface of the version its &lt;code&gt;/status&lt;/code&gt; reports. The output is a Markdown table and a JSON file in &lt;code&gt;baselines/&lt;/code&gt;, committed to the repo.&lt;/p&gt;

&lt;p&gt;Across the seven servers that came to 142 versions. In all seven, &lt;strong&gt;the live server matches the version it reports&lt;/strong&gt;. It also found three things I didn't know.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Two releases that don't even start
&lt;/h2&gt;

&lt;p&gt;Two old releases of my Senate server, &lt;code&gt;1.1.0&lt;/code&gt; and &lt;code&gt;1.1.2&lt;/code&gt;, crash on startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Dynamic require of "events" is not supported
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The esbuild bundle emitted ESM that still contained a CommonJS &lt;code&gt;require&lt;/code&gt; for a Node built-in. Nobody had noticed, me included, because nobody installs a two-version-old release on purpose. But npm would have offered both as valid versions indefinitely. Any version diff would have called them fine forever, because their version numbers are perfectly well-formed. They're deprecated now.&lt;/p&gt;

&lt;p&gt;The lesson: "the version string is right" and "the version runs" are different claims, and only one of them shows up in a registry.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. A minor release that removed six tools
&lt;/h2&gt;

&lt;p&gt;My medical terminology server went from &lt;code&gt;1.0.2&lt;/code&gt; to &lt;code&gt;1.1.x&lt;/code&gt; and six SNOMED CT tools disappeared from the default surface. They moved behind a licensing flag, off by default.&lt;/p&gt;

&lt;p&gt;The CHANGELOG for that release even called it "technically breaking". It shipped as a minor anyway. There was a reason, since SNOMED access depends on a licence the user has to hold. But the client that was calling &lt;code&gt;snomed_search&lt;/code&gt; yesterday doesn't read my reasoning. It gets "unknown tool".&lt;/p&gt;

&lt;p&gt;The replay lists cases like this without judging them. Some removals have a good reason, and the CHANGELOG is where that reason lives. What matters is that the list exists, so the decision is made on purpose rather than discovered later by a user.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The replay was wrong once, too
&lt;/h2&gt;

&lt;p&gt;The first version of the replay flagged &lt;code&gt;ilo-mcp-server&lt;/code&gt; &lt;code&gt;0.5.0 → 0.6.0&lt;/code&gt;, where a parameter became required, as a breaking change outside a major release. But under semver, &lt;code&gt;0.x&lt;/code&gt; is allowed to break in a minor. The line of compatibility in &lt;code&gt;0.x&lt;/code&gt; is the minor, not the major.&lt;/p&gt;

&lt;p&gt;The rule is fixed now: in &lt;code&gt;0.x&lt;/code&gt;, breaks are compared within the same minor. I mention it because a tool that reports drift has to earn its alarms. One false alarm on day one teaches people to ignore the next real one.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Check the probe before you write the lock
&lt;/h2&gt;

&lt;p&gt;This is the one I'd most like other people to avoid.&lt;/p&gt;

&lt;p&gt;In one server (&lt;code&gt;sih-br-mcp&lt;/code&gt;) the MCP backend sits behind an edge proxy, so the no-token test can't call the real handler in-process. My first test double for that backend answered &lt;code&gt;result&lt;/code&gt; to &lt;em&gt;every&lt;/em&gt; method. The lock was written from it, and it faithfully recorded "&lt;code&gt;prompts/list&lt;/code&gt; answers" for a server that has no prompts.&lt;/p&gt;

&lt;p&gt;All the tests passed, because the tests were checking the lock against the same lying double. The post-deploy check against the live endpoint is what caught it.&lt;/p&gt;

&lt;p&gt;The fix has two parts. A double that answers everything is not a double of your server, so where the real handler can run in-process, run it. And the probe's sanity checks (a method that must answer, a method that must not) now run &lt;strong&gt;before&lt;/strong&gt; anything is written to the lock, not after. A lock written from a broken probe is worse than no lock, because it makes a wrong surface look verified.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do first, next time
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lock the surface next to the version&lt;/strong&gt; from the first release: &lt;code&gt;initialize&lt;/code&gt;, the four lists, and which methods answer without a token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the lock script refuse to rewrite under the old version.&lt;/strong&gt; Otherwise the red test just becomes a ritual.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the live endpoint after every deploy.&lt;/strong&gt; Green tests prove the code; only this proves what's served.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the replay once over your whole history.&lt;/strong&gt; It's cheap, it's one command, and the first run is the one that finds things.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove the probe can say "no"&lt;/strong&gt; before you trust anything it says "yes" to.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The package is MIT: &lt;a href="https://www.npmjs.com/package/@sbissoli/mcp-surface" rel="noopener noreferrer"&gt;&lt;code&gt;@sbissoli/mcp-surface&lt;/code&gt;&lt;/a&gt;, source in &lt;a href="https://github.com/SidneyBissoli/mcp-br-commons/tree/main/packages/mcp-surface" rel="noopener noreferrer"&gt;mcp-br-commons&lt;/a&gt;. Fair warning: the API and CLI verbs are in Portuguese (&lt;code&gt;travar&lt;/code&gt; = lock, &lt;code&gt;verificar&lt;/code&gt; = verify), and the roadmap follows my own servers' needs.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a class="mentioned-user" href="https://dev.to/yahhi"&gt;@yahhi&lt;/a&gt; for the thread that started all this. There is also a write-up of the other side of it, in Russian, &lt;a href="https://habr.com/ru/articles/1089802/" rel="noopener noreferrer"&gt;on Habr&lt;/a&gt;. One comment thread changed how eight servers ship.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>typescript</category>
      <category>testing</category>
    </item>
    <item>
      <title>Building an MCP server for financial data: lessons learned</title>
      <dc:creator>Sidney da S. P. Bissoli</dc:creator>
      <pubDate>Sat, 26 Sep 2026 20:34:12 +0000</pubDate>
      <link>https://dev.to/sidneybissoli/building-an-mcp-server-for-financial-data-lessons-learned-24gi</link>
      <guid>https://dev.to/sidneybissoli/building-an-mcp-server-for-financial-data-lessons-learned-24gi</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://medium.com/@sbissoli76/building-an-mcp-server-for-financial-data-lessons-learned-647bc33c9bac" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/SidneyBissoli/bcb-br-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;bcb-br-mcp&lt;/code&gt;&lt;/a&gt;, an open-source MCP server that gives AI agents the Brazilian Central Bank's public data: the SGS time series (Selic, IPCA, exchange rates, GDP, credit), the Focus market-expectations survey and the PTAX reference rates. It runs over stdio from npm and over Streamable HTTP on Cloudflare Workers, has 17 tools, 388 tests, and serves a couple of thousand npm downloads a month plus a hosted endpoint.&lt;/p&gt;

&lt;p&gt;None of the lessons below are about the MCP protocol itself. The protocol was the easy part. The hard parts were the data source, the two transports, and the difference between a response that &lt;em&gt;looks&lt;/em&gt; right and one that &lt;em&gt;is&lt;/em&gt; right. Here they are, roughly in the order they cost me.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Your source will lie to you with HTTP 200
&lt;/h2&gt;

&lt;p&gt;The single most expensive class of bug: the upstream API answers a bad request with a success code.&lt;/p&gt;

&lt;p&gt;The SGS endpoint, asked for a series code that does not exist, returns &lt;strong&gt;HTTP 200 with an HTML page&lt;/strong&gt; ("invalid request"), after about &lt;strong&gt;30 seconds&lt;/strong&gt;. A valid series answers in 0.2 to 0.4 seconds. So the naive server waited half a minute and then tried to parse HTML as JSON, and the error message blamed the source for being down.&lt;/p&gt;

&lt;p&gt;Worse: the same HTML page occasionally comes back for a series that &lt;strong&gt;does&lt;/strong&gt; exist. I measured it on series 432, the Selic target: 3 HTML pages out of 9 calls within a few minutes, then 20 consecutive good JSON answers. My first fix treated the page as a verdict ("this series does not exist") and raised without retrying. That meant a hiccup at the source made the server assert that the Selic target rate did not exist.&lt;/p&gt;

&lt;p&gt;The page proves only that &lt;em&gt;that attempt&lt;/em&gt; brought no data. The server now resolves the suspicion by repetition: a code that does not exist fails every attempt; a good series comes back on the next one. And small requests (&lt;code&gt;ultimos/N&lt;/code&gt;, at most 20 observations) get their own 6-second budget, because they can never be a legitimate long query — which cuts the 30-second silence without touching the long windows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; classify errors by the &lt;em&gt;shape of the body&lt;/em&gt;, not by the status code, and never let a single anomalous response become a permanent verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. "Last minus first" is wrong for half of the interesting series
&lt;/h2&gt;

&lt;p&gt;Price indexes — IPCA, IGP-M, INPC — are published as &lt;strong&gt;monthly rates&lt;/strong&gt;, not levels. Compute the variation of IPCA over 2024 as &lt;code&gt;(last − first) / first&lt;/code&gt; and you get &lt;strong&gt;+23.81%&lt;/strong&gt;. The real accumulated inflation was &lt;strong&gt;4.83%&lt;/strong&gt;. For the IGP-M in 2023 the naive formula gave &lt;strong&gt;+252.38%&lt;/strong&gt; for an index that actually &lt;em&gt;fell&lt;/em&gt; 3.18%.&lt;/p&gt;

&lt;p&gt;Nobody reported this. A paid evaluation run, where a real model picked tools for 44 Portuguese prompts, caught it by accident, because one answer was absurd enough to notice.&lt;/p&gt;

&lt;p&gt;The fix is a per-series decision (&lt;code&gt;level&lt;/code&gt; vs &lt;code&gt;chained rate&lt;/code&gt; vs &lt;code&gt;already accumulated&lt;/code&gt;), and it is &lt;em&gt;partial by construction&lt;/em&gt;: some series are recognised by the unit the open-data portal publishes, some by their curated name, some by an explicit list of period rates. The index heads (433, 189…) have no unit at all in the source; only the name identifies them. A code outside the catalogue is treated as a level, and the contract says so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; a numeric tool has domain semantics. If your server computes anything, the computation needs to know what kind of number it is holding, and the response must say which convention it used.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Ordering is not a property of the endpoint
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ultimos/N&lt;/code&gt; — "the last N observations" — returns &lt;strong&gt;22 of the 169 curated series newest-first and the other 147 oldest-first&lt;/strong&gt;. The direction is not deducible from the code or the family: series 4390 comes reversed, series 433 does not. Meanwhile, the date-window endpoint returned ascending order in 151 of 151 series.&lt;/p&gt;

&lt;p&gt;The visible symptom was a variation published with the &lt;strong&gt;wrong sign&lt;/strong&gt;: −7.40% for a period in which the series rose 8.00%.&lt;/p&gt;

&lt;p&gt;Every observation that enters the server now passes through one sort-by-date function. Nothing else reads &lt;code&gt;data[0]&lt;/code&gt; as "the oldest".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; measure invariants you assume, across the &lt;em&gt;whole&lt;/em&gt; catalogue, not on the three series you tested with.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Two transports, one registry — or you will ship two products
&lt;/h2&gt;

&lt;p&gt;At the start the server had two independent surfaces: the stdio entry derived JSON Schemas from zod through the SDK, and the Cloudflare Worker re-implemented JSON-RPC by hand with its own copy of the schemas. They had genuinely diverged:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;stdio (npm)&lt;/th&gt;
&lt;th&gt;hosted (Worker)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;version in production&lt;/td&gt;
&lt;td&gt;1.3.5&lt;/td&gt;
&lt;td&gt;1.3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tool description length&lt;/td&gt;
&lt;td&gt;~1,200 chars&lt;/td&gt;
&lt;td&gt;~100 chars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;default&lt;/code&gt;, &lt;code&gt;minimum&lt;/code&gt;, &lt;code&gt;maximum&lt;/code&gt;, &lt;code&gt;minItems&lt;/code&gt;, &lt;code&gt;additionalProperties&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;resource names&lt;/td&gt;
&lt;td&gt;one set&lt;/td&gt;
&lt;td&gt;a different set&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The agent-readable descriptions I had spent a release writing &lt;strong&gt;never reached the hosted endpoint&lt;/strong&gt;, because there was no deploy pipeline for the Worker. And the HTTP client got a weaker contract than the stdio client, because the JSON Schema was maintained twice.&lt;/p&gt;

&lt;p&gt;The fix was structural: one &lt;code&gt;registerAll(server)&lt;/code&gt; function is the &lt;em&gt;only&lt;/em&gt; place that projects tools, resources and prompts onto an &lt;code&gt;McpServer&lt;/code&gt;; stdio calls it once, the Worker calls it per request. The JSON Schemas are the published surface and go on the wire verbatim. A normalised dump of &lt;code&gt;tools/list&lt;/code&gt; + resources + prompts is committed as a baseline, and the CI compares the surface before and after every change; the hosted endpoint is deployed on every push, with a production smoke test at the end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; if two entry points can describe the same tool differently, one of them is lying to some client right now. Make a single registry and test that the two transports are byte-identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The runtime will veto your validator
&lt;/h2&gt;

&lt;p&gt;The obvious JSON Schema validator for TypeScript is ajv. Ajv compiles schemas with &lt;code&gt;new Function&lt;/code&gt;. Cloudflare Workers &lt;strong&gt;forbid &lt;code&gt;new Function&lt;/code&gt;&lt;/strong&gt; — and the failure mode is not a nice error on the offending tool; it is &lt;strong&gt;HTTP 500 on the whole &lt;code&gt;/mcp&lt;/code&gt; route&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The server uses a non-compiling validator on &lt;em&gt;both&lt;/em&gt; runtimes on purpose, so that stdio and hosted validate identically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; pick the validator for the most restrictive runtime you deploy to, and use the same one everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. A field that can be &lt;code&gt;null&lt;/code&gt; must say so, or a validating client rejects everything
&lt;/h2&gt;

&lt;p&gt;The tools return &lt;code&gt;null&lt;/code&gt; deliberately where the source does not publish a field — null is the information that there is no data, and the normalisation never omits keys. But an &lt;code&gt;outputSchema&lt;/code&gt; that declares &lt;code&gt;"type": "string"&lt;/code&gt; while the server emits &lt;code&gt;null&lt;/code&gt; violates the spec, and a client that validates (the MCP Inspector does) &lt;strong&gt;rejects the entire response&lt;/strong&gt;, not the one field.&lt;/p&gt;

&lt;p&gt;Runtime output validation in the SDK is permissive by design, so nothing at runtime catches this. I added a test that validates the &lt;code&gt;structuredContent&lt;/code&gt; of every tool against its own announced &lt;code&gt;outputSchema&lt;/code&gt;, with the same validator the server uses on input, and made the null-producing paths the test cases.&lt;/p&gt;

&lt;p&gt;Closely related SDK rule: any tool that declares &lt;code&gt;outputSchema&lt;/code&gt; &lt;strong&gt;must&lt;/strong&gt; return &lt;code&gt;structuredContent&lt;/code&gt; on every success; the check runs before any validator and cannot be disabled. All handlers go through one &lt;code&gt;structuredResult()&lt;/code&gt; helper, so this can only be forgotten in one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; the output schema is a promise to a stranger's validator. Test the promise in CI; do not rely on the runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Provenance is a contract, and the timestamp is the hard part
&lt;/h2&gt;

&lt;p&gt;Every successful response carries a provenance block: source, series, period, the canonical URL that reproduces the query, the licence (ODbL), and &lt;code&gt;retrieved_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The tempting implementation of &lt;code&gt;retrieved_at&lt;/code&gt; is &lt;code&gt;new Date()&lt;/code&gt; at response time. That is a false statement whenever the answer came from a cache: a search served from the 24-hour metadata cache reports the instant of the &lt;strong&gt;original&lt;/strong&gt; fetch, which may be yesterday — and that is the date with legal weight, because it is when the extraction actually happened. The server collects the real instant at the single network call site through &lt;code&gt;AsyncLocalStorage&lt;/code&gt;, aggregates by taking the &lt;em&gt;oldest&lt;/em&gt; instant across the accesses that built the answer, and sets &lt;code&gt;served_from_cache&lt;/code&gt; only when everything came from cache.&lt;/p&gt;

&lt;p&gt;The second subtlety: the same channel that carries the source's numbers verbatim must mark anything the server &lt;em&gt;computed&lt;/em&gt; — a variation, a correlation, a deflated series — as derived, with the convention used. A provenance block can match the schema and still lie; there is a dedicated test for the block's truthfulness, with a coverage assertion so that a new tool without a provenance test fails the suite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; provenance is not metadata decoration. It is a claim about when and from where each number came, and it needs its own tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Latency is queue depth, not request count
&lt;/h2&gt;

&lt;p&gt;Daily series are capped at 10 years per request (HTTP 406 above that), and a legal 10-year window takes 10–20 seconds at the source, with a server-side cut around 30 seconds that returns — you guessed it — &lt;strong&gt;200 with HTML&lt;/strong&gt;. The Worker has a 10-second budget. So the server slices windows into 3-year chunks (~2.6 s each).&lt;/p&gt;

&lt;p&gt;Then a comparison of two daily series over 10 years still took 10.7 s, and five series took 10.4 s: the problem was not the number of requests but the &lt;strong&gt;depth of the queue&lt;/strong&gt; per series. Splitting a budget of 10 concurrent requests across the series brought the worst case to ~8.8 s.&lt;/p&gt;

&lt;p&gt;One measurement rule saved me from fooling myself here: &lt;strong&gt;always measure on a window never requested before&lt;/strong&gt;. The source serves repeats from cache, and an already-requested window measures 600 ms where the cold one measures 10 s.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Hand-curated catalogues rot; verify them against the source
&lt;/h2&gt;

&lt;p&gt;The server ships a curated catalogue of 135 series so that agents can find "IPCA" or "household credit" without knowing a code. When I finally verified every entry against the source, about &lt;strong&gt;half of the earlier errors&lt;/strong&gt; had come from names edited by hand: two series swapped, individuals and companies inverted in a pair of credit series, a block of household-debt series sold as Focus expectations.&lt;/p&gt;

&lt;p&gt;Each entry now records where its name came from — transcribed from the portal's dataset (82) or measured (53: no dataset anywhere, name inherited, only periodicity and magnitude checked) — and there is a rule not to "improve" a transcribed name by hand.&lt;/p&gt;

&lt;p&gt;The obvious shortcut — validate a code against the open-data catalogue before hitting the network — is wrong too: the catalogue lists 4,261 datasets but &lt;strong&gt;10 of 20 well-known valid codes are absent from it&lt;/strong&gt;, including 433 (IPCA). Pre-validating would have refused good series.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; a catalogue is a claim about the source. Store the evidence for each claim, and never let a secondary index veto the primary one.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Telemetry that counts by status code is blind to the errors you most need
&lt;/h2&gt;

&lt;p&gt;The Worker keeps usage counters. Reconciliation between the tool-call hook and the HTTP layer was by status: &lt;code&gt;if (status &amp;lt; 400) continue&lt;/code&gt;, on the assumption that 200 implies the hook recorded the call. But a &lt;strong&gt;schema refusal&lt;/strong&gt; is answered by the validator &lt;em&gt;before&lt;/em&gt; the handler, inside an HTTP 200 — so nobody recorded it and the &lt;code&gt;continue&lt;/code&gt; closed the only other door. Calls with malformed arguments were counted neither as calls nor as errors. Measured in production: schema refusal, unknown tool (−32602) and unknown method (−32601) all arrive as HTTP 200.&lt;/p&gt;

&lt;p&gt;The fix reconciles by &lt;em&gt;name&lt;/em&gt; against the hook's receipt and reads the outcome from the JSON-RPC envelope matched by &lt;code&gt;id&lt;/code&gt;. The same defect existed in a sibling server, where it was fixed first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; in MCP, protocol-level failures live inside 200s. Instrument the envelope, not the transport.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Directory listings drift too
&lt;/h2&gt;

&lt;p&gt;Every directory that lists your server keeps its own copy of your manifest. Mine on one marketplace sat at 1.11.0 while npm was at 1.14.1, with descriptions and schemas from before a vocabulary change — because the manifest file in the repo was not tied to anything. Now a test compares the committed manifest with the live &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;resources/list&lt;/code&gt; and &lt;code&gt;prompts/list&lt;/code&gt; of the real server and with the identity in &lt;code&gt;package.json&lt;/code&gt;; a stale manifest fails before a release can happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do first, next time
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One registry, two transports, a committed surface baseline.&lt;/strong&gt; Everything else is cheaper once the surface cannot fork.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Characterisation tests with mocked fetch&lt;/strong&gt; for every tool, value by value, before any refactor — they were the gate for replacing the statistics engine, and produced exactly two explained differences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An output-contract test&lt;/strong&gt; (structured output vs announced schema) and a &lt;strong&gt;provenance test&lt;/strong&gt; with coverage assertions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A paid eval with a real model&lt;/strong&gt;, run rarely and on purpose. It found the one bug that no unit test could have, because the bug was in what the number &lt;em&gt;meant&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the source across the whole catalogue&lt;/strong&gt; — ordering, caps, timeouts, error shapes — and write the numbers into the repo where the next maintainer (often me, six weeks later) will read them before "optimising" something.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code is MIT, the data is ODbL from the Banco Central do Brasil, and the repository's &lt;code&gt;CLAUDE.md&lt;/code&gt; holds the full list of the sharp edges above with the dates they were measured: &lt;a href="https://github.com/SidneyBissoli/bcb-br-mcp" rel="noopener noreferrer"&gt;https://github.com/SidneyBissoli/bcb-br-mcp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>typescript</category>
      <category>fintech</category>
    </item>
    <item>
      <title>How to query Brazilian economic data with AI using MCP (Selic, IPCA, exchange rates, Focus)</title>
      <dc:creator>Sidney da S. P. Bissoli</dc:creator>
      <pubDate>Sat, 26 Sep 2026 15:02:22 +0000</pubDate>
      <link>https://dev.to/sidneybissoli/how-to-query-brazilian-economic-data-with-ai-using-mcp-selic-ipca-exchange-rates-focus-n2e</link>
      <guid>https://dev.to/sidneybissoli/how-to-query-brazilian-economic-data-with-ai-using-mcp-selic-ipca-exchange-rates-focus-n2e</guid>
      <description>&lt;p&gt;&lt;em&gt;Resumo em português: este tutorial mostra como dar a um agente de IA (Claude, Cursor, ChatGPT) acesso aos dados oficiais do Banco Central do Brasil — Selic, IPCA, câmbio, Focus e PTAX — com o servidor MCP &lt;code&gt;bcb-br-mcp&lt;/code&gt;, em cinco minutos e sem chave de API. Cada resposta vem com proveniência: série, período, URL canônica e instante da extração.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ask a language model "what is the Selic rate today?" and you get a number from its training data. It may be right, it may be three Copom meetings stale, and it comes with no source. This tutorial fixes that by giving the model a tool that reads the Banco Central do Brasil (BCB) directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;bcb-br-mcp&lt;/code&gt; is an open-source &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; server over three public BCB APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SGS&lt;/strong&gt; time series: Selic, IPCA, exchange rates, GDP, credit, fiscal accounts and thousands more, with a curated catalogue of 135 verified series;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus&lt;/strong&gt;: the weekly market-expectations survey (IPCA, Selic, GDP, exchange rate, by horizon and by Copom meeting);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PTAX&lt;/strong&gt;: the official reference exchange rates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;17 tools, no API key, MIT licence. Data is ODbL from the BCB.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Connect it (pick one)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Claude.ai or Claude Desktop, by URL&lt;/strong&gt; — add a custom connector with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://bcb.sidneybissoli.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Desktop or Cursor, local via npx&lt;/strong&gt; — in &lt;code&gt;claude_desktop_config.json&lt;/code&gt; or &lt;code&gt;.cursor/mcp.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bcb-br"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bcb-br-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ChatGPT Deep Research&lt;/strong&gt; — the server also exposes the &lt;code&gt;search&lt;/code&gt;/&lt;code&gt;fetch&lt;/code&gt; pair that ChatGPT requires; use the same URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Ask questions
&lt;/h2&gt;

&lt;p&gt;Once connected, plain language is enough. Some prompts that map cleanly onto the tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;Tool the agent picks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Current Selic, IPCA and USD/BRL in one shot"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bcb_indicadores_atuais&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Find the series code for household credit"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bcb_buscar_serie&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"IPCA month by month in 2025"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bcb_serie_valores&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"How much did the IGP-M change in 2023?"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bcb_variacao&lt;/code&gt; (it accumulates monthly rates instead of subtracting endpoints)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"What does Focus expect for IPCA 2026 and 2027?"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bcb_focus_expectativas&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Selic expected at each Copom meeting this year"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bcb_focus_selic&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"EUR/BRL PTAX for the whole of March 2026"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bcb_cambio_cotacao&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Turn this nominal revenue series into 2025 reais"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bcb_deflacionar&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Do Selic and the dollar move together since 2020?"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bcb_correlacao&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  3. Read the provenance block
&lt;/h2&gt;

&lt;p&gt;Every successful answer carries &lt;code&gt;provenance&lt;/code&gt; and &lt;code&gt;attribution&lt;/code&gt; in the structured result: the source, the series and period, the canonical URL that reproduces the query, the real extraction timestamp (a cache hit reports the &lt;em&gt;original&lt;/em&gt; fetch time, not "now"), and the licence. If the agent computed something — a variation, a correlation, a deflated series — the result is marked as derived and says which convention it used. What the BCB published is returned verbatim.&lt;/p&gt;

&lt;p&gt;That is the difference between "the model said 10.5%" and "SGS series 432, 2026-09-17, &lt;a href="https://api.bcb.gov.br/.." rel="noopener noreferrer"&gt;https://api.bcb.gov.br/..&lt;/a&gt;.".&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Why not let the model call the REST API itself?
&lt;/h2&gt;

&lt;p&gt;Because the BCB APIs have edges that produce &lt;em&gt;wrong numbers silently&lt;/em&gt;, and a model calling &lt;code&gt;fetch&lt;/code&gt; will hit every one of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unknown series codes return &lt;strong&gt;HTTP 200 with an HTML page&lt;/strong&gt;, after about 30 seconds — and the same page occasionally comes back for series that exist;&lt;/li&gt;
&lt;li&gt;daily series are capped at &lt;strong&gt;10 years per request&lt;/strong&gt; (HTTP 406), and a legal 10-year window can take 20 s, so the server slices it into 3-year chunks;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ultimos/N&lt;/code&gt; returns &lt;strong&gt;22 of the 169 curated series newest-first&lt;/strong&gt; and the rest oldest-first, which flipped the sign of variations;&lt;/li&gt;
&lt;li&gt;price indexes (IPCA, IGP-M, INPC) are published as &lt;strong&gt;monthly rates&lt;/strong&gt;: last-minus-first reported +23.8% for IPCA 2024 when the real accumulated figure was 4.83%;&lt;/li&gt;
&lt;li&gt;the PTAX endpoint wants &lt;strong&gt;MM-DD-YYYY&lt;/strong&gt; and returns zero rows, not an error, for ISO dates;&lt;/li&gt;
&lt;li&gt;the Focus OData service does not paginate, does not count, and one of its resources publishes field names in lower case, alone among thirteen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of that is handled server-side and pinned by tests, so the agent gets numbers you can put in a report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/SidneyBissoli/bcb-br-mcp" rel="noopener noreferrer"&gt;https://github.com/SidneyBissoli/bcb-br-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/bcb-br-mcp" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/bcb-br-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hosted endpoint: &lt;a href="https://bcb.sidneybissoli.com/mcp" rel="noopener noreferrer"&gt;https://bcb.sidneybissoli.com/mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Portuguese deep-dive on the three APIs and their traps: &lt;a href="https://github.com/SidneyBissoli/bcb-br-mcp/blob/main/docs/artigo-sgs-series-do-banco-central.pt-BR.md" rel="noopener noreferrer"&gt;https://github.com/SidneyBissoli/bcb-br-mcp/blob/main/docs/artigo-sgs-series-do-banco-central.pt-BR.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool descriptions are in Portuguese because most users are Brazilian; English prompts work fine.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>braziliandevs</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Seven medical terminologies, one MCP server: a practical walkthrough for clinical and research use</title>
      <dc:creator>Sidney da S. P. Bissoli</dc:creator>
      <pubDate>Tue, 12 May 2026 01:55:45 +0000</pubDate>
      <link>https://dev.to/sidneybissoli/seven-medical-terminologies-one-mcp-server-a-practical-walkthrough-for-clinical-and-research-use-5gia</link>
      <guid>https://dev.to/sidneybissoli/seven-medical-terminologies-one-mcp-server-a-practical-walkthrough-for-clinical-and-research-use-5gia</guid>
      <description>&lt;p&gt;If you've ever asked an LLM to "find the LOINC code for procalcitonin" or "list the active ingredients in Janumet," you've probably watched it confidently invent a code that doesn't exist. Medical terminologies are exactly the kind of structured, frequently-updated reference data that language models are bad at memorizing and good at &lt;em&gt;looking up&lt;/em&gt; — if you give them the right tool.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;medical-terminologies-mcp&lt;/code&gt; is a Model Context Protocol server that gives any MCP-compatible client (Claude Desktop, Claude Code, Continue, and others) unified access to seven medical terminology systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ICD-11&lt;/strong&gt; (WHO International Classification of Diseases, 11th Revision)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LOINC&lt;/strong&gt; (Logical Observation Identifiers Names and Codes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RxNorm&lt;/strong&gt; (NIH normalized clinical drug names)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MeSH&lt;/strong&gt; (NLM Medical Subject Headings)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ATC&lt;/strong&gt; (WHO Anatomical Therapeutic Chemical, served via NLM RxClass)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CID-10&lt;/strong&gt; (Brazilian Portuguese translation of ICD-10, DataSUS V2008 — bundled)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SNOMED CT&lt;/strong&gt; (Systematized Nomenclature of Medicine, optional, license required)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Twenty-six tools work out of the box with no authentication for LOINC, RxNorm, MeSH, ATC, CID-10, plus a bundled authoritative ICD-10 → ICD-11 mapping, a cross-terminology batch validator, and versioning + cross-revision diff. ICD-11 live lookup needs free WHO API credentials (a five-minute signup), bringing the default count to 31. SNOMED is gated behind an explicit feature flag and requires an IHTSDO license plus a self-hosted Snowstorm instance — more on why below.&lt;/p&gt;

&lt;p&gt;The server also exposes 3 MCP &lt;strong&gt;Prompts&lt;/strong&gt; that orchestrate tool calls into named user actions (&lt;code&gt;find-medical-code&lt;/code&gt;, &lt;code&gt;drug-info&lt;/code&gt;, &lt;code&gt;cid10-portuguese-lookup&lt;/code&gt;) and 3 &lt;strong&gt;Resources&lt;/strong&gt; for in-process reference content (server metadata, CID-10 chapter listing, per-terminology license disclaimers) — MCP clients render Prompts as one-click actions and read Resources by URI.&lt;/p&gt;

&lt;p&gt;This post walks through three concrete clinical and research workflows where the server earns its place in the toolbelt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup in 60 seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"medical-terminologies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"medical-terminologies-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"WHO_CLIENT_ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-who-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"WHO_CLIENT_SECRET"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-who-secret"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop that into your Claude Desktop config (&lt;code&gt;%APPDATA%\Claude\claude_desktop_config.json&lt;/code&gt; on Windows, &lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json&lt;/code&gt; on macOS), restart Claude, and 31 tools appear. WHO credentials are optional — without them the 5 ICD-11 live-lookup tools throw a clear configuration error and the other 26 work fine (including the authoritative ICD-10 → ICD-11 mapping, which uses bundled WHO data and needs no auth).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer a hosted endpoint?&lt;/strong&gt; The server runs on Cloudflare Workers at the edge — point any Streamable-HTTP MCP client at &lt;code&gt;https://medical-terminologies-mcp.sidneybissoli.workers.dev/mcp&lt;/code&gt; and skip the local install. Also listed on &lt;a href="https://smithery.ai/server/@SidneyBissoli/medical-terminologies-mcp" rel="noopener noreferrer"&gt;Smithery&lt;/a&gt;, &lt;a href="https://glama.ai/mcp/servers/SidneyBissoli/medical-terminologies-mcp" rel="noopener noreferrer"&gt;Glama&lt;/a&gt;, and &lt;a href="https://mcpservers.org/servers/sidneybissoli/medical-terminologies-mcp" rel="noopener noreferrer"&gt;mcpservers.org&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 1: sepsis triage — finding the right LOINC for procalcitonin
&lt;/h2&gt;

&lt;p&gt;A clinical scenario. An ED resident is documenting a sepsis workup and needs the LOINC code for serum procalcitonin to attach to the lab order in the EMR. They could open loinc.org, run a search, scroll through results, and copy the code. Or they could ask their LLM-powered scribe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;You: What's the LOINC code for procalcitonin in serum?

Claude (calling loinc_search):
&lt;span class="gu"&gt;## LOINC Search Results for "procalcitonin"&lt;/span&gt;
Found 23 total results (showing 10):
&lt;span class="p"&gt;
1.&lt;/span&gt; &lt;span class="gs"&gt;**33959-8**&lt;/span&gt; - Procalcitonin [Mass/volume] in Serum or Plasma
   Component: Procalcitonin | Property: MCnc | Timing: Pt | System: Ser/Plas
   Class: CHEM | Status: ACTIVE
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**75241-0**&lt;/span&gt; - Procalcitonin [Mass/volume] in Body fluid
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM picks &lt;code&gt;33959-8&lt;/code&gt; and proposes it. The clinician confirms or asks for details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;You: Give me the full LOINC details for 33959-8.

Claude (calling loinc_details):
&lt;span class="gh"&gt;# 33959-8 - Procalcitonin [Mass/volume] in Serum or Plasma&lt;/span&gt;

| Attribute | Value |
|-----------|-------|
| Component | Procalcitonin |
| Property  | MCnc |
| Timing    | Pt |
| System    | Ser/Plas |
| Scale Type| Qn |
| Method    | (none) |
| Class     | CHEM |
| Status    | ACTIVE |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this replaces in practice: a 30-second context switch from EMR to a browser tab, every time a clinician needs to pin down a specific code. Multiplied across the day, across a team, that's real time.&lt;/p&gt;

&lt;p&gt;The same workflow applies to any biomarker, vital sign, or observation: lactate (&lt;code&gt;2524-7&lt;/code&gt;), troponin I high-sensitivity (&lt;code&gt;67151-1&lt;/code&gt;), SpO₂ (&lt;code&gt;59408-5&lt;/code&gt;). The tool is &lt;code&gt;loinc_search&lt;/code&gt; for the discovery step, &lt;code&gt;loinc_details&lt;/code&gt; for the verification step. Both are deterministic API calls against NLM Clinical Tables — the LLM doesn't memorize codes, it looks them up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 2: medication reconciliation — breaking down a combination product
&lt;/h2&gt;

&lt;p&gt;A scenario familiar to any inpatient pharmacist or hospitalist: a patient is admitted on Janumet (a brand combo of sitagliptin and metformin), and the team needs to reconcile this against the formulary, check for class-level interactions, and document the active ingredients separately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;You: I need to break down Janumet for the admission med rec.
What are the active ingredients and their drug classes?

Claude (calling rxnorm_search, then rxnorm_ingredients, then atc_classify):

Step 1 — search for Janumet:
&lt;span class="gs"&gt;**860975**&lt;/span&gt; - Janumet (sitagliptin 50 MG / metformin hydrochloride 500 MG Oral Tablet)
   Type: SBD | Synonym: sitagliptin/metformin

Step 2 — get the ingredients:
| RxCUI | Name | Type |
|-------|------|------|
| 593411 | sitagliptin | Single Ingredient |
| 6809 | metformin | Single Ingredient |

Step 3 — get ATC classes for each ingredient:
For sitagliptin (593411): A10BH (Dipeptidyl-Peptidase IV Inhibitors)
For metformin (6809):     A10BA (Biguanides)
                          A10BD (Combinations of oral blood glucose lowering drugs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three tool calls, structured data, no hallucination risk. The LLM can compose this into a med rec note, flag that both ingredients sit in the same A10B (oral glucose-lowering) ATC bucket, and surface the ATC codes for any pharmacy system that uses ATC.&lt;/p&gt;

&lt;p&gt;What this enables that bare LLMs don't: the chain &lt;code&gt;brand → ingredients → ATC classes&lt;/code&gt; is a graph traversal across a real database. The model isn't recalling that sitagliptin is a DPP-4 inhibitor; it's &lt;em&gt;looking it up&lt;/em&gt; against the same source the FDA uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 3: evidence synthesis — building a precise PubMed search with MeSH
&lt;/h2&gt;

&lt;p&gt;A scenario for a researcher running a systematic review on the impact of primary health care interventions on avoidable hospitalizations. The quality of a literature search hinges on hitting the right MeSH descriptors and qualifiers — not on free-text keyword guesswork.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;You: I'm building a search strategy for a systematic review on
primary health care and avoidable hospitalizations. Help me find
the right MeSH descriptors and applicable qualifiers.

Claude (calling mesh_search):
| MeSH ID | Label |
|---------|-------|
| D011320 | Primary Health Care |
| D006297 | Health Services Accessibility |
| D004472 | Hospitalization |
| D006308 | Health Services Misuse |

You: Get me the tree position and allowed qualifiers for Primary Health Care.

Claude (calling mesh_tree, then mesh_qualifiers):
&lt;span class="gh"&gt;# Tree Numbers for D011320&lt;/span&gt;

&lt;span class="gu"&gt;### Health Care&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`N04.590.233.727`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`N02.421.143.827`&lt;/span&gt;

&lt;span class="gh"&gt;# Allowed Qualifiers for D011320&lt;/span&gt;

| Qualifier ID | Label |
|--------------|-------|
| Q000208 | ethics |
| Q000379 | methods |
| Q000523 | organization &amp;amp; administration |
| Q000592 | standards |
| Q000656 | trends |
| Q000706 | statistics &amp;amp; numerical data |
| Q000941 | utilization |

&lt;span class="ge"&gt;*Use qualifiers with descriptors like: "Primary Health Care/utilization"*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last detail — the seven allowed qualifiers, with &lt;code&gt;/utilization&lt;/code&gt; and &lt;code&gt;/statistics &amp;amp; numerical data&lt;/code&gt; highlighted — is exactly what a precise PubMed search needs. The model now has enough to draft &lt;code&gt;("Primary Health Care/utilization"[MeSH] OR "Primary Health Care/statistics &amp;amp; numerical data"[MeSH]) AND "Hospitalization/prevention and control"[MeSH]&lt;/code&gt; and explain why each leg matters.&lt;/p&gt;

&lt;p&gt;The use case generalizes: any research informatics workflow that needs controlled vocabulary mapping (PubMed, Cochrane, OVID) benefits from &lt;code&gt;mesh_search&lt;/code&gt; + &lt;code&gt;mesh_qualifiers&lt;/code&gt; + &lt;code&gt;mesh_tree&lt;/code&gt;. For systematic reviews specifically, the qualifier list is the part that's hardest to remember and easiest to get wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new in 1.4.0 (data-integrity release)
&lt;/h2&gt;

&lt;p&gt;Three additions that change the depth of what the server can answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;map_icd10_to_icd11&lt;/code&gt; now returns an authoritative WHO mapping&lt;/strong&gt;, not a text-search heuristic. The 2025-01 release of the WHO transition tables is bundled (5.4 MB raw / 0.95 MB gzipped) covering 11,243 ICD-10 categories. 1,461 of those have multiple WHO-documented ICD-11 candidates — the tool surfaces the primary mapping plus all alternatives, with Foundation/Linearization URIs ready to navigate into ICD-11.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;validate_codes&lt;/code&gt;&lt;/strong&gt; is a new batch validator. Pass up to 50 &lt;code&gt;{ code, terminology }&lt;/code&gt; pairs and get back per-item &lt;code&gt;{ valid, active, title, replaced_by, source, error }&lt;/code&gt;. Designed for retrospective database analysis: flag codes that no longer exist, surface ICD-10 → ICD-11 replacements at scale, grade activity status where the source terminology exposes it (SNOMED's active flag, LOINC's STATUS field).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;terminology_versions&lt;/code&gt; + &lt;code&gt;terminology_diff&lt;/code&gt;&lt;/strong&gt; for pipeline maintainers. The first lists current version, release date, publisher, and update cadence across all 8 terminologies. The second is guidance-only for terminologies without bundled history — except for ICD-10, where the bundled transition tables let it surface a real cross-revision summary (1:1 mappings vs splits vs average alternatives when split).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together with the per-tool &lt;code&gt;language&lt;/code&gt; parameter added to SNOMED + MeSH, that's four sub-tasks shipped as one data-integrity release. The earlier 1.3.0 added MCP Prompts (orchestration templates) and Resources (in-process reference content) — both visible in clients like LobeChat and Claude Desktop as one-click actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the server doesn't claim to do
&lt;/h2&gt;

&lt;p&gt;A few things worth being explicit about, because the README is honest about them and the LLM should be too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;map_loinc_to_snomed&lt;/code&gt; returns guidance&lt;/strong&gt;, not a mapping. Direct LOINC ↔ SNOMED CT mappings live in UMLS Metathesaurus (license required) or the LOINC SNOMED CT Expression Association files (LOINC license required).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;map_snomed_to_icd10&lt;/code&gt;&lt;/strong&gt; also returns guidance only today. The authoritative source is SNOMED International's ICD-10 Complex Map refset (447562003), which needs a Snowstorm instance with the refset loaded — planned as Phase 13.7 once that's tractable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SNOMED tools are off by default&lt;/strong&gt; because the historical public IHTSDO Snowstorm endpoint was retired (HTTP 410 Gone). Operators with an IHTSDO license and a self-hosted Snowstorm instance flip them on with &lt;code&gt;ENABLE_SNOMED_TOOLS=true SNOMED_BASE_URL=...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;None of this is a substitute for clinical judgment.&lt;/strong&gt; It's a lookup layer for already-known codes, not a diagnostic tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Under the hood, briefly
&lt;/h2&gt;

&lt;p&gt;For developers curious about the engineering: TypeScript on Node 20+, bundled with esbuild, built around a token-bucket rate limiter (5 req/s for WHO, 10 req/s for NLM, 20 req/s for RxNorm) and exponential-backoff retry with ±25% jitter. WHO OAuth tokens are cached using the actual &lt;code&gt;expires_in&lt;/code&gt; from the API response, not a hardcoded TTL. Every default tool declares &lt;code&gt;outputSchema&lt;/code&gt; and returns &lt;code&gt;structuredContent&lt;/code&gt; alongside markdown — so MCP clients that consume structured data get typed objects, not parsed prose. The 313-test Vitest suite (unit + contract via nock + integration against live APIs gated by env flag) gates CI on PR. A daily integration cron catches upstream API drift — work that already caught three silent production regressions where the API had drifted and the client gracefully returned empty data.&lt;/p&gt;

&lt;p&gt;Two transports: stdio (default — Claude Desktop, IDE clients) and Streamable HTTP. The HTTP path runs on Cloudflare Workers at the edge by default — single web-standard fetch handler, ~0.95 MB gzipped including the bundled CID-10 (DataSUS V2008) and ICD-10 → ICD-11 (WHO 2025-01) datasets, well within Cloudflare's 3 MB compressed script limit. Same source tree; per-isolate cache + rate limiter (KV + Durable Object swap available when traffic crosses the threshold).&lt;/p&gt;

&lt;p&gt;It's MIT licensed. The medical terminology content has its own licenses, all linked in the README and surfaced as the &lt;code&gt;info://licenses&lt;/code&gt; Resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/medical-terminologies-mcp" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/medical-terminologies-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/SidneyBissoli/medical-terminologies-mcp" rel="noopener noreferrer"&gt;https://github.com/SidneyBissoli/medical-terminologies-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosted endpoint:&lt;/strong&gt; &lt;a href="https://medical-terminologies-mcp.sidneybissoli.workers.dev/mcp" rel="noopener noreferrer"&gt;https://medical-terminologies-mcp.sidneybissoli.workers.dev/mcp&lt;/a&gt; (Streamable HTTP, Cloudflare Workers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smithery:&lt;/strong&gt; &lt;a href="https://smithery.ai/server/@SidneyBissoli/medical-terminologies-mcp" rel="noopener noreferrer"&gt;https://smithery.ai/server/@SidneyBissoli/medical-terminologies-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glama:&lt;/strong&gt; &lt;a href="https://glama.ai/mcp/servers/SidneyBissoli/medical-terminologies-mcp" rel="noopener noreferrer"&gt;https://glama.ai/mcp/servers/SidneyBissoli/medical-terminologies-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mcpservers.org:&lt;/strong&gt; &lt;a href="https://mcpservers.org/servers/sidneybissoli/medical-terminologies-mcp" rel="noopener noreferrer"&gt;https://mcpservers.org/servers/sidneybissoli/medical-terminologies-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Registry:&lt;/strong&gt; &lt;code&gt;io.github.SidneyBissoli/medical-terminologies-mcp&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WHO API credentials (free): &lt;a href="https://icd.who.int/icdapi" rel="noopener noreferrer"&gt;https://icd.who.int/icdapi&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Issues, PRs, and use-case reports welcome — especially from clinical informatics teams, research informatics groups, and public-health data analysts using LLMs in real workflows. There's a real gap between "LLM as scribe" and "LLM with reliable terminology access," and closing it is what this server is for.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>healthcare</category>
      <category>ai</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
