enterprise-mcp
An MCP server exposing scoped, read-only enterprise operations tools with fail-closed credential handling. It returns opaque approval IDs for mutations and requires a separate operator approval command to release one-time capabilities.
README
Hermes Enterprise Deployment Lab
Provenance. Sanitized public extract published August 2026. Public git history is publication history, not the original private development timeline. Incident
INC-2026-0042, operators, and API data are fictional fixtures. No client data or credentials appear in this repository. Private client history remains confidential; public claims are limited to inspectable artifacts.
History and scope
I built this lab organically in July–August 2026. Early August work covered Hermes MCP discovery, scoped tool surfaces, and a credential-injection regression (2026-08-01). Provider spend for model-driven invocation was declined that day, so every tool call in the repository comes from scripts or tests—not from a model. The separated operator-approval design landed 2026-08-11 (ADR 005). The public extract was published later in August; GitHub dates mark publication, not a longer private timeline.
This is a synthetic lab: mock enterprise API, fixture bearer tokens, in-memory writes, and 73 credential-free tests. It is not evidence of customer-environment deployment, production identity integration, or model-driven agent runs.
I built this small deployment lab to work through the failure cases that matter when an agent can touch an internal system. It includes a mock enterprise API, a FastMCP stdio server, a workflow runner, and a separate operator command for approvals.
A real Hermes CLI build connects to the server and lists its tools. The server decides which tools are available, refuses to start without credentials, and keeps the write path separate from the approval path. The demo then forces an error after the API has committed a write and shows that resuming creates no duplicate.
There are two important limits. Hermes only performs discovery here; scripts and tests make every tool call. The approval command records an identity string but does not authenticate the person behind it. See Limits for the full list.
Run the whole thing in one command:
./scripts/demo.sh
./scripts/proof.sh runs all 73 tests, inspects the MCP server, parses the
Compose file, and exercises the failure/resume path. The same test suite and a
fresh-clone check run in GitHub Actions.
No provider credentials are needed. PROOF.md lists the check behind
each claim.
What you can check
You can rerun each result below:
| Claim | Established by |
|---|---|
| A real Hermes Agent build connects to this MCP server over stdio and enumerates its tools (discovery only — Hermes does not invoke anything here) | ./scripts/hermes-mcp-proof.sh — real hermes CLI, isolated HERMES_HOME |
| The tool surface Hermes sees is scoped, and changing the scope changes what Hermes sees | ./scripts/hermes-tool-filter-proof.sh — differential: 4 tools vs 1 |
| An excluded tool is neither listed nor callable | pytest enterprise-mcp/tests/test_tool_filtering.py |
A mutation request returns only an opaque approval_id; it leaks neither the capability nor idempotency key and sends no write request |
pytest workflow-runner/tests/test_executor.py (asserted against observed HTTP traffic) |
| Only the separate operator path can approve; approver identity is recorded and the plaintext capability is not stored | same file, test_operator_identity_is_recorded_and_plaintext_capability_is_not |
| A forged, expired, already-applied, or wrongly bound capability is refused before dispatch | workflow-runner unit tests + MCP end-to-end tests |
| A fault injected after commit is survivable: the resume replays instead of re-applying | same file, test_forced_failure_then_resume_leaves_one_side_effect |
| Exactly one side effect exists after failure + resume, then the capability becomes terminal | same test, and ./scripts/demo.sh STEP 8 |
| A read-only credential cannot mutate | pytest enterprise-api/tests/test_actions.py + demo STEP 9 |
| Credentials really reach the MCP subprocess — a wrong token actually fails | pytest enterprise-mcp/tests/test_stdio_credential_injection.py |
| The server fails closed with no token instead of falling back to a default | same file, test_server_fails_closed_without_a_token |
| The workflow runner's audit log records request, named operator grant, capability acceptance, failure, and replay | pytest workflow-runner/tests/test_audit.py, demo STEP 10. The log is not tamper-evident |
| A clean clone of HEAD reproduces the test suite and the demo | ./scripts/fresh-clone-check.sh — it runs pytest and scripts/demo.sh only; it does not re-run the Hermes or container proofs |
Hermes is an external client, not a Compose service. The discovery scripts use
an isolated HERMES_HOME and never touch ~/.hermes/config.yaml. Hermes lists
the tools; scripts/*.sh and pytest call them.
Limits
hermes mcp testonly discovers tools. It makes no provider call. A true model-driven run would requirehermes -z, a tool-call transcript, and provider spend. I declined that spend on 2026-08-01, so this repository does not claim model-driven invocation.- The demo calls the operator command with
demo-operator@example.com. The MCP server cannot grant its own approval, but the lab does not authenticate that identity or prove human judgment. A real deployment would put this command behind authenticated operator access. APPROVAL_STORE_PATHis an unauthenticated JSON file. It stores only a SHA-256 hash of the capability, but any local process that can edit the file can bypass the control. It is not a transactional authorization service.- The
.jsonlaudit log has no signature, chain hash, or WORM storage.run_startedandrun_finishedhave a null correlation ID. The API has no audit log of its own, so direct API writes do not appear here. - Deduplication is per approval, not per action. Two approvals for the same
action_idhave different idempotency keys and create two records. - The server's allowlist is tested. Hermes's own
tools.includebehavior is not. On 2026-08-01, narrowing that list still madehermes mcp testprint all three server-advertised tools. - No second operator has run the validation steps. The blank second-operator protocol is a checklist, not a result, and nobody is scheduled to run it.
- The approval guard lives in the workflow runner. A client with the write token can bypass it and call the fixture API directly, with no audit entry.
- This is not a production deployment: no OIDC, Kubernetes, real identity
provider, cloud/hybrid scaling, or customer data. It has one deterministic
incident (
INC-2026-0042), and a "write" adds a record to an in-memory store. - CI parses
compose.yamlbut does not start containers; optional Podman/Docker smoke runs are not attested in the public tree.
How it works
Hermes / script ──stdio──► enterprise-mcp ──Bearer+Idempotency-Key──► enterprise-api
│ ▲ │
│ approval_id │ └──────────────► audit log
▼ │
approval store ◄── operator command --approver <identity>
│ │
└─ one-time capability (plaintext never persisted)
- Choose the surface. The default allowlist exposes read/plan tools only.
- Read and plan.
propose_incident_planreturns runbook steps with stableaction_ids andapproval_requiredflags. - Stop the first write.
apply_incident_planwithout a capability writes nothing and returns onlypending_approvalplus an opaqueapproval_id. - Get an operator grant.
python -m workflow_runner.approval_operator approve <approval_id> --approver <identity>records the approver and returns the expiring capability once; only its hash is persisted. - Force the awkward failure. With
ENTERPRISE_INJECT_FAILURE=error_after_commit, the API commits the record and then returns 500. The caller seesupstream_5xxand resume instructions. - Resume. The same capability reuses the idempotency key; the API
returns the original record with
replayed: true. - Check the result. The store holds one record and another use of the applied capability is rejected without dispatch.
Fresh-clone setup
Prerequisites: Python 3.11, 3.12, or 3.13. Not 3.14 — pydantic-core has no
wheel for it and its vendored PyO3 tops out at 3.13, so a source build fails.
Podman and the Hermes CLI are optional and only needed for the container and
Hermes proofs.
git clone https://github.com/dbett4/hermes-enterprise-deployment-lab
cd hermes-enterprise-deployment-lab
cp .env.example .env
python3 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt \
-r workflow-runner/requirements.txt \
-r enterprise-mcp/requirements.txt
.venv/bin/python -m pytest -q
./scripts/demo.sh # the whole arc; boots its own API, no containers needed
Optional container and Hermes checks:
podman machine start # once, if the default machine is stopped
podman compose up -d --build
./scripts/smoke.sh # containerized workflow-runner receipt
ENTERPRISE_API_URL=http://127.0.0.1:8080 ./scripts/mcp-smoke.sh
ENTERPRISE_API_URL=http://127.0.0.1:8080 ./scripts/hermes-tool-filter-proof.sh
podman compose down -v
workflow-runner is a run-to-completion container that exits 0 by design; some
compose providers report that as a failure under --wait.
Protocol-only smoke (CI mode)
MCP_SMOKE_PROTOCOL_ONLY=1 ./scripts/mcp-smoke.sh
Runs the FastMCP inspect/list/call checks without the Hermes CLI. Full local smoke requires Hermes and fails closed when it is absent.
Configuration
| Variable | Meaning |
|---|---|
ENTERPRISE_API_TOKEN |
Read scope. Required — there is no default; the server exits 2 without it |
ENTERPRISE_API_WRITE_TOKEN |
Write scope. Absent means the server cannot mutate |
ENTERPRISE_MCP_ENABLED_TOOLS |
Tool allowlist. Unset = read/plan only; all = also expose apply_incident_plan |
ENTERPRISE_INJECT_FAILURE |
Deterministic fault: error, error_after_commit, timeout |
APPROVAL_TTL_SECONDS |
Lifetime of a pending/approved request; default 900 seconds |
AUDIT_LOG_PATH / APPROVAL_STORE_PATH |
Where the audit trail and approval store live |
MCP stdio does not inherit your environment. The SDK forwards only
HOME, LOGNAME, PATH, SHELL, USER. Anything else must be passed
explicitly via env= on the stdio transport or the Hermes env: block. This
repository got that wrong once and the failure was silent — see
ADR 004.
Hermes MCP config (isolated)
Never merge this into ~/.hermes/config.yaml.
export ENTERPRISE_API_TOKEN=lab-read-token
export HERMES_HOME=/tmp/hermes-mcp-lab
mkdir -p "$HERMES_HOME"
{
echo "_config_version: 9"
./scripts/emit-hermes-mcp-config.sh "$PWD" all
} > "$HERMES_HOME/config.yaml"
hermes mcp test enterprise_ops
The second argument is the server-side allowlist. Example shape:
config/hermes-mcp-example.yaml.
Fixture tokens
Local lab only — non-secret test data, documented in .env.example:
ENTERPRISE_API_TOKEN=lab-read-token
ENTERPRISE_API_WRITE_TOKEN=lab-write-token
MCP tool surface
| Tool | Mutating | Behavior |
|---|---|---|
check_enterprise_api |
no | Health/readiness, correlation ID, whether a write credential is present |
get_incident_context |
no | Incident + runbook with per-dependency call evidence |
propose_incident_plan |
no | Plan receipt; consequential steps carry approval_required and an action_id |
apply_incident_plan |
yes | Requests or consumes a separately granted, expiring capability; idempotent execution of one runbook step. Opt-in via the allowlist |
More detail
| Document | Contents |
|---|---|
docs/architecture.md |
Components, checks, and security model |
docs/runbook.md |
Operator commands and troubleshooting |
docs/adr/003-stdio-mcp-read-plan-tools.md |
Why stdio MCP |
docs/adr/004-enforced-approval-idempotency-and-scoped-tools.md |
Historical two-call guard and credential/scoping decision |
docs/adr/005-separated-operator-approval.md |
Superseding separated approval state machine and resume semantics |
docs/second-operator-protocol.md |
A validation checklist that has not been run |
docs/build-spec.md |
Original target and shipped status |
Build status
| Milestone | Status |
|---|---|
| M1 Local deployment | Complete |
| M2 Identity/integration boundary | Partial — two static bearer scopes; no OIDC, no connector pagination/retry |
| M3 Agent workflow (MCP + Hermes discovery) | Partial by design — the real Hermes CLI discovers the scoped surface; no model-driven invocation (provider spend declined 2026-08-01) |
| M4 Separated approval, idempotency, resume, audit | Partial — role separation, expiry, terminal use, and ambiguous-failure resume work. Identity is supplied by the caller and the audit is not tamper-evident |
| M5 CI from a fresh clone | Complete — tests and fresh-clone job pass in GitHub Actions |
Work not done
Nothing in this table is currently in progress.
| Item | State | Note |
|---|---|---|
| Production approval identity/policy integration | Not implemented | The operator command records a supplied identity but does not authenticate it. |
| Model-driven tool invocation | Declined, 2026-08-01 | Provider spend declined. Permanent; this repository will never demonstrate it. |
| Second-operator validation | Unrun | docs/second-operator-protocol.md is a script nobody has executed. |
| CI run | Complete | Tests and the separate fresh-clone job pass in GitHub Actions. |
| Action-level deduplication | Not implemented | "Exactly once" is per approval, not per action. |
| Approval consumption and expiry | Implemented locally | pending → approved → applied or expired; applied/expired are terminal. |
| Authenticated approval store | Not implemented | Plain JSON at APPROVAL_STORE_PATH; capability plaintext is not persisted. |
| Enterprise-API-side audit | Not implemented | A direct write to the API leaves no trace. |
What production approval would require
The local implementation returns an opaque request ID, grants through a
different command, records the supplied identity, stores a capability hash,
enforces expiry and binding, and safely resumes after an ambiguous commit. It
does not authenticate that identity or protect the JSON file from a local
writer. A production version would replace the command and file with an
IdP-backed approval service and transactional audit store. Details are in
docs/architecture.md.
License
MIT — see LICENSE. Security notes: SECURITY.md.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。