MCP Servers¶
The agent reaches the outside world through tools. One set is built in; anything else, a page reader for example, comes from an MCP server you register. This page says what is built in, where your own servers are stored, how to carry them into a container, and the entry that adds a URL fetcher.
What is built in¶
The built-in server is always on and needs no configuration. It carries the search tools (search_geo_sra, search_pubmed, search_chembl, search_clinical_trials, web_search_perplexity, web_search_duckduckgo) and the engine tools (validate_pipeline, lint_nextflow, corpus_lookup, check_run_readiness, analyze_run_log, list_compliance_rules, build_container_image). build_container_image and lint_nextflow are off until you switch them on in the Tools panel, like the other domain tools; Container builds with Wave says what the first may do. The agent adds web_research and fetch_test_data itself; they are not MCP tools, so the Tools panel does not list them.
There is no tool that reads an arbitrary URL, and that is deliberate. The search tools return links, and web_research reads pages for a research turn. To let the agent open a specific page (a tool's documentation, an ENA report), register the fetch server below. Until then the Tools panel says that no URL fetcher is connected.
Where your servers are stored¶
Servers you register are stored in one file in the state directory:
GET /api/mcp/status reports the exact path on the running backend (servers_file), whether the file is there (servers_file_exists), and a one-line storage_note that the Tools panel shows. The file is not part of the image and is not created until you add a server.
The file is a JSON list, one object per server (it is not the {"mcpServers": {...}} shape the add dialog accepts when you paste a config; the dialog converts that for you):
| Field | Meaning |
|---|---|
name | Lowercase letters, digits, _ and -, starting with a letter, 32 characters at most. |
command, args, env | A stdio server: the program to start, its arguments, extra environment variables. |
url | A streamable-HTTP server (http:// or https://) instead of a command. Give exactly one of command or url. |
headers_env | A url server only: header name to the name of a backend environment variable that holds its value, for example {"Authorization": "TOWER_ACCESS_TOKEN"}. The value is read when the server connects and is never stored in this file, logged, or shown. A bare token in Authorization is sent as Bearer <token>. Only the variables the operator listed in CODARIS_MCP_HEADER_VARS may be named (default TOWER_ACCESS_TOKEN and SEQERA_ACCESS_TOKEN); a provider key, PATH, HOME or a CODARIS_ setting is refused whatever the list says, and an entry that names one is skipped with a warning when the file is read. If the variable is not set, the server stays listed as not connected and the Tools panel names the variable. |
prefix | Namespace for the server's tools (fetch__fetch). Defaults to name. |
allowlist | Admit only these tools from the server. |
max_output_chars | Truncate a tool result longer than this, with a note. 0 means no cap. |
keywords | Words search_tools matches on when the agent looks for one of this server's tools, which cost the model no tokens. |
At start-up Codaris merges three sources in order: the built-in server, the operator's CODARIS_MCP_SERVERS variable, then this file. A later entry whose name or prefix clashes with an earlier one is skipped with a warning in the backend log. After editing the file by hand, restart the backend; adding or removing a server from the Tools panel reloads it without a restart.
The agent cannot edit this file. mcp_servers.json, settings.json, notifications.json, permissions*, grants* and the chat database codaris.db in the state directory are refused to the agent's shell and file tools, hard and with no approval card, however the path is spelled (.., an absolute path, a symlink or hard link); the file decides where credentials are sent, so only you change it: by hand, from the Tools panel (with the one-time code from the backend log), or with the CLI.
Adding servers from the panel¶
Adding and removing from the Tools panel is off by default. A backend started without CODARIS_MCP_UI_ADD=1 answers POST /api/mcp/servers with 403, and the panel says so in one line with the variable to set. With it on, each add also needs a one-time code that the backend writes to its own log (docker logs <container> for a container), because adding a stdio server runs its command on the backend host. Servers already in the file load whether or not adding is on.
Add a URL fetcher¶
The fetch server (mcp-server-fetch) reads a page and returns it as text. There are two entries, one for a host and one for the published image, and the Tools panel names both when no fetcher is registered. On a host, put this in mcp_servers.json:
[
{
"name": "fetch",
"command": "uvx",
"args": ["mcp-server-fetch"],
"keywords": "fetch url web page read http link"
}
]
keywords is what makes a pasted link bring the fetch__fetch tool into the turn. The server refuses private-network addresses unless it is started with --allow-private-ips, and honours robots.txt unless it is started with --ignore-robots-txt; add either to args only if you mean it.
uvx downloads mcp-server-fetch from PyPI the first time it runs, and Codaris gives a server 60 seconds to start. On a host with uv installed that is fine. A container with no outbound network cannot do it, so inside the published image use the mcp-server-fetch command itself, the copy that is already installed there:
[
{
"name": "fetch",
"command": "mcp-server-fetch",
"keywords": "fetch url web page read http link"
}
]
Both entries use a bare command name, which the backend resolves on its own PATH. That is what lets one file work on a host and in the container; see the next section for why an absolute path usually does not.
Add the Seqera MCP server (read-only)¶
Seqera hosts an MCP server at https://mcp.seqera.io/mcp. It searches nf-core modules, conda and PyPI, inspects Wave containers, and finds public sequencing runs and builds samplesheets from them. It is not registered by default: a fresh install has only the built-in server, and nothing here is contacted until you add the entry.
Codaris admits 13 of its tools, all read-only, and drops the rest at load time, before the model ever sees them:
| Admitted | What they do |
|---|---|
search_nfcore_module, describe_nfcore_module, nfcore_suggest_analysis | Search nf-core modules, read one module's interface, suggest modules for an analysis |
seqerahub_search_conda, seqerahub_search_pypi, wave_inspect_container | Find a package, inspect an existing container |
sra_search_ncbi, sra_search_ebi, sra_fetch_geo_study, sra_list_runs_detailed, sra_group_by_assay, sra_fetch_download_urls, sra_create_samples_sheet | Find public runs, group them by assay, list download URLs, write a samplesheet |
Left out: platform_launch_workflow, platform_cancel_workflow, platform_create_*, platform_add_pipeline, call_seqera_api and search_seqera_api (which reach any Platform operation), and wave_get_container. The platform_list_* and platform_get_* tools are read-only too and are left out only to keep the schemas the model reads small.
1. Set the token where the backend runs. Create a Seqera Platform access token and give it to the backend as TOWER_ACCESS_TOKEN: -e TOWER_ACCESS_TOKEN on docker run, or an environment: entry in the Compose file. The server also accepts OAuth, which needs an interactive browser and cannot work in a backend, so a token is the only route here.
2. Add the entry to mcp_servers.json (see Where your servers are stored; the file is the place for it because the Tools panel's add dialog does not offer headers_env, allowlist or keywords), then restart the backend. The same entry ships as backend/app/mcp_examples/seqera.json:
[
{
"name": "seqera",
"prefix": "sq",
"url": "https://mcp.seqera.io/mcp",
"headers_env": {
"Authorization": "TOWER_ACCESS_TOKEN"
},
"max_output_chars": 8000,
"keywords": "nf-core module search describe pipeline wave container conda bioconda pypi SRA ENA GEO samplesheet",
"allowlist": [
"search_nfcore_module",
"describe_nfcore_module",
"nfcore_suggest_analysis",
"seqerahub_search_conda",
"seqerahub_search_pypi",
"wave_inspect_container",
"sra_search_ncbi",
"sra_search_ebi",
"sra_fetch_geo_study",
"sra_list_runs_detailed",
"sra_group_by_assay",
"sra_fetch_download_urls",
"sra_create_samples_sheet"
]
}
]
max_output_chars caps each reply at 8,000 characters; keywords is what lets a search for modules, containers or accessions find these tools: the agent calls search_tools, which ranks on a tool's name, description and its server's keywords, and binds what it finds for its next step. The tools appear in the Tools panel as sq__search_nfcore_module and so on, off until you switch them on.
Caveats that matter:
- The token carries your Platform rights. The allowlist is Codaris's own: it stops the model being offered a launch or cancel tool, but the server accepts any tool from whoever holds the token. Use a token for a user whose Platform role is read-only, not your own administrator token.
- The token never reaches
mcp_servers.json. The file holds only the variable's name (and Codaris writes it with mode 0600 in any case). The backend log showsAuthorization<-TOWER_ACCESS_TOKEN, never a value. It is also the variablebuild_container_imageuses, so setting it for this entry raises Wave's build quota as well. The agent's own shell does not see it: the backend reads it once at start-up into memory and removes it from its own environment (a changed token needs a backend restart), shell-outs run with secret variables removed, the built-in server receives it on a private pipe at start-up and not in its environment, and a command that names it or the Seqera hosts is refused; see what the agent's shell can see. - Queries go to Seqera. Send no unpublished sequence, sample name or patient identifier in a search.
- It is Seqera's service, under Seqera's terms. The server is closed source and carries no licence of its own; Codaris calls it and ships none of it. If it is down or answers 401 (an expired token), only this server shows as not connected, with the reason, and every other tool keeps working; the backend retries it at most once a minute, on the next turn.
Container builds with Wave¶
build_container_image builds a container for a tool that has no nf-core module and returns the image reference and the build id. It calls Seqera Wave (https://wave.seqera.io/v1alpha2/container) and is meant for the last case after corpus_lookup found nothing.
- Input: conda package specs (
samtools=1.20 bwa=0.7.17,bioconda::fastp), a condaenvironment.yml, or a Dockerfile. Channels are limited to conda-forge, bioconda and defaults. An environment file must be a mapping with onlyname,channelsanddependencies; apip:section, YAML anchors and anything that is not a plain conda spec are refused. - Output: a frozen reference such as
community.wave.seqera.io/library/seqtk:1.4--81a414bb80ec8294, returned only after the build has succeeded (about a minute). Wave hands out the reference before the build runs, and a misspelt package gets one too, so the tool waits for the build to complete and saysFAILEDwith the last lines of the log when it did not. An expiringwave.seqera.io/wt/...URL is never returned. If the wait runs out the answer saysBUILDING; asking again with the same input is served from cache and keeps waiting. - Quota: anonymous callers get 25 builds a day. HTTP 429 is reported with that figure and is not retried. With
TOWER_ACCESS_TOKENset the limit is 250 an hour. - Public by default: a build goes to Wave's community registry, so the recipe is public.
Pushing to your own registry is the part a model must not decide. It needs all three of these:
| Gate | Set by | Default |
|---|---|---|
The registry is listed in CODARIS_WAVE_BUILD_REPOSITORIES (docker.io/acme/images, or docker.io/acme/*) | the operator | empty: no custom registry |
A Dockerfile is accepted only with CODARIS_WAVE_DOCKERFILE_BUILDS=1, and then needs TOWER_ACCESS_TOKEN for the registry's Platform credentials | the operator | off |
| A person approves on a card that shows the registry, the platform, the recipe's sha256 and the whole recipe | you, per push | asked every time |
The approval covers exactly the text on the card: it is signed over the recipe, registry and platform, and the build is refused if any of them differs, if the recipe is over 8,000 characters (it would not fit the card), or if it holds a control, bidirectional or zero-width character that would make the card read differently from what is built. With no chat to ask, as when another harness runs the built-in server, a push is refused unless CODARIS_ACTION_GATE=0. A built-in server started without --provision-fd (by another harness or a script, not by Codaris) has no one to hand it credentials and builds from its own environment, whereas one that Codaris started and that did not receive its credentials refuses Wave builds.
Using your servers in a container¶
The published image carries no mcp_servers.json: a fresh docker run starts with the built-in server only, and so does any run whose CODARIS_HOME differs from the one where you registered your servers. Carry the file in one of two ways:
- Mount the state directory. The Compose file mounts
${HOME}/.codarisat the same path inside and outside the container, so it reads the file you already have. Fordocker run, pass-e HOME="${HOME}" -v "${HOME}/.codaris:${HOME}/.codaris"as shown in plaindocker run. - Copy the file in. With the container running,
docker cp mcp_servers.json <container>:/root/.codaris/mcp_servers.json, thendocker restart <container>. The path is/root/.codariswhenHOMEandCODARIS_HOMEare unset.
A stdio command runs inside the container, not on your machine. It has to exist in the image, at the path the file names:
| Present in the published image | Absent |
|---|---|
/usr/local/bin/uvx, /usr/local/bin/uv, /usr/local/bin/mcp-server-fetch, /usr/local/bin/node, python | npx and npm, and anything installed only on your host |
So a file written on a Mac with /opt/homebrew/bin/uvx or /Users/you/.local/bin/uvx fails in the container, and a file copied out of the container with /usr/local/bin/uvx fails on a Mac host. Bare names (uvx, mcp-server-fetch) avoid both. A server whose command cannot be found stays listed in the Tools panel as not connected, with the missing path and the file that names it, and the backend log carries one warning:
MCP server 'fetch' unavailable (FileNotFoundError: Command '/usr/local/bin/uvx' was not found on the backend's PATH. It is set in /Users/you/.codaris/mcp_servers.json.); continuing
A url server needs no command in the container, so it moves between hosts unchanged.
The operator's CODARIS_MCP_SERVERS variable is stricter: it takes a JSON list of stdio servers whose command must be an absolute path, and refuses a relative one. Use the file when you want a bare name, a url, or an env block.