MCP Integration

LINE exposes its queueing analysis as a Model Context Protocol (MCP) server, so that Large Language Models such as Claude can build, solve and analyze queueing models conversationally: you describe your system in natural language and the LLM calls LINE tools to produce quantitative results.

Setup

Installation

Install the LINE solver Python package, which ships the MCP server and puts a line-mcp command on your path:

pip install line-solver

The server speaks MCP over stdin and stdout and is meant to be launched by the MCP client, not run interactively. To install from source instead:

git clone https://github.com/imperial-qore/line-solver.git
cd line-solver/python
pip install -e .

Claude Desktop Configuration

Add the following to your claude_desktop_config.json:

{
  "mcpServers": {
    "line-solver": {
      "command": "line-mcp"
    }
  }
}

Claude Code Configuration

For Claude Code (CLI) the shortest route is the command line:

claude mcp add line-solver -- line-mcp

If line-mcp is not on the path Claude Code sees, as is usual after installing into a virtual environment, give its absolute path (for instance the one which line-mcp prints):

claude mcp add line-solver -s local -- [YOUR_PATH]/line-mcp/.venv/bin/line-mcp

-s local keeps the entry private to you in the current project, -s user makes it available in every project, and -s project shares it through a .mcp.json file at the project root:

{
  "mcpServers": {
    "line-solver": {
      "type": "stdio",
      "command": "line-mcp"
    }
  }
}

Claude Code does not read mcpServers from .claude/settings.json, and ignores a server declared there without a warning. Run claude mcp list to confirm the server was picked up; a server from .mcp.json stays pending until approved once in an interactive session. From a source checkout without installing, use "command": "python" with "args": ["/path/to/line-solver/python/mcp_server.py"].

Available Tools

ToolDescriptionCode Required
build_networkBuild ANY network from a description: open, closed or mixed, many stations, many classesNo
build_topologyBuild a multi-station network from a demand matrix, with no routingNo
build_from_galleryBuild one of the 71 ready-made gallery modelsNo
list_galleryThe gallery catalogue, with each model's argumentsNo
convert_modelConvert a model to LINE json, JMT, PNML, .lqnx or PythonNo
sweep_modelSweep any field of any model descriptionNo
analyze_queueBuild and solve ONE station from arguments: open, closed or mixedNo
analyze_networkBuild and solve a multi-station network from a demand matrixNo
sweep_parameterSweep one parameter of a single station and collect metricsNo
solve_model_fileSolve a model document: the whole line-cli command lineNo
find_solverWhich solvers and methods can analyze a model, and why notNo
list_solversThe vocabulary: solvers, analyses, input and output formatsNo
solve_line_modelExecute arbitrary LINE Python code (sandboxed)Yes
compare_solversRun the same model with multiple solvers side-by-sideNo
visualize_modelGenerate a Mermaid diagram of a queueing networkNo
list_examplesBrowse available example models by categoryNo
get_example_codeRetrieve source code of a named exampleNo
check_installEnvironment check: which optional backends are reachableNo
get_versionLINE version, interpreter and examples directoryNo
clear_sessionRemove a persistent code sessionNo

Building Models Without Code

The analyze tools build a model and answer with its numbers. The builders below instead register one, reaching shapes no demand matrix expresses (explicit routing, fork/join, class switching), and return a model_id to pass to solve_model_file, find_solver, visualize_model, compare_solvers or convert_model.

build_network takes stations, classes, service and routing, with open and closed classes freely mixed:

build_network(
  stations='[{"name":"Src","type":"Source"},'
           '{"name":"Q1","type":"Queue","scheduling":"PS"},'
           '{"name":"Q2","type":"Queue"},'
           '{"name":"Snk","type":"Sink"},'
           '{"name":"Think","type":"Delay"}]',
  classes='[{"name":"web","type":"Open"},'
          '{"name":"batch","type":"Closed","population":4,"refstation":"Think"}]',
  service='{"Q1":{"web":"exp(4.0)","batch":"exp(2.0)"},'
          '"Q2":{"web":"exp(3.0)","batch":"exp(2.5)"},'
          '"Think":{"batch":"exp(0.5)"}}',
  arrival='{"web":"exp(1.0)"}',
  routing='[["Src","Q1","web",1.0],["Q1","Q2","web",1.0],["Q2","Snk","web",1.0],'
          '["Think","Q1","batch",1.0],["Q1","Q2","batch",1.0],["Q2","Think","batch",1.0]]')

A station may carry scheduling, servers and capacity; a Join also names the Fork it closes in fork, since nested fork-join pairs cannot be paired from the routing. A class may carry a priority. A routing hop is [from, to, class, prob], or [from, to, from_class, to_class, prob] to switch class. arrival is keyed station → class → distribution, or by class alone when there is one Source.

Distributions are written name(args), e.g. exp(2.0), erlang(3.0, 2), hyperexp(0.5, 1.0, 4.0), det(0.25) or replayer('trace.txt'), and any distribution class LINE publishes is accepted. A bare number is refused, since a rate and a mean are not the same thing.

build_topology skips routing entirely and takes a demand matrix, which is the textbook (N, Z, D) input:

build_topology(kind="cluster_mixed", demands='[[0.4, 0.5], [0.3, 0.2]]',
               population='[3]', think_time='[2.0]', arrival_rates='[1.0]',
               scheduling="PS")

kind is tandem (open chain), cyclic (closed loop), cluster (open fan-out), cluster_closed or cluster_mixed. population and think_time are per closed class, arrival_rates per open class; scheduling (default PS) and servers also take per-station JSON lists.

list_gallery catalogues the 71 ready-made models in line_solver.gallery, from fork-join and caches to layered networks, finite-capacity regions and random environments:

list_gallery()
build_from_gallery(name="cqn", params='{"M": 3}')

sweep_model varies any field of any builder's arguments, named by a dotted path; give values as a range or, for a non-numeric field, as a JSON list:

sweep_model(param="population.0", values="2:20:2", builder="topology",
            args='{"kind":"cyclic","demands":[[0.5],[0.2]],"population":[2]}')

sweep_model(param="service.Q.c", values='["exp(2.0)", "exp(4.0)", "exp(8.0)"]',
            builder="network", args='{...}')

builder is network, topology or gallery, and args holds its arguments as one JSON object. Each point is solved with solver (default auto) and reported under analysis (default avg).

convert_model renders a model as LINE json, JMT .jsimg, PNML, layered .lqnx or a Python script. The json round-trips: passed back to solve_model_file as content, it solves identically.

convert_model(target="json", model_id="m1")
convert_model(target="python", model_id="m1", name="my_model")

Solving Model Files

solve_model_file is the line-cli surface: it reads a model document, picks a solver by method name and runs any analysis the library publishes, with no code.

solve_model_file(file="model.json", solver="mva", analysis="avg")
solve_model_file(file="webapp.jsimg", solver="ctmc", analysis="prob", node=1, state="2")
solve_model_file(file="database.lqnx", solver="ln", analysis="avg", output="json")
solve_model_file(content=document_text, solver="nc", analysis="normconst")

The model comes as file (a path), content (the document itself) or the model_id of a model built this session. Accepted formats are json (LINE's interchange format, for a Network, LayeredNetwork or Environment), jsim/jsimg/jsimw (JMT), lqnx/xml (layered), pnml (place/transition nets), and mat/pkl by path; the format is detected unless input_format states it.

analysis takes one name or a comma-separated list, covering the average, system, stage, chain and node tables; cache, retrial, loss and deadline-miss tables; the normalizing constant and busy period; response- and passage-time distributions and percentiles; transient tables; state probabilities; sample paths; rewards; sensitivities; and the SolverUQ interval. Every numeric control of the command line is an argument, and other SolverOptions go in options as a JSON object. Targeted analyses take node, state, class_idx, events, percentiles or reward_name.

When it is not obvious which solver fits, find_solver reports one row per (solver, method) pair: whether it runs, whether it is exact, approximate, a bound or a simulation, and which measures it answers (show_all=True also lists the refused pairs and why). Its Method column can be passed straight to solve_model_file, and list_solvers prints the full vocabulary.

Quick-Start Examples

These tools build, solve and discard a model in one call, with no code. They are not a reduced world: any distribution, any scheduling discipline, any number of open, closed or mixed classes, and the same solver, method, analysis, seed, samples, cutoff, timespan and options as solve_model_file. The default solver is auto, and the header names the one that ran.

One Station

Prompt: “Analyze an M/M/1 queue with arrival rate 2 and service rate 5.”

The LLM calls analyze_queue(arrival_rate=2.0, service_rate=5.0) and returns:

Model: M/M/1
  scheduling        = FCFS
  servers (k)       = 1
  class Class1      = open, arrival exp(2.0), service exp(5.0)
  utilization (rho) = 0.400000
  solver            = auto -> MVA
Station  JobClass     QLen  Util    RespT   ResidT  ArvR  Tput
Queue    Class1    0.66667   0.4  0.33333  0.33333     2     2
Source   Class1          0     0        0        0     0     2

A class with an arrival is open and fed by a Source; a class with a population is closed and cycles through a Delay holding its think time. A closed system is therefore the same tool, given a population instead of an arrival:

# "10 users, 5 seconds think time, service rate 2":
analyze_queue(population=10, think_time=5.0, service_rate=2.0)

Several classes go in a JSON list, open and closed together, each with its own distribution:

analyze_queue(
  classes='[{"name":"web","arrival_rate":1.0,"service_rate":4.0},'
          '{"name":"batch","population":4,"think_time":2.0,'
          '"service":"erlang(2.0, 3)"}]',
  scheduling="PS")
Model: mixed queue, 2 classes, k=1
  scheduling        = PS
  servers (k)       = 1
  class web         = open, arrival exp(1.0), service exp(4.0)
  class batch       = closed, N=4, think exp(0.5), service erlang(2.0, 3)
  utilization (rho) = 0.250000
  solver            = auto -> CTMC
Station  JobClass     QLen     Util   RespT  ResidT     ArvR     Tput
Queue    web        1.3379  0.24997   1.338   1.338  0.99988  0.99988
Queue    batch      3.0153  0.73849  6.1247  6.1247  0.49233  0.49233
Source   web             0        0       0       0        0  0.99988
Think    batch     0.98465  0.98465       2       2  0.49233  0.49233

Several Stations

Prompt: “Eight jobs circulating between two stations with demands 0.5 and 0.3.”

analyze_network takes the textbook (N, Z, D) input, one demand per station and class, with kind supplying the routing as in build_topology.

analyze_network(kind="cyclic", demands="[[0.5],[0.3]]", population="[8]")
Model: cyclic(D=2x1)
  kind           = cyclic
  stations       = 2
  classes        = 1
  scheduling     = PS
  population (N) = [8]
  solver         = auto -> MVA
Station  JobClass    QLen     Util   RespT  ResidT    ArvR    Tput
Queue1   Class1    6.5916  0.99321  3.3183  3.3183  1.9864  1.9864
Queue2   Class1    1.4084  0.59593   0.709   0.709  1.9864  1.9864

Parameter Sweep

Prompt: “How does queue length change as arrival rate goes from 0.5 to 2.5 in steps of 0.5, with service rate 5?”

sweep_parameter varies one of arrival_rate, service_rate, servers, population, think_time or queue_capacity over the analyze_queue model, taking all its other arguments. Failed points are listed with their reason under the table.

sweep_parameter(param="arrival_rate", values="0.5:2.5:0.5", service_rate=5.0)

Custom Models

The solve_line_model tool executes arbitrary LINE Python code in a sandboxed environment with from line_solver import *, numpy, and pandas pre-loaded.

Open Network

model = Network('M/M/1')
source = Source(model, 'Source')
queue = Queue(model, 'Queue', SchedStrategy.FCFS)
sink = Sink(model, 'Sink')

oclass = OpenClass(model, 'Class1')
source.setArrival(oclass, Exp(1.0))
queue.setService(oclass, Exp(2.0))

model.link(Network.serial_routing([source, queue, sink]))
avg_table = MVA(model).avg_table()
print(avg_table)

Closed Network

model = Network('ClosedQN')
delay = Delay(model, 'Think')
queue = Queue(model, 'Queue', SchedStrategy.PS)

cclass = ClosedClass(model, 'Users', 5, delay)
delay.setService(cclass, Exp(1.0))
queue.setService(cclass, Exp(3.0))

model.link(Network.serial_routing([delay, queue]))
avg_table = MVA(model).avg_table()
print(avg_table)

Advanced Tools

Solver Comparison

compare_solvers runs a model, given as code like the examples above, with several solvers (e.g. solvers="MVA,NC,MAM") and compares the results side by side.

Model Visualization

visualize_model generates a Mermaid diagram of the network topology, node types and classes:

model = Network('WebApp')
source = Source(model, 'Arrivals')
lb = Queue(model, 'LoadBalancer', SchedStrategy.PS)
app = Queue(model, 'AppServer', SchedStrategy.FCFS)
sink = Sink(model, 'Departures')
oclass = OpenClass(model, 'Requests')
source.setArrival(oclass, Exp(5.0))
lb.setService(oclass, Exp(100.0))
app.setService(oclass, Exp(10.0))
P = model.initRoutingMatrix()
P.set(oclass, oclass, source, lb, 1.0)
P.set(oclass, oclass, lb, app, 1.0)
P.set(oclass, oclass, app, sink, 1.0)
model.link(P)

It returns Mermaid source, which GitHub, Notion, the Mermaid Live Editor and other tools render as:

graph LR
    N0(["Arrivals"])
    N1["LoadBalancer"]
    N2["AppServer"]
    N3(["Departures"])
    N0 --> N1
    N1 --> N2
    N2 --> N3
    style N1,N2 fill:#2196F3,color:#fff,stroke:#2196F3
    style N3 fill:#F44336,color:#fff,stroke:#F44336
    style N0 fill:#4CAF50,color:#fff,stroke:#4CAF50

    %% Classes: Requests (open)

Node shape shows node type: a stadium for a Source or Sink, a rectangle for a Queue or ClassSwitch, a rounded box for a Delay or Router, a hexagon for a Fork or Join, a cylinder for a Cache. Station labels add (k=N) for a multiserver or (IS) for an infinite server, and a trailing comment lists each class with (open) or (N=...).

Routing is set per hop with P.set(from_class, to_class, from_node, to_node, prob), or per class by assigning a node-by-node matrix to P[jobclass, jobclass]; Network.serial_routing([...]) builds a plain chain. Node indexing such as P[jobclass, src, dst] = p raises ValueError.

Example Gallery

list_examples() lists LINE's built-in examples by category (open queues, closed networks, layered QN, fork-join, caches, random environments), and get_example_code("gallery_mm1") retrieves the source of one.

Solver Guide

SolverBest For
MVAProduct-form networks with exponential service (fast, exact)
NCClosed networks needing exact normalizing-constant analysis
CTMCSmall state-space models; supports non-product-form features
MAMPhase-type / MAP service distributions (matrix analytic)
SSAStochastic simulation; works for any supported model
FLDFluid / ODE approximation for transient and large models
LDESLINE's own discrete-event engine; the widest feature set in simulation
JMTSimulation through Java Modelling Tools (needs a Java runtime)
LNLayered queueing networks, with the per-layer engine named as ln.mva, ln.nc or ln.comom
LQNSLayered models through the external lqns/lqsim binaries
QNSThe qnsolver binary
ENVModels in a random environment; stage_solver names the engine run at each stage
UQUncertainty quantification and the interval analysis; uq_solver names the engine run at each design point
AGAgent-based (RCAT) analysis
BABounding analysis: asymptotic, balanced-job and LP bounds
AUTOPicks for you, as do the names line, sim, exact, fast and accurate

solve_model_file, sweep_model, analyze_queue, analyze_network and sweep_parameter accept every name in the table, plus solver.method, and find_solver reports which of them can analyze a given model. compare_solvers takes only MVA, CTMC, NC, SSA, FLD and MAM.

Output Formats

Most tools accept a format parameter:

compare_solvers, visualize_model, list_examples, get_example_code and clear_session have no format and answer with their single rendering. solve_model_file takes output instead: readable (the default), json, csv, or pickle and mat written to output_file. In json mode an error (rejected input, unstable system, solver failure) also comes back as JSON, {"error": "..."}, so a caller that parses the reply reads the reason.

JSON Output Example

{
  "results": [
    {
      "Station": "Source",
      "JobClass": "Class1",
      "QLen": 0.0,
      "Util": 0.0,
      "RespT": 0.0,
      "ResidT": 0.0,
      "ArvR": 0.0,
      "Tput": 2.0
    },
    {
      "Station": "Queue",
      "JobClass": "Class1",
      "QLen": 0.6666666666666666,
      "Util": 0.4,
      "RespT": 0.3333333333333333,
      "ResidT": 0.3333333333333333,
      "ArvR": 2.0,
      "Tput": 2.0
    }
  ],
  "metadata": {
    "model": "M/M/1",
    "arrival_rate": 2.0,
    "service_rate": 5.0,
    "servers": 1,
    "utilization": 0.4,
    "solver": "MVA"
  }
}