Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ DOCKSTORE_MCP_LOG_LEVEL=INFO

# The Dockstore instance whose APIs this server exposes.
DOCKSTORE_MCP_DOCKSTORE_URL=https://dockstore.org

# Git tag or ref reported in the User-Agent sent to Dockstore. Normally set at
# build time; left unset, the package version is used.
# DOCKSTORE_MCP_GIT_REF=
1 change: 1 addition & 0 deletions .github/workflows/deploy_tagged.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
context: .
push: true
tags: quay.io/dockstore/dockstore-mcp:${{ steps.ref.outputs.sanitized }}
build-args: GIT_REF=${{ steps.ref.outputs.sanitized }}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This supports the ability to tag the user agent with the version (tag or branch) that this repo is on


- name: Create checksums
run: |
Expand Down
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ RUN pip install .
# ---- runtime --------------------------------------------------------------
FROM python:3.13-slim-bookworm

# Git tag or ref the image is built from, reported in the User-Agent sent to
# Dockstore. Left empty, the server falls back to the package version.
ARG GIT_REF=""

LABEL org.opencontainers.image.title="dockstore-mcp" \
org.opencontainers.image.description="Model Context Protocol server for Dockstore" \
org.opencontainers.image.url="https://dockstore.org" \
Expand All @@ -54,7 +58,8 @@ ENV PATH="/opt/venv/bin:${PATH}" \
DOCKSTORE_MCP_HOST=0.0.0.0 \
DOCKSTORE_MCP_PORT=8000 \
DOCKSTORE_MCP_PATH=/mcp \
DOCKSTORE_MCP_DOCKSTORE_URL=https://dockstore.org
DOCKSTORE_MCP_DOCKSTORE_URL=https://dockstore.org \
DOCKSTORE_MCP_GIT_REF=${GIT_REF}

USER dockstore
WORKDIR /home/dockstore
Expand Down
8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ VENV ?= .venv
PY ?= $(VENV)/bin/python
PIP ?= $(VENV)/bin/pip
IMAGE ?= dockstore/dockstore-mcp:local
# Reported in the User-Agent the server sends to Dockstore.
GIT_REF ?= $(shell git describe --tags --always 2>/dev/null)

help: ## Show this help
@grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
Expand Down Expand Up @@ -35,13 +37,13 @@ typecheck: ## Run the type checker
check: lint typecheck test ## Everything CI runs

run: ## Run the server over stdio
$(VENV)/bin/dockstore-mcp
DOCKSTORE_MCP_GIT_REF=$(GIT_REF) $(VENV)/bin/dockstore-mcp

run-http: ## Run the server over HTTP on port 8000
$(VENV)/bin/dockstore-mcp --transport http --port 8000
DOCKSTORE_MCP_GIT_REF=$(GIT_REF) $(VENV)/bin/dockstore-mcp --transport http --port 8000

docker-build: ## Build the container image
docker build -t $(IMAGE) .
docker build --build-arg GIT_REF=$(GIT_REF) -t $(IMAGE) .

docker-run: ## Run the container image on port 8000
docker run --rm -p 8000:8000 $(IMAGE)
Expand Down
64 changes: 41 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ MCP, so it is deployed alongside the Dockstore webservice rather than inside it.

It is built on [FastMCP](https://gofastmcp.com) 4 and ships as a container image.

> **Status: scaffold.** `hello`, `get_trs_info`, and `list_tool_classes` have working
> bodies; the other four Dockstore tools are declared — names, arguments, and response
> shapes — but each one raises `NotImplementedError` until it is wired up to the
> Dockstore API.
> **Status: scaffold.** `hello` and the GA4GH TRS tools (`get_trs_info` through
> `get_tool_containerfile`) have working bodies; the other four Dockstore tools are
> declared — names, arguments, and response shapes — but each one raises
> `NotImplementedError` until it is wired up to the Dockstore API.

## Requirements

Expand Down Expand Up @@ -116,29 +116,47 @@ over the environment.
| `DOCKSTORE_MCP_PATH` | `--path` | `/mcp` | Path the MCP endpoint is served from |
| `DOCKSTORE_MCP_LOG_LEVEL` | `--log-level` | `INFO` | Logging verbosity |
| `DOCKSTORE_MCP_DOCKSTORE_URL` | `--dockstore-url` | `https://dockstore.org` | Dockstore instance whose APIs are exposed |
| `DOCKSTORE_MCP_GIT_REF` | | package version | Version in the `dockstore-mcp/<ref>` User-Agent |

The container image overrides the first four so that it listens on `0.0.0.0:8000` out of
the box. It also sets a few `FASTMCP_*` variables so that a deployed server logs plainly
and does not check PyPI for updates on startup; see the
[FastMCP settings](https://gofastmcp.com) for the full list.
the box, and sets `DOCKSTORE_MCP_GIT_REF` from its `GIT_REF` build argument, which the
release workflow and `make docker-build` fill in with the git tag or ref being built. It
also sets a few `FASTMCP_*` variables so that a deployed server logs plainly and does not
check PyPI for updates on startup; see the [FastMCP settings](https://gofastmcp.com) for
the full list.

## Tools

| Tool | Description |
| ------------------- | ------------------------------------------------------------------------------------- |
| `hello` | Greets the caller and reports the Dockstore instance and server version. No I/O. |
| `get_trs_info` | Describes this instance's GA4GH TRS API: identifiers, version, and operator. |
| `list_tool_classes` | Lists the tool classes (e.g. `Workflow`) this instance's TRS API sorts entries into. |
| `search_entries` | Searches entries by keyword and facet, the equivalent of the site's Search page. |
| `get_entry` | Retrieves the requested fields of one entry. |
| `get_version` | Retrieves the requested fields of one version of an entry. |
| `get_file` | Retrieves the requested fields of one file belonging to a version. |

`get_trs_info` and `list_tool_classes` call Dockstore's GA4GH TRS V2 API directly. The
last four are scaffolding and are not implemented yet. They are a chain: `search_entries`
yields entry identifiers, an entry yields version identifiers, and a version yields file
paths. Each lookup takes a list of fields so that a caller can ask for a name and a date
without also pulling down a README or a whole descriptor.
| Tool | Description |
| ----------------------------- | ------------------------------------------------------------------------------------ |
| `hello` | Greets the caller and reports the Dockstore instance and server version. No I/O. |
| `get_trs_info` | Describes this instance's GA4GH TRS API: identifiers, version, and operator. |
| `list_tool_classes` | Lists the tool classes (e.g. `Workflow`) this instance's TRS API sorts entries into. |
| `list_tools` | Lists one page of every tool and workflow the TRS API serves, with the total count. |
| `search_tools` | Finds TRS tools by name, organization, author, class, descriptor language, etc. |
| `get_tool` | Retrieves one TRS tool by id, including all of its versions. |
| `list_tool_versions` | Lists every version of one TRS tool. |
| `get_tool_version` | Retrieves one version of a TRS tool: authors, images, descriptor languages. |
| `get_tool_descriptor` | Fetches the primary descriptor (CWL, WDL, etc., or a notebook) of a version. |
| `get_tool_descriptor_by_path` | Fetches one of a version's files by its relative path. |
| `get_tool_files` | Lists every file of a version, without content. |
| `get_tool_tests` | Fetches a version's test parameter files. |
| `get_tool_containerfile` | Fetches the containerfile (e.g. Dockerfile) that builds a version's image. |
| `search_entries` | Searches entries by keyword and facet, the equivalent of the site's Search page. |
| `get_entry` | Retrieves the requested fields of one entry. |
| `get_version` | Retrieves the requested fields of one version of an entry. |
| `get_file` | Retrieves the requested fields of one file belonging to a version. |

The TRS tools, from `get_trs_info` to `get_tool_containerfile`, call Dockstore's GA4GH
TRS V2 API directly. They form a chain: `list_tools` and `search_tools` yield tool ids, a
tool yields version names, and a version's `get_tool_files` yields the paths that
`get_tool_descriptor_by_path` takes. Pass `summary` to `list_tools` or `search_tools` to get
each tool's id, languages, and version names without its full README and version details.

The last four are scaffolding and are not implemented yet. They are a chain too:
`search_entries` yields entry identifiers, an entry yields version identifiers, and a
version yields file paths. Each lookup takes a list of fields so that a caller can ask
for a name and a date without also pulling down a README or a whole descriptor.

## Layout

Expand All @@ -154,7 +172,7 @@ src/dockstore_mcp/
├── entries.py get_entry, get_version, get_file
├── hello.py the hello tool
├── search.py search_entries
└── trs.py get_trs_info, list_tool_classes
└── trs.py get_trs_info, list_tool_classes, and the other GA4GH TRS tools
tests/ pytest suite, using FastMCP's in-memory client
Dockerfile two-stage build of the deployable image
```
Expand Down
15 changes: 15 additions & 0 deletions src/dockstore_mcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

from dockstore_mcp import __version__

Transport = Literal["stdio", "http"]
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]

Expand Down Expand Up @@ -52,6 +54,14 @@ class Settings(BaseSettings):
description="Base URL of the Dockstore instance whose APIs this server exposes.",
)

git_ref: str | None = Field(
default=None,
description=(
"Git tag or ref this server was built from, e.g. '1.21.0'; set at build time. "
"Falls back to the package version."
),
)

@field_validator("dockstore_url")
@classmethod
def _strip_trailing_slash(cls, value: str) -> str:
Expand All @@ -74,6 +84,11 @@ def api_url(self) -> str:
"""Base URL of the instance's proprietary Dockstore API."""
return f"{self.dockstore_url}/api"

@property
def user_agent(self) -> str:
"""User-Agent sent with every request to Dockstore, e.g. 'dockstore-mcp/1.21.0'."""
return f"dockstore-mcp/{self.git_ref or __version__}"


@lru_cache(maxsize=1)
def get_settings() -> Settings:
Expand Down
132 changes: 132 additions & 0 deletions src/dockstore_mcp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,27 @@
from pydantic import BaseModel, Field

__all__ = [
"Checksum",
"DescriptorLanguage",
"Entry",
"EntryField",
"EntrySummary",
"EntryType",
"File",
"FileField",
"FileWrapper",
"ImageData",
"ServiceOrganization",
"ServiceType",
"SortBy",
"SortOrder",
"Tool",
"ToolClass",
"ToolFile",
"ToolPage",
"ToolSummary",
"ToolVersion",
"TrsDescriptorType",
"TrsInfo",
"Version",
"VersionField",
Expand Down Expand Up @@ -216,6 +225,129 @@ class ToolClass(BaseModel):
description: str | None = Field(default=None, description="Longer explanation of what this class is.")


class TrsDescriptorType(StrEnum):
"""The descriptor languages the GA4GH TRS API addresses files by, spelled as its URLs expect."""

CWL = "CWL"
WDL = "WDL"
NEXTFLOW = "NFL"
GALAXY = "GALAXY"
SNAKEMAKE = "SMK"
JUPYTER = "JUPYTER"
SERVICE = "SERVICE"


class Checksum(BaseModel):
"""A checksum of a file or container image."""

checksum: str | None = Field(default=None, description="The hex-encoded checksum value.")
type: str | None = Field(default=None, description="Hash algorithm used, for example 'sha-256'.")


class ImageData(BaseModel):
"""A container image a TRS tool version runs in."""

registry_host: str | None = Field(default=None, description="Registry hosting the image, e.g. 'quay.io'.")
image_name: str | None = Field(default=None, description="Name of the image, including its registry and tag.")
size: int | None = Field(default=None, description="Size of the image in bytes.")
updated: str | None = Field(default=None, description="When the image was last updated.")
checksum: list[Checksum] | None = Field(default=None, description="Checksums of the image.")
image_type: str | None = Field(default=None, description="Container technology, for example 'Docker'.")


class ToolVersion(BaseModel):
"""One version of a GA4GH TRS tool, for example a Git branch or tag."""

id: str | None = Field(default=None, description="TRS identifier of this version, '<tool id>:<version name>'.")
name: str | None = Field(default=None, description="Version name; pass this as version_id to the version tools.")
url: str | None = Field(default=None, description="TRS API URL of this version.")
author: list[str] | None = Field(default=None, description="Authors of this version.")
is_production: bool | None = Field(default=None, description="Whether the version is marked production-ready.")
images: list[ImageData] | None = Field(default=None, description="Container images this version runs in.")
descriptor_type: list[str] | None = Field(
default=None, description="Descriptor languages this version is available in, for example ['CWL']."
)
descriptor_type_version: dict[str, list[str]] | None = Field(
default=None, description="Language versions used, keyed by descriptor type, e.g. {'WDL': ['1.0']}."
)
containerfile: bool | None = Field(default=None, description="Whether a containerfile (e.g. Dockerfile) exists.")
meta_version: str | None = Field(default=None, description="Revision of this version's metadata.")
verified: bool | None = Field(default=None, description="Whether this version has been verified.")
verified_source: list[str] | None = Field(default=None, description="Who or what verified this version.")
signed: bool | None = Field(default=None, description="Whether this version is signed.")
included_apps: list[str] | None = Field(default=None, description="Apps bundled with this version.")


class Tool(BaseModel):
"""A GA4GH TRS tool: a Dockstore tool, workflow, or other entry as the TRS API describes it."""

id: str | None = Field(default=None, description="TRS identifier of the tool; pass this as tool_id.")
url: str | None = Field(default=None, description="TRS API URL of the tool.")
aliases: list[str] | None = Field(default=None, description="Other identifiers the tool is known by.")
organization: str | None = Field(default=None, description="Organization that published the tool.")
name: str | None = Field(default=None, description="Name of the tool.")
toolclass: ToolClass | None = Field(default=None, description="Category of the tool, e.g. 'Workflow'.")
description: str | None = Field(default=None, description="Description of the tool, usually its README.")
meta_version: str | None = Field(default=None, description="Revision of this tool's metadata.")
has_checker: bool | None = Field(default=None, description="Whether the tool has a checker workflow.")
checker_url: str | None = Field(default=None, description="TRS URL of the checker workflow, if any.")
versions: list[ToolVersion] | None = Field(default=None, description="Every version of the tool.")


class ToolSummary(BaseModel):
"""The handful of fields that identify a TRS tool in a list, without its README or version details."""

id: str | None = Field(default=None, description="TRS identifier of the tool; pass this as tool_id.")
name: str | None = Field(default=None, description="Name of the tool.")
organization: str | None = Field(default=None, description="Organization that published the tool.")
tool_class: str | None = Field(default=None, description="Category of the tool, e.g. 'Workflow'.")
descriptor_types: list[str] = Field(
default_factory=list, description="Every descriptor language any of its versions is available in."
)
version_names: list[str] = Field(
default_factory=list, description="Name of each version; pass one as version_id to the version tools."
)
description: str | None = Field(default=None, description="The start of the tool's description, shortened.")


class ToolPage(BaseModel):
"""One page of TRS tools, with enough context to fetch the rest."""

tools: list[Tool] | list[ToolSummary] = Field(
description="The tools on this page: in full, or as summaries if they were asked for."
)
offset: int = Field(description="Which page this is, counting from 0.")
limit: int = Field(description="Most tools a page holds.")
total: int | None = Field(
default=None, description="How many tools there are across every page, if Dockstore reported it."
)
next_offset: int | None = Field(
default=None, description="Offset of the next page; unset when this is the last page."
)


class FileWrapper(BaseModel):
"""The content of one file from a TRS tool version: a descriptor, test parameter file, or containerfile."""

content: str | None = Field(default=None, description="The file's full text.")
checksum: list[Checksum] | None = Field(default=None, description="Checksums of the file.")
url: str | None = Field(default=None, description="Where the raw file can be fetched from.")


class ToolFile(BaseModel):
"""One entry in the file listing of a TRS tool version."""

path: str | None = Field(
default=None,
description="Path relative to the primary descriptor; pass this to get_tool_descriptor_by_path.",
)
file_type: str | None = Field(
default=None,
description="One of TEST_FILE, PRIMARY_DESCRIPTOR, SECONDARY_DESCRIPTOR, CONTAINERFILE, or OTHER.",
)
checksum: Checksum | None = Field(default=None, description="Checksum of the file.")


class EntryField(StrEnum):
"""Fields of an :class:`Entry` that get_entry can return."""

Expand Down
Loading
Loading