MCP Ops Agent
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
MCP Ops Agent
Live demo: upkero.com. The agent there runs anthropic/claude-haiku-5.5
through OpenRouter and reads synthetic data from ops-core-api.
A real Model Context Protocol server (built on FastMCP from the official MCP Python SDK) for an operations desk, plus a self-consuming agentic orchestrator that reaches those tools the same way any external client does — over genuine MCP JSON-RPC. The tools are defined exactly once and every consumer goes through the protocol; nothing calls the business logic behind its back.

The live demo: every tool call and result arrives as its own SSE event.
The service owns no database. All data comes from a separate internal API
(ops-core-api) over HTTP; this repo is a pure
MCP + orchestration layer.
A companion project. The MCP-specific architecture notes are in
docs/architecture.md.
What it does
Five MCP tools, each a thin wrapper over a service that calls ops-core-api through an
interface:
Tool | Does |
| Is a slot free? Returns match/availability/capacity and other free times that day. |
| Lists every service on the price list with its unit price. |
| Finds customers by name fragment or exact UUID. |
| Prices a service with volume discounts (money stays exact). |
| Simulated — logs the send and returns a receipt; nothing is actually delivered. The recipient must be one existing customer (id or full name, checked against |
Related MCP server: dat-ai-mcp
Two consumers, one server
flowchart LR
CD["Claude Desktop /<br/>MCP Inspector"] -- "MCP over<br/>Streamable HTTP" --> MCP
BR["Browser<br/>(portfolio UI)"] -- "SSE" --> API["POST /api/v1/invoke"]
API --> ORC["Orchestrator<br/>(LLM + MCP client)"]
ORC -- "MCP over<br/>Streamable HTTP (self-loopback)" --> MCP["FastMCP server<br/>/mcp/"]
MCP --> TOOLS["mcp/v1/tools"]
TOOLS --> SVC["services"]
SVC --> GW["ops-core gateways"]
GW -- "X-API-Key" --> CORE["ops-core-api"]External MCP client (Claude Desktop, MCP Inspector, …) connects straight to
/mcp/over Streamable HTTP and drives the tools by hand.Internal orchestrator is itself an MCP client to the same
/mcp/: an LLM loop that lists the tools, calls them over the protocol, and streams each step.POST /api/v1/invokeis a thin HTTP route over the orchestrator. It streams the agent's steps to the browser as Server-Sent Events — you watch the agent decide, call a tool, read the result, and answer — without ever putting the LLM key or an MCP client in the browser.
Because the orchestrator connects to the server's own mounted /mcp/ over real HTTP,
there is no code path that invokes a tool outside the MCP protocol.
Architecture
Layered, with a strict inward dependency rule (outer depends on inner, never the
reverse) — the shared portfolio architecture, plus a versioned mcp/v1/ layer that
mirrors api/v1/:
mcp/v1/tools ─┐ api/v1 (HTTP: SSE route, health)
├─► services (business logic) ─► interfaces (ABC) ◄─ gateways / llm
orchestrator ──┘ ▲
contracts (DTOs) ──────────┘ core · exceptions · bootstrapgateways/ rather than repositories/: this service owns no store, so every adapter is
somebody else's service over the network.
Design patterns (named and commented in the code):
One place each client is built —
llm/factory.pybuilds the SDK client andgateways/ops_core/client.pythe ops-core HTTP client.Adapter — the httpx
gateways/ops_core/*adapters andgateways/agent/mcp_tool_gateway.py(an MCP client behind a plain interface).Delivery port, one simulated implementation —
NotificationChannel(a real email/SMS channel is a new implementation, but read the notification limits under Exposing this service first).Services depend on interfaces — the DI container in
bootstrap/container.pywires the concretes, so a test swaps in a fake without patching anything.
Run locally
Prerequisites: Docker, and a running
ops-core-api with its demo data loaded; plus
an OpenAI API key (or any OpenAI-compatible, tool-calling endpoint).
cp .env.example .env
# edit .env: set LLM_API_KEY, and OPS_CORE_API_KEY / OPS_CORE_BASE_URL to match ops-core-apiDocker — this agent has no database of its own, so compose runs just the service, and
reaches ops-core-api through host.docker.internal. There is no shared docker network,
so both compose files stay independent:
docker compose up --build # agent on http://127.0.0.1:8003The port is published on loopback only (127.0.0.1:8003:8000) on purpose — see
Exposing this service. Inside the container it still listens on
8000, which is why AGENT_MCP_SELF_URL stays http://localhost:8000/mcp/. With
ops-core-api running on the host, set
OPS_CORE_BASE_URL=http://host.docker.internal:8000.
Without Docker — pick 8003 so ops-core-api keeps 8000, and move the self-loopback to
the same port or the orchestrator will call nothing:
uv sync
AGENT_MCP_SELF_URL=http://localhost:8003/mcp/ \
uv run uvicorn src.main:app --reload --port 8003Try it
Composite request over SSE (-N disables curl buffering so you see events arrive).
POST /api/v1/invoke needs the X-API-Key header; set SECURITY_API_KEY to the value in
your .env (see Auth below), or the call answers 401 invalid_api_key:
curl -N -X POST http://localhost:8003/api/v1/invoke \
-H "X-API-Key: $SECURITY_API_KEY" -H "Content-Type: application/json" \
-d '{"message": "Is the 18:00 table free tomorrow, and can you look up Anna Petrova?"}'event: tool_call
data: {"id":"call_1","name":"check_calendar_availability","arguments":{"date":"2026-07-26","time":"18:00","resource_type":"table"}}
event: tool_result
data: {"id":"call_1","name":"check_calendar_availability","content":"{\"available\": true, \"capacity\": 4, ...}","is_error":false}
event: tool_call
data: {"id":"call_2","name":"lookup_customer","arguments":{"name_or_id":"Anna Petrova"}}
event: tool_result
data: {"id":"call_2","name":"lookup_customer","content":"{\"count\": 1, ...}","is_error":false}
event: final
data: {"content":"The 18:00 table is free tomorrow (seats 4), and I found Anna Petrova (active)."}Health — liveness is the process alone; readiness reports both upstreams:
curl http://localhost:8003/health/live # {"status":"ok"}
curl http://localhost:8003/health/ready # {"status":"ok"}; 503 {"detail":"dependencies unavailable: llm",...} if a dependency is downDocker's HEALTHCHECK polls /health/live, so the container is not marked unhealthy
because the LLM provider is having a bad morning.
Auth — POST /api/v1/invoke requires SECURITY_API_KEY (≥16 chars) as X-API-Key.
The service does not start without it; .env.example ships the portfolio's shared
placeholder change-me-min-16-chars:
curl -N -X POST http://localhost:8003/api/v1/invoke \
-H "X-API-Key: $SECURITY_API_KEY" -H "Content-Type: application/json" \
-d '{"message":"..."}'Connect an external MCP client to /mcp/
Run the service locally and point your MCP client at your own localhost — that is the
intended shape, not a hosted URL. Use the URL with the trailing slash: the endpoint is
/mcp/, and a request to /mcp is answered with a 307 redirect that not every client follows.
MCP Inspector (quickest way to see tools and call them live):
npx @modelcontextprotocol/inspector
# In the UI: Transport = "Streamable HTTP", URL = http://localhost:8003/mcp/ → ConnectClaude Desktop — bridge stdio to your local endpoint via mcp-remote in
claude_desktop_config.json:
{
"mcpServers": {
"ops-agent": {
"command": "npx",
"args": ["mcp-remote", "http://localhost:8003/mcp/"]
}
}
}Restart Claude Desktop; the five tools appear and execute against the very same server the internal orchestrator uses.
Observability
Traces: OpenTelemetry spans for each request, ops-core call and LLM completion. The trace continues across the MCP self-loopback through the request's
_meta, so a tool's span is a child of the agent run that called it.Metrics:
agent.runsby outcome,agent.run.duration,agent.iterations,agent.refusals,mcp.tool.callsandmcp.tool.duration, thegen_ai.client.*LLM histograms andllm.costin USD.Export: set
OTEL_EXPORTER_OTLP_ENDPOINT; unset, spans are still created and nothing leaves the process.Logs: JSON lines carry
trace_idandspan_id; each run ends with oneagent.run.finishedline (outcome, steps, tokens). Customer names reach neither logs nor spans: the query string is stripped from span URLs.Dashboards: Grafana overview, LLM cost and per-service boards in
portfolio-deploy.
Exposing this service
/mcp is unauthenticated: MCP clients rely on the protocol's own auth story, and the
self-loopback needs no key. That is fine on localhost and not fine on the open
internet, so this is a deployment decision rather than a code one:
/mcpis closed at the perimeter. OnlyPOST /api/v1/invoke, with its ownSECURITY_API_KEY, faces outward.Replace the placeholder
SECURITY_API_KEYbefore exposing anything publicly. The per-IP rate limit (INVOKE_RATE_LIMIT_PER_MINUTE, 20 by default) protects against abuse, not against cost: 20 requests a minute from one address × up toAGENT_MAX_STEPSLLM calls each is real money.Compose publishes on
127.0.0.1:8003rather than0.0.0.0:8003so none of the above can happen by accident on a machine with a public IP.Whoever holds
SECURITY_API_KEYreaches everything the tools return throughPOST /api/v1/invoke: customer records including their notes. The public demo runs on synthetic data; with real data, treat the key accordingly./mcpis protected only by the SDK's DNS-rebinding check: aHostthat is notlocalhost/127.0.0.1gets421, a foreignOrigingets403. Behind a reverse proxy that forwards a publicHost,/mcptherefore answers421; keep it internal.What bounds one run:
AGENT_MAX_STEPS,AGENT_RUN_TIMEOUT_SECONDS(60), at mostAGENT_MAX_TOOL_CALLS_PER_STEP(4) tool calls per LLM turn,AGENT_MAX_NOTIFICATIONS_PER_RUN(3) andLLM_MAX_TOKENS(1024) per turn. The tokens a run used are logged asagent.run.finished.One rate-limit bucket per source address. Behind a proxy or a site bridge every visitor arrives from the same address and shares the 20 a minute; limit per visitor there.
Tests
uv run ruff check .
uv run mypy src
uv run pytest --cov=src/app/services --cov-report=term-missing --cov-fail-under=60The suite includes unit tests for every service (mocked interfaces), Pydantic
tool-argument validation, the orchestrator loop (scripted LLM + fake gateway), an
in-memory MCP round-trip, and — the headline — a hermetic real-loopback test
(tests/integration/test_self_loopback.py) that boots the whole app on a loopback port
and asserts a compound request streams two sequential tool_call events then a final
answer, with the orchestrator self-calling the mounted /mcp/ over genuine Streamable
HTTP. No live LLM or ops-core-api is required — both are mocked.
Configuration
All via environment (see .env.example); grouped by prefix:
Prefix | Concern |
| Log level and format (json/text). |
| Provider, model, key, base URL — OpenAI-compatible. |
|
|
|
|
|
|
|
|
Three things worth knowing before the first run:
The default provider is OpenAI, deliberately — unlike the sibling services, which default to a local Ollama model. An agentic tool-calling loop is unreliable on a small local model, and reliable tool calling is precisely what this service exists to demonstrate. Point
LLM_BASE_URLat Ollama if you would rather trade that for free.CORS_ALLOWED_ORIGINSis empty by default, which blocks every browser client. A frontend needs its origin listed there before it can call in.The
OPS_CORE_API_KEYplaceholderchange-me-min-16-charsis shared by all five services in the portfolio and is rotated in all five at once, so it matches every sibling's.env.example. You still setLLM_API_KEYyourself.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Operator-as-agent MCP hub. 6 tools. First $5 free, then $0.001/call.
184 MCP tools for field-service CRM, scheduling and double-entry accounting. Hosted; BYO agent.
AI agent platform: manage leads, conversations, bots, calendar and CRM via MCP.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceProvides AI agents with operational customer context, including typed revenue objects, persistent state, scoped tools, and human-in-the-loop handoffs through MCP, REST, and CLI.6 npm13Apache 2.0
- AlicenseAqualityDmaintenanceProvides browser automation, audio transcription, and LLM chat as MCP tools for any agent.7MIT
- FlicenseNot gradedqualityCmaintenanceProvides MCP tools for hybrid knowledge base search, grounded Q&A with citations, agent execution, and ticket/account lookups.-
- FlicenseAqualityCmaintenanceProvides reusable MCP tools for Amazon Connect AI Agents and Bedrock AgentCore to look up customers, manage support tickets, schedule appointments, update CRM records, retrieve knowledge, and create tasks.7-