From 5e7577eff57a6fdbf5de9b74f9947bf86359f316 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Wed, 6 May 2026 15:24:29 -0400 Subject: [PATCH 1/2] feat: surface structured rejection fields from gRPC response (RFC-008 B8) - Regenerate mcp_pb2.py from updated proto with error_code, rejection_detail, requested_capability, presented_capability fields (15-18) and SCOPE_INSUFFICIENT enum value (9) - Add SCOPE_INSUFFICIENT to deny_reason_map in evaluate_tool_access() - Return error_code, requested_capability, presented_capability in the evaluate_tool_access() result dict - Restore try/except TypeError handler for descriptor pool collisions --- capiscio_sdk/_rpc/client.py | 4 + capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2.py | 105 +++++++++++------- .../_rpc/gen/capiscio/v1/mcp_pb2_grpc.py | 75 ++++++++++++- 3 files changed, 138 insertions(+), 46 deletions(-) diff --git a/capiscio_sdk/_rpc/client.py b/capiscio_sdk/_rpc/client.py index 6933e95..297b341 100644 --- a/capiscio_sdk/_rpc/client.py +++ b/capiscio_sdk/_rpc/client.py @@ -1607,6 +1607,7 @@ def evaluate_tool_access( mcp_pb2.MCP_DENY_REASON_TOOL_NOT_ALLOWED: "tool_not_allowed", mcp_pb2.MCP_DENY_REASON_ISSUER_UNTRUSTED: "issuer_untrusted", mcp_pb2.MCP_DENY_REASON_POLICY_DENIED: "policy_denied", + mcp_pb2.MCP_DENY_REASON_SCOPE_INSUFFICIENT: "scope_insufficient", } auth_level_map = { @@ -1636,6 +1637,9 @@ def evaluate_tool_access( "evidence_json": response.evidence_json, "evidence_id": response.evidence_id, "timestamp": timestamp_str, + "error_code": response.error_code, + "requested_capability": response.requested_capability, + "presented_capability": response.presented_capability, } def verify_server_identity( diff --git a/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2.py b/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2.py index 3a89650..15dd587 100644 --- a/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2.py +++ b/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: capiscio/v1/mcp.proto -# Protobuf Python Version: 6.33.5 +# Protobuf Python Version: 6.31.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -12,8 +12,8 @@ _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, 6, - 33, - 5, + 31, + 1, '', 'capiscio/v1/mcp.proto' ) @@ -25,16 +25,21 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + try: - DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x63\x61piscio/v1/mcp.proto\x12\x0b\x63\x61piscio.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa9\x02\n\x19\x45valuateToolAccessRequest\x12\x1b\n\ttool_name\x18\x01 \x01(\tR\x08toolName\x12\x1f\n\x0bparams_hash\x18\x02 \x01(\tR\nparamsHash\x12#\n\rserver_origin\x18\x03 \x01(\tR\x0cserverOrigin\x12\x1d\n\tbadge_jws\x18\x04 \x01(\tH\x00R\x08\x62\x61\x64geJws\x12\x19\n\x07\x61pi_key\x18\x05 \x01(\tH\x00R\x06\x61piKey\x12%\n\x0epolicy_version\x18\x06 \x01(\tR\rpolicyVersion\x12\x33\n\x06\x63onfig\x18\x07 \x01(\x0b\x32\x1b.capiscio.v1.EvaluateConfigR\x06\x63onfigB\x13\n\x11\x63\x61ller_credential\"\xb2\x01\n\x0e\x45valuateConfig\x12\'\n\x0ftrusted_issuers\x18\x01 \x03(\tR\x0etrustedIssuers\x12&\n\x0fmin_trust_level\x18\x02 \x01(\x05R\rminTrustLevel\x12*\n\x11\x61\x63\x63\x65pt_level_zero\x18\x03 \x01(\x08R\x0f\x61\x63\x63\x65ptLevelZero\x12#\n\rallowed_tools\x18\x04 \x03(\tR\x0c\x61llowedTools\"\xc5\x03\n\x1a\x45valuateToolAccessResponse\x12\x34\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32\x18.capiscio.v1.MCPDecisionR\x08\x64\x65\x63ision\x12;\n\x0b\x64\x65ny_reason\x18\x02 \x01(\x0e\x32\x1a.capiscio.v1.MCPDenyReasonR\ndenyReason\x12\x1f\n\x0b\x64\x65ny_detail\x18\x03 \x01(\tR\ndenyDetail\x12\x1b\n\tagent_did\x18\x04 \x01(\tR\x08\x61gentDid\x12\x1b\n\tbadge_jti\x18\x05 \x01(\tR\x08\x62\x61\x64geJti\x12\x38\n\nauth_level\x18\x06 \x01(\x0e\x32\x19.capiscio.v1.MCPAuthLevelR\tauthLevel\x12\x1f\n\x0btrust_level\x18\x07 \x01(\x05R\ntrustLevel\x12#\n\revidence_json\x18\x08 \x01(\tR\x0c\x65videnceJson\x12\x1f\n\x0b\x65vidence_id\x18\t \x01(\tR\nevidenceId\x12\x38\n\ttimestamp\x18\n \x01(\x0b\x32\x1a.google.protobuf.TimestampR\ttimestamp\"\xe5\x01\n\x1bVerifyServerIdentityRequest\x12\x1d\n\nserver_did\x18\x01 \x01(\tR\tserverDid\x12!\n\x0cserver_badge\x18\x02 \x01(\tR\x0bserverBadge\x12)\n\x10transport_origin\x18\x03 \x01(\tR\x0ftransportOrigin\x12#\n\rendpoint_path\x18\x04 \x01(\tR\x0c\x65ndpointPath\x12\x34\n\x06\x63onfig\x18\x05 \x01(\x0b\x32\x1c.capiscio.v1.MCPVerifyConfigR\x06\x63onfig\"\xe1\x01\n\x0fMCPVerifyConfig\x12\'\n\x0ftrusted_issuers\x18\x01 \x03(\tR\x0etrustedIssuers\x12&\n\x0fmin_trust_level\x18\x02 \x01(\x05R\rminTrustLevel\x12*\n\x11\x61\x63\x63\x65pt_level_zero\x18\x03 \x01(\x08R\x0f\x61\x63\x63\x65ptLevelZero\x12!\n\x0coffline_mode\x18\x04 \x01(\x08R\x0bofflineMode\x12.\n\x13skip_origin_binding\x18\x05 \x01(\x08R\x11skipOriginBinding\"\x91\x02\n\x1cVerifyServerIdentityResponse\x12\x31\n\x05state\x18\x01 \x01(\x0e\x32\x1b.capiscio.v1.MCPServerStateR\x05state\x12\x1f\n\x0btrust_level\x18\x02 \x01(\x05R\ntrustLevel\x12\x1d\n\nserver_did\x18\x03 \x01(\tR\tserverDid\x12\x1b\n\tbadge_jti\x18\x04 \x01(\tR\x08\x62\x61\x64geJti\x12>\n\nerror_code\x18\x05 \x01(\x0e\x32\x1f.capiscio.v1.MCPServerErrorCodeR\terrorCode\x12!\n\x0c\x65rror_detail\x18\x06 \x01(\tR\x0b\x65rrorDetail\"\xaa\x01\n\x1aParseServerIdentityRequest\x12@\n\x0chttp_headers\x18\x01 \x01(\x0b\x32\x1b.capiscio.v1.MCPHttpHeadersH\x00R\x0bhttpHeaders\x12@\n\x0cjsonrpc_meta\x18\x02 \x01(\x0b\x32\x1b.capiscio.v1.MCPJsonRpcMetaH\x00R\x0bjsonrpcMetaB\x08\n\x06source\"t\n\x0eMCPHttpHeaders\x12.\n\x13\x63\x61piscio_server_did\x18\x01 \x01(\tR\x11\x63\x61piscioServerDid\x12\x32\n\x15\x63\x61piscio_server_badge\x18\x02 \x01(\tR\x13\x63\x61piscioServerBadge\"-\n\x0eMCPJsonRpcMeta\x12\x1b\n\tmeta_json\x18\x01 \x01(\tR\x08metaJson\"\x8a\x01\n\x1bParseServerIdentityResponse\x12\x1d\n\nserver_did\x18\x01 \x01(\tR\tserverDid\x12!\n\x0cserver_badge\x18\x02 \x01(\tR\x0bserverBadge\x12)\n\x10identity_present\x18\x03 \x01(\x08R\x0fidentityPresent\"9\n\x10MCPHealthRequest\x12%\n\x0e\x63lient_version\x18\x01 \x01(\tR\rclientVersion\"\xa4\x01\n\x11MCPHealthResponse\x12\x18\n\x07healthy\x18\x01 \x01(\x08R\x07healthy\x12!\n\x0c\x63ore_version\x18\x02 \x01(\tR\x0b\x63oreVersion\x12#\n\rproto_version\x18\x03 \x01(\tR\x0cprotoVersion\x12-\n\x12version_compatible\x18\x04 \x01(\x08R\x11versionCompatible*Z\n\x0bMCPDecision\x12\x1c\n\x18MCP_DECISION_UNSPECIFIED\x10\x00\x12\x16\n\x12MCP_DECISION_ALLOW\x10\x01\x12\x15\n\x11MCP_DECISION_DENY\x10\x02*\x82\x01\n\x0cMCPAuthLevel\x12\x1e\n\x1aMCP_AUTH_LEVEL_UNSPECIFIED\x10\x00\x12\x1c\n\x18MCP_AUTH_LEVEL_ANONYMOUS\x10\x01\x12\x1a\n\x16MCP_AUTH_LEVEL_API_KEY\x10\x02\x12\x18\n\x14MCP_AUTH_LEVEL_BADGE\x10\x03*\xd3\x02\n\rMCPDenyReason\x12\x1f\n\x1bMCP_DENY_REASON_UNSPECIFIED\x10\x00\x12!\n\x1dMCP_DENY_REASON_BADGE_MISSING\x10\x01\x12!\n\x1dMCP_DENY_REASON_BADGE_INVALID\x10\x02\x12!\n\x1dMCP_DENY_REASON_BADGE_EXPIRED\x10\x03\x12!\n\x1dMCP_DENY_REASON_BADGE_REVOKED\x10\x04\x12&\n\"MCP_DENY_REASON_TRUST_INSUFFICIENT\x10\x05\x12$\n MCP_DENY_REASON_TOOL_NOT_ALLOWED\x10\x06\x12$\n MCP_DENY_REASON_ISSUER_UNTRUSTED\x10\x07\x12!\n\x1dMCP_DENY_REASON_POLICY_DENIED\x10\x08*\xac\x01\n\x0eMCPServerState\x12 \n\x1cMCP_SERVER_STATE_UNSPECIFIED\x10\x00\x12\'\n#MCP_SERVER_STATE_VERIFIED_PRINCIPAL\x10\x01\x12\'\n#MCP_SERVER_STATE_DECLARED_PRINCIPAL\x10\x02\x12&\n\"MCP_SERVER_STATE_UNVERIFIED_ORIGIN\x10\x03*\xd7\x02\n\x12MCPServerErrorCode\x12\x19\n\x15MCP_SERVER_ERROR_NONE\x10\x00\x12 \n\x1cMCP_SERVER_ERROR_DID_INVALID\x10\x01\x12\"\n\x1eMCP_SERVER_ERROR_BADGE_INVALID\x10\x02\x12\"\n\x1eMCP_SERVER_ERROR_BADGE_EXPIRED\x10\x03\x12\"\n\x1eMCP_SERVER_ERROR_BADGE_REVOKED\x10\x04\x12\'\n#MCP_SERVER_ERROR_TRUST_INSUFFICIENT\x10\x05\x12$\n MCP_SERVER_ERROR_ORIGIN_MISMATCH\x10\x06\x12\"\n\x1eMCP_SERVER_ERROR_PATH_MISMATCH\x10\x07\x12%\n!MCP_SERVER_ERROR_ISSUER_UNTRUSTED\x10\x08\x32\x93\x03\n\nMCPService\x12\x65\n\x12\x45valuateToolAccess\x12&.capiscio.v1.EvaluateToolAccessRequest\x1a\'.capiscio.v1.EvaluateToolAccessResponse\x12k\n\x14VerifyServerIdentity\x12(.capiscio.v1.VerifyServerIdentityRequest\x1a).capiscio.v1.VerifyServerIdentityResponse\x12h\n\x13ParseServerIdentity\x12\'.capiscio.v1.ParseServerIdentityRequest\x1a(.capiscio.v1.ParseServerIdentityResponse\x12G\n\x06Health\x12\x1d.capiscio.v1.MCPHealthRequest\x1a\x1e.capiscio.v1.MCPHealthResponseB\xae\x01\n\x0f\x63om.capiscio.v1B\x08McpProtoP\x01ZDgithub.com/capiscio/capiscio-core/pkg/rpc/gen/capiscio/v1;capisciov1\xa2\x02\x03\x43XX\xaa\x02\x0b\x43\x61piscio.V1\xca\x02\x0b\x43\x61piscio\\V1\xe2\x02\x17\x43\x61piscio\\V1\\GPBMetadata\xea\x02\x0c\x43\x61piscio::V1b\x06proto3') + DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x63\x61piscio/v1/mcp.proto\x12\x0b\x63\x61piscio.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xbe\x03\n\x19\x45valuateToolAccessRequest\x12\x11\n\ttool_name\x18\x01 \x01(\t\x12\x13\n\x0bparams_hash\x18\x02 \x01(\t\x12\x15\n\rserver_origin\x18\x03 \x01(\t\x12\x13\n\tbadge_jws\x18\x04 \x01(\tH\x00\x12\x11\n\x07\x61pi_key\x18\x05 \x01(\tH\x00\x12\x16\n\x0epolicy_version\x18\x06 \x01(\t\x12+\n\x06\x63onfig\x18\x07 \x01(\x0b\x32\x1b.capiscio.v1.EvaluateConfig\x12\x18\n\x10\x65nforcement_mode\x18\x08 \x01(\t\x12\x18\n\x10\x63\x61pability_class\x18\n \x01(\t\x12\x13\n\x0b\x65nvelope_id\x18\x0b \x01(\t\x12\x18\n\x10\x64\x65legation_depth\x18\x0c \x01(\x05\x12\x18\n\x10\x63onstraints_json\x18\r \x01(\t\x12\x1f\n\x17parent_constraints_json\x18\x0e \x01(\t\x12\"\n\x15\x64\x65ny_on_unknown_class\x18\x0f \x01(\x08H\x01\x88\x01\x01\x42\x13\n\x11\x63\x61ller_credentialB\x18\n\x16_deny_on_unknown_classJ\x04\x08\t\x10\n\"t\n\x0e\x45valuateConfig\x12\x17\n\x0ftrusted_issuers\x18\x01 \x03(\t\x12\x17\n\x0fmin_trust_level\x18\x02 \x01(\x05\x12\x19\n\x11\x61\x63\x63\x65pt_level_zero\x18\x03 \x01(\x08\x12\x15\n\rallowed_tools\x18\x04 \x03(\t\"\xbd\x04\n\x1a\x45valuateToolAccessResponse\x12*\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32\x18.capiscio.v1.MCPDecision\x12/\n\x0b\x64\x65ny_reason\x18\x02 \x01(\x0e\x32\x1a.capiscio.v1.MCPDenyReason\x12\x13\n\x0b\x64\x65ny_detail\x18\x03 \x01(\t\x12\x11\n\tagent_did\x18\x04 \x01(\t\x12\x11\n\tbadge_jti\x18\x05 \x01(\t\x12-\n\nauth_level\x18\x06 \x01(\x0e\x32\x19.capiscio.v1.MCPAuthLevel\x12\x13\n\x0btrust_level\x18\x07 \x01(\x05\x12\x15\n\revidence_json\x18\x08 \x01(\t\x12\x13\n\x0b\x65vidence_id\x18\t \x01(\t\x12-\n\ttimestamp\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1a\n\x12policy_decision_id\x18\x0b \x01(\t\x12\x17\n\x0fpolicy_decision\x18\x0c \x01(\t\x12\x18\n\x10\x65nforcement_mode\x18\r \x01(\t\x12/\n\x0bobligations\x18\x0e \x03(\x0b\x32\x1a.capiscio.v1.MCPObligation\x12\x12\n\nerror_code\x18\x0f \x01(\t\x12\x18\n\x10rejection_detail\x18\x10 \x01(\t\x12\x1c\n\x14requested_capability\x18\x11 \x01(\t\x12\x1c\n\x14presented_capability\x18\x12 \x01(\t\"2\n\rMCPObligation\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x13\n\x0bparams_json\x18\x02 \x01(\t\"\xe3\x01\n\x15PolicyDecisionRequest\x12+\n\x07subject\x18\x01 \x01(\x0b\x32\x1a.capiscio.v1.PolicySubject\x12)\n\x06\x61\x63tion\x18\x02 \x01(\x0b\x32\x19.capiscio.v1.PolicyAction\x12-\n\x08resource\x18\x03 \x01(\x0b\x32\x1b.capiscio.v1.PolicyResource\x12)\n\x06\x63onfig\x18\x04 \x01(\x0b\x32\x19.capiscio.v1.PolicyConfig\x12\x18\n\x10\x62reakglass_token\x18\x05 \x01(\t\"d\n\rPolicySubject\x12\x0b\n\x03\x64id\x18\x01 \x01(\t\x12\x11\n\tbadge_jti\x18\x02 \x01(\t\x12\x0b\n\x03ial\x18\x03 \x01(\t\x12\x13\n\x0btrust_level\x18\x04 \x01(\t\x12\x11\n\tbadge_exp\x18\x05 \x01(\x03\";\n\x0cPolicyAction\x12\x11\n\toperation\x18\x01 \x01(\t\x12\x18\n\x10\x63\x61pability_class\x18\x02 \x01(\t\"$\n\x0ePolicyResource\x12\x12\n\nidentifier\x18\x01 \x01(\t\"\x98\x01\n\x0cPolicyConfig\x12\x14\n\x0cpdp_endpoint\x18\x01 \x01(\t\x12\x16\n\x0epdp_timeout_ms\x18\x02 \x01(\x05\x12\x18\n\x10\x65nforcement_mode\x18\x03 \x01(\t\x12\x0e\n\x06pep_id\x18\x04 \x01(\t\x12\x11\n\tworkspace\x18\x05 \x01(\t\x12\x1d\n\x15\x62reakglass_public_key\x18\x06 \x01(\x0c\"\xab\x02\n\x16PolicyDecisionResponse\x12\x10\n\x08\x64\x65\x63ision\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65\x63ision_id\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x0b\n\x03ttl\x18\x04 \x01(\x05\x12/\n\x0bobligations\x18\x05 \x03(\x0b\x32\x1a.capiscio.v1.MCPObligation\x12\x18\n\x10\x65nforcement_mode\x18\x06 \x01(\t\x12\x11\n\tcache_hit\x18\x07 \x01(\x08\x12\x1b\n\x13\x62reakglass_override\x18\x08 \x01(\x08\x12\x16\n\x0e\x62reakglass_jti\x18\t \x01(\t\x12\x12\n\nerror_code\x18\n \x01(\t\x12\x16\n\x0epdp_latency_ms\x18\x0b \x01(\x03\x12\x0e\n\x06txn_id\x18\x0c \x01(\t\"\xa6\x01\n\x1bVerifyServerIdentityRequest\x12\x12\n\nserver_did\x18\x01 \x01(\t\x12\x14\n\x0cserver_badge\x18\x02 \x01(\t\x12\x18\n\x10transport_origin\x18\x03 \x01(\t\x12\x15\n\rendpoint_path\x18\x04 \x01(\t\x12,\n\x06\x63onfig\x18\x05 \x01(\x0b\x32\x1c.capiscio.v1.MCPVerifyConfig\"\x91\x01\n\x0fMCPVerifyConfig\x12\x17\n\x0ftrusted_issuers\x18\x01 \x03(\t\x12\x17\n\x0fmin_trust_level\x18\x02 \x01(\x05\x12\x19\n\x11\x61\x63\x63\x65pt_level_zero\x18\x03 \x01(\x08\x12\x14\n\x0coffline_mode\x18\x04 \x01(\x08\x12\x1b\n\x13skip_origin_binding\x18\x05 \x01(\x08\"\xd1\x01\n\x1cVerifyServerIdentityResponse\x12*\n\x05state\x18\x01 \x01(\x0e\x32\x1b.capiscio.v1.MCPServerState\x12\x13\n\x0btrust_level\x18\x02 \x01(\x05\x12\x12\n\nserver_did\x18\x03 \x01(\t\x12\x11\n\tbadge_jti\x18\x04 \x01(\t\x12\x33\n\nerror_code\x18\x05 \x01(\x0e\x32\x1f.capiscio.v1.MCPServerErrorCode\x12\x14\n\x0c\x65rror_detail\x18\x06 \x01(\t\"\x90\x01\n\x1aParseServerIdentityRequest\x12\x33\n\x0chttp_headers\x18\x01 \x01(\x0b\x32\x1b.capiscio.v1.MCPHttpHeadersH\x00\x12\x33\n\x0cjsonrpc_meta\x18\x02 \x01(\x0b\x32\x1b.capiscio.v1.MCPJsonRpcMetaH\x00\x42\x08\n\x06source\"L\n\x0eMCPHttpHeaders\x12\x1b\n\x13\x63\x61piscio_server_did\x18\x01 \x01(\t\x12\x1d\n\x15\x63\x61piscio_server_badge\x18\x02 \x01(\t\"#\n\x0eMCPJsonRpcMeta\x12\x11\n\tmeta_json\x18\x01 \x01(\t\"a\n\x1bParseServerIdentityResponse\x12\x12\n\nserver_did\x18\x01 \x01(\t\x12\x14\n\x0cserver_badge\x18\x02 \x01(\t\x12\x18\n\x10identity_present\x18\x03 \x01(\x08\"*\n\x10MCPHealthRequest\x12\x16\n\x0e\x63lient_version\x18\x01 \x01(\t\"m\n\x11MCPHealthResponse\x12\x0f\n\x07healthy\x18\x01 \x01(\x08\x12\x14\n\x0c\x63ore_version\x18\x02 \x01(\t\x12\x15\n\rproto_version\x18\x03 \x01(\t\x12\x1a\n\x12version_compatible\x18\x04 \x01(\x08*Z\n\x0bMCPDecision\x12\x1c\n\x18MCP_DECISION_UNSPECIFIED\x10\x00\x12\x16\n\x12MCP_DECISION_ALLOW\x10\x01\x12\x15\n\x11MCP_DECISION_DENY\x10\x02*\x82\x01\n\x0cMCPAuthLevel\x12\x1e\n\x1aMCP_AUTH_LEVEL_UNSPECIFIED\x10\x00\x12\x1c\n\x18MCP_AUTH_LEVEL_ANONYMOUS\x10\x01\x12\x1a\n\x16MCP_AUTH_LEVEL_API_KEY\x10\x02\x12\x18\n\x14MCP_AUTH_LEVEL_BADGE\x10\x03*\xfb\x02\n\rMCPDenyReason\x12\x1f\n\x1bMCP_DENY_REASON_UNSPECIFIED\x10\x00\x12!\n\x1dMCP_DENY_REASON_BADGE_MISSING\x10\x01\x12!\n\x1dMCP_DENY_REASON_BADGE_INVALID\x10\x02\x12!\n\x1dMCP_DENY_REASON_BADGE_EXPIRED\x10\x03\x12!\n\x1dMCP_DENY_REASON_BADGE_REVOKED\x10\x04\x12&\n\"MCP_DENY_REASON_TRUST_INSUFFICIENT\x10\x05\x12$\n MCP_DENY_REASON_TOOL_NOT_ALLOWED\x10\x06\x12$\n MCP_DENY_REASON_ISSUER_UNTRUSTED\x10\x07\x12!\n\x1dMCP_DENY_REASON_POLICY_DENIED\x10\x08\x12&\n\"MCP_DENY_REASON_SCOPE_INSUFFICIENT\x10\t*\xac\x01\n\x0eMCPServerState\x12 \n\x1cMCP_SERVER_STATE_UNSPECIFIED\x10\x00\x12\'\n#MCP_SERVER_STATE_VERIFIED_PRINCIPAL\x10\x01\x12\'\n#MCP_SERVER_STATE_DECLARED_PRINCIPAL\x10\x02\x12&\n\"MCP_SERVER_STATE_UNVERIFIED_ORIGIN\x10\x03*\xd7\x02\n\x12MCPServerErrorCode\x12\x19\n\x15MCP_SERVER_ERROR_NONE\x10\x00\x12 \n\x1cMCP_SERVER_ERROR_DID_INVALID\x10\x01\x12\"\n\x1eMCP_SERVER_ERROR_BADGE_INVALID\x10\x02\x12\"\n\x1eMCP_SERVER_ERROR_BADGE_EXPIRED\x10\x03\x12\"\n\x1eMCP_SERVER_ERROR_BADGE_REVOKED\x10\x04\x12\'\n#MCP_SERVER_ERROR_TRUST_INSUFFICIENT\x10\x05\x12$\n MCP_SERVER_ERROR_ORIGIN_MISMATCH\x10\x06\x12\"\n\x1eMCP_SERVER_ERROR_PATH_MISMATCH\x10\x07\x12%\n!MCP_SERVER_ERROR_ISSUER_UNTRUSTED\x10\x08\x32\xf6\x03\n\nMCPService\x12\x65\n\x12\x45valuateToolAccess\x12&.capiscio.v1.EvaluateToolAccessRequest\x1a\'.capiscio.v1.EvaluateToolAccessResponse\x12\x61\n\x16\x45valuatePolicyDecision\x12\".capiscio.v1.PolicyDecisionRequest\x1a#.capiscio.v1.PolicyDecisionResponse\x12k\n\x14VerifyServerIdentity\x12(.capiscio.v1.VerifyServerIdentityRequest\x1a).capiscio.v1.VerifyServerIdentityResponse\x12h\n\x13ParseServerIdentity\x12\'.capiscio.v1.ParseServerIdentityRequest\x1a(.capiscio.v1.ParseServerIdentityResponse\x12G\n\x06Health\x12\x1d.capiscio.v1.MCPHealthRequest\x1a\x1e.capiscio.v1.MCPHealthResponseB;Z9github.com/capiscio/capiscio-core/pkg/rpc/gen/capiscio/v1b\x06proto3') except TypeError as e: - # Proto already registered by another package (e.g. capiscio_mcp). - # Only fall back for the known duplicate-file-name collision case. if 'duplicate file name' not in str(e): raise pool = _descriptor_pool.Default() file_descriptor = pool.FindFileByName('capiscio/v1/mcp.proto') - # Validate the existing descriptor is schema-compatible try: pool.FindMessageTypeByName('capiscio.v1.EvaluateToolAccessRequest') except KeyError as lookup_err: @@ -50,41 +55,55 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'capiscio.v1.mcp_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n\017com.capiscio.v1B\010McpProtoP\001ZDgithub.com/capiscio/capiscio-core/pkg/rpc/gen/capiscio/v1;capisciov1\242\002\003CXX\252\002\013Capiscio.V1\312\002\013Capiscio\\V1\342\002\027Capiscio\\V1\\GPBMetadata\352\002\014Capiscio::V1' - _globals['_MCPDECISION']._serialized_start=2449 - _globals['_MCPDECISION']._serialized_end=2539 - _globals['_MCPAUTHLEVEL']._serialized_start=2542 - _globals['_MCPAUTHLEVEL']._serialized_end=2672 - _globals['_MCPDENYREASON']._serialized_start=2675 - _globals['_MCPDENYREASON']._serialized_end=3014 - _globals['_MCPSERVERSTATE']._serialized_start=3017 - _globals['_MCPSERVERSTATE']._serialized_end=3189 - _globals['_MCPSERVERERRORCODE']._serialized_start=3192 - _globals['_MCPSERVERERRORCODE']._serialized_end=3535 + _globals['DESCRIPTOR']._serialized_options = b'Z9github.com/capiscio/capiscio-core/pkg/rpc/gen/capiscio/v1' + _globals['_MCPDECISION']._serialized_start=3199 + _globals['_MCPDECISION']._serialized_end=3289 + _globals['_MCPAUTHLEVEL']._serialized_start=3292 + _globals['_MCPAUTHLEVEL']._serialized_end=3422 + _globals['_MCPDENYREASON']._serialized_start=3425 + _globals['_MCPDENYREASON']._serialized_end=3804 + _globals['_MCPSERVERSTATE']._serialized_start=3807 + _globals['_MCPSERVERSTATE']._serialized_end=3979 + _globals['_MCPSERVERERRORCODE']._serialized_start=3982 + _globals['_MCPSERVERERRORCODE']._serialized_end=4325 _globals['_EVALUATETOOLACCESSREQUEST']._serialized_start=72 - _globals['_EVALUATETOOLACCESSREQUEST']._serialized_end=369 - _globals['_EVALUATECONFIG']._serialized_start=372 - _globals['_EVALUATECONFIG']._serialized_end=550 - _globals['_EVALUATETOOLACCESSRESPONSE']._serialized_start=553 - _globals['_EVALUATETOOLACCESSRESPONSE']._serialized_end=1006 - _globals['_VERIFYSERVERIDENTITYREQUEST']._serialized_start=1009 - _globals['_VERIFYSERVERIDENTITYREQUEST']._serialized_end=1238 - _globals['_MCPVERIFYCONFIG']._serialized_start=1241 - _globals['_MCPVERIFYCONFIG']._serialized_end=1466 - _globals['_VERIFYSERVERIDENTITYRESPONSE']._serialized_start=1469 - _globals['_VERIFYSERVERIDENTITYRESPONSE']._serialized_end=1742 - _globals['_PARSESERVERIDENTITYREQUEST']._serialized_start=1745 - _globals['_PARSESERVERIDENTITYREQUEST']._serialized_end=1915 - _globals['_MCPHTTPHEADERS']._serialized_start=1917 - _globals['_MCPHTTPHEADERS']._serialized_end=2033 - _globals['_MCPJSONRPCMETA']._serialized_start=2035 - _globals['_MCPJSONRPCMETA']._serialized_end=2080 - _globals['_PARSESERVERIDENTITYRESPONSE']._serialized_start=2083 - _globals['_PARSESERVERIDENTITYRESPONSE']._serialized_end=2221 - _globals['_MCPHEALTHREQUEST']._serialized_start=2223 - _globals['_MCPHEALTHREQUEST']._serialized_end=2280 - _globals['_MCPHEALTHRESPONSE']._serialized_start=2283 - _globals['_MCPHEALTHRESPONSE']._serialized_end=2447 - _globals['_MCPSERVICE']._serialized_start=3538 - _globals['_MCPSERVICE']._serialized_end=3941 + _globals['_EVALUATETOOLACCESSREQUEST']._serialized_end=518 + _globals['_EVALUATECONFIG']._serialized_start=520 + _globals['_EVALUATECONFIG']._serialized_end=636 + _globals['_EVALUATETOOLACCESSRESPONSE']._serialized_start=639 + _globals['_EVALUATETOOLACCESSRESPONSE']._serialized_end=1212 + _globals['_MCPOBLIGATION']._serialized_start=1214 + _globals['_MCPOBLIGATION']._serialized_end=1264 + _globals['_POLICYDECISIONREQUEST']._serialized_start=1267 + _globals['_POLICYDECISIONREQUEST']._serialized_end=1494 + _globals['_POLICYSUBJECT']._serialized_start=1496 + _globals['_POLICYSUBJECT']._serialized_end=1596 + _globals['_POLICYACTION']._serialized_start=1598 + _globals['_POLICYACTION']._serialized_end=1657 + _globals['_POLICYRESOURCE']._serialized_start=1659 + _globals['_POLICYRESOURCE']._serialized_end=1695 + _globals['_POLICYCONFIG']._serialized_start=1698 + _globals['_POLICYCONFIG']._serialized_end=1850 + _globals['_POLICYDECISIONRESPONSE']._serialized_start=1853 + _globals['_POLICYDECISIONRESPONSE']._serialized_end=2152 + _globals['_VERIFYSERVERIDENTITYREQUEST']._serialized_start=2155 + _globals['_VERIFYSERVERIDENTITYREQUEST']._serialized_end=2321 + _globals['_MCPVERIFYCONFIG']._serialized_start=2324 + _globals['_MCPVERIFYCONFIG']._serialized_end=2469 + _globals['_VERIFYSERVERIDENTITYRESPONSE']._serialized_start=2472 + _globals['_VERIFYSERVERIDENTITYRESPONSE']._serialized_end=2681 + _globals['_PARSESERVERIDENTITYREQUEST']._serialized_start=2684 + _globals['_PARSESERVERIDENTITYREQUEST']._serialized_end=2828 + _globals['_MCPHTTPHEADERS']._serialized_start=2830 + _globals['_MCPHTTPHEADERS']._serialized_end=2906 + _globals['_MCPJSONRPCMETA']._serialized_start=2908 + _globals['_MCPJSONRPCMETA']._serialized_end=2943 + _globals['_PARSESERVERIDENTITYRESPONSE']._serialized_start=2945 + _globals['_PARSESERVERIDENTITYRESPONSE']._serialized_end=3042 + _globals['_MCPHEALTHREQUEST']._serialized_start=3044 + _globals['_MCPHEALTHREQUEST']._serialized_end=3086 + _globals['_MCPHEALTHRESPONSE']._serialized_start=3088 + _globals['_MCPHEALTHRESPONSE']._serialized_end=3197 + _globals['_MCPSERVICE']._serialized_start=4328 + _globals['_MCPSERVICE']._serialized_end=4830 # @@protoc_insertion_point(module_scope) diff --git a/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2_grpc.py b/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2_grpc.py index cac02f6..186cb73 100644 --- a/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2_grpc.py +++ b/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2_grpc.py @@ -1,12 +1,32 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc +import warnings from capiscio_sdk._rpc.gen.capiscio.v1 import mcp_pb2 as capiscio_dot_v1_dot_mcp__pb2 +GRPC_GENERATED_VERSION = '1.76.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in capiscio/v1/mcp_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + class MCPServiceStub(object): - """MCPService provides unified MCP security operations (RFC-006 + RFC-007) + """MCPService provides unified MCP security operations (RFC-005, RFC-006, RFC-007) """ def __init__(self, channel): @@ -20,6 +40,11 @@ def __init__(self, channel): request_serializer=capiscio_dot_v1_dot_mcp__pb2.EvaluateToolAccessRequest.SerializeToString, response_deserializer=capiscio_dot_v1_dot_mcp__pb2.EvaluateToolAccessResponse.FromString, _registered_method=True) + self.EvaluatePolicyDecision = channel.unary_unary( + '/capiscio.v1.MCPService/EvaluatePolicyDecision', + request_serializer=capiscio_dot_v1_dot_mcp__pb2.PolicyDecisionRequest.SerializeToString, + response_deserializer=capiscio_dot_v1_dot_mcp__pb2.PolicyDecisionResponse.FromString, + _registered_method=True) self.VerifyServerIdentity = channel.unary_unary( '/capiscio.v1.MCPService/VerifyServerIdentity', request_serializer=capiscio_dot_v1_dot_mcp__pb2.VerifyServerIdentityRequest.SerializeToString, @@ -38,7 +63,7 @@ def __init__(self, channel): class MCPServiceServicer(object): - """MCPService provides unified MCP security operations (RFC-006 + RFC-007) + """MCPService provides unified MCP security operations (RFC-005, RFC-006, RFC-007) """ def EvaluateToolAccess(self, request, context): @@ -49,6 +74,18 @@ def EvaluateToolAccess(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def EvaluatePolicyDecision(self, request, context): + """RFC-005: Centralized policy decision via PDP + Go core owns decision logic, cache, break-glass, telemetry. + SDK callers own obligation execution and response propagation. + NEVER returns an RPC error for PDP unreachability — encodes the outcome + in the response (ALLOW_OBSERVE + error_code) so SDKs don't need to + distinguish transport errors from policy outcomes. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def VerifyServerIdentity(self, request, context): """RFC-007: Verify server identity from disclosed DID + badge """ @@ -78,6 +115,11 @@ def add_MCPServiceServicer_to_server(servicer, server): request_deserializer=capiscio_dot_v1_dot_mcp__pb2.EvaluateToolAccessRequest.FromString, response_serializer=capiscio_dot_v1_dot_mcp__pb2.EvaluateToolAccessResponse.SerializeToString, ), + 'EvaluatePolicyDecision': grpc.unary_unary_rpc_method_handler( + servicer.EvaluatePolicyDecision, + request_deserializer=capiscio_dot_v1_dot_mcp__pb2.PolicyDecisionRequest.FromString, + response_serializer=capiscio_dot_v1_dot_mcp__pb2.PolicyDecisionResponse.SerializeToString, + ), 'VerifyServerIdentity': grpc.unary_unary_rpc_method_handler( servicer.VerifyServerIdentity, request_deserializer=capiscio_dot_v1_dot_mcp__pb2.VerifyServerIdentityRequest.FromString, @@ -102,7 +144,7 @@ def add_MCPServiceServicer_to_server(servicer, server): # This class is part of an EXPERIMENTAL API. class MCPService(object): - """MCPService provides unified MCP security operations (RFC-006 + RFC-007) + """MCPService provides unified MCP security operations (RFC-005, RFC-006, RFC-007) """ @staticmethod @@ -132,6 +174,33 @@ def EvaluateToolAccess(request, metadata, _registered_method=True) + @staticmethod + def EvaluatePolicyDecision(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/capiscio.v1.MCPService/EvaluatePolicyDecision', + capiscio_dot_v1_dot_mcp__pb2.PolicyDecisionRequest.SerializeToString, + capiscio_dot_v1_dot_mcp__pb2.PolicyDecisionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + @staticmethod def VerifyServerIdentity(request, target, From f586b58ff0c6e0d03a7b388479b67de578cde67f Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Wed, 6 May 2026 15:58:50 -0400 Subject: [PATCH 2/2] fix: address Copilot review feedback on RFC-008 B8 - Remove duplicated import block in mcp_pb2.py - Remove grpcio>=1.76.0 version gate from mcp_pb2_grpc.py (SDK supports grpcio>=1.60.0) - Remove unused warnings import from mcp_pb2_grpc.py - Add rejection_detail to evaluate_tool_access() return dict - Add unit tests for structured rejection fields --- capiscio_sdk/_rpc/client.py | 1 + capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2.py | 8 -- .../_rpc/gen/capiscio/v1/mcp_pb2_grpc.py | 20 ---- tests/unit/test_mcp_structured_fields.py | 98 +++++++++++++++++++ 4 files changed, 99 insertions(+), 28 deletions(-) create mode 100644 tests/unit/test_mcp_structured_fields.py diff --git a/capiscio_sdk/_rpc/client.py b/capiscio_sdk/_rpc/client.py index 297b341..ff216dd 100644 --- a/capiscio_sdk/_rpc/client.py +++ b/capiscio_sdk/_rpc/client.py @@ -1638,6 +1638,7 @@ def evaluate_tool_access( "evidence_id": response.evidence_id, "timestamp": timestamp_str, "error_code": response.error_code, + "rejection_detail": response.rejection_detail, "requested_capability": response.requested_capability, "presented_capability": response.presented_capability, } diff --git a/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2.py b/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2.py index 15dd587..192aec4 100644 --- a/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2.py +++ b/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2.py @@ -25,14 +25,6 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 - - try: DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x63\x61piscio/v1/mcp.proto\x12\x0b\x63\x61piscio.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xbe\x03\n\x19\x45valuateToolAccessRequest\x12\x11\n\ttool_name\x18\x01 \x01(\t\x12\x13\n\x0bparams_hash\x18\x02 \x01(\t\x12\x15\n\rserver_origin\x18\x03 \x01(\t\x12\x13\n\tbadge_jws\x18\x04 \x01(\tH\x00\x12\x11\n\x07\x61pi_key\x18\x05 \x01(\tH\x00\x12\x16\n\x0epolicy_version\x18\x06 \x01(\t\x12+\n\x06\x63onfig\x18\x07 \x01(\x0b\x32\x1b.capiscio.v1.EvaluateConfig\x12\x18\n\x10\x65nforcement_mode\x18\x08 \x01(\t\x12\x18\n\x10\x63\x61pability_class\x18\n \x01(\t\x12\x13\n\x0b\x65nvelope_id\x18\x0b \x01(\t\x12\x18\n\x10\x64\x65legation_depth\x18\x0c \x01(\x05\x12\x18\n\x10\x63onstraints_json\x18\r \x01(\t\x12\x1f\n\x17parent_constraints_json\x18\x0e \x01(\t\x12\"\n\x15\x64\x65ny_on_unknown_class\x18\x0f \x01(\x08H\x01\x88\x01\x01\x42\x13\n\x11\x63\x61ller_credentialB\x18\n\x16_deny_on_unknown_classJ\x04\x08\t\x10\n\"t\n\x0e\x45valuateConfig\x12\x17\n\x0ftrusted_issuers\x18\x01 \x03(\t\x12\x17\n\x0fmin_trust_level\x18\x02 \x01(\x05\x12\x19\n\x11\x61\x63\x63\x65pt_level_zero\x18\x03 \x01(\x08\x12\x15\n\rallowed_tools\x18\x04 \x03(\t\"\xbd\x04\n\x1a\x45valuateToolAccessResponse\x12*\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32\x18.capiscio.v1.MCPDecision\x12/\n\x0b\x64\x65ny_reason\x18\x02 \x01(\x0e\x32\x1a.capiscio.v1.MCPDenyReason\x12\x13\n\x0b\x64\x65ny_detail\x18\x03 \x01(\t\x12\x11\n\tagent_did\x18\x04 \x01(\t\x12\x11\n\tbadge_jti\x18\x05 \x01(\t\x12-\n\nauth_level\x18\x06 \x01(\x0e\x32\x19.capiscio.v1.MCPAuthLevel\x12\x13\n\x0btrust_level\x18\x07 \x01(\x05\x12\x15\n\revidence_json\x18\x08 \x01(\t\x12\x13\n\x0b\x65vidence_id\x18\t \x01(\t\x12-\n\ttimestamp\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1a\n\x12policy_decision_id\x18\x0b \x01(\t\x12\x17\n\x0fpolicy_decision\x18\x0c \x01(\t\x12\x18\n\x10\x65nforcement_mode\x18\r \x01(\t\x12/\n\x0bobligations\x18\x0e \x03(\x0b\x32\x1a.capiscio.v1.MCPObligation\x12\x12\n\nerror_code\x18\x0f \x01(\t\x12\x18\n\x10rejection_detail\x18\x10 \x01(\t\x12\x1c\n\x14requested_capability\x18\x11 \x01(\t\x12\x1c\n\x14presented_capability\x18\x12 \x01(\t\"2\n\rMCPObligation\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x13\n\x0bparams_json\x18\x02 \x01(\t\"\xe3\x01\n\x15PolicyDecisionRequest\x12+\n\x07subject\x18\x01 \x01(\x0b\x32\x1a.capiscio.v1.PolicySubject\x12)\n\x06\x61\x63tion\x18\x02 \x01(\x0b\x32\x19.capiscio.v1.PolicyAction\x12-\n\x08resource\x18\x03 \x01(\x0b\x32\x1b.capiscio.v1.PolicyResource\x12)\n\x06\x63onfig\x18\x04 \x01(\x0b\x32\x19.capiscio.v1.PolicyConfig\x12\x18\n\x10\x62reakglass_token\x18\x05 \x01(\t\"d\n\rPolicySubject\x12\x0b\n\x03\x64id\x18\x01 \x01(\t\x12\x11\n\tbadge_jti\x18\x02 \x01(\t\x12\x0b\n\x03ial\x18\x03 \x01(\t\x12\x13\n\x0btrust_level\x18\x04 \x01(\t\x12\x11\n\tbadge_exp\x18\x05 \x01(\x03\";\n\x0cPolicyAction\x12\x11\n\toperation\x18\x01 \x01(\t\x12\x18\n\x10\x63\x61pability_class\x18\x02 \x01(\t\"$\n\x0ePolicyResource\x12\x12\n\nidentifier\x18\x01 \x01(\t\"\x98\x01\n\x0cPolicyConfig\x12\x14\n\x0cpdp_endpoint\x18\x01 \x01(\t\x12\x16\n\x0epdp_timeout_ms\x18\x02 \x01(\x05\x12\x18\n\x10\x65nforcement_mode\x18\x03 \x01(\t\x12\x0e\n\x06pep_id\x18\x04 \x01(\t\x12\x11\n\tworkspace\x18\x05 \x01(\t\x12\x1d\n\x15\x62reakglass_public_key\x18\x06 \x01(\x0c\"\xab\x02\n\x16PolicyDecisionResponse\x12\x10\n\x08\x64\x65\x63ision\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65\x63ision_id\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x0b\n\x03ttl\x18\x04 \x01(\x05\x12/\n\x0bobligations\x18\x05 \x03(\x0b\x32\x1a.capiscio.v1.MCPObligation\x12\x18\n\x10\x65nforcement_mode\x18\x06 \x01(\t\x12\x11\n\tcache_hit\x18\x07 \x01(\x08\x12\x1b\n\x13\x62reakglass_override\x18\x08 \x01(\x08\x12\x16\n\x0e\x62reakglass_jti\x18\t \x01(\t\x12\x12\n\nerror_code\x18\n \x01(\t\x12\x16\n\x0epdp_latency_ms\x18\x0b \x01(\x03\x12\x0e\n\x06txn_id\x18\x0c \x01(\t\"\xa6\x01\n\x1bVerifyServerIdentityRequest\x12\x12\n\nserver_did\x18\x01 \x01(\t\x12\x14\n\x0cserver_badge\x18\x02 \x01(\t\x12\x18\n\x10transport_origin\x18\x03 \x01(\t\x12\x15\n\rendpoint_path\x18\x04 \x01(\t\x12,\n\x06\x63onfig\x18\x05 \x01(\x0b\x32\x1c.capiscio.v1.MCPVerifyConfig\"\x91\x01\n\x0fMCPVerifyConfig\x12\x17\n\x0ftrusted_issuers\x18\x01 \x03(\t\x12\x17\n\x0fmin_trust_level\x18\x02 \x01(\x05\x12\x19\n\x11\x61\x63\x63\x65pt_level_zero\x18\x03 \x01(\x08\x12\x14\n\x0coffline_mode\x18\x04 \x01(\x08\x12\x1b\n\x13skip_origin_binding\x18\x05 \x01(\x08\"\xd1\x01\n\x1cVerifyServerIdentityResponse\x12*\n\x05state\x18\x01 \x01(\x0e\x32\x1b.capiscio.v1.MCPServerState\x12\x13\n\x0btrust_level\x18\x02 \x01(\x05\x12\x12\n\nserver_did\x18\x03 \x01(\t\x12\x11\n\tbadge_jti\x18\x04 \x01(\t\x12\x33\n\nerror_code\x18\x05 \x01(\x0e\x32\x1f.capiscio.v1.MCPServerErrorCode\x12\x14\n\x0c\x65rror_detail\x18\x06 \x01(\t\"\x90\x01\n\x1aParseServerIdentityRequest\x12\x33\n\x0chttp_headers\x18\x01 \x01(\x0b\x32\x1b.capiscio.v1.MCPHttpHeadersH\x00\x12\x33\n\x0cjsonrpc_meta\x18\x02 \x01(\x0b\x32\x1b.capiscio.v1.MCPJsonRpcMetaH\x00\x42\x08\n\x06source\"L\n\x0eMCPHttpHeaders\x12\x1b\n\x13\x63\x61piscio_server_did\x18\x01 \x01(\t\x12\x1d\n\x15\x63\x61piscio_server_badge\x18\x02 \x01(\t\"#\n\x0eMCPJsonRpcMeta\x12\x11\n\tmeta_json\x18\x01 \x01(\t\"a\n\x1bParseServerIdentityResponse\x12\x12\n\nserver_did\x18\x01 \x01(\t\x12\x14\n\x0cserver_badge\x18\x02 \x01(\t\x12\x18\n\x10identity_present\x18\x03 \x01(\x08\"*\n\x10MCPHealthRequest\x12\x16\n\x0e\x63lient_version\x18\x01 \x01(\t\"m\n\x11MCPHealthResponse\x12\x0f\n\x07healthy\x18\x01 \x01(\x08\x12\x14\n\x0c\x63ore_version\x18\x02 \x01(\t\x12\x15\n\rproto_version\x18\x03 \x01(\t\x12\x1a\n\x12version_compatible\x18\x04 \x01(\x08*Z\n\x0bMCPDecision\x12\x1c\n\x18MCP_DECISION_UNSPECIFIED\x10\x00\x12\x16\n\x12MCP_DECISION_ALLOW\x10\x01\x12\x15\n\x11MCP_DECISION_DENY\x10\x02*\x82\x01\n\x0cMCPAuthLevel\x12\x1e\n\x1aMCP_AUTH_LEVEL_UNSPECIFIED\x10\x00\x12\x1c\n\x18MCP_AUTH_LEVEL_ANONYMOUS\x10\x01\x12\x1a\n\x16MCP_AUTH_LEVEL_API_KEY\x10\x02\x12\x18\n\x14MCP_AUTH_LEVEL_BADGE\x10\x03*\xfb\x02\n\rMCPDenyReason\x12\x1f\n\x1bMCP_DENY_REASON_UNSPECIFIED\x10\x00\x12!\n\x1dMCP_DENY_REASON_BADGE_MISSING\x10\x01\x12!\n\x1dMCP_DENY_REASON_BADGE_INVALID\x10\x02\x12!\n\x1dMCP_DENY_REASON_BADGE_EXPIRED\x10\x03\x12!\n\x1dMCP_DENY_REASON_BADGE_REVOKED\x10\x04\x12&\n\"MCP_DENY_REASON_TRUST_INSUFFICIENT\x10\x05\x12$\n MCP_DENY_REASON_TOOL_NOT_ALLOWED\x10\x06\x12$\n MCP_DENY_REASON_ISSUER_UNTRUSTED\x10\x07\x12!\n\x1dMCP_DENY_REASON_POLICY_DENIED\x10\x08\x12&\n\"MCP_DENY_REASON_SCOPE_INSUFFICIENT\x10\t*\xac\x01\n\x0eMCPServerState\x12 \n\x1cMCP_SERVER_STATE_UNSPECIFIED\x10\x00\x12\'\n#MCP_SERVER_STATE_VERIFIED_PRINCIPAL\x10\x01\x12\'\n#MCP_SERVER_STATE_DECLARED_PRINCIPAL\x10\x02\x12&\n\"MCP_SERVER_STATE_UNVERIFIED_ORIGIN\x10\x03*\xd7\x02\n\x12MCPServerErrorCode\x12\x19\n\x15MCP_SERVER_ERROR_NONE\x10\x00\x12 \n\x1cMCP_SERVER_ERROR_DID_INVALID\x10\x01\x12\"\n\x1eMCP_SERVER_ERROR_BADGE_INVALID\x10\x02\x12\"\n\x1eMCP_SERVER_ERROR_BADGE_EXPIRED\x10\x03\x12\"\n\x1eMCP_SERVER_ERROR_BADGE_REVOKED\x10\x04\x12\'\n#MCP_SERVER_ERROR_TRUST_INSUFFICIENT\x10\x05\x12$\n MCP_SERVER_ERROR_ORIGIN_MISMATCH\x10\x06\x12\"\n\x1eMCP_SERVER_ERROR_PATH_MISMATCH\x10\x07\x12%\n!MCP_SERVER_ERROR_ISSUER_UNTRUSTED\x10\x08\x32\xf6\x03\n\nMCPService\x12\x65\n\x12\x45valuateToolAccess\x12&.capiscio.v1.EvaluateToolAccessRequest\x1a\'.capiscio.v1.EvaluateToolAccessResponse\x12\x61\n\x16\x45valuatePolicyDecision\x12\".capiscio.v1.PolicyDecisionRequest\x1a#.capiscio.v1.PolicyDecisionResponse\x12k\n\x14VerifyServerIdentity\x12(.capiscio.v1.VerifyServerIdentityRequest\x1a).capiscio.v1.VerifyServerIdentityResponse\x12h\n\x13ParseServerIdentity\x12\'.capiscio.v1.ParseServerIdentityRequest\x1a(.capiscio.v1.ParseServerIdentityResponse\x12G\n\x06Health\x12\x1d.capiscio.v1.MCPHealthRequest\x1a\x1e.capiscio.v1.MCPHealthResponseB;Z9github.com/capiscio/capiscio-core/pkg/rpc/gen/capiscio/v1b\x06proto3') except TypeError as e: diff --git a/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2_grpc.py b/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2_grpc.py index 186cb73..648b596 100644 --- a/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2_grpc.py +++ b/capiscio_sdk/_rpc/gen/capiscio/v1/mcp_pb2_grpc.py @@ -1,29 +1,9 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc -import warnings from capiscio_sdk._rpc.gen.capiscio.v1 import mcp_pb2 as capiscio_dot_v1_dot_mcp__pb2 -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in capiscio/v1/mcp_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) - class MCPServiceStub(object): """MCPService provides unified MCP security operations (RFC-005, RFC-006, RFC-007) diff --git a/tests/unit/test_mcp_structured_fields.py b/tests/unit/test_mcp_structured_fields.py new file mode 100644 index 0000000..ec3b2a8 --- /dev/null +++ b/tests/unit/test_mcp_structured_fields.py @@ -0,0 +1,98 @@ +"""Unit tests for structured rejection fields in MCPClient.evaluate_tool_access().""" + +from unittest.mock import MagicMock + +from google.protobuf.timestamp_pb2 import Timestamp + +from capiscio_sdk._rpc.client import MCPClient, _ensure_mcp_protos +from capiscio_sdk._rpc.gen.capiscio.v1 import mcp_pb2 + + +# Ensure the lazy-loaded module-level mcp_pb2 is populated before tests run +_ensure_mcp_protos() + + +class TestEvaluateToolAccessStructuredFields: + """Verify RFC-008 structured rejection fields are surfaced in the return dict.""" + + def _make_mock_response(self, **overrides): + """Build a mock EvaluateToolAccessResponse.""" + defaults = { + "decision": mcp_pb2.MCP_DECISION_DENY, + "deny_reason": mcp_pb2.MCP_DENY_REASON_SCOPE_INSUFFICIENT, + "deny_detail": "capability mismatch", + "agent_did": "did:key:z6Mktest", + "badge_jti": "jti-abc", + "auth_level": mcp_pb2.MCP_AUTH_LEVEL_BADGE, + "trust_level": 2, + "evidence_json": "{}", + "evidence_id": "ev-001", + "timestamp": Timestamp(seconds=1700000000), + "error_code": "SCOPE_MISMATCH", + "rejection_detail": "requested read:sensitive but badge grants read:public", + "requested_capability": "read:sensitive", + "presented_capability": "read:public", + "policy_decision_id": "", + "policy_decision": "", + "enforcement_mode": "", + "obligations": [], + } + defaults.update(overrides) + resp = MagicMock(**defaults) + return resp + + def test_deny_response_includes_structured_fields(self): + stub = MagicMock() + stub.EvaluateToolAccess.return_value = self._make_mock_response() + client = MCPClient(stub) + + result = client.evaluate_tool_access( + tool_name="read_file", + params_hash="h123", + ) + + assert result["decision"] == "deny" + assert result["deny_reason"] == "scope_insufficient" + assert result["error_code"] == "SCOPE_MISMATCH" + assert result["rejection_detail"] == "requested read:sensitive but badge grants read:public" + assert result["requested_capability"] == "read:sensitive" + assert result["presented_capability"] == "read:public" + + def test_allow_response_has_empty_structured_fields(self): + stub = MagicMock() + stub.EvaluateToolAccess.return_value = self._make_mock_response( + decision=mcp_pb2.MCP_DECISION_ALLOW, + deny_reason=mcp_pb2.MCP_DENY_REASON_UNSPECIFIED, + deny_detail="", + error_code="", + rejection_detail="", + requested_capability="", + presented_capability="", + ) + client = MCPClient(stub) + + result = client.evaluate_tool_access(tool_name="list_files") + + assert result["decision"] == "allow" + assert result["error_code"] == "" + assert result["rejection_detail"] == "" + assert result["requested_capability"] == "" + assert result["presented_capability"] == "" + + def test_all_expected_keys_present(self): + stub = MagicMock() + stub.EvaluateToolAccess.return_value = self._make_mock_response() + client = MCPClient(stub) + + result = client.evaluate_tool_access(tool_name="any_tool") + + expected_keys = { + "decision", "deny_reason", "deny_detail", + "agent_did", "badge_jti", "auth_level", + "trust_level", "evidence_json", "evidence_id", + "timestamp", "error_code", "rejection_detail", + "requested_capability", "presented_capability", + } + assert expected_keys.issubset(result.keys()), ( + f"Missing keys: {expected_keys - result.keys()}" + )