diff --git a/capiscio_mcp/connect.py b/capiscio_mcp/connect.py index a8648f4..2423944 100644 --- a/capiscio_mcp/connect.py +++ b/capiscio_mcp/connect.py @@ -144,6 +144,21 @@ def _derive_domain(url: str) -> str: return host +def _derive_did_web(server_url: str, server_id: str) -> str: + """Derive the did:web identity for a registered server. + + Matches the Go CA's did.NewAgentDID(domain, agentID) output: + did:web::agents: + + In CA-connected mode the badge subject is always a did:web — the server + must present the same DID for identity verification to pass. + """ + domain = _derive_domain(server_url) + # did:web spec encodes colons in the domain as %3A + encoded_domain = domain.replace(":", "%3A") + return f"did:web:{encoded_domain}:agents:{server_id}" + + def _issue_badge_sync( server_id: str, api_key: str, @@ -516,6 +531,18 @@ async def connect( if is_new_identity: _log_key_capture_hint(server_id, private_key_pem) + # ------------------------------------------------------------------ + # Step 4: Derive did:web identity (CA-connected mode) + # ------------------------------------------------------------------ + # The CA issues badges with sub = did:web:{domain}:agents:{uuid}. + # For identity verification to pass, the server must present the same + # did:web — NOT the did:key from key generation. The keypair is still + # used for PoP signing, but the identity DID is always did:web when + # connected to a CA. + did = _derive_did_web(server_url, server_id) + did_file.write_text(did) + logger.info("Server identity DID (CA-connected): %s", did) + # Update bundle URL if registration yielded a new/different org_id. # (API key and cached bundle URL were already set in Step 1.5 before # the Go core could be started.) diff --git a/capiscio_mcp/integrations/mcp.py b/capiscio_mcp/integrations/mcp.py index c1bc5b1..643fab0 100644 --- a/capiscio_mcp/integrations/mcp.py +++ b/capiscio_mcp/integrations/mcp.py @@ -682,9 +682,19 @@ def __init__( self.min_trust_level = min_trust_level self.fail_on_unverified = fail_on_unverified self.require_pop = require_pop - self.verify_config = verify_config or VerifyConfig(min_trust_level=min_trust_level) self._extra_env = env or {} + # For stdio transports (subprocess-based), origin binding is not + # applicable — there is no HTTP origin to bind against. Auto-skip + # the check unless the caller provided an explicit verify_config. + if verify_config is not None: + self.verify_config = verify_config + else: + self.verify_config = VerifyConfig( + min_trust_level=min_trust_level, + skip_origin_binding=(command is not None), + ) + self._credential = CallerCredential( badge_jws=badge, api_key=api_key, diff --git a/capiscio_mcp/server.py b/capiscio_mcp/server.py index 608503f..3063519 100644 --- a/capiscio_mcp/server.py +++ b/capiscio_mcp/server.py @@ -155,7 +155,16 @@ async def verify_server( # Full verification path: DID + badge requires gRPC validation # Get core client - client = await CoreClient.get_instance() + try: + client = await CoreClient.get_instance() + except Exception as exc: + logger.warning("Cannot reach capiscio-core for server verification: %s", exc) + return VerifyResult( + state=ServerState.UNVERIFIED_ORIGIN, + server_did=server_did, + error_code=ServerErrorCode.BADGE_INVALID, + error_detail=f"capiscio-core unavailable: {exc}", + ) # Import proto from capiscio_mcp._proto.capiscio.v1 import mcp_pb2 @@ -176,7 +185,16 @@ async def verify_server( ) # Make RPC call - response = await client.stub.VerifyServerIdentity(request) + try: + response = await client.stub.VerifyServerIdentity(request) + except Exception as exc: + logger.warning("gRPC call to VerifyServerIdentity failed: %s", exc) + return VerifyResult( + state=ServerState.UNVERIFIED_ORIGIN, + server_did=server_did, + error_code=ServerErrorCode.BADGE_INVALID, + error_detail=f"verification RPC failed: {exc}", + ) # Map response state state_map = {