hermes-frihet
Official Hermes Agent integration for Frihet. Discoverability, setup, diagnostics, and operating guidance for the Frihet MCP. Zero duplicated tools — the canonical Frihet surface (158 MCP operations) stays in
@frihet/mcp-serverand the remote endpointhttps://mcp.frihet.io/mcp.
What this plugin is
Hermes Agent is an open-source agent framework by Nous Research. Frihet is an official business-management platform. This plugin is the discoverability + operating layer between them.
The plugin does not re-implement any of the 158 Frihet MCP operations. It adds three things on top of the canonical MCP:
/frihetslash command with three sub-actions:status,setup,doctor.pre_tool_callhook that flags Frihet writes whose names imply irreversibility (send, markPaid, registerVerifactu, delete, …) so downstream policy or the model can pause and require human confirmation.- Bundled skill (
skills/frihet/SKILL.md) that teaches Hermes the Frihet operating contract: read-before-write, draft-first, Idempotency-Key, redaction, Frihet-server authority for workspace/scopes/roles/resources.
The actual data operations flow through the canonical Frihet MCP, which
Hermes already speaks natively (hermes mcp add / mcp_servers.frihet block).
Install
From the Hermes plugin catalog (recommended)
hermes plugins search frihet
hermes plugins install frihet
The installer clones this repo at the exact SHA pinned in the catalog entry
and drops it under ~/.hermes/plugins/frihet/.
Manual install
git clone https://github.com/Frihet-io/hermes-frihet.git \
~/.hermes/plugins/frihet
Restart Hermes or start a new chat (/reset) so the loader picks it up.
Configure
Add a Frihet API key (generate one at https://app.frihet.io/settings/api)
and the MCP server block. The plugin's .env.example ships the canonical
values:
cp ~/.hermes/plugins/frihet/.env.example ~/.hermes/plugins/frihet/.env
# Edit the new .env and paste your key.
Or add the MCP server block to ~/.hermes/config.yaml:
mcp_servers:
frihet:
url: https://mcp.frihet.io/mcp
auth: oauth # ← MUST be the string "oauth", NOT a dict.
oauth:
flow: browser # browser PKCE (default) or "device"
trust: untrusted # Frihet is a third-party MCP; default to untrusted
# Alternatively, for unattended callers:
# auth: api_key
# env:
# FRIHET_API_KEY: ${FRIHET_API_KEY}
⚠ Trap to avoid. Hermes v0.21.5 reads
cfg["auth"]as a string ("oauth"or"api_key"), not as a nested mapping. Writingauth: {type: oauth, flow: browser}— which is whathermes config set mcp_servers.frihet.auth.type oauthproduces — silently makes the client connect with no auth and the MCP server's 401 retries spin forever as a "Connecting…" panel. If you copy this config verbatim it works; if you wire it via the granularconfig setkeys you must end withhermes config set mcp_servers.frihet.auth oauth(string), not a sub-dict.
The plugin itself never stores your API key. Strings matching
fri_<24+ chars>,Bearer …, or anytoken|secret|api_key|password: <value>shape are redacted by thestatus(),setup(),doctor(), andredact()helpers before they reach slash-command output. Thepre_tool_callhook does NOT redact — it only classifies and escalates; the redaction contract lives in the helper functions, not in the hook.
Use
In any Hermes chat session:
/frihet status # local snapshot — no network
/frihet setup # prints host-level auth guidance; does NOT accept credentials
/frihet doctor # live MCP handshake — reports four explicit states
/frihet setup no longer accepts a candidate API key. Passing one in chat
would land it in the slash-command transcript. Use hermes mcp login frihet
(OAuth/PKCE) or hermes auth add frihet (unattended API key) instead —
those are the host-level auth surfaces, and they leave no plugin transcript
behind.
/frihet doctor reports four states side by side rather than a single
connected flag, because those four states are NOT the same thing:
| State | Type | Means |
|---|---|---|
endpoint_reachable |
bool | The network handshake landed on a server (HTTP 2xx/4xx/5xx). |
mcp_configured_in_hermes |
tri-state | A mcp_servers.frihet block exists in $HERMES_HOME/config.yaml. "configured" / "missing" / "unknown" (the last when the file is unreadable). |
authenticated |
tri-state | The server accepted the credential we presented. True/False/"unknown". Anonymous 200 is reachable but NOT authenticated. |
tools_available |
tri-state | The initialize response carries a populated serverInfo and parses as MCP. |
An anonymous HTTP 200 is reachable but NOT authenticated. The umbrella
ok flips True only when every state is True or "configured". We
deliberately do NOT use api_key_present as a synonym for
mcp_configured_in_hermes: an OAuth-first flow has no env var at all,
so conflating the two would be a known false negative.
Then ask Hermes:
show me my overdue invoices from the last 30 days
Hermes will:
- Resolve
mcp__frihet__list_invoices(provided by the canonical MCP). - Filter by status and due date on the Hermes side, or pass the filter through to the MCP tool.
- Render the result.
The bundled skill guides every read/write to honour the Frihet operating
contract. Irreversible writes escalate to Hermes's human approval gate
via the pre_tool_call hook ({"action": "approve", ...}) — Hermes
prompts the user before the tool runs. Reads and draft-writes pass
through; the model sees the skill and pauses to show totals before
sending anything. The hook is fail-closed: any Frihet MCP operation
that the classifier does not recognise also escalates.
Safety contract (summary)
The full version lives in skills/frihet/SKILL.md.
The non-negotiables:
- Read before write. Confirm the target exists before mutating it.
- Draft first (when the operation supports drafts).
createInvoice,createQuote,createCreditNotedefault tostatus: "draft". Show totals, hand back.createPaymentis NOT draft-capable — it is classified as irreversible by the plugin'spre_tool_callhook and always escalates to human approval before it lands. - Honour
Idempotency-Key. Reuse on retry-with-same-intent; generate a new one for a different business reason. Never retry blindly. - Reconcile before retry on ambiguity. If a write returned
idempotency_pending,unknown_status, a 5xx, or a timeout, read the record by the key you used before retrying. - No secrets in the model context. The
pre_tool_callhook redactsfri_…,Bearer …, andAuthorization:lines. - Frihet is the authority. Workspace, scopes, roles, and resources belong to Frihet. Don't invent rows that contradict it.
Disclosure
Before installing, please note:
- The plugin makes outbound HTTPS calls to
https://mcp.frihet.io/mcpwhen you run/frihet doctoror any Frihet MCP tool. No other third-party endpoints are contacted. /frihet setupvalidates a candidate key against the canonical endpoint, but does not persist the value — that's your job, in the Hermes secret directory.- The plugin does not run background processes, does not write to any file outside the plugin directory, and does not auto-update itself.
Development
The plugin is stdlib-only by design — zero runtime dependencies keeps the catalog admission floor uncontested and the install footprint minimal.
# Run the test suite.
python -m pytest -q
# Smoke-load the plugin via the official Hermes loader
# (skipped if hermes_cli is not importable in this env).
python -m pytest -q tests/test_plugin_discovery.py
# Build the wheel.
python -m build
# Validate the manifest against the current Hermes admission rules.
hermes plugins validate ~/.hermes/plugins/frihet --install-deps
Compatibility
- Hermes Agent:
>=0.21.5(declared inplugin.yamlasrequires_hermes). - Python: Hermes Agent currently
we *only* support 3.14per itspyproject.toml(the>=3.11,<3.15range is only so older installs can run the updater and reach 3.14). CI runs the authoritative job on 3.14 and a non-blocking smoke on 3.11/3.12/3.13. - Frihet MCP protocol:
2025-06-18(forward-compatible with later minor).
Upstream MCP catalog candidate
In addition to this plugin, the repo carries a ready-to-submit candidate for
Hermes's native optional-mcps/ directory:
upstream/frihet-mcp-manifest.yaml— modelled onoptional-mcps/linear/manifest.yaml, declares native MCP OAuth 2.1 + PKCE, no third-party provider, post-install guidance.
When we're ready to publish (after dogfooding the plugin), the upstream PR
lands this manifest at NousResearch/hermes-agent/optional-mcps/frihet/,
so Frihet shows up under Capabilities → Connectors in addition to
Capabilities → Plugins.
License
MIT © 2026 Frihet-io.