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
| Tool | Description | Code Required |
|---|---|---|
build_network | Build ANY network from a description: open, closed or mixed, many stations, many classes | No |
build_topology | Build a multi-station network from a demand matrix, with no routing | No |
build_from_gallery | Build one of the 71 ready-made gallery models | No |
list_gallery | The gallery catalogue, with each model's arguments | No |
convert_model | Convert a model to LINE json, JMT, PNML, .lqnx or Python | No |
sweep_model | Sweep any field of any model description | No |
analyze_queue | Build and solve ONE station from arguments: open, closed or mixed | No |
analyze_network | Build and solve a multi-station network from a demand matrix | No |
sweep_parameter | Sweep one parameter of a single station and collect metrics | No |
solve_model_file | Solve a model document: the whole line-cli command line | No |
find_solver | Which solvers and methods can analyze a model, and why not | No |
list_solvers | The vocabulary: solvers, analyses, input and output formats | No |
solve_line_model | Execute arbitrary LINE Python code (sandboxed) | Yes |
compare_solvers | Run the same model with multiple solvers side-by-side | No |
visualize_model | Generate a Mermaid diagram of a queueing network | No |
list_examples | Browse available example models by category | No |
get_example_code | Retrieve source code of a named example | No |
check_install | Environment check: which optional backends are reachable | No |
get_version | LINE version, interpreter and examples directory | No |
clear_session | Remove a persistent code session | No |
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
| Solver | Best For |
|---|---|
| MVA | Product-form networks with exponential service (fast, exact) |
| NC | Closed networks needing exact normalizing-constant analysis |
| CTMC | Small state-space models; supports non-product-form features |
| MAM | Phase-type / MAP service distributions (matrix analytic) |
| SSA | Stochastic simulation; works for any supported model |
| FLD | Fluid / ODE approximation for transient and large models |
| LDES | LINE's own discrete-event engine; the widest feature set in simulation |
| JMT | Simulation through Java Modelling Tools (needs a Java runtime) |
| LN | Layered queueing networks, with the per-layer engine named as ln.mva, ln.nc or ln.comom |
| LQNS | Layered models through the external lqns/lqsim binaries |
| QNS | The qnsolver binary |
| ENV | Models in a random environment; stage_solver names the engine run at each stage |
| UQ | Uncertainty quantification and the interval analysis; uq_solver names the engine run at each design point |
| AG | Agent-based (RCAT) analysis |
| BA | Bounding analysis: asymptotic, balanced-job and LP bounds |
| AUTO | Picks 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:
text(default): a human-readable table with a model summary headerjson: structured JSON withmetadataandresultskeys
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"
}
}