HTCondor Workshop · Lyon · 2026‑10‑01

The Facility Is
the Context

Building an MCP Platform for Scientific Computing

University of Chicago, MANIAC Lab, Enrico Fermi Institute, IRIS-HEP, CERN
Agentic intelligence for research computing
Slides AI-assisted by Claude (Anthropic) · title illustration AI-generated. · HTCondor Workshop Autumn 2026

The big picture

From prompt to discovery

$ claude mcp add --transport http atlas-af https://mcp.af.uchicago.edu/mcp

what is needed behind this URL?

Claude Code · any model · any harness

› Using the atlas-af mcp, let's find a 1 TeV Higgsino in the Run 2 + Run 3 dataset.

Adapted from “Towards Agentic Analysis”, ATLAS AMG Weekly, 2026-09-04

Where the work is

Swap the model freely; context makes it useful

Modelfrontier API · or self-hosted open weights
HarnessClaude Code · Codex · Copilot
Facility context & toolsdata, storage, batch, identity, fits · via MCP servers

The first two change week to week. That isn't what limits agentic workflows: it's reliable, secure access to the tools, data, and identities users already have.

Same prompt: “Where should my job outputs go?”

Generic model

“Next to the job, in your home area.”

→ big files fill the home quota in minutes

Facility-aware model

“Big outputs to fast local scratch; small summaries in your backed-up home.”

→ same model, more context

Everything here runs in production at the UChicago ATLAS Analysis Facility · mcp-portal.af.uchicago.edu

What is the Model Context Protocol?

An open protocol connecting AI assistants (LLMs like Claude or Gemini) to the libraries, databases, and services scientists already use.

AI assistantClaude, Gemini CLI, VS Code, …

MCP (JSON-RPC)

⟷

MCP serveryou write this · runs on your laptop or hosted at a facility

Python · subprocess · REST

⟷

Existing library or servicerucio.client, pyAMI, HTCondor, …

tools functions the LLM calls     resources data it reads     prompts reusable templates

modelcontextprotocol.io · not prompt engineering, not a custom foundation model

Around an existing Python project · backup

from fastmcp import FastMCP
from rucio.client import Client

mcp = FastMCP("rucio")
client = Client()

@mcp.tool()
def list_dids(scope: str,
              pattern: str = "*") -> str:
    """Search datasets in a scope by wildcard.

    Use a narrow pattern; results are paginated.
    """
    return ", ".join(
        client.list_dids(scope, {"name": pattern}))

mcp.run()

rucio-mcp started from this barebones structure

  • Everything the LLM sees is right here: name list_dids, schema scope: str, pattern: str = "*", description = the docstring
  • Wrap rucio.client, not the CLI: structured data in, curated text out
  • Start on stdio on your laptop; add HTTP when a facility hosts it
  • fastmcp handles the JSON-RPC; you only write the function

Capabilities and knowledge

MCP: arms and hands

The facility lets my agent do things and learn things:

  • find datasets and metadata
  • submit and watch batch jobs
  • read my files, run my notebooks

Skills: domain expertise

The USATLAS marketplace gives it know-how:

  • af-bnl, af-uchicago: how each facility works
  • atlas: Rucio, AMI, Open Data, pyhf, cabinetry, HistFitter, ServiceX, …
  • hep-python-tools: generic HEP Python tooling
“Towards Agentic Analysis”, ATLAS AMG Weekly, 2026-09-04 · github.com/usatlas/marketplace

A facility-native MCP server must be…

Convenient

No extra barriers for analyzers*; self-service access to what the facility already offers.

Secure

Never leak your credentials to the agent; never leak sensitive experiment data**.

Auditable

Telemetry on usage: which tools get used, which don't.

*apart from having your own LLM assistant  ·  **depends on the experiment’s guidance

“Towards Agentic Analysis”, ATLAS AMG Weekly, 2026-09-04

Every MCP tutorial vs. the UChicago AF

every MCP tutorial

Agent

→

MCP server

→

Backend

the UChicago AF, in production

User

→

OIDC identityKeycloak

→

AgentMCP client

→

Gateway + brokerauthn · authz · audit

→

Domain MCPRucio, AMI, …

→

Credential custodiansVOMS · krb5 · condor

→

Scientific servicegrid · KDC · schedd

The MCP layer

The credential layer

7 services behind one gateway · 124 tools · 6 credential types · ~900 AF users · live gateway snapshot, 2026‑09‑05 · ↓ architecture overview

Overview · the platform boundary

A credential-brokered MCP gateway

One endpoint in front of the facility’s services. The LLM client never holds raw credentials; the broker authenticates the user, mints per-service credentials, and audits every tool call.

Claude, Gemini or any MCP client to the mcp.af.uchicago.edu front door (oauth2-proxy, AF Keycloak OIDC), through a FastMCP aggregator and AF credential broker (FastAPI /v1) doing identity, authZ, credential brokering and audit, fanning out to rucio-mcp, ami-mcp, openmagic, panda-mcp, condor-mcp, gitlab-mcp, jupyter-control and an Nth backend with no code change

Identity, authZ, credential brokering, and audit in one place: the same boundary a facility wants in front of its services, with or without AI.

Illustrative (July 2026); current services on the previous slide · rucio-mcp · ami-mcp · atlasopenmagic · panda-mcp · condor-mcp (Bockelman, CHEP 2026).

The first design choice

One MCP, many, or a gateway?

one process boundary atlas cms dune

One server, many sites

one deploy   per-VO auth in one process

atlas server cms server dune server

One server per site

clean isolation   N servers to run

gateway sub-MCP sub-MCP sub-MCP

Single gateway

one identity   token sprawl / choke point

We run the gateway: one identity, one place to authorize and audit. Next: how we handle token sprawl and the choke point.

FastMCP composition = one auth provider per server; that constraint drives a lot of the design.

The MCP Platform: one gateway, many MCP servers

mcp-portal.af.uchicago.edu · live

AI Assistant
MCP Gateway
Auth Policy Audit Broker
Rucio
HTCondor
Jupyter
Storage

Credentials stay behind the broker

The assistant never holds a raw credential; the user's token is never forwarded.

Authorize every call

Group-based access; denials audited too.

Short-lived, scoped

Minted per service, cached at the broker, reused until expiry; the per-call copy is deleted after use.

The agent speaks intent ("get this dataset", "run on the cluster"); the facility wires the backend. Infrastructure‑as‑Config: the Nth MCP server is Helm values on Kubernetes, never a change to the server.

mcp-portal.af.uchicago.edu · the portal landing page's own animation (af-mcp-platform).

The same call, step by step · deep-dive

1

Claude calls rucio-atlas_rucio_list_dataset_replicas with its personal access token

2

Gateway authenticates; groups and POSIX identity come from the IdP (Keycloak) on every call

3

Authorized once: this tool requires read_data; denials are audited too

4

Broker mints a JWT: aud=rucio-mcp-atlas (one service), exp = 10 min; it says who you are, not what you may do

5

rucio-mcp verifies that JWT, then redeems the VOMS proxy directly from the broker's vault

6

One Rucio call as the user; proxy deleted; one audit record, traced and metered

The user's token is never forwarded downstream; proxies and tickets never pass through the gateway.

Identity is ambient, never an argument

@mcp.tool()
async def create_jupyter_server(
    image: str,
    name: str | None = None,
    cpu_cores: int = 2,
    memory_gb: int = 8,
    ...
) -> str:
    """...The server is always owned by the caller
    (from their verified broker identity) --
    there is no owner argument. ..."""
    claims = await get_broker_claims(ctx, verifier)
    ... create_notebook(owner=claims.unixname, ...)
  • No tool takes a username. The schema cannot express "read bob's files"
  • One rule, different ways to implement, e.g.: k8s owner labels · kernel uid/gid · the VOMS nickname in your proxy
  • Who you are comes from the identity provider (e.g. Keycloak) on every call, not from what a token claims

The principle

Do not expose dangerous parameters; derive them from verified identity.

Keep credentials out of the MCP

Lifetime == one call

e.g. a VOMS proxy or Kerberos ticket: fetch it → 0600 temp file → one backend call → delete it. The MCP server keeps nothing.

Exchange, never forward

The user's token never goes downstream (the spec forbids it). Each hop gets a fresh short-lived JWT, valid for one service, expiring in 10 minutes.

One service per secret

e.g. only condor-token-service touches the Condor pool key; the broker is its only allowed caller, and it returns short-lived IDTokens.

bad: a token service deciding permissions

if "atlas-users" in token["groups"]:
    mint_ticket(user)
# a second copy of the access rules,
# trusting whatever the token claims

good: it only verifies who is calling

claims = verify(token, aud="krb5-token-service")
mint_ticket(claims["sub"])
# the gateway already decided permissions;
# this token proves who, from the broker

sketch; written into all three token services' docstrings (krb5 app.py)

Never store secrets in the MCP server. Keep them in a vault (OpenBao, here) behind the broker, and mint short-lived credentials per call.

The custodians · deep-dive

voms-token-service

The only pod that sees home dirs and Globus passphrases. Runs voms-proxy-init impersonated; privilege floor found live, five times.

krb5-token-service

Password → kinit → ticket; zero secrets at rest, passwords zeroed on every exit path. Rate limiter protects CERN's lockout counter, not itself.

condor-token-service

Runs condor_token_create next to the pool key, which "must never reach the broker": it can mint any identity.

Why three services, not one

Different privilege floors (root+caps / none / one init container). Merging them forces the union of privileges onto everything. Each is a one-question audit: "did this call come from the broker?"

Orchestrate the hardened native binary; parse artifacts (ccache bytes, PEM), never CLI prose.

Lessons learned · ↓ four more

What breaks at ten MCPs (that one never sees)

Design decisions baked into the AF MCP Platform

  • Name collisions: two MCPs serve a tool with the same name; fix = a prefix per service (rucio-atlas_*, fs_*)
  • Each service gets its own fixed OAuth audience, pinned in config, never derived from a name that can change
  • Listing needs credentials too: some MCPs require auth even for tools/list, so the gateway mints one just to enumerate
  • New tools are opt-in: when an MCP releases a tool, it stays disabled until an operator maps it at the broker
  • Authorization is plain RBAC: Keycloak group → permission → tool
  • A dead backend disappears from tools/list instead of every call failing

Adding the Nth MCP

Each MCP is added via config (Infrastructure‑as‑Config: Helm charts on Kubernetes). Sometimes it needs a token service, sometimes a bridge, never changes to the MCP itself.

af-mcp-platform issues #113, #257, #121 · 289 commits, 77 days, 1,337 tests. ↓ more scars

Lessons learned · deep-dive

An existing API is not an agent interface

bad: the raw API dump

{"logicalDatasetName": "mc23_13p6TeV.601229...",
 "crossSection": "0.729", "crossSection_unit": "nb",
 "genFiltEff": "1.0", "kFactor": "1.0",
 "principalPhysicsGroup": "...", "prodsysStatus":
 "...", ...95 more fields, duplicate rows...}

every field, no units help, and the same for each of 50 datasets

good: markdown-friendly, LLM-friendly

**mc23_13p6TeV.601229** (PhPy8EG ttbar) · VALID
Cross-section: 729 pb (0.729 nb × 1000)
Events: 2M · k-factor: 1.0

---
**Next steps:**
- ami_get_dataset_prov for provenance
- rucio_list_dataset_replicas to locate copies

~15 curated fields, units converted in the open, follow-up tools named

The docstrings keep getting fixed against each tool's actual behavior, to teach the LLM how to use it correctly. The LLM reproduces examples verbatim, so a doc bug is a behavior bug.

Sketch abridged from real ami-mcp output; the curation commit is 901f339 "optimize MCP tools for LLM usefulness".

Lessons learned · deep-dive

Errors are the agent's control flow

bad: what the agent sees by default

Traceback (most recent call last):
  File ".../replicaclient.py", line 158, ...
rucio.common.exception.DataIdentifierNotFound:
  Data identifier 'user.gstark:mydata'
  not found

a dead end: nothing tells the agent what to try next

good: what rucio-mcp returns instead

Error: Data identifier
'user.gstark:mydata' not found

**Recovery:** The DID does not exist in
Rucio. Use `rucio_list_dids` with a
wildcard pattern to search for it.

returned to the agent, not raised into server logs; the next tool is named

Empty results carry the same routing hints. The error categories also feed my monitoring, so I can see which tools confuse the agent.

MCP results have an isError flag so clients can tell failure from success. I didn't set it at first; do both.

The classifier behind the right panel: rucio-mcp tools/_helpers.py (classify_error).

Lessons learned · deep-dive

Context is a bounded resource

bad: rucio_list_files("mc23..."), unbounded

mc23_13p6TeV:file_000001.root
mc23_13p6TeV:file_000002.root
mc23_13p6TeV:file_000003.root
... 499,997 more lines ...

one call floods the LLM's entire context window, and the server downloaded all 500k names to do it

good: capped, and the output says so

mc23_13p6TeV:file_000001.root
... 49 more ...

---
Showing 50 results (offset=0).
Pass `offset=50` to see more.

a hard server-side limit, announced in the output, with the way to continue spelled out

And cap the server's work too: rucio-mcp once fetched that whole 500k-file container upstream just to serve one 50-row page. Paginate the upstream iterator, not just the output.

The real footer text and iterator fix: rucio-mcp tools/_helpers.py (paginate_iter).

Tried X, discovered Y, ended with Z · backup

TriedDiscoveredEnded with
Rust CLI wrapperparsing text tables is fragilePython + native client
Returning raw API rowsnoise swamped the LLMcurated fields, converted units, next steps
Docstrings written from memorythe LLM reproduced broken examples verbatimdocstrings tested against the live service
Stateful MCP sessions, 2 replicascross-replica "Session terminated"stateless HTTP (now the spec default)
Dynamic Client Registrationyou maintain a database of client ids, and a restart orphans them allCIMD (later adopted by the spec)
Groups inside JWT claimsrevocation waits for token expirylive IdP lookup, a real kill switch
Full catalogue with commit hashes: mcp-design-talk.md §7.2 (about 40 of these).

Also on the facility's plate · deep-dives linked

Other technical considerations

Client identity (CIMD)

The client_id is a URL the server fetches: no registration database, and the facility picks which domains it trusts. Watch for SSRF.

why · mechanics · live trace

Shell access (ssh-oidc)

Swap permanent SSH keys for short-lived federated credentials. An agent with a shell inherits minutes, not a key that lives forever.

overview · PAM vs. SSH cert

Sandboxing

Pods, then bubblewrap off Kubernetes, then eBPF policy, then human-gated privilege. Harnesses can advise, but can't reliably hard-deny.

overview · cgroups vs. eBPF

Deep-dives from the Nikhef colloquium, 2026‑07‑01 · ssh-oidc (KIT / EOSC-synergy) · ebpf.io

Same context layer, for operators

AI at every layer of the facility

Analyst-facing

Find data, generate code, submit, recover, fit. Low autonomy, high convenience.

Operations

Cluster health reports, hold-reason triage, drafted user emails, human-approved.

Sysadmin-facing

Config/runbook copilots in the privileged lane; trust earned, actions gated.

Daily HTCondor cluster report posted to Slack by the AI monitor agent

Morning report: 448 held of 14,671 (3.1%)

Drafted held-job email posted to Slack, approved by a human, then sent by the agent

Drafted fix → human types “approve” → sent

Same context layer, different autonomy and blast radius at each level. The facility decides where the human gate sits.

live · the UChicago AF

Demo

$ claude mcp add --transport http atlas-af https://mcp.af.uchicago.edu/mcp

One prompt: submit an HTCondor job · start a Jupyter server · whoami · service catalog · AMI · Rucio · ATLAS OpenSearch · watch the job finish

mcp-portal.af.uchicago.edu · the demo prompt

Interfaces that travel

Across LLM providers

  • Design for the least-capable LLM client: e.g. Claude Desktop had no OAuth discovery, so I added personal access tokens; gemini-cli choked on an OAuth metadata fallback, so I deleted it
  • Standard OAuth, standard transports, no provider-specific hooks

Across experiments

  • rucio-mcp already serves ATLAS, CMS, DUNE, ESCAPE: the domain layer is VO-agnostic
  • Transplant the shape (broker + custodians + config-only services), not the CERN-specific code

Interoperable tool interfaces are likely to matter more than LLM-specific integrations.

↓ how my choices scored against the evolving MCP spec

Scored against the MCP spec · backup

My choiceMCP 2.0 spec (2026-07-28)Verdict
Stateless HTTP, state as explicit handlesprotocol sessions removedcompliant / anticipated it
CIMD instead of Dynamic Client RegistrationDCR deprecated for CIMDcompliant / anticipated it
Tools only; no sampling, roots, loggingall three deprecatedcompliant / anticipated it
Exchange tokens, audience-validate everywheretoken passthrough forbidden (MUST)compliant
outputSchema on tools that return datastructured content recommendedcompliant; fixed since PyHEP.dev
Read-only / destructive tool annotationsannotations recommendedcompliant; fixed since PyHEP.dev
Spec: modelcontextprotocol.io/specification/2026-07-28 · full comparison in mcp-design-talk.md §6.3 (outdated, pre-fixes).

When is an MCP server appropriate?

Build one when

  • a library or service scientists already use
  • assistants need it repeatedly, across clients
  • you want to control what context the agent gets injected with
  • you can bound outputs and make errors recoverable

Skip it when

  • a one-off script would do
  • the backend's API is still churning under you
  • the "tool" would hand the LLM a credential
From “Model Context Protocol for Scientific Software”, PyHEP.dev 2026 · the full checklist

Open questions for this room

What we don’t know yet

How should facilities expose context?

One MCP per service or a gateway delegating identity? Where does your audit live?

How do we share context between facilities?

Shipped with the facility, or a shared commons? Single-source vs forks; eval/lint for orthogonality; who keeps it true?

How do we evaluate, and secure, an autonomous operator?

Human gates, k8s vs bubblewrap/eBPF isolation, credential brokering so agents never hold raw secrets, a trust ladder with an eval-harness promotion gate.

What makes an environment truly portable?

pixi-native batch as the default? Supply-chain policy for conda-forge?

Who pays for inference, and is that fair?

Hosted frontier vs self-hosted open weights on facility GPUs; equity as policy.

What becomes standardized, across sciences?

A commons spanning facilities, experiments, and sciences: where’s the facility/experiment line?

My bet: the model and harness are the easy part. Context, identity, trust, and equity are the facility’s homework.

Generic LLM → facility-aware collaborator

Swap the model and harness freely. The facility’s context, identity, and credentials are what make it useful and safe, and that layer ports to the next facility.

Still hard: more brokered backends (PanDA next) · hard-deny for agents with a shell · testing against the live services, not mocks

Design the interface for the model; design the architecture for the credential.
Giordon Stark · kratsg · HTCondor Workshop Autumn 2026 · ~40 “tried X, found Y” lessons: mcp-design-talk.md · slides AI-assisted by Claude (Anthropic).
a599170 · 2026-10-01