Skip to main content

Galaxy MCP Server - Python Implementation

This is the Python implementation of the Galaxy MCP server, providing a Model Context Protocol server for interacting with Galaxy instances.

Features

  • Complete Galaxy API integration through BioBlend
  • Optional OAuth login flow for HTTP deployments
  • Intergalactic Workflow Commission (IWC) integration
  • FastMCP 3 server with remote deployment support
  • Session-aware tool visibility: every tool is tagged, and a middleware can hide tags a session cannot use (see Filtering the catalog by tag)
  • Type-annotated Python codebase

Requirements

  • Python 3.10+
  • FastMCP 3.1+

Installation

# Install from PyPI
pip install galaxy-mcp

# Or using uv (recommended)
uvx galaxy-mcp

From Source

# Clone the repository
git clone https://github.com/galaxyproject/galaxy-mcp.git
cd galaxy-mcp/python

# Install with uv (recommended)
uv sync --all-extras

Configuration

At minimum the server needs to know which Galaxy instance to target:

export GALAXY_URL="https://usegalaxy.org.au/"

How you authenticate depends on your transport:

  • Stdio / long-lived sessions – provide an API key:

    export GALAXY_API_KEY="your-api-key"
    
  • HTTP / OAuth – configure the public URL that users reach and a signing secret for session tokens. The server mints short-lived Galaxy API keys on behalf of each user.

    GALAXY_MCP_SESSION_SECRET is required whenever GALAXY_MCP_PUBLIC_URL is set; the server refuses to start without it. Generate it once with openssl rand -hex 32 and store it — the same value must be used on every restart and every replica, since it is the key that encrypts session tokens. A fresh secret invalidates every token issued under the old one.

    export GALAXY_MCP_PUBLIC_URL="https://mcp.example.com"
    export GALAXY_MCP_SESSION_SECRET="<your stored secret>"
    

    Optionally set GALAXY_MCP_CLIENT_REGISTRY to control where OAuth client registrations are stored.

    For non-OAuth HTTP clients, connect(url=..., api_key=...) stores Galaxy credentials per MCP session rather than globally (the URL has to be one the operator allowed -- see below), so sessions don't see each other's credentials. That is not authentication, though -- without OAuth, anything that can reach the listener can call every tool. See Serving over HTTP before exposing the server to anyone else.

You can also steer the transport with GALAXY_MCP_TRANSPORT (stdio, streamable-http, or sse). All variables can be placed in a .env file for convenience.

Usage

Quick Start with uvx

# Local stdio transport (no network listener)
uvx galaxy-mcp

# Remote/browser clients with HTTP + OAuth
export GALAXY_URL="https://usegalaxy.org.au/"
export GALAXY_MCP_PUBLIC_URL="https://mcp.example.com"
export GALAXY_MCP_SESSION_SECRET="$(openssl rand -hex 32)"
uvx galaxy-mcp --transport streamable-http --host 0.0.0.0 --port 8000

Installed CLI

pip install galaxy-mcp
galaxy-mcp --transport streamable-http --port 8000   # listens on 127.0.0.1

If --transport is omitted the server defaults to stdio and reads/writes MCP messages via stdin/stdout.

Working from a checkout

uv sync
uv run galaxy-mcp --transport streamable-http --port 8000

Serving over HTTP

Galaxy MCP is first of all a local server for the person running it. Over stdio the caller is whoever launched the process; over HTTP the caller is anything that can reach the socket, so the HTTP transports come with a few guards:

  • Loopback by default. The listener binds to 127.0.0.1 unless you pass --host or set GALAXY_MCP_HOST.
  • No accidental open servers. Binding to a non-loopback address without OAuth is refused. If something else controls access to the listener (a container's published port, an authenticating reverse proxy), say so with --allow-unauthenticated or GALAXY_MCP_ALLOW_UNAUTHENTICATED=1. Remember that any GALAXY_API_KEY in the environment is then usable by every caller.
  • Host and Origin checks. Without OAuth, requests must carry a loopback Host header, and browser requests are only answered for pages served from the same machine. This keeps web pages you happen to have open from driving a local server. Add names with GALAXY_MCP_ALLOWED_HOSTS and GALAXY_MCP_ALLOWED_ORIGINS (comma-separated; * disables the check). With OAuth the bearer token is the gate and any origin is answered unless GALAXY_MCP_ALLOWED_ORIGINS is set.
  • No server-filesystem access. upload_file and download_dataset(file_path=...) take paths on the machine running the server, which is only what you want when that machine is yours. They are disabled over HTTP; use upload_file_from_url and in-memory downloads instead. A trusted single-user deployment can turn them back on with GALAXY_MCP_ALLOW_LOCAL_FILES=1. Never set that on a server other people can reach -- it lets every caller read and write whatever the server process can.
  • Allowed Galaxy destinations. Over HTTP, connect only reaches Galaxies the operator configured: GALAXY_URL, plus any base URLs in GALAXY_MCP_EXTRA_ALLOWED_URLS (comma separated). connect(url=...) has to match one of them exactly -- scheme, host, port and path, with host/scheme case and a trailing slash normalised; no wildcards or prefixes -- so the server cannot be pointed at an arbitrary host. Over HTTP GALAXY_URL is therefore required. Extra destinations need an explicit api_key; the environment's key only goes to GALAXY_URL, and with OAuth enabled the environment's credentials are never lent to a request. Galaxy requests do not follow redirects, and a failed connection reports the HTTP status but not the body the remote host sent. Both URL settings are read at startup, so restart after changing them. stdio is not restricted: there the caller is the operator and can connect to any Galaxy.

For example, to allow two additional Galaxy instances:

GALAXY_URL="https://primary.example/"
GALAXY_MCP_EXTRA_ALLOWED_URLS="https://second.example/,https://third.example/galaxy/"

None of this can see a reverse proxy: a loopback listener behind nginx looks local from here, so a proxied deployment needs OAuth (or authentication at the proxy) just as a directly exposed one does.

In a container the listener has to bind 0.0.0.0 for the published port to work, so running the image over HTTP without OAuth needs the explicit opt-out, and should only be published to loopback:

docker run --rm -it -p 127.0.0.1:8000:8000 \
  -e GALAXY_URL="https://usegalaxy.org/" \
  -e GALAXY_API_KEY="your-api-key" \
  -e GALAXY_MCP_TRANSPORT="streamable-http" \
  -e GALAXY_MCP_ALLOW_UNAUTHENTICATED=1 \
  galaxyproject/galaxy-mcp

See USAGE_EXAMPLES.md for detailed tool usage patterns.

Tool discovery mode (experimental)

Galaxy MCP registers one @mcp.tool per operation. The client fetches that catalog once per session, but it carries every one of those definitions into the model's context on each turn -- expensive for agents that only ever use a handful. --discovery-mode code (also honored via GALAXY_MCP_DISCOVERY_MODE=code) collapses the catalog into three meta-tools:

  • search -- BM25 search over tool names and descriptions
  • get_schema -- fetch the full schema for specific tools
  • run_galaxy_tool -- execute any tool by name
pip install 'galaxy-mcp[code-mode]'   # pulls in pydantic-monty sandbox
galaxy-mcp --discovery-mode code

The code-mode extra installs pydantic-monty, the sandboxed Python interpreter that backs run_galaxy_tool. Without it the server still starts in full mode but raises a clear error if --discovery-mode code is requested.

Default is full, which keeps the existing catalog unchanged. CodeMode is useful when you want to keep the agent's context lean and are willing to trade a few extra turns (search -> schema -> execute) per tool call. It's built on FastMCP's experimental CodeMode transform, so the API may shift before it stabilizes.

The server also ships agent-facing usage guidance via the MCP instructions field (returned during the initial handshake). It explains the typical workflow, the difference between MCP tools and Galaxy tools (e.g. FastQC isn't an MCP tool -- find it via search_tools_by_name), and -- when code mode is active -- how to use run_galaxy_tool and call_tool. Agents that respect the instructions field will read this without you having to prompt them.

Filtering the catalog by tag

Every tool is registered with three tags: a family (connection, user, histories, datasets, jobs, tools, workflows, iwc, pages), an access level (read or write), and a tier (core, extended, or niche). ToolVisibilityMiddleware in middleware.py filters both tools/list and direct calls against those tags, so a hidden tool cannot be invoked by name either.

Two environment variables drive it, each a comma-separated tag list:

export GALAXY_MCP_INCLUDE_TAGS="core,histories"   # show only tools carrying one of these
export GALAXY_MCP_EXCLUDE_TAGS="write"            # then drop any tool carrying one of these

The middleware also hides the admin and user_tools tags from callers that cannot use them, probing the connected Galaxy for is_admin and for enable_unprivileged_tools. Mind the cache scope: the middleware is constructed once when the server module loads, so those answers are cached for the life of the process, not per session -- the admin answer keyed by URL and API key, the user_tools answer by URL alone, and so shared across every key on that server. No tool currently carries either tag, so this half is inert until tools are labelled for it.

The code-mode meta-tools are added by FastMCP's transform and carry no tags at all, so tag filtering and --discovery-mode code do not combine: with GALAXY_MCP_INCLUDE_TAGS set, all three meta-tools are hidden.

Available MCP Tools

Every tool returns a GalaxyResult: data, success and message always, count on most list operations, and pagination on the ones that actually page. Names and behavior are the contract; arguments live in each tool's own description, which is what the MCP client sees.

Version requirements

A few tools need a Galaxy newer than the oldest one this server speaks to. Each declares its own minimum, says so in its description, and is refused before anything is sent -- so a write can never half-succeed against a server that was never going to accept it:

list_pages needs Galaxy 26.1 or newer; this server reports 26.0. Nothing was sent to Galaxy.

The version comes from /api/version, asked once per server. A server that will not answer leaves its version unknown, and an unknown version refuses nothing: the tool is attempted and stands or falls on its own. get_server_info reports what the connected server cannot run in unsupported_tools, and says in version_known whether an empty list means anything.

A tool is gated as a whole, and some of them are gated for part of what they do: the check runs before the arguments are looked at, and answering the wrong thing quietly is worse than refusing. list_pages needs 26.1 because 26.0 has no history filter and no history_id on a page, so it would answer a request for one history's notebooks with every page the user can see and no way to tell which is which; create_page because 26.0 cannot attach a page to a history and answers with a summary that has no editable content in it. get_page is not gated -- 26.0 answers it with the editable markdown, which is what it is for.

Connection and account

  • connect: Point the session at a Galaxy instance and validate the credentials
  • get_server_info: Version, URL, and public configuration of the connected Galaxy, plus the tools it is too old to run
  • get_user: The authenticated user

Histories

  • get_histories: List histories, optionally filtered by name
  • list_history_ids: A compact id-and-name list, for when you just need an id; 100 per page
  • get_history_details: One history's metadata and item counts, without its contents
  • get_history_contents: The datasets and collections inside a history
  • create_history: Create a history
  • update_history: Rename, annotate, tag, delete, or publish a history

Datasets, collections, and jobs

  • get_dataset_details: Dataset metadata, optionally with a short content preview
  • get_collection_details: A dataset collection and its elements
  • get_job_details: The job that produced a dataset, with its state and parameters
  • upload_file: Upload a local file into a history
  • upload_file_from_url: Have Galaxy fetch a file from a URL into a history
  • download_dataset: Download a dataset to a file_path on disk

Galaxy tools

  • search_tools_by_name: Substring search over tool name, id, and description; 25 per page
  • search_tools_by_keywords: Match keywords against tool names, descriptions, and the file extensions a tool accepts as input; 50 per page
  • get_tool_details: A tool's metadata, optionally including its full input schema
  • get_tool_panel: Browse the tool panel one level at a time -- the sections and their sizes, or one section's tools with section_id. The whole panel is megabytes on a production server, so it is never returned whole; 100 entries per page
  • get_tool_citations: How to cite a tool
  • get_tool_run_examples: The tool's own XML test definitions, as written -- good for seeing how inputs are shaped, but some expect failure and their input files are test fixtures
  • get_tool_input_template: A ready-to-fill inputs skeleton plus a compact schema; call this before run_tool when the shape is unclear
  • run_tool: Run a Galaxy tool in a history
  • recommend_biocontainer: Resolve a verified quay.io/biocontainers image for a set of conda packages. Registered only when the container-recommend extra is installed

User-defined tools

User-defined tools are unprivileged, containerized tools a user creates themselves. They are addressed by UUID rather than by tool id, and run_user_tool resolves that UUID before submitting the run, so they need their own run tool even though the run itself goes through the same Galaxy tools API as a catalog tool.

  • list_user_tools: The current user's user-defined tools; 25 per page, because each entry carries the tool's full representation
  • create_user_tool: Create one from a tool representation
  • delete_user_tool: Deactivate one, so it stops loading into the toolbox
  • run_user_tool: Run one in a history

Workflows and invocations

  • list_workflows: Stored workflows, optionally filtered by name or published state; 50 per page
  • get_workflow_details: One workflow's steps and inputs, at a given version
  • get_workflow_input_template: A ready-to-fill input template plus a run guide; call this before invoke_workflow
  • invoke_workflow: Run a workflow, validating the inputs against its steps first
  • get_invocations: Invocations, by invocation, workflow, or history
  • cancel_workflow_invocation: Cancel a running invocation

IWC catalog

The Intergalactic Workflow Commission publishes a curated, versioned workflow catalog. These tools read that public manifest; only the import touches your Galaxy.

  • get_iwc_workflows: The IWC manifest, 20 workflows per page
  • search_iwc_workflows: Substring search across the catalog; 20 per page
  • recommend_iwc_workflows: Rank catalog workflows against a free-text description of what you want to do; the best 5 by default, up to 25
  • get_iwc_workflow_details: Everything about one workflow before you commit to importing it
  • import_workflow_from_iwc: Import a catalog workflow into the connected Galaxy

Pages

Pages are Galaxy-flavored markdown documents. One attached to a history is a Notebook; a standalone one is a Report. Embedded datasets are referenced by encoded id. Pages written as HTML in the Galaxy UI are the exception: their body is in content, which get_page only returns with include_rendered, and they stay HTML when updated, so don't send markdown to one.

All of these need Galaxy 26.1 or newer except get_page, which 26.0 answers in full.

  • list_pages: Pages, optionally filtered by history or search term
  • get_page: A page and its latest revision, editable markdown and optionally rendered
  • create_page: Create a page
  • update_page: Update a page; a content change records a new revision
  • list_page_revisions: A page's revision history
  • get_page_revision: One revision's editable content, and content_editor_source saying whether that came from the server or from the export-expanded content standing in for it
  • revert_page_revision: Restore an earlier revision as a new one

Testing

The suite is mock-based: it stands up a GalaxyInstance mock and never talks to a real server, so it runs anywhere in seconds. Test dependencies come from the dev extra, which uv sync --all-extras already installs.

# Run everything
uv run pytest

# One file
uv run pytest tests/test_history_operations.py

# Coverage has to be asked for -- see the config note below
uv run pytest --cov=galaxy_mcp --cov-report=term-missing

# Type checking is a separate gate -- neither pytest nor pre-commit runs it
uv run mypy src/galaxy_mcp

Two pytest configs exist and pytest.ini wins, which pytest says out loud on every run (ignoring pytest config in pyproject.toml). The addopts in pyproject.toml therefore never apply, and the ones in pytest.ini sit under a [tool:pytest] header that pytest.ini does not read. A bare uv run pytest gets no -v and no coverage; pass the flags yourself, as CI does.

How the tests are put together

  • conftest.py builds the shared mock_galaxy_instance fixture and resets the server's module-level connection and cache state between tests.
  • test_helpers.py unwraps the registered tools back to plain functions, so most tests call a tool directly and assert on the GalaxyResult it returns.
  • test_job_operations.py mocks at the HTTP layer with responses instead, where the code under test goes around BioBlend.
  • tests/mcp_session.py drives the tools through an in-memory FastMCP client. Only the live suite uses it today. That path matters because a tool called without an MCP request context has no session id, and the session-scoped connection store then has nowhere to put the client -- which is what made the live suite look broken for months.

The live suite

tests/test_real_integration.py runs against a real Galaxy and is skipped by default. It needs GALAXY_TEST_API_KEY set to a valid key and GALAXY_TEST_URL (default http://localhost:8080) answering on /api/version; if either is missing the whole module is skipped. A green pytest run therefore says nothing about these tests -- check the skip count before reading it as coverage.

export GALAXY_TEST_URL="http://localhost:8080"
export GALAXY_TEST_API_KEY="a-real-key"
uv run pytest tests/test_real_integration.py

Development

Code Style Guidelines

  • Use Python 3.10+ features
  • Employ type hints where appropriate
  • Follow PEP 8 style guidelines
  • Use ruff for code formatting and linting
  • All code should pass type checking with mypy

Development Setup

# Install every dependency, including the dev and optional extras
make install

# Set up pre-commit hooks (required for contributing)
uv run pre-commit install

Pre-commit hooks will automatically format your code and run linting checks when you commit. All contributors should install these hooks to maintain consistent code quality.

Development Commands

We use a Makefile for consistent development commands:

# Show all available commands
make help

# Install dependencies
make install       # Install all dependencies

# Code quality
make lint          # Run the pre-commit hooks (formatting and lint -- no type checking)

# Testing
make test          # Type-check with mypy, then run tests with coverage

# Building
make clean         # Clean build artifacts
make build         # Build distribution packages

# Running
make run           # Run the MCP server
make dev           # Run the FastMCP dev inspector

Using uv directly

All commands can also be run directly with uv:

# Install dependencies
uv sync --all-extras

# Format and lint code
uv run pre-commit run --all-files

# Type check
uv run mypy src/galaxy_mcp

# Run tests with coverage
uv run pytest --cov=galaxy_mcp --cov-report=html

# Update dependencies
uv lock --upgrade

Cross-version Testing

Test across multiple Python versions using tox:

# Test on all supported Python versions
uv run tox

# Test on specific version
uv run tox -e py312

# Run only linting
uv run tox -e lint

# Run type checking
uv run tox -e type

Pre-commit Hooks

The project uses pre-commit hooks for automatic code quality checks:

# Install pre-commit hooks (one-time setup)
uv run pre-commit install

# Run pre-commit manually on all files
uv run pre-commit run --all-files

# Skip pre-commit for a single commit (not recommended)
git commit --no-verify

Pre-commit runs automatically on git commit and includes:

  • Code formatting with ruff
  • Linting with ruff
  • Trailing whitespace removal
  • File cleanup (EOF, YAML/JSON/TOML validation)
  • Large file detection
  • Merge conflict detection

It does not type-check. CI runs mypy src/galaxy_mcp as its own job, so run that yourself before pushing.

License

MIT

Metadata

Release files for galaxy-mcp 1.11.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for galaxy-mcp 1.11.2
File Size Uploaded
galaxy_mcp-1.11.2.tar.gz 287.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for galaxy-mcp 1.11.2
File Interpreter ABI Platform
galaxy_mcp-1.11.2-py3-none-any.whl Python 3 none any Details

Total release size: 404.2 kB

Release files / galaxy_mcp-1.11.2.tar.gz

Download URL galaxy_mcp-1.11.2.tar.gz
Size 287.8 kB
Tags Source
SHA-256 checksum
How to use checksums
fe99934960a5a86642e31053d189391fedfe42b61fec184ecafa89368f1edae7
BLAKE2b-256 checksum
How to use checksums
7fcff13eb7c4e1bfe44876e01545aae2bb6ba3357830712beebdf3f1cb72949c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.15

Release files / galaxy_mcp-1.11.2-py3-none-any.whl

Download URL galaxy_mcp-1.11.2-py3-none-any.whl
Size 116.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6031ca92fadd028be65839f7b63eb342eea93c0e8eca76ac2bc7ffabf7cd8ab9
BLAKE2b-256 checksum
How to use checksums
60579ce412ff11d409b18463a85390359b598f5abfc049fb4a54c42f7431b2ce
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.15

Release history Release notifications | RSS feed

This release

1.11.2 This release

2 release files

1.10.0

2 release files

1.9.0

2 release files

1.8.0

2 release files

1.7.0

2 release files

1.6.0

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.2.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page