main
31 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dc50403a81 |
feat(desktop): the Connectors page replaces the MCP tab (#119074)
* feat(connectors): the backend serves a connector's tool list, cached for 24 hours
The Connectors page opens one app and shows every tool it has. The backend
had no way to read that list.
- `tools/connectors/portal/`: a client for the portal's tool-list route and a
JSON cache under the Hermes home, one file per portal origin and connector.
An entry is fresh for 24 hours. After that the read revalidates with the
stored ETag: 304 keeps the list, 404 deletes the entry, an upstream failure
serves the stored list marked stale, and a 401 never serves the cache.
- `connectors.tools {slug, refresh}`: account-level, routed by `profile`, no
chat session. Errors carry a fixed `reason` from one closed set on the rail.
- Every connector model that is not operation state moves into
`tui_gateway/contracts/connectors.py`. Handlers that no chat session owns
live in `tui_gateway/methods_connectors_account.py`.
The wire model is tolerant: an unknown facet reads as unclassified and one odd
tool never blanks a connector.
* feat(connectors): catalog, accounts and member tool rules by RPC
The Connectors page needs the app catalog, the connected account of one app,
a way to disconnect it, and the member's own on/off rules. None had an RPC.
- `connectors.catalog`: name, description, category and logo of each app.
- `connectors.accounts`, `connectors.accounts.remove`: read the accounts at
the tool gateway and remove one by id.
- `connectors.policy.get`: the rule layers that apply to the member, widest
first. The body is a union on `mode`, so a reader can name who turned a
tool off.
- `connectors.policy.set`: one change, a union on `type` (the tools of one
connector, or one connector on or off), with the revision the user saw. A
stale revision answers `POLICY_CONFLICT`. The backend composes the upstream
write in one pure function, so no renderer learns the upstream rules.
- Bundled MCP manifests can name their hosted twin with `connector:`, so the
page can show one card per app.
* feat(connectors): connect an app without a chat session
Every connector RPC took a `session_id`, and a connect that did not come from
the model's tool call minted a link with no watcher. The Connectors page has
no chat session, and its card must flip to connected by itself.
- `connectors.list`, `connectors.connect`, `connectors.operation.status`,
`connectors.operation.wake` and `connection.respond` take `owner`, a union
on `type`: `session` (today's behaviour and authorization) or `account`
(routed by `profile`, authorized by the live transport like `mcp.*`).
`session_id` is gone from these params; every desktop caller sends `owner`.
- An account connect runs the same operation lifecycle on a background
thread, under the profile's scope, so the watcher reads the account and
settles the operation. A second connect for an app that is already
connecting returns the open operation and mints nothing.
- `connection.update` carries `owner`. An account operation has no session to
address, so its updates go out on the session-less broadcast path.
* feat(mcp-catalog): eighteen more bundled entries name their hosted connector
A bundled MCP entry and a hosted connector for the same app are one card
on the Connectors page only when the manifest names its hosted twin.
Linear and Notion had the field. These entries get it too: airtable,
asana, attio, calendly, dropbox, figma, railway, supabase, todoist,
betterstack, canva, cloudflare, datadog, intercom, neon, sentry, stripe
and vercel. Atlassian maps to two hosted connectors and Prisma Postgres
is not clearly the same app, so both stay without one.
* refactor(connectors): the account handlers share one gate, one params model and one write table
The six account-level handlers each repeated the availability gate, the
auth catch and the catch-all reply. One decorator now owns that, and each
handler validates its params with its contract model instead of a ladder
of isinstance checks. The five connection RPCs share one guard for the
unexpected-failure reply.
The four write composers for the member rules were the same function
with a different list key and polarity. They are one table now.
The owner union lives in contracts/common.py, so the params side and the
event side stop declaring it twice and the import cycle is gone.
An account operation start carries one event and a flag, so the wait for
the sign-in link blocks instead of polling every 50 ms. run_operation
loses its two account-only parameters; drive_operation is the second
entry point.
Tests: four deleted (they exercised pydantic or the mock), three merged
into tables, two added (a client that still sends the old top-level
session_id is refused; all six account RPCs run off the server loop).
The shared reply helper and the HTTP and managed-client fakes move to
one place each. Comments are one line or gone.
* fix(connectors): a missing tool-list route reads as "unavailable", not "connector gone"
The tool-list read treated every 404 as the portal's "this connector is
not in the catalog" answer. It deleted the cache entry and answered
CONNECTOR_NOT_FOUND, so a page would offer to remove an app that is
connected and works. A portal that does not serve the route yet answers
a bare 404 for every app.
Only the portal's own {"error": "connector_not_found"} means the
connector is gone. Any other 404 is now a tool-list outage: the cached
list is served as stale, or the RPC answers TOOLS_UNAVAILABLE.
* fix(connectors): a connect from the page returns to the app after sign-in
The sign-in link carries a return target only when the session's surface
is the desktop. A chat session binds that surface. An account-owned call
has no chat session, so nothing bound it: the link was minted without a
return target and the browser ended on the portal's done page instead of
coming back to Hermes.
Every account-owned call now runs with the process's own surface bound,
next to its profile scope. The operation thread copies that context, so
the first link and every reissued link carry the return target and the
operation id.
* test(connectors): defer the new connector RPC coverage
The tests for the new account RPCs, the portal client, the tool-list cache
and the rule composer leave this PR and come back in one later change, after
the API is settled. The same was done for #111008.
Kept: the edits that existing tests need because the five connection RPCs
now take `owner` instead of `session_id`, and the rename of the managed
client seam.
Removed: six new test files, their two fakes and the gateway conftest, and
the new cases in test_mcp_catalog.py, test_connectors_gateway_client.py,
gateway-rpc.test.ts and notifications.test.ts. Reverting this commit restores
all of them.
* fix(cli): the connection panel hands the tool thread back at once
The classic CLI's connection callback waited on a queue for the user's first
decision. The operation's watcher starts only after the callback returns, and
the watcher is what polls a hosted account, runs the 300-second deadline and
sees Ctrl+C.
For a hosted connector the panel opens on the sign-in link, where the only
key that filled the queue was Cancel. The account was never polled: the user
signed in, the panel never changed, and Esc reported the app as skipped.
Ctrl+C set the interrupt flag but left the thread parked on the queue, so the
turn never ended.
The callback now opens the panel and returns, as the gateway's callback does
for the desktop and the Ink TUI. The panel's actions already reach the
operation through apply_answer on the UI thread, so the queue is removed. An
install with a form still waits for Connect, because the backend starts no
work for a pending row. Ctrl+C now settles the operation as `interrupt`, and
open rows become `not_connected`.
Checked on the e2e rig with the fake tool gateway: hosted connect completes on
the third status read; Ctrl+C ends the turn and the polling stops; an MCP
install with a plain and a secret field still saves config and both values.
* fix(connectors): "run it again" lives in the library, so the classic CLI can use it
Making a new sign-in link for a failed or expired hosted connector was
implemented only in the JSON-RPC layer (`_reissue`). The classic CLI does not
go through JSON-RPC: its Connect button on a failed row called apply_answer,
which does nothing for a hosted operation because it has no MCP runner. The
panel showed "Waiting…" until the deadline.
`tools.connectors.run.reissue(operation, names)` now holds the checks and the
per-kind action, and returns a refusal reason or None. The gateway maps each
reason to the same JSON-RPC error as before. The CLI calls it for a hosted
row; a refusal is shown on the row. MCP rows keep their path, because Connect
on a failed MCP row re-sends the form values.
Checked on the e2e rig: a scripted failed sign-in, then Connect: a second mint
with `reinitiate: true`, a new link with a new connection id, then connected.
* feat(connectors): the account list and disconnect go through the portal
`connectors.accounts` and `connectors.accounts.remove` called the tool
gateway. They now call the portal's account-management routes
(`GET /api/v1/connectors/accounts`, `DELETE /api/v1/connectors/accounts/{id}`),
which apply the organisation membership checks and write the disconnect audit
row. There is no fallback to the gateway when the portal is unavailable, and a
removal is never retried.
The read of ONE account stays on the gateway (`GET v1/connectors/accounts/{id}`):
the portal has no such route, and the operation watcher polls it once per second.
`ConnectorClient.list_accounts` and `delete_account` are removed. The removed
account's reply model carries `connector`, which both services send.
* fix(connectors): the account RPCs answer what the portal really sends
Checked against the portal source and against the staging and production
services.
- Errors are read from the upstream error code, not the HTTP status. A rule
write answered 409 for a stale revision and for a user with no organisation;
both read as "the policy changed". `org_required` is now `ORG_REQUIRED` and
403 `no_access` is `ORG_ACCESS_DENIED` on every account RPC; only a rejected
sign-in is `NEEDS_NOUS_AUTH`. `connectors.list` and `connectors.connect` with
the account owner map these too.
- `connectors.policy.get` and `connectors.policy.set` carry `effective`: the
portal's own result for this user, with its stamp and without provider or
subject ids. Nothing is recomputed locally.
- A rule write needs the revision the user saw: `expected_revision` is required
and must be a revision string; a bad one is refused before any HTTP call.
- A tool row carries `no_auth`; a list without the upstream flag is an invalid
answer, not `false`.
- `connectors.accounts.remove` returns the app of the removed account. An
invalid id is `INVALID_PARAMS`.
- The tool-list cache is per signed-in member (a hash of the token's `sub`),
so two Nous accounts on one profile do not share entries.
- A malformed slug is a local error, not a 404 from a server nobody called.
Live, staging: no revision and a malformed revision refused locally; a good
revision wrote one disabled Gmail tool and returned it in `effective`; the
same revision again answered `POLICY_CONFLICT`; the list row showed the tool;
the restore brought the member rules back to the start. Live, staging and
production, read-only: all 60 tool lists (5483 tools) parse.
* fix(connectors): the operation RPCs match their contract; a settled card cannot start a new link
Found by two adversarial reviews of the RPC layer and its types.
- `connectors.connect` from a chat session with no open operation is refused
(`UNKNOWN_OPERATION`). It used to call `manage_connections` through the tool
registry with no card: it made a link nobody watched, returned a reply
without the required `settled` field, and named an operation that was never
registered. There is one way into an operation: the agent's call, or the
account owner's `connectors.connect`. "Run it again" inside an open
operation is unchanged.
- `connection.update` for a session is routed by session key AND profile; two
profiles with the same key no longer cross-deliver a sign-in link. The event
payload gets the same redaction as the RPC replies.
- `connection.respond` runs on the long-handler pool: an approval can start MCP
OAuth discovery, which blocked every RPC of the gateway while it ran.
- `connectors.list` rows are a closed snake_case model: `connector`, `enabled`,
`connected`, `connection_status`, `status_reason`, `gateway_disabled_tools`.
The last one is display data: the gateway enforces the rules, the backend
only passes the list on. The phantom `name` and `description` are gone, and
the desktop uses the generated types instead of hand-written copies.
- `tools_listing` (model-only data) no longer rides on `connectors.operation.status`.
- `unavailable` is removed from the target states and settle reasons: nothing
produces it. The contract generator now fails when a contract enum and its
domain enum differ.
- `ConnectorErrorReason` is part of the generated TypeScript and OpenRPC.
- The desktop sends `connection.respond` on the socket that holds the session,
as wake and reissue already did.
- Contract violations are logged every time, at error level.
- An account connect whose prepare step is slow returns the live operation
instead of an error while the operation keeps running.
- The MCP-manifest `connector` field leaves this PR (it moves to a later one
on top of the catalog-reader change). `hermes_cli/mcp_catalog.py` and
`optional-mcps/` are untouched by this PR again.
anti-slop: no net-new findings (15 touched files).
* fix(connectors): the model gets no sign-in link wherever a card exists; side agents cannot connect
The flag that tells the model "a connection card exists" was the session
platform (`== "desktop"`). The Ink TUI and the classic CLI also draw a card,
so there a connector call on an unconnected app handed the model the raw
`connect_url` and told it to pass the link to the user.
- The agent turn now declares how a link can reach the user
(`tools/connectors/turn.py`): CARD when the agent was built with a
connection callback, SIDE for a subagent or a background turn, LINK for a
headless run (`-q`, cron, ACP, api_server, messaging). It is set once per
tool batch in the agent loop and read by the connector dispatch path, which
never sees the agent. The session platform decides return-to-app only.
- CARD: the result carries `connect_card_available` and our hint, never the
link and never the gateway's own hint.
- SIDE: subagents (`delegate_tool`), gateway background turns and the classic
CLI `/bg` are built with `side_agent=True`. They hold no `manage_connections`
tool on any path that derives the tool list, and a connector call on an
unconnected app gets no link, only "report this to the main agent".
- LINK is unchanged.
- The hosted path with no card builds a detached operation, as the MCP path
does, so no `connection.update` is emitted for an operation no client asked
for. Names and docstrings that said "off desktop" now say "no card".
- A settled card is dead on the desktop: `reissueConnectionTarget` and
`respondToConnectionRequest` share one guard and send nothing for a settled
or unknown operation.
- The model-facing settled result no longer carries `connection_id`; the model
repeated it to the user.
Shown on the real clients with a real model (rig, fake tool gateway): Ink TUI
and classic CLI get `connect_card_available` and no link, the model opens the
card, the account connects, the retried call succeeds; `-q` still gets the
link; a subagent and a background turn have no `manage_connections` and get
the no-link hint; on the desktop a card settled with Continue has no enabled
control and sends no RPC.
* feat(tools): every call made through tool_search + tool_call shows a real label on all three clients
A bridged call showed as a generic `tool_call` row in the Ink TUI and as
`⚡ tool_call` in the classic CLI, because the display looked the name up in
the tool registry and bridged names are made at run time. The desktop labelled
only batches that were all hosted connector calls, by parsing names itself.
- `tools/tool_labels.py` is the one place that turns a bridged call into a
label: kind, app, action, emoji and text. Hosted: `connectors__gmail__GMAIL_SEND_EMAIL`
→ "Gmail · send email". MCP: "Linear · list issues". A local deferred tool
keeps its own emoji, verb and primary-argument preview. A batch gets exactly
one label per entry, always; an entry with no name gets a generic label.
- Classic CLI: one row per inner call; the duration on the last row; the
failure text on the row of the call that failed. With friendly labels off
it prints what it printed before.
- Gateway: tool start, progress and complete events and stored transcript rows
carry a typed `labels` field. It does not depend on the classic CLI's
display setting. Clients no longer parse tool names.
- Ink TUI: rows from the labels; the verbose trail keeps Args and Result.
- Desktop: `ConnectorExecution` renders hosted, MCP and mixed turns from the
labels, one row per call. The labels reach the row under a key no tool
argument can use. The connect card it drew under a failed tool result is
gone: after `CONNECTION_REQUIRED` the one way in is the agent's own
`manage_connections` call.
- `tool_search` and `tool_describe` rows read "Searching tools · <query>" and
"Reading tool details · N tools".
Shown on the real desktop (video and screenshots), the Ink TUI and the classic
CLI with the rig: hosted rows, MCP rows, a two-entry batch, a failed entry, a
`CONNECTION_REQUIRED` row with no card under it, labels after a reload, and the
desktop rows with the classic CLI setting off.
* fix(connectors): the model can tell "hosted tools unavailable" from "no such tool"; manage_connections routes MCP names correctly
- A failed hosted search or describe used to return nothing, by design, so the
model saw only local tools and told the user that a connected app was
missing. The local results are unchanged; when the hosted leg failed, the
`tool_search` and `tool_describe` results carry
`connectors: {status: "unavailable", reason: "unreachable" | "sign_in_expired"}`
and one hint line. A rejected token is `sign_in_expired`; an entitlement
refusal or a shut gate adds nothing. `tool_describe` no longer lists those
names under `not_found` next to "search again".
- NS-932. The description now says which side a name belongs to: a bare name
is a hosted connector account; `mcp: true` only when the user asks for an MCP
server, a local server or an install, or when the name exists only in the
catalog; connect and reconnect are hosted verbs, install, enable and
authorize are MCP verbs. It names the three clients that draw a card.
- A misrouted target is refused with the call that works. Only when the
gateway does not know the connector (confirmed on that failure path) and the
name is a catalog entry does the target fail with "X is a local MCP server.
Call manage_connections with action install ...". It is a per-target
outcome: other targets of the same call keep their links and their card. A
vendor failure on a name both sides know stays an ordinary failed row. The
MCP side mirrors it, and never for an entry that is only not installed.
- "Do not re-ask after a skip or a timeout" no longer stops the model when the
USER asks for that app again; the description and the settled-result notes
say so. A builder saw the model refuse a direct user request.
Shown on the Ink TUI and the classic CLI with a real model: a dead gateway and
a 401; "connect fxmail" goes hosted; "install the fx-noauth MCP server" goes
MCP; "connect fx-noauth" reaches the MCP install card in one corrective round
with no hosted mint; a two-target call where one is misrouted still connects
the other with exactly one mint.
* fix(tui): the connection card answers every key, shows what is happening, and is dead once settled
Reproduced on the real Ink TUI with the rig, then fixed:
- The keyboard was dead during the sign-in wait: the card kept a `submitting`
flag that the normal OAuth path never cleared, and Esc went through the same
guard. The in-flight state now belongs to the answered row and clears when
that row moves, when any later frame of the operation arrives, or after
five seconds. Esc skips the row in every phase; Ctrl+C interrupts the turn
(the input handler had no branch for this overlay); Shift+arrows scroll the
transcript and the card ignores them; arrow keys no longer move the text
cursor and the field focus at once.
- The card was lost at turn idle: the overlay flag was cleared while the
operation stayed in the store, and a resume dropped the pending card. The
flag survives idle, a resume shows the pending card again, a session switch
clears it.
- States with no branch: `not_connected` and a row with no link fell into the
credential form; `expired` vanished with no note. The title and the row text
now name the action (connect, reconnect, install, enable, authorize); a
failed or expired row with no fields offers Try again / Skip; a failed row
WITH fields reopens the form over the typed draft, with the failure above it.
- A settled card is dead: at settle the overlay closes and one transcript line
per app states the outcome. A settled or dismissed operation id is
remembered, so no replay or resume can reopen its card. Esc in the last
"Finishing…" moment hides the card and still writes the outcome lines.
- A failed `connection.respond` and a browser that did not open are shown on
the card in one sentence.
Also: `tui_gateway/connector_payload.py` redacted the BOOLEAN `secret` flag of
a credential field to the string "[REDACTED]". On the desktop every credential
field therefore rendered as a password and lost its prefilled default. A
boolean is no longer redacted.
* chore(connectors): remove the comments and docstrings this branch added
Deletions only. Kept: tool directives (`# noqa`, `// eslint-disable`, ...),
`// SAFETY:` lines, and the docstrings of the contract models under
`tui_gateway/contracts/`, which become the descriptions in the generated
OpenRPC and TypeScript.
Checked that no code changed: every Python file has the same AST as before
once docstrings and `pass` are ignored (62 files), and every TypeScript file
prints the same with comments stripped by the TypeScript printer (32 files).
The generated contract files are unchanged.
* fix(connectors): a card restored after a reload answers again; every account RPC names auth and org failures
Found by the end-to-end runs on the pushed head.
- Desktop: after a window reload, Continue on the restored card sent nothing.
The answer looked up the backend that holds the session with the runtime
session id, the lookup wants the stored id, and a failed lookup returned
silently. When the lookup gives no owner the answer now goes out on the
window's active socket, which is what main does.
- `connectors.policy.get` answered `POLICY_UNAVAILABLE` for a rejected sign-in,
a refused scope, a non-member and a missing organisation alike: the handler
runs with the gateway's globals and did not import the reason enum, so its
own error mapping raised. `connectors.accounts.remove` caught auth failures
in its generic branch. `org_required` was mapped on `policy.set` only. All
six account RPCs now answer `NEEDS_NOUS_AUTH`, `FORBIDDEN_SCOPE`,
`ORG_ACCESS_DENIED` and `ORG_REQUIRED` for those four upstream answers.
* wip(desktop): port the Connectors tab files and wiring onto the #115191 head
* wip(desktop): Connectors tab on the #115191 contract, catalog arm removed, audit defects fixed
* wip(desktop): Connectors tab passes the anti-slop ratchet; dormant two-ways code and the Available collapse removed
* wip(mcp): every server row says whether config or a plugin provides it; writes refuse plugin rows
* wip(desktop): Connectors tab, the owner's first live round (custom MCP form, kind words, compact dialog)
* wip(desktop): the connector dialog fits its content
* wip(desktop): catalog MCPs show on the Connectors tab until the catalog dies; connector_slug pairs a manifest with its managed app; the closed-gate state
* wip(desktop): connectors cache v3, the seed shape gained connector_slug
* wip(desktop): the owner's answers on the connectors page
A plugin-provided server now shows its tool list: the dialog probes it
through the existing read-only test endpoint, shows the tools without
switches (the plugin owns them), and shows the probe's error with a
Retry when the server cannot start. Its card is named after the server
key in the plugin's mcp.json, not the namespaced runtime key.
The paste box no longer parses `--header` on a `hermes mcp add` line;
the CLI has no such flag.
The rule write sends the member layer's revision only. The portal
always returns a member layer (baseline revision when no row exists)
and compares the write against that row, so the effective revision was
never the right guess. Verified live on staging: two writes in a row,
both accepted, policy restored.
The page cache keeps every read for signed-in accounts too and only
clears itself when the account is signed out. The storage version moves
to v4 so old blobs are ignored.
* chore(desktop): strip the prose comments the connectors page branch added
Comments and docstrings this branch added relative to main are gone;
tool directives, SAFETY lines and the contract docstrings that feed the
generated OpenRPC stay. Guards: Python AST and TypeScript printer output
are identical before and after; ruff, tsc, eslint, the ratchet and the
generated contracts are unchanged.
|
||
|
|
b1ff5939c9 |
fix(mcp): feed dashboard-supplied env into catalog install without re-prompt
The dashboard route pre-wrote only secrets to .env and then called
install_entry, which prompted again for every auth.env var on a non-TTY
server stdin — the user's supplied non-secret value was read as EOF,
discarded, and config.yaml ended up with an unresolved ${VAR} ref
(asana: silent OAuth break; n8n: hard 400 on the required URL).
install_entry now accepts preloaded_env (dashboard form values) and
_prompt_env_vars skips the prompt for any spec already supplied, so the
route passes body.env straight through and the non-secret inline path
works end-to-end. The boundary test now exercises the real install_entry
(dashboard route, no mock) and asserts the non-secret lands in the server
config; asana manifest comments updated to match the inline behavior.
|
||
|
|
17b5df02f2 |
feat(mcp): connect to the official n8n server from the catalog (#116063)
* feat(mcp): add the official n8n server to the catalog Connect to the user's instance over HTTP with browser OAuth. Keep a separate n8n-official identifier so the retired n8n bridge is neither relabeled nor overwritten and retains its credentials and tool filter. Save ordinary catalog setup values in server config, retaining secret references in .env. Pass field secrecy through the catalog API so URLs and client IDs remain visible while credentials stay masked. Keep the existing install-then-authorize lifecycle. Transactional setup and cancellation changes are outside this catalog addition. * refactor(mcp): limit n8n PR to catalog addition Remove shared installer, storage, field-masking, and input-handler changes. Those behaviors are being handled in a separate PR. Restore their tests and Asana guidance to the base branch. Keep only the official n8n manifest and setup documentation, using the catalog's existing setup and persistence behavior. |
||
|
|
8d4abc3ea6 |
fix(mcp): retire the n8n bridge catalog entry (#116048)
Stop offering the third-party bridge for new catalog installs. Existing connections keep their saved transport, credentials, and tool selection; the runtime and configured-server controls do not require a manifest. Update CLI examples and document that catalog reinstall is unavailable. Adding n8n's official server remains separate work. |
||
|
|
b0cd35e259 |
fix(mcp): dashboard/Desktop Authorize honours a pre-registered client's pinned loopback redirect
A no-DCR entry (client_id + redirect_port, e.g. the Asana manifest) has http://localhost:<port>/callback registered with the vendor, which matches redirect URLs exactly; the dashboard flow overrode it with its own callback URL, so the in-app Authorize button could never complete for such entries. The pinned loopback listener now wins (over the dashboard URL and any cached registration URI); the dashboard flow only publishes the authorization URL, and the stdin paste reader stays off under a dashboard flow. Docs/post_install say the approving browser must run on the Hermes machine. |
||
|
|
c553df915c |
fix(mcp): Asana catalog installs a working V2 pre-registered OAuth client
The V1 beta server https://mcp.asana.com/sse is retired and Asana's V2 server (https://mcp.asana.com/v2/mcp, Streamable HTTP) has no Dynamic Client Registration: every client must be an Asana "MCP app" the user registers in the developer console. The salvaged manifest fixed the URL and documented the manual `oauth:` block; this commit makes the catalog install itself produce that block so no hand-edit of config.yaml is needed. - hermes_cli/mcp_catalog.py: manifests may pin a closed `auth.oauth` mapping (client_id, client_secret, redirect_host, redirect_port, scope) that `_build_server_config` copies to `mcp_servers.<name>.oauth`; every `${VAR}` it references must be declared in `auth.env`, mirroring the api_key header contract, so a placeholder can never reach the token endpoint as a literal. `install_entry` now prompts `auth.env` for OAuth entries too (dashboard and Desktop already render `required_env` regardless of auth type). - optional-mcps/asana/manifest.yaml: declare ASANA_CLIENT_ID/SECRET, pin the client + `http://localhost:27890/callback` (Asana matches the redirect URL exactly), and rewrite post_install around the MCP-app registration steps. - tests: shipped-catalog invariant (no manifest installs the retired `/sse` URL; the Asana client credentials are declared `${VAR}` refs; callback pinned) + install-path invariant for the new `auth.oauth` block, including the undeclared-reference rejection. - docs: catalog section on entries that need a user-owned OAuth app. Live: `hermes mcp install asana` + `hermes mcp login asana` under a temp HERMES_HOME. Before: config `url: …/sse`, no oauth block, authorize URL on the V1 server (mcp.asana.com/authorize) with a DCR client and 127.0.0.1 redirect. After: config `url: …/v2/mcp` + oauth `${ASANA_CLIENT_ID}` refs, .env holds the values, authorize URL on app.asana.com/-/oauth_authorize with the configured client_id, redirect_uri=http://localhost:27890/callback and resource=https://mcp.asana.com/v2/mcp — the flow Asana's guide documents. |
||
|
|
7954418d90 | fix(mcp): migrate Asana catalog to V2 | ||
|
|
d9fedb066d | revert: remove onboarding catalog additions | ||
|
|
12f495feac | feat(mcp): curate first-task examples and add Blender bridge | ||
|
|
95668f5eab |
fix(mcp): Atlassian catalog entry no longer 404s + Grafana defensive curation
- atlassian: /v1/sse was deprecated by Atlassian after June 30 2026 — installs OAuth'd fine then failed every handshake with 404 (#91538). Now points at /v1/mcp/authv2, the endpoint Atlassian's docs recommend for custom clients (live-probed: OAuth 2.1 + DCR intact). - grafana (on top of @cedricziel's #93183 entry): defensive default_excluded for ask_assistant (opaque Assistant delegation meta-tool, bills usage) and agento11y_* (Grafana's own agent-obs product suite) — both from Grafana's published tool tables, both no-ops until the Cloud surface serves them; billing note added to post_install. |
||
|
|
56f94faaef |
feat(mcp-catalog): add Grafana Cloud MCP server
Vendor-hosted remote MCP at https://mcp.grafana.com/mcp over Streamable HTTP with native OAuth 2.1 + Dynamic Client Registration -- the same transport/auth shape as the existing datadog and sentry catalog entries, handled by Hermes's MCP client + mcp_oauth_manager. Exposes Prometheus/Loki/Tempo/Pyroscope queries, dashboards, datasources, alerting, incidents, and Grafana Assistant investigations, user-scoped to the operator's Grafana RBAC. This is the hosted Grafana Cloud MCP, not the self-run OSS mcp-grafana binary; post_install points self-hosted users to the OSS server. Source: https://grafana.com/docs/grafana-cloud/ai-tools/mcp-servers/cloud-mcp/ |
||
|
|
45b35f962f |
fix(mcp): tool-selection UIs stay in exclude mode instead of freezing include lists
Closes the two config-UI halves of the exclude-mode review (GottZ findings 5-7 on #94513): - hermes mcp configure: on an exclude-mode server, unchecking a tool now APPENDS a literal exclude and re-checking drops it — glob patterns are preserved so future vendor tools keep getting filtered. Previously one uncheck converted the whole config to a frozen include list (globs silently deleted, new vendor tools invisible). Re-checked tools still shadowed by a kept glob get an explicit warning instead of a silent no-op. - hermes tools MCP checklist: same exclude-mode write-back, plus display now matches excludes via matches_name_filter (fnmatch) — glob excludes previously rendered as if nothing were excluded. - klaviyo manifest: post_install no longer tells users to append a param the URL already pins; now documents how to get the FULL surface. Live-verified through the real cmd_mcp_configure path: exclude-mode server with ['*_secret_*', 'docs'], uncheck beta + re-check docs -> exclude becomes ['*_secret_*', 'beta'], no include written. 171 tests green across test_mcp_catalog/test_mcp_config/test_mcp_tool. |
||
|
|
054cba271e |
fix(mcp): review findings — reinstall no longer clobbers user exclude lists + 4 curation gaps
Review blockers (independent reviewer on #94513): 1. Reinstalling an exclude-mode catalog entry wiped the user's edited tools.exclude, replacing it with manifest defaults. install_entry now reads the prior exclude (like it already did for include) and re-writes it verbatim on reinstall. Regression test added + sabotage-verified (fails on old behavior); include-priority test added too. 2. aws-knowledge: exclude aws___retrieve_skill — vendor SKILL.md loader is a vendor skill layer (live tools/list confirmed the tool exists). 3. betterstack: exclude list rewritten to cover the snake_case wire names (vendor's own header examples show remove_dashboard) via globs alongside the doc display-labels; caveat documented in the manifest — server is OAuth-gated so pre-auth enumeration is impossible. 4. railway: exclude railway-agent (opaque server-side agent delegation, acts outside Hermes's per-tool approval loop). 5. twelve-data: exclude oauth plumbing pseudo-tools + quota probe. 6. betterstack post_install no longer claims a fully-checked checklist — exclude-mode bypasses the checklist; text now describes the applied exclude list. Live E2E: fresh temp HERMES_HOME — install applies manifest excludes, user edit survives reinstall. 33/33 catalog tests green. |
||
|
|
88369af1c7 |
refine(mcp): debloat the catalog batch — vendor-doc tool audit applied to every entry
Policy applied (per Teknium direction, matching the Cloudflare precedent): raw tool surfaces only — no server-side code-mode/search-execute layers, no vendor tool_search; bloat (telemetry, feedback, docs-lookup, static-guidance pseudo-tools, plan-gated upsells, dupe batch/compat shims) pruned via manifest defaults. DROPPED (meta-tool gateway IS the server, no vendor off-switch): zapier (discover/enable/execute over 40k actions), wix (CallWixSiteAPI generic invoke), customer-io (cio_read/write/delete_api generic HTTP executors), omnisend (4 generic verb executors), apify (dynamic actor-mount + telemetry-on-by-default), ramp (undocumented SQL/ETL layer, no tool list, money-moving approval tools) URL-LEVEL DEBLOAT (vendor-documented switches): postman -> /minimal variant; klaviyo -> ?core-tools-only=true& disable-tools-with-user-generated-content=true (262 -> ~40 tools) CURATED default_excluded (20 entries) / default_enabled (kiwi, motherduck): monday (GraphQL escape hatch trio...), close (voice-agent cluster that places real AI phone calls, search/fetch layer, 14 excl), betterstack (Execute query SQL hatch, 8 instruction pseudo-tools, team mgmt, 14 excl), mixpanel (6 guidance pseudo-tools, bulk dupes, 10 excl), neon (search/ fetch, docs pair, logs beta, auth product, 10 excl), miro (6 deprecated), gamma (viewer-tracking analytics), robinhood (upsell+social), dropbox, todoist, fireflies, calendly, plaid, attio, gitlab, circleci, buildkite (secrets-exposing get_job_env), semgrep, globalping, prisma-postgres, motherduck (9-tool core enable), kiwi (feedback tool pruned) Clean after audit (no changes needed): canva, clickup, linear-class lean servers, twelve-data (read-only), algolia (read-only, vendor-curated), indeed, strava (vendor read-only, no tool list published), craft, wordpress-com (user-side toggles documented), trivago, alltrails, deepwiki, context7, microsoft-learn, aws-knowledge, wolfram, twilio-docs |
||
|
|
9a37325717 |
feat(mcp): add 18 more live-verified remote MCPs from the final sweep
OAuth+DCR (14): mixpanel, algolia, klaviyo, amplitude, gitlab, circleci, customer-io, omnisend, motherduck, strava, gamma, craft, wordpress-com, robinhood (trade-execution caution in post_install) No-auth (4): kiwi, trivago, alltrails, twilio-docs (search/read-only; kiwi+trivago link out for booking, no in-conversation payment) Probed and rejected: expedia, booking, uber, uber-eats, doordash, instacart, spotify, audible, resy, stubhub, lastminute, coinbase, posthog, dbt (all OAUTH_NO_DCR on the wire despite directory claims of DCR); tripadvisor (token-in-URL endpoint); binance, crypto-com, cash-app (policy: no in-chat trading/checkout beyond robinhood), autodesk-help (too niche), viator (alltrails preferred) |
||
|
|
99f5aec733 | feat(mcp): add Better Stack and Railway remote MCPs (OAuth+DCR, live-verified) | ||
|
|
3e570dd42d |
refine(mcp): drop github, perplexity, telnyx, exa, parallel-search from the catalog batch
- github: 44 tools / ~31K schema tokens duplicating the built-in gh CLI skill - perplexity/telnyx: api_key friction — batch policy is browser-OAuth or no-auth only - exa/parallel-search: redundant with Hermes's native web_search backend |
||
|
|
753f362a23 |
feat(mcp): add 34 official vendor-hosted remote MCP servers to the catalog
Competitor-parity expansion (Perplexity Computer, Manus, Claude, ChatGPT connector catalogs) limited to endpoints a generic MCP client can actually use. Every entry was live-probed: initialize handshake for no-auth servers, RFC 9728 protected-resource metadata -> AS metadata with a registration_endpoint for OAuth servers (true DCR only). No-auth (7): deepwiki, context7, microsoft-learn, exa, aws-knowledge, wolfram, telnyx* API key (3): github (PAT bearer - GitHub reserves browser OAuth for pre-registered IDEs), perplexity, telnyx OAuth 2.1 + DCR (24): canva, zapier, monday, clickup, todoist, dropbox, wix, miro, calendly, ramp, plaid, fireflies, neon, prisma-postgres, postman, globalping, buildkite, semgrep, attio, close, apify, cloudinary, parallel-search, twelve-data, indeed Deliberately excluded after probing OAUTH_NO_DCR (vendor requires a pre-registered app; generic clients cannot connect): Box, Slack, HubSpot, Zoom, PagerDuty, Brex, Docusign, Stack Overflow, Google Workspace. |
||
|
|
3a1a3a1c8f |
feat(mcp): curated exclude list for cloudflare + glob tool filters + default_excluded manifests
The cloudflare entry's 3,320-endpoint surface is ~43% product families a personal/dev account never touches (Zero Trust org-fleet suite, Magic Transit/WAN, Cloudforce One, Radar analytics, API Shield, legacy migration surfaces). Ship a 34-pattern curated exclude list in the manifest: 3,320 -> 1,905 tools kept, and everything Cloudflare adds later stays enabled by default. Mechanism, two small extensions: - tools/mcp_tool.py: tools.include/exclude entries containing glob metacharacters now match via fnmatch (plain names stay exact-match), so a product family is one pattern instead of hundreds of stale literals. - hermes_cli/mcp_catalog.py: manifests may declare tools.default_excluded (mutually exclusive with default_enabled); install writes it to tools.exclude and skips the probe/checklist — a 3,320-row curses checklist is not a UX. Prior user include selections still win on reinstall. Verified by replaying the real filter functions over the live-probed 3,320-tool list: 1,415 excluded, zero overmatch against a per-product target audit; DNS/Workers/R2/D1/tunnels/Access/AI kept. |
||
|
|
53015d3eb5 |
feat(mcp): pin ?codemode=false so tool_search sees the full endpoint surface
The server's default Code Mode surface (search/execute meta-tools) is itself a tool-discovery layer; stacking it under Hermes tool_search would mean two search hops and an opaque 2-tool surface. With ?codemode=false each of the ~3,300 API endpoints registers as its own tool with a full JSON Schema, and Hermes's own progressive disclosure defers and searches the complete catalog — one layer, total information. Verified live: tools/list returns 3,320 tools, all with input schemas. post_install documents the trade-off and how to opt back into Code Mode. |
||
|
|
90fd9a838b |
feat(mcp): add Cloudflare's official API MCP server to the catalog
Adds optional-mcps/cloudflare — Cloudflare's managed remote MCP server (mcp.cloudflare.com/mcp) fronting the entire Cloudflare API (2,500+ endpoints across DNS, Workers, R2, KV, D1, Zero Trust, WAF, Pages) through two Code Mode tools, search() and execute(), at a fixed ~1k-token schema footprint. HTTP transport + native MCP OAuth 2.1 with DCR — no install block, nothing to pin. post_install documents the scoped OAuth grant, the bearer-token path for headless/CI, and Cloudflare's product-specific servers for narrower surfaces. Docs: mention Cloudflare in the hosted-OAuth MCP examples. |
||
|
|
fc8ebff6d8 |
feat(mcp): unify the desktop MCP suggestion directory into the catalog
The desktop app carried its own hardcoded list of 17 vendor MCP endpoints (apps/desktop/src/lib/mcp-directory.ts) powering the composer suggestion pills — a second PR-reviewed vendor list, overlapping and drifting from the Nous-approved MCP catalog (optional-mcps/). This makes the catalog the single source of truth: - manifest schema: optional `suggest:` block (keywords + hosts), parsed, validated, and normalized in mcp_catalog.py - 15 new URL-only hosted-remote catalog entries (atlassian, sentry, datadog, notion, stripe, vercel, supabase, netlify, hugging_face, asana, intercom, airtable, webflow, paypal, square); figma + linear manifests gain suggest blocks - GET /api/mcp/catalog now serves the suggest metadata - desktop suggestion provider builds its match index from the catalog; the static directory remains only as a compatibility rung for older backends without suggest metadata - setup card source line prefers the catalog entry's transport URL GitHub stays out of the catalog on purpose: its hosted MCP rejects generic DCR and the bundled github/* skills (gh CLI) are the stronger integration. New desktop `github` suggestion provider offers the github-auth skill instead — gated on a new cached GET /api/git/gh-auth probe so already- authenticated users never see the pill. |
||
|
|
bdbdfead04 |
fix(security): remove blender MCP catalog entry and skill after upstream compromise
The upstream ahujasid/blender-mcp and ahujasid/ableton-mcp GitHub repos were hijacked on 2026-08-08: the maintainer (@sidahuj) publicly reported his account was compromised and ownership stripped, and both repos now redirect to an attacker-controlled org (MCPBlender, created the same day, pushing new commits since). Although our catalog pinned blender-mcp==1.6.4 from PyPI (pre-compromise, sha256 verified unchanged), the server is only half the bridge: the manifest's post-install instructions and the optional skill directed users to download addon.py — arbitrary Python executed inside Blender — from the now-compromised GitHub repo (the raw URL currently 404s, and the addon ships in no PyPI artifact). There is no trustworthy source for the addon half, so the entry cannot be installed safely end-to-end. Removing the catalog entry and skill entirely until the maintainer confirms account recovery; re-adding is a follow-up PR once upstream is verified clean. - optional-mcps/blender/: removed - optional-skills/creative/blender-mcp/: removed - docs: catalog rows, sidebar entry, skill pages (en + zh-Hans) removed - cross-references in unreal-mcp and kanban-video-orchestrator cleaned |
||
|
|
d9101bef0a |
fix(mcp): curate comfy-cloud default tool set + drop legacy packaging line
- tools.default_enabled: 20-tool curated subset (discovery, generation, job lifecycle, billing). The server exposes ~37 tools; all-enabled adds ~16-22k tokens of schema to every API call — larger than the entire Hermes core toolset (~12.7k). Curated default lands at ~9-12k. Batch, saved/shared workflow, and App Mode tools remain opt-in via 'hermes mcp configure comfy-cloud'. - report_session_summary excluded from defaults per telemetry policy (no outbound telemetry without explicit user opt-in). - description trimmed to catalog guideline length. - revert pyproject data-files line: the per-entry packaging enforcement was removed (no-pip policy); blender/unreal-engine entries have no data-files lines either. |
||
|
|
fee0eae6d8 |
feat(mcp): add Comfy Cloud to the MCP catalog (remote HTTP + native OAuth 2.1)
New catalog entry for Comfy Cloud's hosted remote MCP server at https://cloud.comfy.org/mcp — Streamable HTTP with native MCP OAuth 2.1 (Dynamic Client Registration + PKCE), the same shape as the linear entry. Nothing to install locally; Hermes's MCP client handles discovery and the browser flow on first connect. The server exposes ~30 tools for AI generation on Comfy Cloud: image / video / audio / 3D via ComfyUI workflows (submit_workflow), curated templates (run_template), and partner models like Flux, Kling, and Veo (partner_generate), plus job lifecycle and discovery tools. tools.default_enabled is left unset so the install-time checklist starts all-on, mirroring the linear entry. Also adds the per-entry data-files target in pyproject.toml per the one-target-per-entry pattern documented there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1eb5ee1eaa |
fix(mcp): make Figma remote OAuth work via DCR allowlist defaults
Figma's mcp.figma.com register endpoint is a client_name allowlist (Claude Code / Codex succeed; Hermes Agent 403s) and returns a client secret while advertising auth_method=none, then requires the secret on token exchange. Auto-set client_name + client_secret_post for Figma hosts, pass oauth cfg through login/add paths, force interactive OAuth for hermes mcp login from non-TTY desktop shells, and ship a catalog entry. Proven: hermes mcp login figma → 26 tools. |
||
|
|
9df5f879b4 |
feat(mcp): enforce exact version pins across the whole MCP catalog
Catalog entries now follow the same supply-chain rules as pyproject dependencies: - n8n: install.ref main -> full commit SHA 7a9ae007 (2026-05-23, branches/tags can be moved by the upstream owner; SHAs cannot) - new contract test: every shipped manifest must pin exactly — git installs need a 40-char SHA, uvx/npx-style launchers need pkg==X / pkg@X with a digit-leading version (rejects bare names, ranges, and npm dist-tags like @latest) - module docstring documents the pin policy (exact version, 2-week cooldown) unreal-engine and linear are http transports (server runs elsewhere) so there is nothing to pin at the transport layer. Verified: unpinning blender-mcp in the manifest makes the contract test fail with a named diagnostic; restoring the pin passes. |
||
|
|
a52393a3b6 |
fix(mcp): pin blender-mcp to 1.6.4 per catalog dependency policy
MCP catalog entries follow the same supply-chain rules as pyproject dependencies: exact version pin, and the pinned release must be at least 2 weeks old. blender-mcp 1.6.4 released 2026-06-11 (~5 weeks old, also the latest release). uvx now resolves the exact version instead of latest-at-launch. |
||
|
|
9be941dac1 |
feat(mcp): add Blender to the MCP catalog with a curated 4-tool default
Adds optional-mcps/blender (ahujasid/blender-mcp, stdio via uvx). The server advertises 22 tools; 18 front optional asset services with no upstream trim mechanism, so tools.default_enabled pins the install to the core surface (scene/object info, viewport screenshot, code exec) and the rest stay opt-in through 'hermes mcp configure blender'. Manifests can now declare transport.env (static, non-secret subprocess env vars), parsed/validated in _parse_manifest and written by _build_server_config — used here to ship DISABLE_TELEMETRY=true per the no-telemetry-without-opt-in policy. Runtime already honored per-server env; manifests just couldn't declare it. |
||
|
|
5ffbfed193 |
feat(mcp-catalog): add official Unreal Engine 5.8 MCP server
Epic's experimental Unreal MCP plugin embeds an MCP server inside the Unreal Editor process, served over local HTTP (127.0.0.1:8000/mcp by default). HTTP transport, no auth, no install block — the user enables the plugin in-editor and Hermes connects to the URL. Also drops test_optional_mcps_manifests_ship_in_both_wheel_and_sdist: it asserted wheel/sdist packaging targets for pip/Homebrew/Nix installs, which Hermes does not support — installs run from the repo checkout, where the catalog is discovered by directory iteration with no packaging step. |
||
|
|
8b69ec03af |
feat(mcp): Nous-approved MCP catalog with interactive picker (#30870)
* feat(mcp): Nous-approved MCP catalog with interactive picker
Adds an optional-mcps/ directory mirroring optional-skills/: curated,
Nous-approved MCP servers shipped with the repo but disabled by default.
Presence in optional-mcps/ = approval. No community tier, no trust signals.
Entries are added by merging a PR.
New surface:
hermes mcp Interactive catalog picker (default)
hermes mcp catalog Plain-text list, scriptable
hermes mcp install <name> Install a catalog entry
Picker behavior:
not installed -> install (clone/bootstrap if needed, prompt for creds)
installed/off -> enable
installed/on -> menu (disable / uninstall / reinstall)
Manifest schema (manifest_version: 1) supports:
- transport: stdio (command/args, ${INSTALL_DIR} substitution) or http (url)
- install: optional git clone + bootstrap commands (for repos that need
local venv setup, like the n8n bridge); omit for npx/uvx servers
- auth: api_key (prompts -> ~/.hermes/.env), oauth (provider-mediated
or native MCP), or none
Catalog entries are never auto-updated. Users re-run `hermes mcp install`
to refresh. Credentials always go to ~/.hermes/.env (the .env-is-for-secrets
rule), never to per-server env blocks.
Ships n8n as the reference manifest (https://github.com/CyberSamuraiX/hermes-n8n-mcp).
Tests: 19 catalog tests + E2E install/uninstall round-trip via the shipped
manifest.
* feat(mcp): tool-selection checklist + Linear catalog entry
Adds install-time tool selection so users only enable the MCP tools they
actually want, and ships Linear as a second reference catalog entry to
demonstrate the http+oauth path alongside n8n's stdio+api_key+git-bootstrap.
Tool selection flow:
install (clone/auth/credentials) ->
probe server for available tools ->
curses checklist with pre-checked rows ->
write mcp_servers.<name>.tools.include
Pre-check priority:
1. user's prior tools.include (reinstall preserves selection)
2. manifest's tools.default_enabled (curated subset)
3. all probed tools (default)
Probe-failure fallback (server unreachable, OAuth not yet complete,
backing service offline):
- manifest declared default_enabled -> applied directly
- no default declared -> no filter written (all-on when reachable)
- both cases point user at hermes mcp configure <name>
Manifest schema additions:
tools:
default_enabled: [list, of, tool, names] # optional
Updates:
- optional-mcps/linear/manifest.yaml -- new reference entry (http+oauth)
- optional-mcps/n8n/manifest.yaml -- tools.default_enabled set to the
8 read-mostly tools; mutating tools (activate/deactivate, container_logs)
pruned by default
- docs: new 'Tool selection at install time' section in features/mcp.md
Tests: 7 new tests in TestToolSelection covering probe-success / probe-fail
matrix, manifest-default filtering, reinstall-preserves-selection, and
invalid-default-enabled rejection. 26 catalog tests + 32 existing
mcp_config tests passing.
* feat(mcp): polish — picker unification, include-mode convergence, hardening
Addresses review findings on PR #30870. Lands all improvements that
belong in this PR before merge; defers separate cleanup (consolidating
two probe implementations, change-detector tests) to follow-ups.
Picker UX (mcp_picker.py)
- Unifies catalog + custom (user-added) MCPs in one view with distinct
status badges (available / enabled / installed (disabled) /
custom — enabled / custom — disabled)
- Adds 'Configure tools (probe server + re-pick)' action to both the
catalog-installed and custom-row submenus — the existing
hermes mcp configure flow was previously unreachable from the picker
- Loops until ESC/q so the user can manage several entries in one
session instead of having to re-launch
- Uninstall message now mentions .env credentials are preserved with a
pointer to clean them up manually if no longer needed
- Surfaces a 'requires a newer Hermes' warning per future-manifest
entry instead of silently hiding it
Catalog (mcp_catalog.py)
- catalog_diagnostics() exposes which manifests were skipped and why
(future_manifest vs invalid) so UIs can give actionable feedback
- _do_git_install detects SHA-shaped refs (regex /[0-9a-f]{7,40}/)
and skips the doomed 'git clone --branch <sha>' attempt — clone --branch
only accepts branches/tags, so SHAs always failed noisily before
falling back to the full-clone path
- Probe-success all-tools-enabled message now mentions that new tools
the server adds later will be auto-enabled (no-filter mode)
Convergence (tools_config.py)
- _configure_mcp_tools_interactive now writes tools.include (whitelist)
instead of tools.exclude (blacklist), matching the catalog flow and
hermes mcp configure. The on-disk config shape no longer depends on
which UI the user touched last
- Two existing tests updated to assert the new include-mode contract
Discoverability
- Setup wizard final step now prints 'Browse curated MCPs: hermes mcp'
- Three tip-corpus entries pointing at the new catalog
- Docs updated with: trust model (manifests run code locally, gated by
PR review, but read before installing), runtime ${ENV_VAR} substitution
semantics, and the manifest_version forward-compat behavior
Tests
- 7 new tests covering future-manifest diagnostics, custom MCP picker
rows, SHA-ref git-install path, branch-ref git-install path, and the
tools_config include-mode write contract
- 80 MCP-related tests passing across test_mcp_catalog.py,
test_mcp_config.py, test_mcp_tools_config.py
* fix(mcp): drop setup-wizard catalog hint to satisfy supply-chain scanner
The wizard line 'Browse curated MCPs: hermes mcp' triggered the
CI supply-chain scanner because it pattern-matches on edits to any
file named hermes_cli/setup.py — that filename matches the Python
'install-hook file' heuristic even though this setup.py is the
user-facing 'hermes setup' wizard, not a packaging install hook.
The catalog is already surfaced via three tip-corpus entries in
hermes_cli/tips.py (which the scanner doesn't flag), so dropping the
wizard mention loses no discoverability. Worth revisiting after a
scanner allowlist for this specific file lands.
|