Skip to main content
Glama

eLabFTW MCP

Standalone Model Context Protocol server for eLabFTW — 41 tools, pure Python, MCP protocol revision 2026-07-28 (stateless) with older handshakes still served. Single-user (stdio) and multi-user hosted mode (register page → personal URL with scoped token).

No R runtime, no external tool library: everything talks to the eLabFTW REST API v2 directly.

Credits

The tool surface, parameter names, validation rules and the safety rails (append-only body updates, provenance blocks, dry-run link operations) are a faithful port of

Marvin Luepke — elabR / elabrmcp

which implemented the same 41 tools in R on top of mcptools/`ellmer. That implementation is the reference this port was written against, and its documented behaviour is what the tests assert. Thank you — this would have been a much worse server without it.

Also derived from that work: the hosted mode's feature set (register flow, profile presets, per-token tool scope, audit log), which previously lived in the unified-researchdata-mcp deployment.

Related MCP server: protocol-mcp

Tools (41)

Group

Tools

Read (13)

list_experiments, list_items, list_experiment_templates, list_item_types, get_experiment, get_item, get_experiment_template, get_item_type, list_experiment_categories, list_experiment_statuses, list_item_categories, list_item_statuses

Meta (3)

get_connection_info, get_current_user_capabilities, refresh_team_caps

Create / update (7)

create_experiment, create_item, create_experiment_from_template, update_experiment_body, update_item_body, update_entity_fields, update_entity_metadata

Upload (1)

upload_image_from_content

Steps (5)

list_steps, add_step, update_step, toggle_step, delete_step

Links (8)

get_entity_links, ensure_link, ensure_link_by_query, bulk_ensure_links, delete_link, bulk_delete_links, expand_links_network, resolve_entity_by_query

AI (5)

review_experiment, suggest_tags, suggest_metadata, apply_tag_suggestions, add_ai_review_comment

Write tools honour the profile of the token (r read-only, h hybrid, f full) and the features.write_* switches in the config; destructive operations require an explicit confirmation argument.

Install

pip install .                 # or: uv pip install .

Requires Python ≥ 3.10 and an eLabFTW API key (eLabFTW → Account → API keys).

Local (stdio) usage

{
  "mcpServers": {
    "elabftw": {
      "command": "elabftw-mcp",
      "env": {
        "ELABFTW_BASE_URL": "https://elntest.ub.tum.de",
        "ELABFTW_API_KEY": "your-key"
      }
    }
  }
}

Or against any HTTP client: elabftw-mcp --transport streamable-http --port 8081 (stateless by default, --stateful for session-based clients, --json-response if the client cannot read SSE).

Hosted (multi-user) usage

export MCP_JWT_SECRET="$(python -c 'import base64,os;print(base64.urlsafe_b64encode(os.urandom(32)).decode())')"
elabftw-mcp --hosted --host 0.0.0.0 --port 8081
  • /register — enter instance URL + API key, pick a profile and the tools to expose

  • /mcp?token=… — the MCP endpoint for the issued personal URL

  • /status — service and protocol info

  • audit log (JSONL), registration rate limit, X-Forwarded-Proto aware

Tokens are HMAC-signed and carry the instance URL, the API key, the profile and the tool allow-list; keys are never stored server-side. Requests without a valid token get 401; tools outside a token's scope are refused with -32601.

Configuration

config.example.yml → config.yml (path via ELABFTW_MCP_CONFIG). Everything can be overridden by environment variables: ELABFTW_BASE_URL, ELABFTW_API_KEY, ELABFTW_MCP_AI_MODEL, ELABFTW_MCP_AI_KEY, ELABFTW_MCP_AI_BASE_URL, ELABFTW_MCP_RATE_LIMIT, MCP_JWT_SECRET, MCP_TOKEN_EXPIRY_DAYS, ELABFTW_MCP_AUDIT_LOG.

features:
  write_enabled: true        # master switch for every write tool
  write_create: true         # create_experiment / create_item / …
  write_update: true         # body / field / metadata updates
  allow_body_overwrite: true # otherwise only append mode is allowed
  ai_review: true            # review_experiment etc.
  cache_ttl_seconds: 300     # cache for team catalogues and /users/me

AI tools

review_experiment, suggest_tags and suggest_metadata call an OpenAI-compatible chat-completions endpoint (ai.base_url, ai.model, ai.api_key). Without a key they answer with the upstream placeholder wording instead of failing — the response contract (trace_id, entity_type, entity_id, suggestions, status) stays identical either way.

Tests

python tests/test_api_contract.py     # offline: request shapes vs. the OpenAPI spec
python tests/test_tools_offline.py    # offline: all 41 tools against a stub eLabFTW
python tests/test_transports.py       # offline: stdio + stateless HTTP, real MCP client
python tests/test_ai_tools.py         # offline: AI tools vs. a local LLM stub (no quota spent)
python tests/test_ai_tools.py --live  # live instance + local LLM stub
python tests/test_live_tools.py       # live: all 41 tools against a real instance
python tests/test_proxy_live.py       # live: register flow, scope, protocol eras

For a running deployment there are two checks that need nothing but a URL:

python tests/diagnostics/deployed_e2e.py    # one endpoint: register, 41 tools, read + write + cleanup
python3 tests/diagnostics/service_sweep.py  # the whole host: web hosts, register pages, /el, /dt, /nm

Live suites read the key from ../.elab_key (never printed), tag everything they create as elabmcp-test-<timestamp> and delete it again. tests/diagnostics/ holds the small probes used to pin down the API behaviour on a new instance (step fields, link direction, metadata payload).

Status of the last full run (eLabFTW 6.0.1, MCP Python SDK 2.2.0):

Suite

Result

test_live_tools.py — every tool against the live instance

46 / 46 checks PASS, test data cleaned up

test_tools_offline.py — every tool against the stub

45 / 45 PASS

test_api_contract.py — requests vs. OpenAPI (83 paths, 104 requests)

PASS

test_ai_tools.py — AI tools against a local OpenAI-compatible stub

15 / 15 PASS

test_ai_tools.py --live — same, but the entry comes from the real instance

17 / 17 PASS

test_transports.py — stdio + stateless HTTP + legacy handshake

8 / 8 PASS

test_proxy_live.py — register, scope, tokens, audit

13 / 13 PASS

API notes (measured against a running instance, not just the spec)

Observation

Consequence for the tools

PATCH .../steps/{id} rejects any action value other than finish ("Incorrect parameter for steps.") although the 6.0.2 spec declares action: update

update_step sends the spec-shaped payload first and retries without action — works on both behaviours

action: finish toggles and ignores a given finished value

toggle_step(finished=0/1) verifies the stored state and toggles a second time if needed

deadline is accepted on a step PATCH, deadline_notif is not (HTTP 400)

add_step sets the deadline in a follow-up PATCH and reports the stored value; deadline_notif is reported as unsupported instead of silently dropped

metadata must be sent as a JSON string; a nested object produces HTTP 500 / MySQL 3140

update_entity_metadata serialises before PATCHing

Links are stored on one side only: /{type}/{id}/experiments_links lists own links, rows carry entityid

outgoing via the subresource, incoming via ?related=<id>&related_origin=<type>

Tags are canonicalised on write (tga becomes TGA if the team already knows the tag) and the entity payload reports them as a `a

b

DELETE is a soft delete (state=3): the record disappears from listings but GET still answers

the live suites verify removal through the listing instead of a 404

Single-entity GETs can be very large (item types with long HTML bodies)

results are always valid JSON: long strings are shortened before anything is truncated

Response statuses used by the write tools: created, updated, deleted, uploaded, linked, already_linked, would_link, unlinked, already_absent, would_unlink, ok, partial — the same vocabulary across all write tools.

Relation to the previous /el deployment

Measured against the running R-based service (researchmcp.duckdns.org/el, reproduced by tests/diagnostics/compare_with_deployed.py): the tool surface is identical — 41 tools on both sides, none missing, none extra — and the same entities and ids come back (categories, statuses and user info 100% identical, get_experiment 81%).

Deliberate differences:

previous deployment

this server

answer format

R print output (list(limit = 2, …))

JSON

entity rows

whole row

whole row (only body_html is dropped, tags normalised to a list of strings)

protocol

2025-06-18 only

2026-07-28 stateless plus legacy handshakes

AI tools

bootstrap placeholders

real LLM calls, same response contract

X-Write-Scope

forwarded to the worker

accepted, can only narrow the token's profile

personal URLs

HMAC(instance, key, profile, tools, expiry)

byte-compatible format, verified with the same MCP_JWT_SECRET

Only /el (proxy + R worker) is replaced. /nm (NOMAD MCP), /dt (DataTagger MCP), the eLabFTW instance, the databases, Caddy and the two Streamlit apps keep running untouched.

Documented differences from the upstream R implementation

Topic

Behaviour here

Metadata structure

eLabFTW 6 format (metadata.extra_fields keyed by field name, group_id per field) instead of the 5.x group-keyed layout

Incoming links

Resolved via the documented ?related=<id>&related_origin=<type> query — eLabFTW stores a link on one side only, so the related_* keys of the entity payload do not carry incoming links

Linkable types

eLabFTW's link routes exist for experiments and items only; template/item-type link calls fail with a clear message instead of a silent no-op

AI tools

Real LLM calls (upstream shipped bootstrap placeholders); same response contract

Provenance

Key elabrmcp_provenance is kept, source reports elabftw-mcp

Team capability flags

A missing users_canwrite_* flag allows the call (the API enforces permissions) instead of denying it

Low-level/inventory/compound flags

Present in the config for compatibility; the API is called directly

License

MIT, see LICENSE.

This server is a rewrite of Marvin Luepke's elabMCP R server and its elabR library, both MIT licensed. The tool surface, the response shapes and the runtime quirks documented below follow those sources; the credits are in the section above.

Available Tools

41 tools
add_ai_review_commentA

Attach an AI-generated review comment to an eLabFTW experiment or item. The provenance header ('[AI-GENERATED] | | trace: …') is added automatically. WRITE OPERATION — requires the comment write scope.

Inputs: id (required), comment (required), entity_type (required), trace_id. Returns: result

add_stepA

Add a new step (task) to an existing eLabFTW experiment, item, experiment template or item type. WRITE OPERATION — requires the step write scope. A deadline (date/datetime string) is set with a follow-up update because the create endpoint only stores the body. Returns entity_type, entity_id, step_id, the stored deadline and status='created'.

Inputs: id (required), body (required), entity_type (required), deadline, +1 more. Returns: result

apply_tag_suggestionsB

Persist a list of (suggested) tags to an eLabFTW experiment or item. WRITE OPERATION — requires the tag write scope.

Inputs: id (required), tags (required), entity_type (required). Returns: result

create_experimentA

Create a new eLabFTW experiment. WRITE OPERATION — requires the create write scope. Discover valid category/status IDs first; never embed IDs in the title or body. If the team enforces experiment templates (force_exp_tpl), this tool is refused and create_experiment_from_template must be used. Returns the new id.

Inputs: body, tags, title, status_id, +1 more. Returns: result

create_experiment_from_templateA

Create a new eLabFTW experiment from an experiment template. WRITE OPERATION — requires the create write scope. Use list_experiment_templates to find template IDs. include_steps/include_body decide whether the template's steps and body are copied. Returns the new id.

Inputs: template_id (required), tags, title, trace_id, +4 more. Returns: result

create_itemA

Create a new eLabFTW database item (resource). WRITE OPERATION — requires the create write scope. Items belong to item categories; discover valid IDs first and never embed IDs in the title or body. Returns the new id.

Inputs: body, tags, title, status_id, +1 more. Returns: result

delete_stepA

Delete a step from an eLabFTW experiment, item, experiment template or item type. WRITE OPERATION — requires the step write scope. Returns status='deleted'.

Inputs: id (required), step_id (required), entity_type (required). Returns: result

get_connection_infoA

Return connection and session details for the active server: eLabFTW base URL, authenticated user, active team, effective server mode (read-only vs write-enabled), enabled tool groups, and a category catalogue (experiments and items) for the active team. Always safe to call; never writes anything.

Inputs: no named parameters. Returns: result

get_current_user_capabilitiesA

Return a sanitized subset of the current user's capabilities and team roles. Calls /users/me and returns only the fields relevant for capability detection: userid, fullname, current team id, team memberships (id, name, is_admin, is_owner, is_archived), is_sysadmin, can_manage_compounds and can_manage_inventory_locations. Sensitive fields (email, API keys, signatures, preferences) are deliberately omitted.

Inputs: no named parameters. Returns: result

get_experimentA

Fetch a single elabFTW experiment by its numeric ID. Returns the full experiment record including body, metadata, tags, comments, links and uploads. Always safe to call; never writes anything.

Inputs: id (required). Returns: result

get_experiment_templateA

Fetch a single elabFTW experiment template by its numeric ID. Returns the full template record including body, metadata and steps. Use list_experiment_templates to discover templates and create_experiment_from_template to create an experiment from one. Always safe to call; never writes anything.

Inputs: id (required). Returns: result

get_itemA

Fetch a single elabFTW database item (resource) by its numeric ID. Returns the item with metadata, tags, comments, links and uploads. Always safe to call; never writes anything.

Inputs: id (required). Returns: result

get_item_typeA

Fetch a single elabFTW item type by its numeric ID. Returns the full item type record including body, metadata and steps. Use list_item_types to discover item types. Always safe to call; never writes anything.

Inputs: id (required). Returns: result

list_experiment_categoriesA

List all experiment categories for the active elabFTW team. Specify team_id='current' (default) or a numeric team ID. Returns id, title, color and is_default per category — use those ids when filtering list_experiments by category. Always safe to call; never writes anything.

Inputs: team_id. Returns: result

list_experimentsA

Search or list elabFTW experiments. Returns a structured response with paging metadata, applied filters, and result records. Check paging.returned_count vs paging.limit to detect further pages. Use the dedicated filter arguments for known category/user/status IDs or tags instead of putting them into q. Always safe to call; never writes anything.

Inputs: q, sort, tags, limit, +7 more. Returns: result

list_experiment_statusesA

List all experiment statuses for the active eLabFTW team. Specify team_id='current' (default) or a numeric team ID. Read-only.

Inputs: team_id. Returns: result

list_experiment_templatesA

List elabFTW experiment templates (/experiments_templates). Returns a structured response with paging metadata and result records. Templates are reusable pre-filled starting points for new experiments and are DIFFERENT from item types (use list_item_types for those). Use get_experiment_template for a single template and create_experiment_from_template to use one. Always safe to call; never writes anything.

Inputs: q, sort, tags, limit, +3 more. Returns: result

list_item_categoriesA

List all item categories (resource categories) for the active eLabFTW team. Specify team_id='current' (default) or a numeric team ID. Use the returned ids when filtering list_items by category. Always safe to call; never writes anything.

Inputs: team_id. Returns: result

list_itemsB

Search or list elabFTW database items (resources). Returns a structured response with paging metadata, applied filters, and result records. Check paging.returned_count vs paging.limit to detect further pages. Use the dedicated filter arguments for known IDs or tags instead of q. Always safe to call; never writes anything.

Inputs: q, sort, tags, limit, +7 more. Returns: result

list_item_statusesA

List all item statuses (resource statuses) for the active eLabFTW team. Specify team_id='current' (default) or a numeric team ID. Read-only.

Inputs: team_id. Returns: result

list_item_typesA

List elabFTW item types (/items_types), also called resource categories. They define the category structure and default content for database items and are DIFFERENT from experiment templates (use list_experiment_templates). Use get_item_type for a single one. Always safe to call; never writes anything.

Inputs: q, sort, tags, limit, +3 more. Returns: result

list_stepsA

List all steps (tasks) attached to an eLabFTW experiment, item, experiment template or item type. Returns entity_type, entity_id, total_steps and a steps array (id, body, finished, ordering, deadline). With render_markdown=True a 'markdown' field with a GFM checkbox task list and deep links to each step is added. Read-only.

Inputs: id (required), entity_type (required), render_markdown. Returns: result

refresh_team_capsA

Refresh and return the cached team capability flags for the current session. Fetches the team record, sanitizes the capability fields used for policy checks, updates the cache and returns the sanitized team_caps object. Does not write to eLabFTW resources.

Inputs: no named parameters. Returns: result

resolve_entity_by_queryA

Search experiments or items by query and return compact candidates for disambiguation. Read-only, always available. Returns status, entity_type, resolution (not_found | unique | ambiguous) and the candidate list — use it to find an id before acting on it.

Inputs: entity_type (required), q, tags, scope, +6 more. Returns: result

review_experimentA

Generate an AI review summary for an eLabFTW experiment (documentation quality, risks, suggestions, completeness score). Read-only — writes nothing. Needs an AI endpoint; without one the response carries the upstream placeholder marker.

Inputs: id (required). Returns: result

suggest_metadataA

Suggest structured metadata (custom fields) for an eLabFTW experiment or item. Read-only — writes nothing; feed the result into update_entity_metadata to persist it.

Inputs: id (required), entity_type (required). Returns: result

suggest_tagsA

Suggest tags for an eLabFTW experiment or item based on its content. Read-only — writes nothing; use apply_tag_suggestions to persist them.

Inputs: id (required), entity_type (required). Returns: result

toggle_stepA

Mark a step as complete or incomplete. WRITE OPERATION — requires the step write scope. finished=1 marks it complete, finished=0 incomplete; when omitted the server-side finish action flips the current state.

Inputs: id (required), step_id (required), entity_type (required), finished. Returns: result

update_entity_fieldsA

Update top-level scalar fields (title, status, category) on an existing eLabFTW experiment or item. WRITE OPERATION — requires the update write scope. At least one of title, status_id, category_id must be given. Returns the applied fields and a read-back verification.

Inputs: id (required), entity_type (required), title, trace_id, +2 more. Returns: result

update_entity_metadataA

Update the metadata (custom fields) of an eLabFTW experiment, item, experiment template or item type. WRITE OPERATION — requires the update write scope. 'fields' is a JSON string with an array of field objects ({key, value | values, type, group_id, allow_multi_values, description}); 'groups' optionally defines extra field groups ([{id, name}]). mode='merge' (default) keeps the existing metadata, 'overwrite' rebuilds it from the supplied fields. Returns the resulting field names and a read-back verification.

Inputs: id (required), fields (required), entity_type (required), mode, +2 more. Returns: result

update_experiment_bodyA

Update the body of an existing eLabFTW experiment. WRITE OPERATION — requires the update write scope. The existing content_type (HTML=1 / Markdown=2) is preserved. mode='append' (default) keeps the original body byte-for-byte and appends the new text; mode='overwrite' replaces it and needs both the config flag allow_body_overwrite and confirm_overwrite=true. The result reports a read-back verification of the change.

Inputs: id (required), text (required), mode, trace_id, +1 more. Returns: result

update_item_bodyA

Update the body of an existing eLabFTW database item. WRITE OPERATION — requires the update write scope. mode='append' (default) preserves the original body and appends; mode='overwrite' replaces it and needs allow_body_overwrite plus confirm_overwrite=true.

Inputs: id (required), text (required), mode, trace_id, +1 more. Returns: result

update_stepA

Update an existing step: body and/or deadline. WRITE OPERATION — requires the step write scope. At least one of body, deadline, deadline_notif must be provided; deadline_notif is reported as unsupported when the API rejects it (see notes).

Inputs: id (required), step_id (required), entity_type (required), body, +2 more. Returns: result

upload_image_from_contentA

Upload a single image attachment to an eLabFTW experiment or item from base64-encoded content. WRITE OPERATION — requires the update write scope. Only image/png, image/jpeg, image/gif and image/tiff are accepted. Returns the new upload id and size.

Inputs: id (required), filename (required), entity_type (required), content_base64 (required), +2 more. Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.5/5.0

    Scored across 41 tools

    Disambiguation4/5

    Most tools target a distinct resource+action and descriptions clarify boundaries well. However, the link family (ensure_link, ensure_link_by_query, bulk_ensure_links, delete_link, bulk_delete_links) and the info family (get_connection_info, get_current_user_capabilities, refresh_team_caps) have overlapping purposes that could cause misselection, and resolve_entity_by_query overlaps with the list_* searches.

    Naming Consistency4/5

    The dominant pattern is consistent verb_noun snake_case (get_item, list_experiments, create_experiment, update_step, delete_link, bulk_ensure_links). A few names deviate slightly (review_experiment, refresh_team_caps, expand_links_network) but remain readable and predictable.

    Tool Count2/5

    41 tools is heavy for a single MCP server and pushes well past the comfortable 3-15 range. While the eLabFTW domain is broad, the count is high enough to burden tool selection and discovery.

    Completeness3/5

    Strong read/link/step/update coverage, but notable lifecycle gaps: no delete for experiments, items, uploads, templates, or metadata. Read+create+update are covered while destructive/cleanup operations are largely absent, and there is no direct tag-list or item-type management.

    Maintenance

    ActivityNo data
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers