MCP Ops Agent
Click on "Install 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., "@MCP Ops Agentlook up customer Jane Smith"
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.
MCP Ops Agent
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 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
Four 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. |
| 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. |
Related MCP server: carly-cli
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
/mcpover 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):
Factory —
llm/factory.py,gateways/ops_core/client.py(the only places the SDK / HTTP clients are built).Adapter — the httpx
gateways/ops_core/*adapters andgateways/agent/mcp_tool_gateway.py(an MCP client behind a plain interface).Strategy —
NotificationChannel(simulated now, real email/SMS later, no service change).Template Method — the orchestrator's fixed
run()loop skeleton.Dependency Inversion throughout — services depend on interfaces; the DI container in
bootstrap/container.pywires concretes.
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):
curl -N -X POST http://localhost:8003/api/v1/invoke \
-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","llm":true,"ops_core":true}; 503 if either is downDocker's HEALTHCHECK polls /health/live, so the container is not marked unhealthy
because the LLM provider is having a bad morning.
Optional auth — leave it open for the demo, or set SECURITY_API_KEY (≥16 chars) to
require X-API-Key on POST /api/v1/invoke:
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.
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 four tools appear and execute against the very same server the internal orchestrator uses.
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.Set
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.
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. |
|
|
|
|
| Optional |
|
|
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 a plaincp .env.example .envstill gives a working demo.
MCP Ops Agent (RU)
Настоящий MCP-сервер (Model Context Protocol, на FastMCP из официального MCP Python SDK) для операционного пульта и внутренний агент-оркестратор, который обращается к тем же инструментам так же, как любой внешний клиент — через настоящий MCP JSON-RPC. Инструменты определены ровно один раз, и каждый потребитель идёт через протокол; ничто не вызывает бизнес-логику в обход.
У сервиса нет собственной базы данных. Все данные приходят из отдельного внутреннего API
(ops-core-api) по HTTP; этот репозиторий —
чистый слой MCP и оркестрации.
MCP-специфика архитектуры — в
docs/architecture.md.
Что делает
Четыре MCP-инструмента, каждый — тонкая обёртка над сервисом, который обращается к
ops-core-api через интерфейс:
Инструмент | Что делает |
| Свободен ли слот? Возвращает совпадение/доступность/вместимость и другие свободные времена в этот день. |
| Ищет клиентов по фрагменту имени или точному UUID. |
| Считает стоимость услуги с объёмными скидками (деньги — точно, без float). |
| Симуляция — логирует отправку и возвращает квитанцию; на самом деле ничего не отправляется. |
Два потребителя, один сервер
Внешний MCP-клиент (Claude Desktop, MCP Inspector) подключается прямо к
/mcpпо Streamable HTTP и вызывает инструменты вручную.Внутренний оркестратор сам является MCP-клиентом к тому же
/mcp: LLM-цикл, который получает список инструментов, вызывает их через протокол и стримит каждый шаг.POST /api/v1/invoke— тонкий HTTP-роутер поверх оркестратора. Стримит шаги агента в браузер как Server-Sent Events: видно, как агент решает, вызывает инструмент, читает результат и отвечает — без переноса LLM-ключа или MCP-клиента в браузер.
Поскольку оркестратор подключается к собственному смонтированному /mcp по настоящему
HTTP, не существует пути, которым инструмент вызывался бы в обход MCP-протокола.
Архитектура
Слоистая, со строгим правилом однонаправленных зависимостей (внешние слои зависят от
внутренних, никогда наоборот) — общая архитектура портфолио плюс версионированный слой
mcp/v1/, зеркалящий api/v1/. Адаптеры лежат в gateways/, а не в repositories/:
своего хранилища у сервиса нет, всё внешнее и по сети.
Паттерны (названы и прокомментированы в коде): Factory (llm/factory.py,
gateways/ops_core/client.py), Adapter (httpx-адаптеры и mcp_tool_gateway.py),
Strategy (NotificationChannel), Template Method (цикл run() оркестратора),
Dependency Inversion — везде.
Запуск локально
Нужны: Docker и запущенный ops-core-api
с загруженными демо-данными; ключ OpenAI (или любой OpenAI-совместимый endpoint
с поддержкой tool-calling).
cp .env.example .env
# в .env задайте LLM_API_KEY, а также OPS_CORE_API_KEY / OPS_CORE_BASE_URL под ops-core-api
docker compose up --build # агент на http://127.0.0.1:8003Порт публикуется только на loopback (127.0.0.1:8003:8000) — см. «Публикация наружу».
Внутри контейнера сервис слушает 8000, поэтому AGENT_MCP_SELF_URL остаётся
http://localhost:8000/mcp. Если ops-core-api поднят на хосте, задайте
OPS_CORE_BASE_URL=http://host.docker.internal:8000.
Без Docker — берите 8003, чтобы 8000 остался за ops-core-api, и переведите
self-loopback на тот же порт:
uv sync
AGENT_MCP_SELF_URL=http://localhost:8003/mcp \
uv run uvicorn src.main:app --reload --port 8003Примеры
Составной запрос через SSE:
curl -N -X POST http://localhost:8003/api/v1/invoke \
-H "Content-Type: application/json" \
-d '{"message": "Свободен ли завтра столик на 18:00 и найди Анну Петрову?"}'Ответ — поток событий tool_call → tool_result (по одному на каждый инструмент) и
финальное final со связным ответом (см. английский пример выше).
Health: /health/live — только процесс (это и опрашивает Docker), /health/ready —
оба upstream'а, 503 если хоть один недоступен.
Подключение внешнего MCP-клиента к /mcp
Поднимите сервис локально и подключайте клиент к своему localhost. Быстрее всего —
MCP Inspector (npx @modelcontextprotocol/inspector, транспорт «Streamable HTTP», URL
http://localhost:8003/mcp). Для Claude Desktop используйте мост mcp-remote
в claude_desktop_config.json (см. английскую версию).
Публикация наружу
/mcp не защищён ключом: MCP-клиенты полагаются на собственную авторизацию протокола,
а self-loopback ключа не требует. На localhost это нормально, в интернете — нет, и это
вопрос деплоя, а не кода:
/mcpзакрывается на периметре. Наружу смотрит толькоPOST /api/v1/invokeсо своимSECURITY_API_KEY.Перед публикацией наружу задайте
SECURITY_API_KEY. Рейт-лимит (INVOKE_RATE_LIMIT_PER_MINUTE, по умолчанию 20) защищает от абьюза, но не от расхода: 20 запросов в минуту с одного IP × доAGENT_MAX_STEPSвызовов LLM — это реальные деньги.Compose биндит порт на
127.0.0.1, чтобы ничего из этого не случилось случайно на машине с публичным IP.
Тесты
uv run ruff check .
uv run mypy src
uv run pytest --cov=src/app/services --cov-report=term-missing --cov-fail-under=60Ключевой тест — герметичный self-loopback (tests/integration/test_self_loopback.py):
поднимает всё приложение на loopback-порту и проверяет, что составной запрос стримит два
последовательных события tool_call и финальный ответ, причём оркестратор реально
дергает смонтированный /mcp по Streamable HTTP. Живые LLM и ops-core-api не нужны —
оба замоканы.
Конфигурация
Дефолтный провайдер — OpenAI, и это осознанно: соседние сервисы по умолчанию ходят в
локальную Ollama, но агентский цикл с tool-calling на маленькой локальной модели работает
ненадёжно, а именно надёжность вызова инструментов этот сервис и демонстрирует.
CORS_ALLOWED_ORIGINS по умолчанию пуст — браузерному клиенту нужно прописать свой
origin. Плейсхолдер change-me-min-16-chars в OPS_CORE_API_KEY общий для всех пяти
сервисов портфолио и меняется во всех пяти сразу.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/upkero/mcp-ops-agent'
If you have feedback or need assistance with the MCP directory API, please join our Discord server