Skip to content
22 changes: 8 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,7 @@ from capiscio_mcp.integrations.mcp import CapiscioMCPServer
# db is your application's database connection (asyncpg, databases, etc.)
db = ... # e.g. databases.Database("postgresql://...")

server = CapiscioMCPServer(
name="data-api",
did="did:web:mcp.example.com:servers:data-api",
badge="eyJhbGc...", # From CapiscIO registry
)
server = CapiscioMCPServer.connect()

@server.tool(min_trust_level=2)
async def get_user(user_id: int) -> dict:
Expand Down Expand Up @@ -204,26 +200,23 @@ async with CapiscioMCPClient(
print(result)
```

## MCPServerIdentity.connect() — "Let's Encrypt" Style Setup
## CapiscioMCPServer.connect() — Let's Encrypt Style Setup

Register your MCP server and get a badge with a single call:

```python
from capiscio_mcp import MCPServerIdentity
from capiscio_mcp.integrations.mcp import CapiscioMCPServer

identity = await MCPServerIdentity.connect(
server_id="550e8400-...", # From the dashboard
api_key="sk_live_...",
)
server = CapiscioMCPServer.connect()

print(identity.did) # did:web:registry.capisc.io:servers:550e8400-...
print(identity.badge) # Current badge JWS (auto-issued)
print(server.did) # did:web:registry.capisc.io:servers:550e8400-...
Comment thread
beonde marked this conversation as resolved.
print(server.badge) # Current badge JWS (auto-issued)
```

### Using Environment Variables

```python
identity = await MCPServerIdentity.from_env()
server = CapiscioMCPServer.connect()
```

| Variable | Required | Description |
Expand Down Expand Up @@ -424,6 +417,7 @@ config = VerifyConfig(

Requires `pip install capiscio-mcp[mcp]`:

- `CapiscioMCPServer.connect()` — One-liner: load identity from env and create server
- `CapiscioMCPServer(name, did, badge, ...)` — FastMCP wrapper with trust enforcement
- `CapiscioMCPServer.tool(min_trust_level=...)` — Decorator for guarded tools
- `CapiscioMCPServer.run(transport="stdio")` — Run the server
Expand Down
20 changes: 9 additions & 11 deletions capiscio_mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,17 @@
- Server identity registration for MCP servers
- PoP (Proof of Possession) handshake for server key verification
- Evidence logging for audit and forensics
- One-line server identity setup via MCPServerIdentity.connect()
- One-line server identity setup via CapiscioMCPServer.connect()

Installation:
pip install capiscio-mcp # Standalone
pip install capiscio-mcp[mcp] # With MCP SDK integration
pip install capiscio-mcp[crypto] # With PoP signing/verification

Quickstart ("Let's Encrypt" style — recommended):
from capiscio_mcp import MCPServerIdentity
from capiscio_mcp.integrations.mcp import CapiscioMCPServer

identity = await MCPServerIdentity.connect(
server_id=os.environ["CAPISCIO_SERVER_ID"],
api_key=os.environ["CAPISCIO_API_KEY"],
)
server = CapiscioMCPServer(identity=identity)
server = CapiscioMCPServer.connect()

@server.tool(min_trust_level=2)
async def read_file(path: str) -> str:
Expand Down Expand Up @@ -61,6 +56,13 @@ async def read_database(query: str) -> list[dict]:
print(f"Server DID: {result['did']}")
"""

import os as _os

# Suppress gRPC C-core stderr noise (ev_poll_posix.cc, fork_posix.cc, etc.)
# before any gRPC import. Library users should not see low-level C-core logs.
_os.environ.setdefault("GRPC_VERBOSITY", "NONE")
Comment on lines +59 to +63
_os.environ.setdefault("GRPC_TRACE", "")

from capiscio_mcp.types import (
Decision,
AuthLevel,
Expand Down Expand Up @@ -88,8 +90,6 @@ async def read_database(query: str) -> list[dict]:
GuardResult,
compute_params_hash,
evaluate_tool_access,
Comment thread
beonde marked this conversation as resolved.
set_pip_config,
get_pip_config,
)
from capiscio_mcp.server import (
verify_server,
Expand Down Expand Up @@ -165,8 +165,6 @@ async def read_database(query: str) -> list[dict]:
"GuardResult",
"compute_params_hash",
"evaluate_tool_access",
"set_pip_config",
"get_pip_config",
# Policy (RFC-005)
"PIPConfig",
"PolicyClient",
Expand Down
71 changes: 41 additions & 30 deletions capiscio_mcp/connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ async def main():

from capiscio_mcp.keeper import ServerBadgeKeeper
from capiscio_mcp.events import GuardEventEmitter, set_event_emitter
from capiscio_mcp.guard import set_pip_config
from capiscio_mcp.pip import PIPConfig
from capiscio_mcp.registration import (
RegistrationError,
generate_server_keypair,
Expand Down Expand Up @@ -280,7 +278,6 @@ async def connect(
auto_badge: bool = True,
renewal_threshold: int = 30,
on_badge_renew: Optional[Callable[[str], None]] = None,
Comment on lines 278 to 280
pdp_endpoint: Optional[str] = None,
) -> "MCPServerIdentity":
"""Connect to CapiscIO and get a fully-configured MCP server identity.

Expand All @@ -306,10 +303,6 @@ async def connect(
auto_badge: If ``True``, issue an initial badge and start auto-renewal.
renewal_threshold: Renew badge this many seconds before expiry.
on_badge_renew: Optional callback ``(badge: str) -> None`` on renewal.
pdp_endpoint: Optional remote PDP URL for org-policy enforcement.
Defaults to empty (local OPA bundle evaluation via Go core).
Use ``CAPISCIO_PDP_ENDPOINT`` env var or this param only when
a remote PDP service is explicitly deployed.

Returns:
:class:`MCPServerIdentity` with ``.did``, ``.badge``, ``.keys_dir``,
Expand Down Expand Up @@ -538,13 +531,15 @@ async def connect(
if auto_badge:
badge = await _issue_badge(server_id, effective_api_key, server_url, domain=domain)
if badge:
effective_domain = domain or _derive_domain(server_url)
keeper = ServerBadgeKeeper(
server_id=server_id,
api_key=api_key,
api_key=effective_api_key,
initial_badge=badge,
ca_url=server_url,
renewal_threshold=renewal_threshold,
on_renew=on_badge_renew,
domain=effective_domain,
Comment thread
beonde marked this conversation as resolved.
)
keeper.start()
else:
Expand All @@ -563,27 +558,6 @@ async def connect(
)
)

# Step 7: Auto-configure PDP for org-policy enforcement
# Policy evaluation is LOCAL: the Go core fetches the OPA bundle via
# CAPISCIO_BUNDLE_URL and evaluates it with its embedded OPA engine.
# pdp_endpoint is only needed if a remote PDP is explicitly configured.
effective_pdp = (
pdp_endpoint
or os.environ.get("CAPISCIO_PDP_ENDPOINT")
or ""
)
set_pip_config(
PIPConfig(
pdp_endpoint=effective_pdp,
pep_id=f"mcp-server:{server_id}",
workspace=server_id,
)
)
if effective_pdp:
logger.info("Remote PDP configured: pdp_endpoint=%s", effective_pdp)
else:
logger.debug("Using local OPA bundle for policy evaluation")

return cls(
server_id=server_id,
did=did, # type: ignore[arg-type]
Expand All @@ -605,7 +579,6 @@ async def from_env(cls, **kwargs: Any) -> "MCPServerIdentity":
- ``CAPISCIO_API_KEY`` (required)
- ``CAPISCIO_SERVER_URL`` (optional, default: production)
- ``CAPISCIO_SERVER_DOMAIN`` (optional, default: hostname from SERVER_URL)
- ``CAPISCIO_PDP_ENDPOINT`` (optional — PDP URL for org-policy enforcement)
- ``CAPISCIO_SERVER_PRIVATE_KEY_PEM`` (optional — PEM-encoded Ed25519
private key for ephemeral environments; printed on first generation)

Expand Down Expand Up @@ -674,3 +647,41 @@ async def from_env(cls, **kwargs: Any) -> "MCPServerIdentity":
domain=domain or None,
**kwargs,
)

# ------------------------------------------------------------------
# Sync convenience methods
# ------------------------------------------------------------------

@classmethod
def connect_sync(
cls,
server_id: str,
api_key: Optional[str] = None,
Comment on lines +655 to +659
**kwargs: Any,
) -> "MCPServerIdentity":
"""Synchronous version of :meth:`connect`.

Accepts the same arguments. Runs ``connect()`` in a fresh event loop
so callers don't need ``asyncio.run()`` boilerplate.

Example::

identity = MCPServerIdentity.connect_sync(
server_id=os.environ["CAPISCIO_SERVER_ID"],
api_key=os.environ["CAPISCIO_API_KEY"],
)
"""
return asyncio.run(cls.connect(server_id, api_key, **kwargs))

@classmethod
def from_env_sync(cls, **kwargs: Any) -> "MCPServerIdentity":
"""Synchronous version of :meth:`from_env`.

Reads the same environment variables. Runs ``from_env()`` in a fresh
event loop so callers don't need ``asyncio.run()`` boilerplate.

Example::

identity = MCPServerIdentity.from_env_sync()
"""
Comment thread
beonde marked this conversation as resolved.
return asyncio.run(cls.from_env(**kwargs))
Comment thread
beonde marked this conversation as resolved.
Comment thread
beonde marked this conversation as resolved.
Loading
Loading