Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions capiscio_mcp/connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:<domain>:agents:<uuid>

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,
Expand Down Expand Up @@ -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)
Comment on lines +534 to +544

# 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.)
Expand Down
12 changes: 11 additions & 1 deletion capiscio_mcp/integrations/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
Comment on lines +687 to +696

self._credential = CallerCredential(
badge_jws=badge,
api_key=api_key,
Expand Down
22 changes: 20 additions & 2 deletions capiscio_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}",
)
Comment on lines 158 to +197

# Map response state
state_map = {
Expand Down
Loading