Skip to content

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_DATADIR environment variable, then ~/.local/share/gapit/db (see ./databases.md). The db tools also accept an explicit datadir argument per call.
  • csv output is CLI-only: MCP screen offers json/tsv/md and screen_reads offers json/md (reads mode rejects tabular output).
  • db_build's fasta must 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: true with the gapit.error/1 envelope 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 default 2025-06-18. The result carries capabilities.tools and serverInfo (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/call naming 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 example notifications/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.