mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-07 08:02:23 +00:00
feat(mcp): support TLS client certificates (mTLS) for HTTP and SSE servers (#33721)
Adds first-class `client_cert` / `client_key` config keys so MCP servers
behind mTLS work without an external TLS-terminating proxy. Resolves
inbound community question (Jeremy W.).
Schema (per `mcp_servers.<name>`, HTTP/SSE only):
- `client_cert: "/path/to/combined.pem"` — single PEM with cert + key
- `client_cert: "/path/to/cert"` + `client_key: "/path/to/key"` — separate
- `client_cert: [cert, key]` or `[cert, key, password]` — list form,
with optional passphrase for encrypted keys
Paths support `~` expansion. Missing files raise a server-scoped
`FileNotFoundError` at connect time rather than failing later with an
opaque TLS handshake error.
Wiring:
- New SDK HTTP path (mcp >= 1.24): `cert=` on the user-owned
`httpx.AsyncClient` alongside the existing `verify=` handling.
- SSE path: routed through an `httpx_client_factory` that wraps the
SDK's defaults (follow_redirects=True) and layers `verify` + `cert`
on top. The factory is only injected when needed, so the SDK's
built-in `create_mcp_http_client` keeps being used in the default
case.
- Deprecated mcp<1.24 path left untouched — that SDK's
`streamablehttp_client` signature doesn't expose `cert`, and adding
it would be dead code.
Also documents the previously-undocumented `ssl_verify` key (bool or
CA bundle path) in the MCP config reference.
Tests:
- `tests/tools/test_mcp_client_cert.py` (new, 19 tests):
- `_resolve_client_cert` helper: all three input forms, `~` expansion,
missing-file and validation errors.
- HTTP transport: `cert=` forwarded into `httpx.AsyncClient` for
string and tuple forms; absent when unset; missing-file error
propagates.
- SSE transport: factory only injected when cert or non-default
verify is set; factory applies cert, custom CA bundle, and
preserves `follow_redirects=True` + forwarded headers/auth.
- Existing tests: 200/200 in `test_mcp_tool.py` + `test_mcp_sse_transport.py`
still pass.
This commit is contained in:
parent
8595281f3c
commit
87e5b2fae0
3 changed files with 671 additions and 0 deletions
|
|
@ -25,6 +25,11 @@ mcp_servers:
|
|||
url: "..." # HTTP servers
|
||||
headers: {}
|
||||
|
||||
# Optional HTTP/SSE TLS settings:
|
||||
ssl_verify: true # bool or path to a CA bundle (PEM)
|
||||
client_cert: "/path/to/cert.pem" # mTLS client certificate (see below)
|
||||
# client_key: "/path/to/key.pem" # optional, when key lives in a separate file
|
||||
|
||||
enabled: true
|
||||
timeout: 120
|
||||
connect_timeout: 60
|
||||
|
|
@ -45,6 +50,9 @@ mcp_servers:
|
|||
| `env` | mapping | stdio | Environment passed to the subprocess |
|
||||
| `url` | string | HTTP | Remote MCP endpoint |
|
||||
| `headers` | mapping | HTTP | Headers for remote server requests |
|
||||
| `ssl_verify` | bool or string | HTTP | TLS verification. `true` (default) uses system CAs, `false` disables verification (insecure), or a string path to a custom CA bundle (PEM) |
|
||||
| `client_cert` | string or list | HTTP | mTLS client certificate. String = path to a PEM file containing cert + key. List `[cert, key]` = separate files. List `[cert, key, password]` = encrypted key |
|
||||
| `client_key` | string | HTTP | Path to the client private key, when `client_cert` is a string and the key is in a separate file |
|
||||
| `enabled` | bool | both | Skip the server entirely when false |
|
||||
| `timeout` | number | both | Tool call timeout |
|
||||
| `connect_timeout` | number | both | Initial connection timeout |
|
||||
|
|
@ -191,6 +199,40 @@ mcp_servers:
|
|||
prompts: false
|
||||
```
|
||||
|
||||
### TLS client certificate (mTLS)
|
||||
|
||||
For HTTP/SSE servers that require a client certificate, set `client_cert` (and optionally `client_key`):
|
||||
|
||||
```yaml
|
||||
mcp_servers:
|
||||
# Combined cert + key in a single PEM file
|
||||
internal_api:
|
||||
url: "https://mcp.internal.example.com/mcp"
|
||||
client_cert: "~/secrets/mcp-client.pem"
|
||||
|
||||
# Separate cert and key files
|
||||
partner_api:
|
||||
url: "https://mcp.partner.example.com/mcp"
|
||||
client_cert: "~/secrets/client.crt"
|
||||
client_key: "~/secrets/client.key"
|
||||
|
||||
# Encrypted key with a passphrase (3-element list form)
|
||||
bank_api:
|
||||
url: "https://mcp.bank.example.com/mcp"
|
||||
client_cert: ["~/secrets/client.crt", "~/secrets/client.key", "my-passphrase"]
|
||||
|
||||
# Custom CA bundle (private CA / self-signed server)
|
||||
lab_api:
|
||||
url: "https://mcp.lab.local/mcp"
|
||||
ssl_verify: "~/secrets/lab-ca.pem"
|
||||
client_cert: "~/secrets/lab-client.pem"
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Paths support `~` expansion. Missing files fail fast at connect time with a server-scoped error message.
|
||||
- `ssl_verify: false` disables server certificate verification entirely. Don't use this with real services.
|
||||
- Works on both Streamable HTTP and SSE transports.
|
||||
|
||||
## Reloading config
|
||||
|
||||
After changing MCP config, reload servers with:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue