MCP Integration
LINE exposes its queueing analysis capabilities as a Model Context Protocol (MCP) server, allowing Large Language Models (such as Claude) to build, solve, and analyze queueing models conversationally. Instead of writing code manually, 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. The MCP server and the MCP SDK come with it, and the installation puts a line-mcp command on your path:
pip install line-solver
Check that the command is on your path:
which line-mcp
The server speaks MCP over stdin and stdout, so starting it by hand simply waits for a client; it is meant to be launched by the MCP client, not run interactively.
Or clone the repository and install in development mode:
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, which is the usual case when the package was installed into a virtual environment, give the executable by absolute path and state the scope explicitly:
claude mcp add line-solver -s local -- [YOUR_PATH]/line-mcp/.venv/bin/line-mcp
Here -s local keeps the entry private to you in the current project; -s user would make it available in every project, and -s project would write it into the .mcp.json below.
To share the server with everyone working on a project, declare it instead in 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; a server declared there is ignored without a warning. Run claude mcp list to confirm the server was picked up. A project-scoped server out of .mcp.json is listed as pending approval until it has been approved once in an interactive session.
If line-mcp is not on the path your MCP client sees, give its absolute location instead, for instance the one which line-mcp prints. Running from a source checkout without installing the package also works: 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 REGISTER one instead, and reach shapes no demand matrix expresses: explicit routing, fork/join, class switching. Each returns a model_id; pass that to solve_model_file for any analysis, or to find_solver, visualize_model, compare_solvers or convert_model.
build_network takes a description of stations, classes, service and routing. Open and closed classes may appear together, which is what makes a model mixed; what needs this tool rather than analyze_queue or analyze_network is the explicit routing:
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, and a Join also carries fork, naming the Fork station it closes: the pairing is a declaration rather than something read off the routing, since a model may nest two fork-join pairs. 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 on the hop. arrival is keyed station → class → distribution, and by class alone when the model has exactly one Source.
Distributions are written name(args): exp(2.0), erlang(3.0, 2), hyperexp(0.5, 1.0, 4.0), det(0.25), cox2(1.0, 2.0, 0.5), gamma(2.0, 0.5), replayer('trace.txt'). The name is matched against LINE's own distribution classes, so every distribution the library publishes is available. A bare number is refused, because 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 defaults to PS and accepts a per-station JSON list, and servers is a JSON list of server counts per station.
list_gallery catalogues the 71 ready-made models in line_solver.gallery with the arguments each takes, covering fork-join, caches, 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. A field holding an integer is swept with integers; for a field that is not numeric, give values 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 carries that builder's arguments as one JSON object, with the nested parts as real json rather than as strings. Each point is solved with solver (default auto) and reported under analysis (default avg).
convert_model renders a model as the LINE json interchange document, a JMT .jsimg, a PNML net, a layered .lqnx, or the equivalent Python script. The json document round-trips: hand it back to solve_model_file as content and it solves identically.
convert_model(target="json", model_id="m1")
convert_model(target="python", model_id="m1", name="my_model")
Solving Model Files
The code tools build a model out of Python. A command line does not: it reads a document, picks a solver by method name, asks for one of the analyses the library publishes and renders the answer. solve_model_file is that surface, so a client holding a model file need not paste it into code, and every analysis LINE publishes is reachable without writing the getter call by hand.
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), as content (the document itself), or as the model_id of a model built this session, which reaches every analysis below exactly as a document does. A document may be in any format LINE reads: json (LINE's own interchange model, carrying a Network, a LayeredNetwork or an Environment), jsim/jsimg/jsimw (JMT), lqnx/xml (layered), pnml (place/transition nets), and mat/pkl by path. The format follows from the extension or from the document's first bytes unless input_format states it.
analysis takes one name or a comma-separated list, and reaches the average, system, stage, chain, node and node-chain tables; the cache and per-item tables; the retrial orbit, the two loss views and the deadline-miss table; the normalizing constant and the subnetwork busy period; response-time and passage-time distributions and percentiles; the transient tables; the state probabilities; simulated sample paths; rewards; sensitivities; and the SolverUQ interval. Every numeric control the command line carries is an argument too, and anything else SolverOptions understands may be passed as a JSON object in options. A few analyses are pointed at part of the model or need a parameter of their own: node and state for the state probabilities, class_idx for prob-marg, node and events for the sample paths, percentiles for perct-respt, and reward_name for reward-value.
When it is not obvious which solver fits, find_solver reports one row per (solver, method) pair, saying whether it runs, whether it is exact, approximate, a bound or a simulation, and which measures it answers; show_all=True keeps the refused pairs and the reason each was refused. Its Method column is the method name to hand straight back to solve_model_file, and list_solvers prints the full vocabulary both of them accept.
Quick-Start Examples
These tools require no code: give the parameters and the model is built, solved and thrown away in one call. They are not a reduced world. Any distribution the library publishes, any scheduling discipline by name, any number of classes, open, closed or mixed, and the same solver, method and analysis vocabulary solve_model_file takes, plus seed, samples, cutoff, timespan and options. The default solver is auto, and the header names the one that actually 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 is fed by a Source; a class with a population is closed and circulates through a Delay holding its think time, or straight back into the queue when there is none. So a closed system is the same tool, said with a population instead of an arrival, and the open/closed split that used to need two tools is gone.
# "10 users, 5 seconds think time, service rate 2":
analyze_queue(population=10, think_time=5.0, service_rate=2.0)
Several classes are a JSON list, and they may be open and closed at once, each with a distribution and the station with a discipline of its own:
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 service demand per station per class, with the topology supplying the routing. kind is tandem, cyclic, cluster, cluster_closed or cluster_mixed.
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 model analyze_queue builds, and takes every other argument of that tool. A point that fails is reported with its reason under the table rather than dropped.
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)
Multiclass Network
model = Network('Multiclass')
source = Source(model, 'Source')
queue = Queue(model, 'Queue', SchedStrategy.FCFS)
sink = Sink(model, 'Sink')
class1 = OpenClass(model, 'Web')
class2 = OpenClass(model, 'API')
source.setArrival(class1, Exp(1.0))
source.setArrival(class2, Exp(0.5))
queue.setService(class1, Exp(4.0))
queue.setService(class2, Exp(3.0))
model.link(Network.serial_routing([source, queue, sink]))
avg_table = MVA(model).avg_table()
print(avg_table)
Advanced Tools
Solver Comparison
Run the same model with multiple solvers and compare results:
# Using compare_solvers tool with solvers="MVA,NC,MAM"
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]))
Model Visualization
Generate Mermaid diagrams showing network topology, node types, and class information:
# Using visualize_model tool
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)
which returns:
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)
rendered by GitHub, Notion, Mermaid Live Editor, and other tools 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 carries node type: a stadium is a Source or Sink, a rectangle a Queue or ClassSwitch, a rounded box a Delay or Router, a hexagon a Fork or Join, a cylinder a Cache. A station label picks up its server count, (k=N) for a multiserver or (IS) for an infinite server, and the trailing comment line lists every class with its population, (open) or (N=...). Labels are always quoted, since a bare Mermaid label may not hold parentheses.
Routing is declared one hop at a time with P.set(from_class, to_class, from_node, to_node, prob), or a whole class at a time by assigning a node-by-node matrix to P[jobclass, jobclass]. Indexing a routing matrix by node, as in P[jobclass, src, dst] = p, is not supported and raises ValueError. For a plain chain, Network.serial_routing([...]) builds the matrix for you.
Example Gallery
Browse and retrieve LINE's built-in examples:
list_examples()— categorized list of all available examplesget_example_code("gallery_mm1")— retrieve source code for a specific example
Example categories include: open queues, closed networks, layered QN, fork-join, caches, and random environments.
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 |
Not every tool reaches every solver. solve_model_file, sweep_model, analyze_queue, analyze_network and sweep_parameter accept every name in the table, plus a solver.method: the analyze tools hand their model to the same dispatch the document surface uses. find_solver reports which of them can analyze the model in hand. compare_solvers takes the six in-process network solvers only: MVA, CTMC, NC, SSA, FLD and MAM.
Output Formats
Most tools accept a format parameter:
text(default) — Human-readable table with model summary headerjson— Structured JSON withmetadataandresultskeys
It is taken by analyze_queue, analyze_network, sweep_parameter, the three builders, sweep_model, convert_model, find_solver, list_solvers, list_gallery, check_install, get_version and solve_line_model. Five tools have no format at all and answer with the one rendering they have: compare_solvers and visualize_model, plus list_examples, get_example_code and clear_session.
solve_model_file is the exception that renders differently: its argument is output, which is readable (the default), json, csv, or pickle and mat written to output_file.
A run that never reaches a result answers in the requested format too: in json mode a rejected input, an unstable system or a solver failure comes back as {"error": "..."} rather than as plain text, so a caller that parses the reply reads the reason instead of a parse failure.
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"
}
}