MCP server¶
gapit ships a Model Context Protocol (MCP) stdio server that exposes the CLI as tools. An agent runtime can screen assemblies and reads, summarize reports, introspect schemas, and self-provision databases without shelling out or parsing terminal output. The protocol is hand-rolled newline-delimited JSON-RPC 2.0 with no extra dependencies: one request per line on stdin, one response per line on stdout.
Entry points¶
| Command | What it is |
|---|---|
gapit mcp |
The mcp subcommand of the CLI. |
gapit-mcp |
Console script installed with the package (same code). |
Both serve until stdin closes (EOF ends the process cleanly). stdout carries protocol lines only; stderr is reserved for protocol-internal errors.
Client configuration¶
Register the console script with an MCP client, Claude-style:
{"mcpServers": {"gapit": {"command": "gapit-mcp"}}}
If gapit-mcp is not on the client's PATH, use the absolute path, or invoke
the module CLI as {"command": "gapit", "args": ["mcp"]}.
Tools¶
Nine tools: five read-only analysis tools and four database
self-provisioning tools (db_fetch, db_build, db_search,
db_outdated) that mirror gapit db ... exactly. Argument lists below are
transcribed from the live tools/list inputSchema objects.
| Tool | Arguments | Returns |
|---|---|---|
screen |
files (array of strings, required), db (string, required — no default), minid (number), mincov (number), format (string: json | tsv | md, default json), aligner (string: blastn | minimap2, default blastn), mergeFragments (boolean, default false; blastn only — cross-contig fragment merging), min_breadth (number 0–100, default 90; minimap2 only), min_identity (number 0–100, default 0; minimap2 only), min_mapq (integer ≥ 0, default 0; minimap2 only), minGeneCov (number 0–100, default 90; cluster dbs only), minGeneId (number 0–100, default 90; cluster dbs only), minClusterCov (number 0–100, default 96; cluster dbs only), datadir (string) |
Report text: gapit.report/1 JSON by default, TSV or Markdown per format; aligner minimap2 runs the reads engine and emits gapit.reads/1. A cluster-kind db emits gapit.cluster/1: best-locus call, per-gene verdicts, and — when the database carries a single-scheme typing.json — the phenotype call with its phenotype_detail score breakdown; a multi-scheme typing.json emits gapit.cluster/2 with a phenotypes map of every scheme's call |
screen_reads |
r1 (array of strings, required — one path per lane), r2 (array of strings, same count as r1), read_type (string: sr | map-ont | map-hifi, default sr), min_breadth (number 0–100, default 90), min_identity (number 0–100, default 0), min_mapq (integer ≥ 0, default 0), format (string: json | md, default json), db (string, required — no default), datadir (string) |
gapit.reads/1 JSON by default or Markdown per format; min_identity/min_mapq > 0 switches to gapit.reads/2 |
summary |
files (array of report table paths, required), identity (boolean), nopath (boolean) |
gapit.summary/1 JSON |
schema |
name (string, required, one of cluster, error, features, floors, reads, reads2, report, summary, typing, typing_result, version) |
The JSON Schema of that output document |
db_list |
none | gapit.dblist/1: database names, install state, record counts |
db_fetch |
name (string, required — a database NAME, or all for the card+vfdb default set), datadir (string), force (boolean, default false) |
One JSON receipt line per database (db, records, dbtype, destination). Every database downloads from its upstream provider, so installs need network and can take minutes |
db_build |
name (string, required), fasta (string, required — a LOCAL filesystem path), tsv (string), dbtype (string: nucl | prot), description (string), datadir (string), force (boolean, default false) |
One JSON receipt line (db, records, dbtype, destination) |
db_search |
term (string, required), db (string), field (string: gene | accession | function | product | any, default any), exact (boolean, default false), limit (integer ≥ 0, default 100; 0 = unlimited), datadir (string) |
TSV hit rows with columns DB, GENE, ACCESSION, FUNCTION, PRODUCT, LENGTH |
db_outdated |
days (integer ≥ 0, default 90), datadir (string) |
TSV rows with columns NAME, FETCHED_AT, AGE_DAYS, STATUS |
Omitted screen thresholds fall back to the CLI defaults (minid 80,
mincov 80; with aligner minimap2: min_breadth 90, min_identity 0,
min_mapq 0), matching gapit screen. The min_breadth/min_identity/
min_mapq arguments are rejected on the default blastn path (they are
reads-engine parameters); minid/mincov are ignored by minimap2.
Notes:
- File paths resolve relative to the server's working directory; absolute paths are safest.
- The datadir comes from the
GAPIT_DATADIRenvironment variable, then~/.local/share/gapit/db(see./databases.md). The db tools also accept an explicitdatadirargument per call. csvoutput is CLI-only: MCPscreenoffersjson/tsv/mdandscreen_readsoffersjson/md(reads mode rejects tabular output).db_build'sfastamust be a path on the server's filesystem — the agent provides a local path, not file contents.- Tool failures do not use JSON-RPC errors. They return
isError: truewith thegapit.error/1envelope serialized as the text content.
Example session¶
Build a throwaway datadir from the repo's test fixture (same recipe the test suite uses), then talk to the server directly:
export PATH="$PWD/.pixi/envs/default/bin:$PATH"
export GAPIT_DATADIR=/tmp/gapit-mcp-demo/datadir
mkdir -p "$GAPIT_DATADIR"
cp -r tests/data/db/tinyamr "$GAPIT_DATADIR/"
makeblastdb -in "$GAPIT_DATADIR/tinyamr/sequences" -dbtype nucl \
-out "$GAPIT_DATADIR/tinyamr/sequences" > /dev/null
Feed three request lines to gapit mcp:
cat <<'EOF' | gapit mcp
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"screen","arguments":{"files":["tests/data/contigs/full.fa"],"db":"tinyamr"}}}
EOF
Response line 1, verbatim:
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"gapit","version":"0.5.6"}}}
Response line 2 (real output, elided in the middle; each tool carries its full
inputSchema):
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"screen","description":"Screen contig files for known genes (json = gapit.report/1; aligner minimap2 = fast assembly survey emitting gapit.reads/1).","inputSchema":{"type":"object","properties":{"files":{"type":"array","items":{"type":"string"}},"db":{"type":"string"}, ... },"required":["files","db"]}}, {"name":"screen_reads","description":"Screen FASTQ reads for genes via minimap2 (json = gapit.reads/1; min_identity/min_mapq > 0 emits gapit.reads/2). Returns the rendered document.","inputSchema":{"type":"object","properties":{"r1":{"type":"array","items":{"type":"string"}},"r2":{"type":"array","items":{"type":"string"}},"read_type":{"type":"string","enum":["sr","map-ont","map-hifi"],"default":"sr"}, ... },"required":["r1","db"]}}, {"name":"summary", ...}, {"name":"schema", ...}, {"name":"db_list", ...}, {"name":"db_fetch", ...}, {"name":"db_build", ...}, {"name":"db_search", ...}, {"name":"db_outdated", ...}]}}
Response line 3 (real output, text content elided). The screen result rides in
result.content[0].text as a JSON string:
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{\n \"schema\": \"gapit.report/1\", ... }"}],"isError":false}}
Decoded, that text is a complete gapit.report/1 document (real output from
the same run):
{
"schema": "gapit.report/1",
"tool": {"name": "gapit", "version": "0.5.6"},
"created_at": "2026-09-19T01:15:44Z",
"params": {"db": "tinyamr", "minid": 80.0, "mincov": 80.0, "threads": 1},
"files": [
{
"file": "tests/data/contigs/full.fa",
"hits": [
{
"sequence": "contig1",
"start": 1,
"end": 79,
"strand": "+",
"gene": "tetA",
"coverage": "1-79/79",
"coverage_map": "===============",
"gaps": "0/0",
"coverage_pct": 100.0,
"identity_pct": 100.0,
"database": "tinyamr",
"accession": "NC_000913.3:100-900",
"product": "tetracycline efflux pump TetA",
"resistance": "TETRACYCLINE"
}
]
}
]
}
Screening reads¶
screen_reads mirrors gapit screen --r1/--r2: minimap2 per lane, presence
on alignment breadth. Same datadir convention as screen, but no BLAST
index is needed — minimap2 reads the sequences file directly. Real session
against a throwaway copy of the repo's tinyreads fixture:
mkdir -p /tmp/gapit-mcp-demo/datadir
cp -r tests/data/reads_db/tinyreads /tmp/gapit-mcp-demo/datadir/
cat <<'EOF' | gapit mcp
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"screen_reads","arguments":{"r1":["tests/data/reads/tetx_full.fq"],"db":"tinyreads","datadir":"/tmp/gapit-mcp-demo/datadir"}}}
EOF
Verbatim response (text content elided):
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{\n \"schema\": \"gapit.reads/1\", ... }"}],"isError":false}}
Decoded, the text is a complete gapit.reads/1 document (real output from
the same run):
{
"schema": "gapit.reads/1",
"tool": {"name": "gapit", "version": "0.5.6"},
"created_at": "2026-09-22T00:07:35Z",
"params": {
"db": "tinyreads",
"read_type": "sr",
"min_breadth": 90.0,
"threads": 1
},
"files": [
{
"reads": [
"tests/data/reads/tetx_full.fq"
],
"genes": [
{
"gene": "tetX",
"database": "tinyreads",
"accession": "SYN-001",
"product": "extended resistance determinant tetX",
"resistance": "TETRACYCLINE",
"tlen": 522,
"breadth_pct": 97.7,
"mean_depth": 2.09,
"reads_mapped": 12,
"present": true
}
]
}
]
}
r2 takes the mate paths (same count as r1), read_type picks the
minimap2 preset, and a nonzero min_identity/min_mapq switches the
document to gapit.reads/2 (semantics: ./reads.md). For assembly FASTA,
call screen with aligner: "minimap2" instead — the survey auto-resolves
the map-ont preset.
Failure shapes¶
A missing input file returns a tool error, not a protocol error (real lines):
{"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"{\"schema\":\"gapit.error/1\",\"code\":\"INPUT_NOT_FOUND\",\"message\":\"input file not found or unreadable: /tmp/gapit-mcp-demo/nope.fa\",\"context\":{\"file\":\"/tmp/gapit-mcp-demo/nope.fa\"}}"}],"isError":true}}
Parse content[0].text as JSON and branch on code (codes and exit-code
mapping: ./outputs.md).
Self-provisioning: build a database, then screen¶
The db tools let an agent provision its own databases mid-session. Real
session against a fresh datadir (my_genes.fa holds one 240 bp synthetic
gene, query.fa a 200 bp substring of it):
mkdir -p /tmp/gapit-mcp-demo/datadir
export GAPIT_DATADIR=/tmp/gapit-mcp-demo/datadir
Two request lines and their verbatim responses:
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"db_build","arguments":{"name":"myamr","fasta":"/tmp/gapit-mcp-demo/my_genes.fa"}}}
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\"db\":\"myamr\",\"records\":1,\"dbtype\":\"nucl\",\"destination\":\"/tmp/gapit-mcp-demo/datadir/myamr\"}"}],"isError":false}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"screen","arguments":{"files":["/tmp/gapit-mcp-demo/query.fa"],"db":"myamr"}}}
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{ ... gapit.report/1 ... }"}],"isError":false}}
Decoded, the screen text is a complete gapit.report/1 document whose only
hit is the gene the agent just built the database from (real output):
{
"schema": "gapit.report/1",
"tool": {"name": "gapit", "version": "0.5.6"},
"created_at": "2026-09-20T13:38:39Z",
"params": {"db": "myamr", "minid": 80.0, "mincov": 80.0, "threads": 1},
"files": [
{
"file": "/tmp/gapit-mcp-demo/query.fa",
"hits": [
{
"sequence": "contig1",
"start": 1,
"end": 200,
"strand": "+",
"gene": "demov2",
"coverage": "1-200/240",
"coverage_map": "=============..",
"gaps": "0/0",
"coverage_pct": 83.33,
"identity_pct": 100.0,
"database": "myamr",
"accession": "",
"product": "demo beta-lactamase variant 2",
"resistance": ""
}
]
}
]
}
The same session pattern works with db_fetch (name all installs the
card+vfdb default set) and db_search/db_outdated for
inspection. Custom database construction rules (header kinds, --tsv
metadata): ./custom-db.md.
Protocol notes¶
- Framing. One JSON-RPC 2.0 message per stdin line, one response line per request on stdout, UTF-8, LF endings. Responses use compact separators.
- initialize. Echoes a requested string
protocolVersion; when absent or empty it answers with the built-in default2025-06-18. The result carriescapabilities.toolsandserverInfo(name: gapit, package version). - Methods. Exactly three are handled:
initialize,tools/list,tools/call. Anything else gets a JSON-RPC error-32601:
text
{"jsonrpc":"2.0","id":4,"error":{"code":-32601,"message":"method not found: resources/list"}}
- Unknown tool.
tools/callnaming an unregistered tool, or with invalid params, gets-32602:
text
{"jsonrpc":"2.0","id":6,"error":{"code":-32602,"message":"unknown tool: nope"}}
- Batch arrays are unsupported. A JSON array line is not a valid frame and is dropped; send one request per line.
- Notifications are silent. Frames without an
id(for examplenotifications/initialized) never produce a response. - Junk tolerance. Non-JSON lines are ignored without a parse-error reply, and the loop keeps serving. EOF terminates cleanly.