Labguru MCP Server
by Paulpey13
README.md
# Labguru MCP Server
A modular, configurable [Model Context Protocol](https://modelcontextprotocol.io)
server that exposes the [Labguru](https://www.labguru.com) laboratory management
REST API as native LLM tools. Point any MCP client (Claude Code, Claude Desktop,
etc.) at it and ask questions or run workflows against your Labguru instance in
plain language.
Built with `FastMCP` + async `httpx`. Open source under the MIT license, so any
lab is free to use and adapt it.
- 67 tools + 4 guided prompts + 4 resources across every Labguru domain
(experiments, protocols, projects, inventory, stocks, elements, shopping list /
PO, reports, companies, instruments, attachments, maintenance, plus CMR and
safety helpers).
- Configurable per instance: domain, custom biocollections, CMR field mapping,
timeouts, concurrency, cache, retries.
- Two auth modes: personal API token, or email/password (auto re-auth on expiry).
- Resilient: retry with exponential backoff on 429/5xx/network errors.
- Fast: in-memory TTL cache for the scan-heavy tools (repeat scans are instant).
- Safe: optional read-only mode that hides every write tool; read/write tool
annotations let clients auto-approve reads.
## Architecture
```
MCP_labguru/
labguru_mcp/ package (importable, installable)
config.py settings from env + optional JSON file
client.py async HTTP client (auth, pagination, fan-out)
formatting.py pure payload-normalising helpers
errors.py typed exceptions
app.py builds settings + client + FastMCP; tool() decorator
prompts.py guided workflow prompts (PO, CMR, duplication)
__main__.py python -m labguru_mcp
tools/ one module per domain; importing registers tools
experiments.py protocols.py projects.py inventory.py stocks.py
elements.py shopping.py reports.py companies.py instruments.py
attachments.py maintenance.py search.py generic.py
mcp_server.py thin entry-point shim (back-compat)
pyproject.toml packaging + `labguru-mcp` console script
.env.example config template
labguru.config.example.json optional JSON config template
LICENSE MIT
```
Adding a tool = add a function decorated with `@tool()` (or `@tool(write=True)`)
in the relevant `tools/*.py` module. No registry to update; importing the module
registers it.
## Install
Requires Python 3.10+. Installing the package puts a `labguru-mcp` command on
your PATH, which is all an MCP client needs.
```bash
# from source
git clone https://github.com/Paulpey13/labguru_MCP_server
cd labguru_MCP_server
pip install .
```
For development, install the editable package with test/lint extras:
```bash
pip install -e ".[dev]"
```
## Configure
Copy `.env.example` to `.env` and fill it in (env vars and an optional
`labguru.config.json` are also supported; env vars win).
### Authentication (choose one)
- **Personal API token** (recommended): set `LABGURU_TOKEN`. Generate it in the
Labguru UI under Account > API token.
- **Email + password**: set `LABGURU_LOGIN` and `LABGURU_PASSWORD`. They are
exchanged for a session token via `POST /api/v1/sessions.json`, and the server
re-authenticates automatically if the token expires (HTTP 401).
Run the `whoami` tool any time to confirm the active base URL and auth mode
(only a short token hint is shown, never the secret).
### Settings
| Variable | Purpose | Default |
|---|---|---|
| `LABGURU_TOKEN` / `LABGURU_API_KEY` | Personal API token | — |
| `LABGURU_LOGIN` / `LABGURU_PASSWORD` | Email + password fallback | — |
| `LABGURU_DOMAIN` | Instance domain (no scheme) | `eu.labguru.com` |
| `LABGURU_BASE_URL` | Full base URL, overrides `LABGURU_DOMAIN` | — |
| `LABGURU_READ_ONLY` | Hide all write tools | `false` |
| `LABGURU_TIMEOUT` | HTTP timeout (seconds) | `60` |
| `LABGURU_MAX_CONCURRENCY` | Max concurrent calls in fan-out scans | `8` |
| `LABGURU_CACHE_TTL` | TTL (s) for the collection cache; `0` disables | `300` |
| `LABGURU_MAX_RETRIES` | Retries on 429/5xx/network errors | `3` |
| `LABGURU_RETRY_BASE_DELAY` | Base backoff delay (seconds) | `0.5` |
| `LABGURU_BIOCOLLECTIONS` | Comma-separated biocollection slugs | built-in list |
| `LABGURU_DIRECT_INVENTORY` | Comma-separated direct inventory types | built-in list |
| `LABGURU_CMR_MAP` | JSON: per-collection CMR `risk`/`measure` fields | see below |
| `LABGURU_CONFIG` | Path to a JSON config file | `labguru.config.json` |
| `LABGURU_DEBUG` | Enable verbose HTTP logs (logs include the token) | `false` |
Adapting to your lab: if your instance uses different custom fields for CMR risk
classification, or has extra custom biocollections, set `LABGURU_CMR_MAP` and
`LABGURU_BIOCOLLECTIONS` rather than editing the code. The default CMR map is:
```json
{
"biochemistry": { "risk": "custom5", "measure": "custom6" },
"culture": { "risk": "custom3", "measure": "custom4" },
"sp_bioch": { "risk": "custom3", "measure": "custom4" }
}
```
## Run
The server speaks stdio; your MCP client launches it. After `pip install`, the
included `.mcp.json` works as-is (it just runs the `labguru-mcp` command):
```json
{
"mcpServers": {
"labguru": {
"type": "stdio",
"command": "labguru-mcp",
"args": [],
"env": {}
}
}
}
```
**Claude Code** — register it once (reads credentials from your `.env`):
```bash
claude mcp add labguru -- labguru-mcp
```
**Claude Desktop** — add the same `mcpServers` block to
`claude_desktop_config.json` and restart the app.
You can also pass credentials inline via the config's `env` block instead of a
`.env` file, e.g. `"env": { "LABGURU_TOKEN": "..." }`.
For local development / inspection:
```bash
python -m labguru_mcp # plain stdio server
mcp dev -m labguru_mcp # MCP Inspector UI
```
## Tools
| Domain | Tools |
|---|---|
| Experiments | `list_experiments`, `count_experiments`, `get_experiment`, `get_experiment_raw`, `get_experiment_samples`, `get_experiment_stock_ids`, `get_experiments_in_range`, `create_experiment`*, `update_experiment`* |
| Protocols | `list_protocols`, `get_protocol`, `search_protocols`, `find_protocols_with_sysid` |
| Projects | `list_projects`, `get_project`, `list_folders`, `create_project`*, `update_project`* |
| Inventory | `list_collections`, `list_inventory`, `search_inventory`, `get_inventory_item`, `get_collection_item`, `find_item_by_sysid`, `get_generic_item` |
| CMR / Safety | `get_cmr_items`, `get_safety_links`, `cmr_experiment_report` |
| Stocks | `list_stocks`, `get_stock`, `get_stock_by_barcode`, `expiring_stocks`, `create_stock`*, `update_stock`* |
| Elements / UUID | `get_element`, `get_element_by_uuid`, `get_element_rows`, `resolve_uuid`, `list_sections`, `update_element`*, `create_element`*, `create_section`* |
| Shopping / PO | `list_shopping_items`, `get_order`, `get_order_summary`, `get_last_order`, `add_shopping_item`* |
| Reports | `list_reports`, `get_report`, `create_report`*, `update_report`*, `tag_report`* |
| Companies | `list_companies`, `get_company` |
| Instruments | `list_instruments`, `get_instrument`, `post_measurement`* |
| Attachments | `list_attachments`, `get_attachment`, `download_attachment`, `upload_attachment`* |
| Maintenance | `list_maintenance_events` |
| Cross-resource | `global_search` |
| Generic / introspection | `api_request`*, `whoami`, `clear_cache`, `list_capabilities` |
`*` = write tool, hidden when `LABGURU_READ_ONLY=true`. Every tool also carries
MCP annotations (`readOnlyHint`, `destructiveHint`) so clients can treat reads
and writes appropriately.
`api_request` is an escape hatch for any endpoint without a dedicated tool; the
auth token is injected automatically. `list_capabilities` reports the live
configuration (collections, CMR map, cache/retry settings, registered tools).
`clear_cache` drops the collection cache so the next scan refetches fresh data.
## Prompts
Guided, parameterised workflows the client can launch (they orchestrate the
tools above; they do not call the API directly):
| Prompt | Purpose |
|---|---|
| `generate_purchase_order(order_number)` | Review a PO, check approvals, summarise totals, optionally create the report |
| `cmr_report(start_id, end_id)` | Cross-reference CMR products against an experiment ID range |
| `duplicate_experiment(experiment_id)` | Inspect an experiment and plan its duplication |
| `safety_data_sheets(collections)` | Collect safety data sheet (fiche de securite) links |
## Resources
Read-only records the client can attach as context (addressable URIs):
| URI | Content |
|---|---|
| `labguru://experiment/{id}` | Full experiment JSON |
| `labguru://protocol/{id}` | Full protocol JSON |
| `labguru://stock/{id}` | Full stock JSON |
| `labguru://inventory_item/{id}` | Full inventory item JSON |
## Notes
- List tools return slim summaries to keep payloads small; use the matching
`get_*` / `*_raw` tool for the full JSON of a single record.
- `list_experiments`, `list_protocols`, and `list_stocks` return the most recent
records first (server-side sort via the Kendo grid syntax, since plain
`sort`/`direction` params are rejected by Labguru). Pass `oldest_first=true`
to reverse. Use `count_experiments` for the true total without fetching every
page (instances can hold tens of thousands of records).
- `list_experiments` accepts `since`/`until` (YYYY-MM-DD) date filters. Recent
ranges are instant; filtering far back in time walks history and is slower.
- Sample tables, CMR cross-references, and per-element fetches are cached for
`LABGURU_CACHE_TTL` seconds, so repeated `cmr_experiment_report` /
`expiring_stocks` / sample reads within a session are fast.
- Pagination is handled internally (`page` / `per_page`), stopping when a page
is empty or shorter than the page size. Wrapped responses (`value`, `data`,
...) are unwrapped automatically.
- Fan-out scans (`get_experiments_in_range`, `find_protocols_with_sysid`,
`find_item_by_sysid`, `get_cmr_items`, `get_safety_links`, `global_search`)
use bounded concurrency and can be slow on large instances. Full-collection
fetches are cached for `LABGURU_CACHE_TTL` seconds, so repeat scans within a
session are near-instant; call `clear_cache` after a write to force a refresh.
- Requests retry automatically on 429/5xx/network errors with exponential
backoff (honouring `Retry-After`), and re-authenticate once on HTTP 401.
- The server never writes to stdout (it would corrupt the JSON-RPC stream);
warnings go to stderr.
## License
MIT (c) 2026 Paul Peyssard, Neuro-Sys. See [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues