Tool servers and the Model Context Protocol (MCP)
Last updated on 2026-07-16 | Edit this page
Estimated time: 65 minutes
Overview
Questions
- What is MCP, and what problem does it solve?
- What does the uproot server expose, and how is it sandboxed?
- How do you connect and drive a server?
Objectives
- Bring the servers up, check them, and read a log when one misbehaves
(
eic-mcp up/status/logs). - Generate the connection file for your own client with
eic-mcp config. - Discover a real DIS dataset by prompting, without hard-coding names or paths.
- Judge which returned quantities are worth verifying, and against what.
The interoperability problem
Tools are the only way an assistant can act (Episode 1). The Model Context Protocol (MCP) standardizes the interface: implement a tool once as a server, and any MCP-compliant client (the assistant) can use it.
MCP is a client–server protocol over JSON-RPC 2.0. A
server offers three object types — tools (callable
functions), resources (readable data), and
prompts (templated instructions). There are two
transports: stdio (the client launches the server as a
subprocess and talks to it over standard input/output) and
streamable HTTP for servers reached over the network.
The lesson’s servers run inside eic-shell and speak streamable HTTP on
127.0.0.1.
%%{init: {'theme':'base', 'themeVariables': {'fontSize':'15px','lineColor':'#94a3b8','edgeLabelBackground':'#e2e8f0','clusterBkg':'#1f293720','clusterBorder':'#94a3b8','titleColor':'#94a3b8'}}}%%
flowchart LR
accTitle: {EIC MCP data tools}
accDescr: {EIC MCP data tools}
A["AI assistant<br/>opencode · Copilot · Cursor"]:::core <-->|"JSON-RPC / HTTP"| S["uproot tool server<br/>(MCP, in eic-shell)"]:::tool
S <-->|"uproot"| F["EDM4eic ROOT file"]:::data
classDef core fill:#e7efff,stroke:#4c6ef5,stroke-width:1.5px,color:#10204a;
classDef tool fill:#e6f7ed,stroke:#2f9e44,stroke-width:1.5px,color:#0b3d1f;
classDef data fill:#fff4e0,stroke:#f08c00,stroke-width:1.5px,color:#5c3b00;
Why run the servers inside eic-shell
The servers reuse the container’s own uproot,
xrdfs, and rucio, so dependencies are pinned
and one environment runs both analysis and tools.
eic-mcp up starts them as background HTTP services;
eic-mcp down stops them. They hold no state between
sessions.
The uproot tool server
The ePIC uproot
tool server reads ROOT/EDM4eic files with uproot and returns JSON
summaries — edges, counts, statistics, fit inputs — not raw
arrays. One caveat: get_file_structure on an EDM4eic file
lists all ~6,000 branches (megabytes of JSON); for schema questions
get_tree_info is the compact choice. It exposes 15 tools in
four groups:
| Group | Representative tools | Purpose |
|---|---|---|
| Inspection |
get_file_structure, get_tree_info,
get_branch_statistics,
validate_dataset_schema
|
enumerate trees, branches, types, and summary statistics |
| Single-file compute |
histogram_branch, execute_kernel
|
histogram a branch; run sandboxed NumPy/awkward over branches |
| Dataset (multi-file) |
get_dataset_file_list, histogram_dataset,
get_dataset_statistics,
execute_kernel_dataset,
estimate_dataset_cost
|
enumerate matching files, then accumulate operations across them |
| Asynchronous jobs |
submit_kernel_dataset, get_job_status,
get_job_result, cancel_job
|
dispatch long dataset jobs and poll them |
The execution sandbox
execute_kernel runs client-supplied Python in a
restricted environment: no import, no file or network I/O,
only np (NumPy) and ak (awkward) in scope.
Limits are enforced at compile time, and the code runs in a subprocess
with a 30-second wall-clock limit.
Start the servers
Start the servers from inside eic-shell (the first run bootstraps them automatically — see Setup):
This launches the uproot, xrootd, and rucio servers as MCP-over-HTTP
endpoints on 127.0.0.1, ports 9101,
9102, 9103. Stop them with
eic-mcp down; eic-mcp status shows what is
listening, and eic-mcp logs xrootd tails a server’s log
when something misbehaves. The assistant connects to those URLs.
If rucio answers but xrootd/uproot time out
The rucio catalog and the data store are different services. If
dataset queries work but every file access hangs, the XRootD store may
be temporarily down — check with
xrdfs root://epicxrd1.sdcc.bnl.gov:1095 ls /eic/EPIC/RECO
(inside eic-shell) and retry later. Your setup is fine; the store isn’t
answering.
Current campaigns (25.12.0 onward) are served from BNL disk, which is
what eic-mcp points the xrootd server at by default. Older
campaigns (up to 25.10.x) live on the JLab store instead — browse those
with
XROOTD_SERVER=root://dtn-eic.jlab.org XROOTD_BASE_DIR=/volatile/eic/EPIC eic-mcp restart
(a plain up skips servers that are already running, so the
new setting would never take effect). Either way, rucio
replicas always tell you where a file really is.
If instead every uproot call starts timing out after one big
one, the server is busy, not broken: it handles one
request at a time, and a call your client gave up on is still running.
Wait a minute, or clear it with
EIC_MCP_SERVERS=uproot eic-mcp restart. Do not let the
assistant “work around” it by installing packages or reading the file
itself — AGENTS.md (Episode 4) forbids exactly that.
Connect the assistant
opencode reads its server list from a JSON config. In the directory where you will launch opencode, one command writes it:
The file it writes, opencode.jsonc, is just the three
server URLs — print it with eic-mcp config opencode -
(committed as the example files/mcp-config/opencode.jsonc):
JSON
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"uproot": { "type": "remote", "url": "http://127.0.0.1:9101/mcp", "enabled": true },
"xrootd": { "type": "remote", "url": "http://127.0.0.1:9102/mcp", "enabled": true },
"rucio": { "type": "remote", "url": "http://127.0.0.1:9103/mcp", "enabled": true }
}
}
Within a session, /mcp lists the connected servers and
their tools.
Which side am I on?
Only the servers need eic-shell — they use the
container’s uproot, xrdfs, and signed-in
rucio. The client just talks to
http://127.0.0.1:910x/mcp, so run it where you normally
work, and put its config in the directory you launch it from.
- Linux and Windows/WSL: the same URLs work inside and outside the container.
-
macOS: you published the ports in Setup; on the Mac itself, copy
files/mcp-config/opencode.jsoncrather than runningeic-mcp config(it needs the container). -
Can’t install a client? eic-shell ships
claudeandcopilot— run one inside it.
Finding the data with MCP
You never download a dataset. The other two MCP servers let the
assistant find and verify the real files — replacing the manual
rucio + xrdfs recipe — and
uproot-mcp then reads them straight from the store.
%%{init: {'theme':'base', 'themeVariables': {'fontSize':'15px','lineColor':'#94a3b8','edgeLabelBackground':'#e2e8f0','clusterBkg':'#1f293720','clusterBorder':'#94a3b8','titleColor':'#94a3b8'}}}%%
flowchart LR
accTitle: {EIC MCP data tools}
accDescr: {EIC MCP data tools}
R["rucio-mcp<br/>list_dids · list_files · list_file_replicas"]:::tool -->|"DID + root:// replica URLs"| X["xrootd-mcp<br/>list_datasets · check_file_exists · get_dataset_event_statistics"]:::tool
X -->|"verified root:// paths"| U["uproot-mcp<br/>analyze in place"]:::core
classDef tool fill:#e6f7ed,stroke:#2f9e44,stroke-width:1.5px,color:#0b3d1f;
classDef core fill:#e7efff,stroke:#4c6ef5,stroke-width:1.5px,color:#10204a;
-
rucio-mcpqueries the data-management catalog:list_didsfinds the dataset identifier (DID) by name,get_did_metadataandlist_filesdescribe its contents,list_file_replicasreturns the physicalroot://locations. -
xrootd-mcpworks directly on the store:list_campaigns/list_datasetsbrowse it,list_directoryandcheck_file_existsenumerate and verify files, andget_dataset_event_statisticsreports total events across a dataset.
rucio tells you what the dataset is and where its
replicas live, xrootd confirms the files are there, and
uproot-mcp reads a root:// URL in
place.
rucio works automatically — no key
Inside eic-shell, rucio-mcp signs in to the
authenticated catalog with the shared, read-only eicread
account. No password or grid proxy. The xrootd path is public, so to
only browse the store you can use xrootd-mcp
alone.
List the available campaigns
ePIC data is organized by production campaign — a
version such as 26.06.0 — together with the beam/target and
physics, all encoded in the rucio DID
(e.g. epic:/RECO/26.06.0/epic_craterlake/DIS/pythia8.316-1.0/NC/noRad/ep/18x275/...).
Before locating a specific dataset, check which campaigns exist so you
use a current one:
Using the rucio tools, find which production campaigns are available (the version field in the DIDs, e.g. 26.06.0) and show the most recent few.
The assistant calls list_dids
on scope epic and groups the DIDs by their campaign
component. Watch how it does this: the catalog holds thousands of DIDs
and the pages are not sorted newest-first, so sampling one page can miss
the current campaigns entirely. Narrowing with a version wildcard
(/RECO/26.*) — or one call to the xrootd
server’s list_campaigns — gives the honest answer.
Exercise: locate a dataset (≈ 10 min)
With rucio and xrootd connected (no
credentials — see the callout), ask your assistant:
Use the rucio tools to find the ePIC reconstructed-DIS dataset for the BeAGLE eCu ep 10x115 GeV sample in campaign 26.04.1, list its files, then use the xrootd tools to confirm those files exist on the store and report the total number of events.
The assistant calls list_scopes/list_dids
(scope epic, narrowing by a name glob on the campaign and
beam/target) to find the DID, list_files to enumerate it
(374 files), and list_file_replicas for the
root:// URLs. It then switches to xrootd-mcp
(list_directory_filtered, check_file_exists)
to verify the files. For the event total: rucio does not store event
counts, and scanning all 374 files would take an hour — so a sensible
assistant checks a few files (≈ 1,220 events each) and extrapolates. The
DID is discovered with list_dids, not hard-coded —
what you want when campaign names change.
Inspect the dataset
You say what you want in plain language and the assistant makes the
matching tool calls. Take one of the root:// URLs from the
previous exercise — written below as
root://epicxrd1.sdcc.bnl.gov:1095//… — and analyze it
in place.
Exercise: enumerate the schema (≈ 10 min)
Issue the request:
Using the uproot tools, report the structure of the events tree in root://epicxrd1.sdcc.bnl.gov:1095//<your-discovered-file>.root and list the members of the ReconstructedChargedParticles collection.
The assistant calls get_tree_info on the
events tree (not the full get_file_structure
dump, which runs to megabytes on an EDM4eic file) and reports something
like:
OUTPUT
File: root://epicxrd1.sdcc.bnl.gov:1095//…/<dataset-file>.root
Tree: events — branches grouped by collection
ReconstructedChargedParticles collection:
ReconstructedChargedParticles.PDG int32[] PDG particle-ID code
ReconstructedChargedParticles.momentum.x float[] p_x [GeV]
ReconstructedChargedParticles.momentum.y float[] p_y [GeV]
ReconstructedChargedParticles.momentum.z float[] p_z [GeV]
… energy, charge, mass, type, referencePoint.*, covMatrix.*
The names are read from the file, not inferred — eliminating the schema-hallucination failure mode from Episode 1. These are the branches you’re looking for.
Exercise: identify the species present (≈ 10 min)
Issue the request:
Histogram ReconstructedChargedParticles.PDG with one bin per integer code, so I can see the reconstructed particle species in the file.
The assistant calls histogram_branch, setting the bins
and range so each integer code gets its own bin (the default
auto-binning would merge neighbouring codes, e.g. 0 and 11). The
distribution is discrete — spikes at the PDG codes present. Counting
over a reconstructed-DIS file gives, for example:
OUTPUT
PDG species count
-211 pi- 11447
211 pi+ 9885
11 e- 4489
0 unID 2971 <- tracks with no PID hypothesis
-321 K- 1662
321 K+ 1588
-11 e+ 967
-2212 pbar 693
2212 p 684 <- protons are rare
Pions dominate; protons are rare (≈ 2%), so the Λ⁰ signal will be small. A sizeable fraction of tracks carry no PID (code 0) or a wrong one — misidentification that feeds the combinatorial background and is why we fit the peak rather than count it.
Verify the returned quantities
Inspect the returned numbers — bin edges, counts, statistics: do the PDG peaks fall at physical codes, and are the proton and pion yields plausible? Episode 4 turns this into explicit success criteria.
One data model, several access paths
ePIC data follow the PODIO model (EDM4eic): an
events tree whose branches are per-event collections such
as ReconstructedChargedParticles and
MCParticles. We read it with uproot
because that needs only eic-shell — no compiled framework. uproot is one
of several equivalent access paths.
Equivalent implementations
The same Λ⁰ peak comes out of:
- ROOT RDataFrame — declarative, columnar, parallel;
- ROOT TTreeReader — an explicit event loop;
- bare uproot — Python with no tool server; and
- the PODIO Frame API — the native interface.
Worked implementations of each are in Alternative analysis approaches.
The assistant can now query the data through a verifiable interface. The next episode captures this procedure as a reusable, versioned skill.
- MCP is a JSON-RPC client–server protocol; a server exposes tools, resources, and prompts to any compliant client.
- The uproot server returns compact, JSON-serializable summaries rather than raw arrays, which keeps results inspectable.
-
execute_kernelruns client-supplied Python in a restricted sandbox: no imports or I/O, only NumPy/awkward, with a timeout. - The servers run inside eic-shell (
eic-mcp up) and speak streamable HTTP; opencode and other clients connect to the same127.0.0.1URLs (eic-mcp config <client>). - PODIO/uproot is one access path; RDataFrame, TTreeReader, and bare uproot give the same result (see the extras).