Skip to content

Commit 5f62eae

Browse files
authored
feat: MCPServerIdentity.connect(), ServerBadgeKeeper, stdio credential-in-_meta (#7)
* feat: MCPServerIdentity.connect(), ServerBadgeKeeper, stdio credential-in-_meta - Add capiscio_mcp/connect.py: MCPServerIdentity dataclass with connect() and from_env() class methods. Handles idempotent key generation, registration, badge issuance, and keeper start. "Let's Encrypt" pattern for one-liner MCP server identity setup (RFC-007). - Add capiscio_mcp/keeper.py: ServerBadgeKeeper background thread that monitors badge expiry (exp claim) and calls POST /v1/sdk/servers/{id}/badge for renewal. Mirrors SDK's BadgeKeeper. - Update capiscio_mcp/integrations/mcp.py: * CapiscioMCPServer.__init__: accept identity=MCPServerIdentity shortcut * _meta injection: patch ServerSession._received_request once (idempotent) wrapping responder.respond for InitializeRequest to inject identity meta * _install_credential_extraction: new function wrapping the FastMCP CallToolRequest handler to extract capiscio_caller_badge / capiscio_caller_api_key from _meta and set _current_credential contextvar before the guarded tool runs. Fixes stdio transport where HTTP headers are not available (RFC-002 ss9.1 equivalent for stdio). * CapiscioMCPClient.call_tool: forward badge/api_key in JSON-RPC _meta (meta={"capiscio_caller_badge": ...}) instead of setting a contextvar in the client process which had no effect on the subprocess server. * CapiscioMCPClient.connect: send PoP nonce in initialize request _meta, extract server identity from InitializeResult.meta, verify via verify_server(), enforce min_trust_level/fail_on_unverified. - Update capiscio_mcp/__init__.py: export MCPServerIdentity, ServerBadgeKeeper - Add tests/test_connect.py: 43 tests for MCPServerIdentity - Add tests/test_keeper.py: 43 tests for ServerBadgeKeeper - Update tests/test_integrations.py: 13 new tests for _install_credential_extraction and client meta propagation (346 total) All 346 tests pass. * fix: correctly extract badge token from nested data.token response field The badge endpoint returns the JWS under data.data.token (not data.data.badge). Update _issue_badge_sync to check both .token and .badge in the nested payload, and add .domain auto-derivation from the CA URL so the badge request includes the required domain field. Also reads CAPISCIO_SERVER_DOMAIN env var in from_env() for user override. * fix: forward CAPISCIO_* env vars to stdio MCP server subprocess mcp.client.stdio.get_default_environment() only passes a small whitelist of vars (HOME, PATH, USER, etc.) to the subprocess. CAPISCIO_* credentials were being stripped, causing the server subprocess to fail with a missing env var error. CapiscioMCPClient now auto-forwards all CAPISCIO_* vars (and MCP_SERVER_COMMAND) from the parent process via StdioServerParameters.env. An explicit env dict can also be passed to __init__() for callers that need per-connection overrides. * feat: env var key injection for ephemeral environments (CAPISCIO_SERVER_PRIVATE_KEY_PEM) Add support for injecting the server private key via environment variable for containerised/serverless deployments where ~/.capiscio is ephemeral. Key priority: env var > local file > generate new keypair. On first-run keygen, a capture hint is logged to stderr with the PEM-encoded key for the operator to persist in their secrets manager. - Add _load_private_key_pem() and _did_from_ed25519_pub_raw() helpers - Add _log_key_capture_hint() with box-formatted capture hint - Move cryptography and base58 to base dependencies - Add 4 unit tests for env var injection and capture hint * docs: add deployment guide and document env var key injection - Add new docs/guides/deployment.md covering Docker, Lambda, Cloud Run, K8s - Update README with MCPServerIdentity.connect() section and env var table - Update server-registration.md with new env vars and deployment link - Add Deployment to mkdocs nav * fix(docs): correct did:key → did:web for production registry usage The registry assigns did:web when an API key is used. did:key is only for local dev mode without a registry. * fix: address Copilot PR review comments - Remove unused imports (base64, sys, KeyGenerationError, threading, etc.) - Remove dead code (der_b64 variable) - Security: write capture hint to stderr instead of logger.warning to prevent private key leaking into log aggregation pipelines - Add JSON decode error handling in _issue_badge_sync and keeper._renew - Fix keeper.stop() to detect still-alive threads after 5s timeout - Fix session resource leak: call __aexit__ on session before clearing - Fix fail_on_unverified to enforce regardless of min_trust_level - Fix ServerVerifyError constructor calls with proper error_code/detail - Fix registration error handling to only swallow 409/None status codes All 350 tests passing.
1 parent e7a855c commit 5f62eae

12 files changed

Lines changed: 2688 additions & 363 deletions

File tree

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,68 @@ async with CapiscioMCPClient(
197197
print(result)
198198
```
199199

200+
## MCPServerIdentity.connect() — "Let's Encrypt" Style Setup
201+
202+
Register your MCP server and get a badge with a single call:
203+
204+
```python
205+
from capiscio_mcp import MCPServerIdentity
206+
207+
identity = await MCPServerIdentity.connect(
208+
server_id="550e8400-...", # From the dashboard
209+
api_key="sk_live_...",
210+
)
211+
212+
print(identity.did) # did:web:registry.capisc.io:servers:550e8400-...
213+
print(identity.badge) # Current badge JWS (auto-issued)
214+
```
215+
216+
### Using Environment Variables
217+
218+
```python
219+
identity = await MCPServerIdentity.from_env()
220+
```
221+
222+
| Variable | Required | Description |
223+
|----------|----------|-------------|
224+
| `CAPISCIO_SERVER_ID` | Yes | Server UUID from dashboard |
225+
| `CAPISCIO_API_KEY` | Yes | Registry API key |
226+
| `CAPISCIO_SERVER_URL` | No | Registry URL (default: production) |
227+
| `CAPISCIO_SERVER_DOMAIN` | No | Domain for badge issuance |
228+
| `CAPISCIO_SERVER_PRIVATE_KEY_PEM` | No | PEM-encoded Ed25519 private key for ephemeral environments |
229+
230+
### Deploying to Containers / Serverless
231+
232+
In ephemeral environments (Docker, Lambda, Cloud Run) the local `~/.capiscio/` directory
233+
doesn't survive restarts. On first run the SDK generates a keypair and logs a capture hint:
234+
235+
```
236+
╔══════════════════════════════════════════════════════════╗
237+
║ New server identity generated — save key for persistence ║
238+
╚══════════════════════════════════════════════════════════╝
239+
240+
Add to your secrets manager / .env:
241+
242+
CAPISCIO_SERVER_PRIVATE_KEY_PEM='-----BEGIN PRIVATE KEY-----\nMC4C...\n-----END PRIVATE KEY-----\n'
243+
```
244+
245+
Copy that value into your secrets manager and set it as an environment variable.
246+
On subsequent starts the SDK will recover the same DID without generating a new identity.
247+
248+
**Key resolution priority:** env var → local file → generate new.
249+
250+
```yaml
251+
# docker-compose.yml
252+
services:
253+
mcp-server:
254+
environment:
255+
CAPISCIO_SERVER_ID: "550e8400-..."
256+
CAPISCIO_API_KEY: "sk_live_..."
257+
CAPISCIO_SERVER_PRIVATE_KEY_PEM: "${MCP_SERVER_KEY}" # from secrets
258+
```
259+
260+
See the [Deployment Guide](https://docs.capisc.io/mcp-guard/guides/deployment/) for full examples.
261+
200262
## Core Connection Modes
201263
202264
MCP Guard connects to capiscio-core for cryptographic operations:
@@ -299,6 +361,11 @@ config = VerifyConfig(
299361

300362
| Variable | Description | Default |
301363
|----------|-------------|---------|
364+
| `CAPISCIO_SERVER_ID` | Server UUID (for `MCPServerIdentity`) ||
365+
| `CAPISCIO_API_KEY` | Registry API key (for `MCPServerIdentity`) ||
366+
| `CAPISCIO_SERVER_URL` | Registry server URL | `https://registry.capisc.io` |
367+
| `CAPISCIO_SERVER_DOMAIN` | Domain for badge issuance | (derived from server URL) |
368+
| `CAPISCIO_SERVER_PRIVATE_KEY_PEM` | PEM-encoded Ed25519 private key (ephemeral envs) ||
302369
| `CAPISCIO_CORE_ADDR` | External core address | (embedded mode) |
303370
| `CAPISCIO_SERVER_ORIGIN` | Server origin for guard | (auto-detect) |
304371
| `CAPISCIO_LOG_LEVEL` | Logging verbosity | `info` |

capiscio_mcp/__init__.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,30 @@
1010
- Server identity registration for MCP servers
1111
- PoP (Proof of Possession) handshake for server key verification
1212
- Evidence logging for audit and forensics
13+
- One-line server identity setup via MCPServerIdentity.connect()
1314
1415
Installation:
1516
pip install capiscio-mcp # Standalone
1617
pip install capiscio-mcp[mcp] # With MCP SDK integration
1718
pip install capiscio-mcp[crypto] # With PoP signing/verification
1819
19-
Quickstart (Server-side):
20+
Quickstart ("Let's Encrypt" style — recommended):
21+
from capiscio_mcp import MCPServerIdentity
22+
from capiscio_mcp.integrations.mcp import CapiscioMCPServer
23+
24+
identity = await MCPServerIdentity.connect(
25+
server_id=os.environ["CAPISCIO_SERVER_ID"],
26+
api_key=os.environ["CAPISCIO_API_KEY"],
27+
)
28+
server = CapiscioMCPServer(identity=identity)
29+
30+
@server.tool(min_trust_level=2)
31+
async def read_file(path: str) -> str:
32+
...
33+
34+
server.run()
35+
36+
Quickstart (@guard decorator):
2037
from capiscio_mcp import guard
2138
2239
@guard(min_trust_level=2)
@@ -33,7 +50,7 @@ async def read_database(query: str) -> list[dict]:
3350
if result.state == ServerState.VERIFIED_PRINCIPAL:
3451
print(f"Trusted at level {result.trust_level}")
3552
36-
Quickstart (Server Registration):
53+
Quickstart (Server Registration, manual):
3754
from capiscio_mcp import setup_server_identity
3855
3956
result = await setup_server_identity(
@@ -95,6 +112,8 @@ async def read_database(query: str) -> list[dict]:
95112
RegistrationError,
96113
KeyGenerationError,
97114
)
115+
from capiscio_mcp.keeper import ServerBadgeKeeper
116+
from capiscio_mcp.connect import MCPServerIdentity
98117
from capiscio_mcp._core.version import (
99118
MCP_VERSION,
100119
CORE_MIN_VERSION,
@@ -154,4 +173,7 @@ async def read_database(query: str) -> list[dict]:
154173
"setup_server_identity_sync",
155174
"RegistrationError",
156175
"KeyGenerationError",
176+
# One-liner identity setup (MCPServerIdentity.connect())
177+
"MCPServerIdentity",
178+
"ServerBadgeKeeper",
157179
]

0 commit comments

Comments
 (0)