analysis-mcp-server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@analysis-mcp-serverWhat Mu2e analyses are available?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
analysis-mcp-server
An example MCP server that exposes Mu2e analyses as agent tools. An agent asks which analyses exist, then runs one — either a mu2e job over art file(s):
mu2e -c <the analysis' fcl> -s <data file> # one file
mu2e -c <the analysis' fcl> -S <file list> # several, one path per lineor a Python computation over a ROOT file an earlier analysis produced. Two
analyses ship: energy deposition (edep) and approximate CE sensitivity
(approx_ce_sensitivity). Adding more is one small module each.
Built to the same pattern as
spectra-mcp-server, so the
multiagent-client-demo client in ../multiagent-client-demo (or Claude
Code, Claude desktop, Codex, Cursor) can drive it unchanged.
The one idea
The science code stays in usual Python. The MCP wrapper only publishes it.
tools/is an ordinary package that never imports MCP.analysis_mcp_server/is a generic wrapper: it reads one line of config frompyproject.toml, imports the science package, and registers every function in its__all__as an MCP tool.
[tool.mcp-server]
tool_modules = ["tools"]Type hints, Pydantic Field constraints, and docstrings become the tool
schema agents see.
Related MCP server: MCP Refana Server
Layout
tools/
spec.py AnalysisSpec, ParamSpec, RunContext/RunOutcome, ArtifactResult
mu2e_job.py running mu2e: env setup, -s/-S inputs, logs, timeouts
root_hist.py a small TH1-like histogram for converted ROOT macros
registry.py the catalogue: name -> AnalysisSpec
analyses/
edep.py energy deposition: fcl + summary parser
approx_ce_sensitivity.py CE sensitivity from EdepAna histograms
analysis_tools.py the MCP tools: list_analyses, run_analysis
__init__.py __all__ — ONLY these names become tools
analysis_mcp_server/ generic drop-in wrapper (FastMCP): server.py, cli.py
tests/test_tools.py parsers, registry, input handling — no mu2e, no MCPThe split that matters: the shared machinery (mu2e_job.py, root_hist.py)
knows how to run jobs and read histograms but nothing about physics;
analyses/*.py knows the physics. The registry joins them, and the two tools
are generic over it.
Each analysis declares an input_kind, which is the only thing the generic
layer needs to know about the difference between them:
input_kind | what it consumes | what its runner does |
| mu2e art file(s) | runs an fcl with |
| a ROOT file from an earlier analysis | Python computation over its histograms |
Tools
tool | what it does |
| catalogue: description, |
| run one analysis, return its metrics |
Both return the same {status, files, message, metadata} contract, so a
workflow can chain several runs and collect metadata uniformly.
The analyses
analysis | input | reports |
| art file(s) | average calo/tracker energy deposition per event and per generated event |
|
|
|
approx_ce_sensitivity declares produced_by = ["edep"], so chaining is
discoverable: run edep, then pass the nts.*.root from its files to the
sensitivity.
Parameters
Analyses declare their own physics knobs, passed as parameters:
run_analysis(analysis="approx_ce_sensitivity",
data_file=".../nts.owner.edep.Run1B.001800_00000000.root",
output_dir=".../sens",
parameters={"sig_eff": 0.1}) # npot defaults to 1e18list_analyses reports each parameter's description, default, range, and
whether it is required; unknown, missing, or out-of-range values come back as
a plain error naming the offender.
Inputs
Pass exactly one of:
data_file— a single absolute art file path →mu2e -s <file>data_files— a list of absolute art file paths → written one per line tofilelist.txtinoutput_dir→mu2e -S <filelist.txt>
With data_files the whole set runs as one job, so the metrics cover all
the inputs together, not one file each — call the tool once per file for
per-file numbers. Raise timeout_s (max 7200) when passing many files.
data_files and max_events apply only to art_files analyses.
max_events (mu2e --nevts) caps events for a quick check before a full run.
Beware: generated-event counts come from the input's subrun bookkeeping and
cover the whole file regardless, so any "per gen event" metric is
meaningless when max_events is set — use it to confirm a job runs, not for
physics numbers.
Results
Every result carries analysis, input_kind, data_files, n_input_files,
log_path, the resolved parameters, and — for the -S case —
file_list_path. On success the analysis' metrics are merged into metadata
under the names list_analyses advertises. files lists what the run wrote:
the job's ROOT output for edep, the figures for approx_ce_sensitivity. On
failure status="error", and for mu2e jobs metadata.stdout_tail holds the
last 20 log lines, so an agent can diagnose without re-running.
edep reports:
metric | unit | from the module's print |
|
| |
|
| |
| events / gen event |
|
| MeV |
|
| MeV |
|
|
| |
| MeV |
|
| MeV |
|
approx_ce_sensitivity reports sensitivity (S/sqrt(B)),
signal_box_low_mev / signal_box_high_mev, signal_rate,
dio_background, cosmic_background, total_background, and the signal
peak's signal_mpv_mev / signal_fwhm_mev. Its metadata also records the
assumptions used (npot, sig_eff, signal_br, cosmic_rate_per_mev,
onspill_seconds), and it writes the macro's figures — sig_vs_bkg.png,
dio.png, response.png, res.png, ce_z.png, ce_r.png — into
<output_dir>/figures.
approx_ce_sensitivity
A Python conversion of Mu2eOptAna/scripts/rough_run1a_sensitivity.C. Signal
is CE, background is DIO plus cosmics, and it estimates S/sqrt(B) for the best
momentum window:
Signal shape —
EDepAna/hist_2/trk_front_energy(energy at the front of the tracker for events leaving >10 MeV in the calorimeter), rebinned x2 and scaled to a rate fornpotprotons atSIGNAL_BR(R_mue = 1e-9) andsig_eff, then smeared by a Gaussian tracker resolution (sigma = 0.2 MeV).DIO — the Heeck/Szafron theoretical spectrum, scaled to a rate, then smeared by the measured energy-loss response (
hist_2/trk_front_energy_diff) and the same resolution.Cosmics — flat in momentum at a rough rate per MeV/c, scaled by the on-spill live time implied by
npot.Window scan — every
[x1, x2]withx1 >= 50 MeV, keeping the best S/sqrt(B). The top 10 windows go to the log.
The numbers are rough by construction: this is a figure of merit for comparing beamline configurations, not a sensitivity calculation. Note it needs a CE signal sample — given a beam file where nothing leaves >10 MeV in the calorimeter it reports that plainly instead of dividing by zero.
Two deviations from the macro, both deliberate:
It prints the 10 best windows to the log rather than all ~100k scanned.
Window sums are accumulated from each window's own edge, never as differences of whole-spectrum prefix sums. The DIO spectrum spans ~18 orders of magnitude, and differencing totals of ~4e17 to get a count of order 1 loses it entirely to float cancellation (it silently reported
dio_background = 0).tests/test_tools.pylocks this in.
Adding an analysis
Write
tools/analyses/<name>.pywith arun(context) -> RunOutcomeand aSPEC. For a mu2e job, handrun_mu2e_jobyour fcl and parse its stdout:
def run(context: RunContext) -> RunOutcome:
outcome = run_mu2e_job(fcl=FCL, input_paths=context.input_paths,
outdir=context.outdir, timeout_s=context.timeout_s,
single=len(context.input_paths) == 1
and not context.wants_file_list,
max_events=context.max_events)
if outcome.failed:
return RunOutcome(error=f"mu2e exited {outcome.returncode}",
log_path=outcome.log_path)
return RunOutcome(metrics=parse_stops_summary(outcome.stdout),
files=outcome.new_root_files, log_path=outcome.log_path)
SPEC = AnalysisSpec(
name="stops",
input_kind="art_files",
fcl=MUSE_WORKAREA / "Mu2eOptAna" / "fcl" / "stops.fcl",
description="Muon stops per POT.",
metrics=("n_stops", "stops_per_pot"),
units={"stops_per_pot": "stops / POT"},
parameters=(ParamSpec(name="pot_per_event", description="...",
default=1.6e7, minimum=0.0),),
run=run, summarize=lambda m: f"{m['n_stops']:g} stops",
input_hint="art file(s) with ...",
) For a ROOT-file analysis use input_kind="root_file", name the analysis
that produces the input in produced_by, read context.input_path, and
leave fcl unset — approx_ce_sensitivity.py is the worked example.
Add
"stops"to_ANALYSIS_MODULESintools/registry.py.Extend
tests/test_tools.py(a stdout sample inSAMPLE_STDOUTfor anart_filesanalysis; direct tests of the computation for aroot_fileone).
That's it — the analysis enum, list_analyses, and the registry tests pick
it up. Step 3 is not optional in practice: the registry tests loop over every
analysis and fail until each one is covered.
Physics knobs go in parameters as ParamSpecs (validated, with defaults and
ranges, and reported by list_analyses) rather than into the tool signature,
which stays the same however many analyses exist.
Environment
No installs needed on the mu2e machines — the ana python already has the
mcp SDK:
source /cvmfs/mu2e.opensciencegrid.org/setupmu2e-art.sh
pyenv ana # Python 3.12 with mcp + pydanticThe tool sets up mu2e itself, once per job, in a fresh bash subprocess:
cd /exp/mu2e/app/users/mmackenz/mu2eopt/ # the muse work area
source /cvmfs/mu2e.opensciencegrid.org/setupmu2e-art.sh
muse setup # backing -> Musings/SimJob/Run1Baq
mu2e -c <fcl> -s <data file>so the server does not care whether the shell that launched it had the
Offline environment. MUSE_WORKAREA in tools/mu2e_job.py is the one path
that encodes this.
Why
muse setupmust run in the work area:EdepAnais a locally built module (not in the Offline release yet). Its library comes frombuild/al9-prof-e29-p103/Mu2eOptAna/lib/, which only lands onCET_PLUGIN_PATHwhenmuse setupruns in the area holdingbacking. Run it anywhere else and art dies withLibrary specification "EdepAna" does not correspond to any library.
Test
python3 tests/test_tools.py36 tests, none of which start a mu2e job. (The ana env has no pytest, so
these are bare asserts.)
Run the server
stdio — the client spawns the server; nothing to start by hand:
python3 -m analysis_mcp_server --transport stdioStreamable HTTP — the server is a visible process with a URL:
python3 -m analysis_mcp_server --transport streamable-http --port 8000Clients connect to http://127.0.0.1:8000/mcp. Stop it with Ctrl+C
(Ctrl+Z only suspends it and keeps the port; just start again — a leftover
analysis_mcp_server holding the port is cleared automatically).
Use it from a client
Claude Code — the checked-in .mcp.json already wires it up; or:
claude mcp add mu2e-analysis -- python3 -m analysis_mcp_server --transport stdiomultiagent-client-demo (../multiagent-client-demo), stdio — the
client spawns the server:
CONFIG = {
"mu2e-analysis": {
"transport": "stdio",
"command": "python3",
"args": ["-m", "analysis_mcp_server"],
"cwd": "/exp/mu2e/app/users/mmackenz/mu2eopt/analysis-mcp-server",
}
}
tools = await load_tools(CONFIG)or over HTTP, against a server started as above:
CONFIG = {"mu2e-analysis": {"transport": "streamable_http",
"url": "http://127.0.0.1:8000/mcp"}}(Note the spelling: the adapter config says streamable_http, the server CLI
says --transport streamable-http.)
A task for the agent, once connected:
List the available analyses, then evaluate the average energy deposition in
the detectors for
/exp/mu2e/app/users/mmackenz/mu2eopt/dts.mmackenz.EarlyMuBeamFlash.Run1Bak_local0818120248.001800_00000000.art
writing outputs to /exp/mu2e/data/users/mmackenz/localtest/agent-output.
Report the average calo and tracker Edep per event and per generated event.Chaining the two analyses, which is what produced_by is for:
Run the edep analysis on <a CE signal art file>, then feed the nts.*.root it
produces to approx_ce_sensitivity with sig_eff = 0.1, and tell me the best
momentum window and its S/sqrt(B).This server cannot be deployed
Maintenance
Related MCP Connectors
Agent-driven search: build, import, tune, search, and score result quality — all over MCP.
Public read-only discovery of agent, model, training, task, and verification opportunities.
Discover, search, invoke, and rate A2A (Agent-to-Agent) protocol agents.
Discover and call AI agents via MCP. Supports A2A agents and platform agents with async tasks.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables coordinating specialist agents through an event-driven backend, allowing submission of goals, retrieval of job status and results, and listing of jobs via MCP tools.MIT
- FlicenseAqualityCmaintenanceEnables Mu2e analysis workflows by exposing event counting, cut analysis, sensitivity computation, and ML selection as MCP tools for agentic frameworks.10-
- AlicenseAqualityCmaintenanceEnables agents to submit and manage persistent, dependency-aware task graphs with immutable artifacts, resource reservations, durable event streaming, and retryable process execution over MCP.12MIT
- AlicenseNot gradedqualityBmaintenanceEnables pulling test data, inspecting telemetry channels, running analysis, and writing results back from any MCP client.MIT