Skip to main content
Glama
README.md
# Agentic Metabolic Systems Biology - Stateful COBRApy via MCP & SKILLS.md

**GSoC 2026 · NRNB (National Resource for Network Biology) · Project #280**
(Self-Project) 

A portable, deterministic bridge between Large Language Models and genome-scale
metabolic modeling (GEM) tooling - built on the **Model Context Protocol (MCP)**
for tool discovery/execution and the **SKILLS.md** standard for encoding expert
workflows as reusable, semantic execution contracts.

This repository is the foundational boilerplate for the project: a stateful
FastMCP server wrapping [COBRApy](https://opencobra.github.io/cobrapy/), a
sample SKILLS.md workflow, a fully containerized one-click deployment, and a
test suite that doubles as a working proof-of-concept report.

---

## Why this exists

Genome-scale metabolic models are computationally powerful but operationally
fragile: COBRApy, CarveMe, and Neo4j each have their own idiosyncratic APIs,
and naively feeding an LLM full model dumps or library docs either overflows
its context window or invites it to hallucinate a metabolic pathway that
doesn't exist. This project's answer is architectural, not prompt-based:

- **MCP is the kitchen** — a slim, dynamically-discoverable set of tools the
  agent can call, with the underlying biological objects (models, solvers)
  kept server-side.
- **SKILLS.md is the recipe book** — the expert standard operating procedure
  (load → validate gene → knock out → re-optimize → compare) that keeps the
  LLM as an *orchestrator* of deterministic linear-programming calls, never
  the source of a numeric result itself.

## Architecture

```
                         Application Host Process
   ┌────────────────────────────────────────────────────┐
   │   LLM Agent (e.g. OpenCode)                         │
   │        │                                            │
   │        │  reads workflow contract                   │
   │        ▼                                            │
   │   skills/fba_workflow.md  (SKILLS.md)               │
   │        │                                            │
   │        │  emits tool calls (JSON-RPC 2.0)            │
   │        ▼                                            │
   │   MCP Client  ─────────────────stdio/HTTP──────────►│──┐
   └────────────────────────────────────────────────────┘  │
                                                             ▼
                                          ┌─────────────────────────────────┐
                                          │  FastMCP Server (server.py)      │
                                          │  ─────────────────────────────   │
                                          │  SESSIONS: dict[session_id ->    │
                                          │             ModelSession]        │
                                          │                                   │
                                          │  tools:                          │
                                          │   load_model                     │
                                          │   knockout_gene                  │
                                          │   run_fba                        │
                                          │   get_flux_distribution          │
                                          │   list_genes                     │
                                          │   list_sessions / reset_session  │
                                          │                                   │
                                          │  COBRApy  ──►  GLPK/CBC solver    │
                                          └─────────────────────────────────┘
```

**Host** - the LLM application (e.g. an OpenCode agent) that decides *when*
to call a tool.
**Client** - the MCP client embedded in the host, speaking JSON-RPC 2.0 over
stdio (or streamable-HTTP inside Docker).
**Server** - `server.py`: a FastMCP process that owns the COBRApy models and
the solver, and exposes a small, versioned tool surface.

### Stateful, URI-addressable models

The proposal's central design constraint is that a GEM must **never** be
serialized into the LLM's context window - a genome-scale SBML file can be
tens of megabytes and instantly blow the token budget. Instead:

1. `load_model` reads the model once, server-side, and returns only a
   **`session_id`** plus a `metabolic://models/{session_id}` resource URI and
   small summary metadata (reaction/gene/metabolite counts).
2. Every subsequent tool call (`knockout_gene`, `run_fba`, ...) takes that
   `session_id` and mutates or queries the *same* in-memory `cobra.Model`
   object held in the server's `SESSIONS` registry.
3. A gene knockout applied in one conversational turn is therefore still in
   effect the next time the agent calls `run_fba` with that `session_id` -
   no reload, no re-serialization, no context bloat.
4. Only scalars (objective value, solver status) or small top-N summaries
   are ever returned to the agent — the full flux vector is available on
   request via `get_flux_distribution`, but is not sent by default.

This in-memory registry is intentionally simple (a Python dict) for this
boilerplate. The proposal's noted limitation - a server restart clears all
sessions - is real; a persistence layer (e.g. pickling `ModelSession` objects
to the `model_data` Docker volume) is documented future scope, not
implemented here.

## Repository layout

```
.
├── Dockerfile              # multi-stage build: builder (solver deps) -> slim runtime
├── docker-compose.yml       # one-click orchestration
├── requirements.txt
├── server.py                 # FastMCP server + stateful COBRApy tools
├── skills/
│   └── fba_workflow.md      # SKILLS.md execution contract for FBA + knockout
└── tests/
    └── test_server.py       # unit tests, MCP protocol integration test, PoC metrics
```

## Tools exposed by the server

| Tool | Purpose |
|---|---|
| `load_model(model_name, sbml_path)` | Load a built-in model (`textbook` / `ecoli_core`, `salmonella`) or a custom SBML file. Returns `session_id` + summary metadata. |
| `knockout_gene(session_id, gene_id)` | Knock out a gene in the session's model; persists for future calls. Errors deterministically on an unknown gene. |
| `run_fba(session_id)` | Runs FBA on current model state; returns only solver status + objective value. |
| `get_flux_distribution(session_id, top_n)` | Opt-in detail: top-N reactions by |flux|, for when the agent needs pathway-level insight. |
| `list_genes(session_id, limit)` | Grounds the agent in real gene IDs before a knockout, preventing hallucinated IDs. |
| `list_sessions()` | Lists all active server-side sessions. |
| `reset_session(session_id)` | Discards a session's in-memory state. |

Built-in models (`textbook` → *E. coli* core, `salmonella`) are loaded from
COBRApy's bundled local data files, so `load_model` works fully offline -
no BiGG/BioModels network call is required, which matters inside an
air-gapped Docker container.

## The SKILLS.md contract

[`skills/fba_workflow.md`](./skills/fba_workflow.md) defines the FBA +
gene-knockout workflow as YAML-frontmatter metadata (inputs, outputs, tool
dependencies, execution constraints, failure-recovery rules) followed by a
plain-language procedure. Two constraints worth highlighting:

- The agent must capture the **wild-type** objective value via `run_fba`
  *before* calling `knockout_gene`, so there is always a baseline to compare
  against.
- The agent must confirm a gene ID against `list_genes` before calling
  `knockout_gene` - a gene ID is never guessed.

This file is mounted read-write into the container (`./skills:/app/skills`)
so new workflows can be authored or edited without rebuilding the image —
the "dynamic SKILLS.md hot reload" behavior described in the proposal.

## Quickstart

### Option A - Docker (recommended, one-click)

```bash
git clone repo_url.git
cd gem-mcp
docker compose up --build
```

This builds the multi-stage image (solver dependencies compiled in a
`builder` stage, copied into a slim `runtime` stage) and starts the
FastMCP server listening on stdio by default. To expose it over HTTP
instead (e.g. for a containerized agent host to reach it across the Docker
network), override the command:

```bash
docker compose run --rm -p 8000:8000 cobrapy-mcp \
  python server.py --transport streamable-http --host 0.0.0.0 --port 8000
```

### Option B - Local Python

```bash
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python server.py                 # stdio transport, for a local MCP host
# or:
python server.py --transport streamable-http --port 8000
```

### Running the tests / PoC metrics

```bash
pip install -r requirements.txt
pytest tests/test_server.py -v          # unit tests + real MCP client/server round trip

python tests/test_server.py             # prints PoC metrics, e.g.:
# === E. coli core model — PoC metrics ===
# Reactions:   95
# Metabolites: 72
# Genes:       137
# Wild-type growth rate (1/hr): 0.873922
# Top 5 fluxes (wild type): ATPS4r +45.51, CYTBD +43.60, NADH16 +38.53, ...
# Knockout gene: b1241
# Post-knockout growth rate (1/hr): 0.873922
# Growth ratio (KO / WT): 1.0000
```

The test suite includes:
- **Unit tests** against the tool functions directly (session creation,
  invalid gene/session handling, knockout persistence across separate calls).
- **An in-process MCP integration test** using FastMCP's `Client`, which
  drives the exact same JSON-RPC path a real agent host would use — proving
  the tools are correctly registered, not just correct as plain functions.
- A **PoC metrics report** (`python tests/test_server.py`) that loads the
  *E. coli* core model, runs a baseline FBA, shows the top fluxes, performs a
  knockout, and reports the resulting growth ratio — the same numbers you'd
  want in a GSoC progress update.