A Model Context Protocol (MCP) server that exposes the EUDAMED public API — the EU's medical device and in vitro diagnostic device registration database — to MCP clients (Claude Desktop, Claude Code, claude.ai, etc.) — locally over stdio, or as a remote server over Streamable HTTP.
It wraps eudamed-public's
EudamedClient library, which already handles pagination, retries, and
resolving numeric ids to human-readable reference labels.
A deployment runs on Railway at https://accurate-illumination-production-75fe.up.railway.app:
- MCP endpoint:
https://accurate-illumination-production-75fe.up.railway.app/mcp - Health check:
https://accurate-illumination-production-75fe.up.railway.app/health
See Configure in an MCP client to connect to it.
| Tool | Description |
|---|---|
search_actors |
Search economic operators (manufacturers, authorised representatives, importers, competent authorities) by name, type, or country. |
get_actor |
Look up a single actor by its exact EUDAMED actor id (UUID). |
search_udi_devices |
Search UDI device records by identifiers, names, manufacturer, risk class, or legislation. |
get_udi_device |
Look up a single UDI device record by its exact Primary DI. |
search_reference_data |
Search reference/nomenclature lookup tables (risk classes, legislations, statuses, ...). |
get_reference_value |
Resolve a single reference value by numeric id, code, and language. |
All read-only; the EUDAMED public API has no write operations and requires no authentication.
swift build -c releaseThe binary is produced at .build/release/eudamed-mcp.
The same binary runs in two modes:
| Mode | Command | Use it for |
|---|---|---|
| stdio (default) | eudamed-mcp |
Local clients that launch the server themselves (Claude Desktop, Claude Code) |
| HTTP | eudamed-mcp serve |
A remote server that clients connect to by URL |
.build/release/eudamed-mcpThe server speaks MCP over stdio: JSON-RPC messages in on stdin, one per line, and responses out on stdout, one per line. All logging goes to stderr so it never corrupts the protocol stream. You normally don't start it by hand; the MCP client launches it (see Configure in an MCP client).
Start it locally (listens on 127.0.0.1:8080 by default):
.build/release/eudamed-mcp serveor, during development, without a separate build step:
swift run eudamed-mcp serveTo accept connections from other machines, bind to all interfaces and run in production mode:
EUDAMED_MCP_TOKEN=change-me \
.build/release/eudamed-mcp serve --env production --hostname 0.0.0.0 --port 8080Check that it is up:
curl http://localhost:8080/health
# ok
curl http://localhost:8080/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Authorization: Bearer change-me' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Stop it with Ctrl+C.
| Option | Default | Effect |
|---|---|---|
--hostname (or HOST) |
127.0.0.1 |
Interface to bind. Use 0.0.0.0 to accept remote connections. |
--port (or PORT) |
8080 |
Port to listen on. |
--env |
development |
production reduces log noise. |
EUDAMED_MCP_TOKEN |
unset | Require Authorization: Bearer <token> on /mcp. Unset = open endpoint. |
EUDAMED_MCP_ALLOWED_HOSTS |
unset | Comma-separated Host header values to accept (e.g. eudamed.example.com,localhost:*), as DNS rebinding protection. Unset = not checked. |
POST /mcp: the MCP endpoint (Streamable HTTP).GET /health: returnsok, for load balancer checks.
The server uses the MCP SDK's stateless HTTP transport. Every request is handled independently, with plain JSON responses and no sessions, so you can run several instances behind a load balancer without sticky routing.
CORS is enabled for all origins, so browser-based clients such as
MCP Inspector can connect directly. Protect a
public deployment with EUDAMED_MCP_TOKEN.
The server speaks plain HTTP. Put TLS in front of it with a reverse proxy or your hosting platform.
The image is defined in Dockerfile. With Apple container:
container build -t eudamed-mcp .
container run -d --name eudamed-mcp -p 8080:8080 -e EUDAMED_MCP_TOKEN=change-me eudamed-mcpWith Docker:
docker build -t eudamed-mcp .
docker run -d --name eudamed-mcp -p 8080:8080 -e EUDAMED_MCP_TOKEN=change-me eudamed-mcpA running container keeps its image, so updating means rebuilding the image and replacing the container:
# 1. Get the latest code.
git pull
# 2. Optional: move dependencies (e.g. eudamed-public) to their newest
# allowed versions. Commit the changed Package.resolved.
swift package update
# 3. Rebuild. --pull refreshes the swift:latest and swift:slim base images,
# picking up Swift and OS security fixes.
container build --pull -t eudamed-mcp .
# 4. Replace the running container.
container stop eudamed-mcp
container rm eudamed-mcp
container run -d --name eudamed-mcp -p 8080:8080 -e EUDAMED_MCP_TOKEN=change-me eudamed-mcp
# 5. Check it is up, then remove the old, now unused images.
curl http://127.0.0.1:8080/health
container image pruneWith Docker, the steps are the same: docker build --pull -t eudamed-mcp .,
then docker stop, docker rm, docker run as above, and docker image prune.
Railway builds the Dockerfile and serves the app over
HTTPS. It sets PORT automatically; the image already binds 0.0.0.0.
Install the Railway CLI first, e.g.
brew install railway.
# 1. Log in and link this directory to your Railway project and service.
# (Create the project in the dashboard first, or run `railway init`.)
railway login
railway link
# 2. Set the access token. Use a long random value and keep it secret.
railway variables --set "EUDAMED_MCP_TOKEN=$(openssl rand -hex 32)"
# 3. Upload this directory, build the Dockerfile and deploy.
# Files in .gitignore (such as .build/) are not uploaded.
railway up
# 4. Create a public https://<name>.up.railway.app domain
# (the hosted instance is https://accurate-illumination-production-75fe.up.railway.app).
railway domainOptionally restrict the accepted Host header to that domain:
railway variables --set "EUDAMED_MCP_ALLOWED_HOSTS=accurate-illumination-production-75fe.up.railway.app"Check the deployment:
curl https://accurate-illumination-production-75fe.up.railway.app/health # → ok
railway logsIn the service settings, set the health check path to /health so Railway
only switches traffic to a new deployment once it responds.
To update, run railway up again from the latest code. Railway builds a new
image and replaces the running deployment. Show the token again with
railway variables when you configure an MCP client.
Claude Code:
claude mcp add --transport http eudamed https://accurate-illumination-production-75fe.up.railway.app/mcp \
--header "Authorization: Bearer change-me"On claude.ai, add it as a custom connector with the same URL. For your own deployment, replace the host with yours.
For Claude Desktop / Claude Code, add to your MCP server config:
{
"mcpServers": {
"eudamed": {
"command": "/absolute/path/to/eudamed-mcp/.build/release/eudamed-mcp"
}
}
}MCP Inspector is a
browser UI (and CLI) for calling the server's tools by hand. It needs
Node.js; npx downloads it on first use.
npx @modelcontextprotocol/inspectorThis opens the Inspector at http://localhost:6274 with a session token
already in the URL. Then connect to the server one of two ways.
Over HTTP. Start the server first (swift run eudamed-mcp serve), then
in the Inspector sidebar set:
| Field | Value |
|---|---|
| Transport Type | Streamable HTTP |
| URL | http://localhost:8080/mcp |
| Authentication | Only if EUDAMED_MCP_TOKEN is set: header Authorization, value Bearer <token> |
Over stdio. Build first (swift build), then set:
| Field | Value |
|---|---|
| Transport Type | STDIO |
| Command | /absolute/path/to/eudamed-mcp/.build/debug/eudamed-mcp |
| Arguments | (empty) |
You can also pass the server on the command line, which pre-fills these fields:
npx @modelcontextprotocol/inspector .build/debug/eudamed-mcpClick Connect, open the Tools tab, click List Tools, pick a tool, fill in its arguments and click Run Tool.
Add --cli to run a single request and print the JSON result, which is
handy for quick checks and scripts:
# List the tools (HTTP; the transport is detected from the /mcp path)
npx @modelcontextprotocol/inspector --cli http://localhost:8080/mcp \
--method tools/list
# Call a tool (stdio)
npx @modelcontextprotocol/inspector --cli .build/debug/eudamed-mcp \
--method tools/call --tool-name search_actors \
--tool-arg "name=Roche Diagnostics GmbH"With a token, add --header "Authorization: Bearer <token>".
- Connection fails over HTTP: check that the server is running
(
curl http://localhost:8080/health) and that the URL ends in/mcp. - 401 Unauthorized: the token is missing or wrong. It must match
EUDAMED_MCP_TOKENand be sent asBearer <token>. - Request rejected for its Host or Origin: if
EUDAMED_MCP_ALLOWED_HOSTSis set, it must include the host you connect to, e.g.localhost:*,127.0.0.1:*. - "Method Not Allowed" on GET: expected. The server is stateless and doesn't offer an SSE stream; the Inspector falls back to plain POST.
swift build
swift testLicensed under PolyForm Noncommercial 1.0.0 —
see LICENSE. It depends on
eudamed-public, which is
licensed under the same terms; see that project's EULA.md for commercial
licensing options.
