From e21bdfd41b48f91a89c0ae3f35032d03130c57c5 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Fri, 15 May 2026 00:05:06 -0700 Subject: [PATCH 1/5] chore: bump version to 2.7.1 --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 879a3e8..7d1e498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.7.1] - 2026-05-15 + +### Fixed +- Use `did:web` identity in CA-connected mode instead of `did:key` — fixes `DID_MISMATCH` during server verification (#38) +- Auto-skip origin binding for stdio (subprocess) transports — fixes `ORIGIN_MISMATCH` for `CapiscioMCPClient` with `command=` (#38) +- Graceful gRPC error handling in `verify_server()` — returns `UNVERIFIED_ORIGIN` instead of crashing when capiscio-core is unavailable (#38) + ## [2.7.0] - 2026-05-13 ### Added diff --git a/pyproject.toml b/pyproject.toml index de5d7f6..3ecccce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "capiscio-mcp" -version = "2.7.0" +version = "2.7.1" description = "Trust badges for MCP tool calls - RFC-006 & RFC-007 implementation" readme = "README.md" requires-python = ">=3.10" From a7da974fa8181d1cae30a51b25f02746bebfd592 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Fri, 15 May 2026 00:07:11 -0700 Subject: [PATCH 2/5] fix: bump CORE_MIN_VERSION to 2.7.1 and capture stderr on core exit --- capiscio_mcp/_core/lifecycle.py | 15 ++++++++++++++- capiscio_mcp/_core/version.py | 5 +++-- tests/test_core_health.py | 8 ++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/capiscio_mcp/_core/lifecycle.py b/capiscio_mcp/_core/lifecycle.py index 31476cf..212e96e 100644 --- a/capiscio_mcp/_core/lifecycle.py +++ b/capiscio_mcp/_core/lifecycle.py @@ -398,7 +398,20 @@ async def _supervise(self) -> None: if not self._running: break # Intentional shutdown - logger.warning(f"capiscio-core exited with code {return_code}") + # Capture stderr for diagnostics + stderr_text = "" + if self._process.stderr: + try: + stderr_bytes = await self._process.stderr.read() + stderr_text = stderr_bytes.decode(errors="replace").strip() + except Exception: + pass + + logger.warning( + "capiscio-core exited with code %d%s", + return_code, + f": {stderr_text}" if stderr_text else "", + ) if self._restart_count >= self.max_restarts: logger.error(f"Max restarts ({self.max_restarts}) exceeded") diff --git a/capiscio_mcp/_core/version.py b/capiscio_mcp/_core/version.py index 39b76b8..e499dd1 100644 --- a/capiscio_mcp/_core/version.py +++ b/capiscio_mcp/_core/version.py @@ -16,8 +16,9 @@ MCP_VERSION = "0.1.0" # Compatible capiscio-core versions (internal constraint) -# 2.7.0 is required for local OPA policy evaluation (PDP wiring). -CORE_MIN_VERSION = "2.7.0" +# 2.7.1 is required: fixes nil-pointer panic in VerifyServerIdentity +# when badgeVerifier is not initialized (verifier.go:162). +CORE_MIN_VERSION = "2.7.1" CORE_MAX_VERSION = "3.0.0" # exclusive # Proto schema version for wire compatibility diff --git a/tests/test_core_health.py b/tests/test_core_health.py index 309c038..e269e38 100644 --- a/tests/test_core_health.py +++ b/tests/test_core_health.py @@ -21,7 +21,7 @@ async def test_compatible_version(self): mock_stub = MagicMock() mock_response = MagicMock() mock_response.healthy = True - mock_response.core_version = "2.7.0" + mock_response.core_version = "2.7.1" mock_response.proto_version = "1.0" mock_response.version_compatible = True @@ -53,7 +53,7 @@ async def test_sends_client_version(self): mock_stub = MagicMock() mock_response = MagicMock() mock_response.healthy = True - mock_response.core_version = "2.7.0" + mock_response.core_version = "2.7.1" mock_response.version_compatible = True mock_stub.Health = AsyncMock(return_value=mock_response) @@ -219,7 +219,7 @@ async def test_full_health_check_flow(self): # Health response with version info mock_response = MagicMock() mock_response.healthy = True - mock_response.core_version = "2.7.0" + mock_response.core_version = "2.7.1" mock_response.proto_version = "1.0" mock_response.version_compatible = True @@ -246,7 +246,7 @@ async def slow_startup(*args, **kwargs): response = MagicMock() # Simulate slow startup - healthy after 5 calls response.healthy = call_count >= 5 - response.core_version = "2.7.0" + response.core_version = "2.7.1" response.version_compatible = True return response From 90e63ef5b3c31fa460725f2eb12048e40eff58f0 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Fri, 15 May 2026 00:17:57 -0700 Subject: [PATCH 3/5] fix: update connect tests to expect did:web after PR #38 changes --- tests/test_connect.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/test_connect.py b/tests/test_connect.py index 9a1f278..59e31c1 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -8,6 +8,7 @@ from capiscio_mcp.connect import ( MCPServerIdentity, + _derive_did_web, _issue_badge_sync, _load_private_key_pem, _log_key_capture_hint, @@ -20,7 +21,9 @@ SERVER_ID = "550e8400-e29b-41d4-a716-446655440000" API_KEY = "sk_test_abc123" +TEST_SERVER_URL = "http://localhost:8080" FAKE_DID = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" +FAKE_DID_WEB = _derive_did_web(TEST_SERVER_URL, SERVER_ID) FAKE_BADGE = "eyJhbGciOiJFZERTQSJ9.eyJleHAiOjk5OTk5OTk5OTl9.fakesig" FAKE_PRIV_KEY_PEM = "-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----\n" FAKE_PUB_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nfake\n-----END PUBLIC KEY-----\n" @@ -188,11 +191,12 @@ async def test_connect_generates_keys_when_none_exist(self, tmp_keys_dir): identity = await MCPServerIdentity.connect( server_id=SERVER_ID, api_key=API_KEY, - server_url="http://localhost:8080", + server_url=TEST_SERVER_URL, keys_dir=tmp_keys_dir, ) - assert identity.did == FAKE_DID + # connect() derives did:web from server_url + server_id (Step 4) + assert identity.did == FAKE_DID_WEB assert identity.server_id == SERVER_ID assert identity.api_key == API_KEY assert identity.badge == FAKE_BADGE @@ -222,13 +226,14 @@ async def test_connect_recovers_existing_keys(self, tmp_keys_dir): identity = await MCPServerIdentity.connect( server_id=SERVER_ID, api_key=API_KEY, - server_url="http://localhost:8080", + server_url=TEST_SERVER_URL, keys_dir=tmp_keys_dir, ) # Should NOT have regenerated keys mock_gen.assert_not_called() - assert identity.did == FAKE_DID + # connect() derives did:web from server_url + server_id (Step 4) + assert identity.did == FAKE_DID_WEB async def test_connect_no_badge_when_auto_badge_false(self, tmp_keys_dir): """connect(auto_badge=False) should skip badge issuance.""" @@ -277,7 +282,8 @@ async def test_connect_handles_badge_failure_gracefully(self, tmp_keys_dir): assert identity.badge is None assert identity._keeper is None - assert identity.did == FAKE_DID + # connect() derives did:web from server_url + server_id (Step 4) + assert identity.did == FAKE_DID_WEB async def test_connect_uses_env_var_private_key(self, tmp_keys_dir): """connect() should load identity from CAPISCIO_SERVER_PRIVATE_KEY_PEM.""" @@ -292,16 +298,18 @@ async def test_connect_uses_env_var_private_key(self, tmp_keys_dir): identity = await MCPServerIdentity.connect( server_id=SERVER_ID, api_key=API_KEY, + server_url=TEST_SERVER_URL, keys_dir=tmp_keys_dir, ) # Should NOT have generated a new keypair mock_gen.assert_not_called() - assert identity.did == real_did + # connect() derives did:web from server_url + server_id (Step 4) + assert identity.did == FAKE_DID_WEB # Should have persisted key to disk assert (tmp_keys_dir / "private_key.pem").exists() assert (tmp_keys_dir / "public_key.pem").exists() - assert (tmp_keys_dir / "did.txt").read_text() == real_did + assert (tmp_keys_dir / "did.txt").read_text() == FAKE_DID_WEB async def test_connect_env_var_takes_precedence_over_local_file(self, tmp_keys_dir): """Env var key should override a different key on disk.""" @@ -321,11 +329,13 @@ async def test_connect_env_var_takes_precedence_over_local_file(self, tmp_keys_d identity = await MCPServerIdentity.connect( server_id=SERVER_ID, api_key=API_KEY, + server_url=TEST_SERVER_URL, keys_dir=tmp_keys_dir, ) mock_gen.assert_not_called() - assert identity.did == real_did # env var DID, not FAKE_DID + # connect() derives did:web from server_url + server_id (Step 4) + assert identity.did == FAKE_DID_WEB async def test_connect_logs_capture_hint_on_new_generation(self, tmp_keys_dir): """connect() should log a capture hint when generating a new identity.""" From d483f95ccaf5d51bceff6b00147fd2ce2c5964e4 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Fri, 15 May 2026 00:20:09 -0700 Subject: [PATCH 4/5] fix: add server_url to badge failure test --- tests/test_connect.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_connect.py b/tests/test_connect.py index 59e31c1..7005152 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -277,6 +277,7 @@ async def test_connect_handles_badge_failure_gracefully(self, tmp_keys_dir): identity = await MCPServerIdentity.connect( server_id=SERVER_ID, api_key=API_KEY, + server_url=TEST_SERVER_URL, keys_dir=tmp_keys_dir, ) From 952e19e74a8ddb7e78fc695179f98f2a9bb2dc48 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Fri, 15 May 2026 00:22:35 -0700 Subject: [PATCH 5/5] fix: add changelog compare links for 2.7.0 and 2.7.1 --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d1e498..e8e26da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,5 +95,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Python 3.10+ - Optional: mcp >= 1.0 for MCP SDK integration -[Unreleased]: https://github.com/capiscio/capiscio-mcp-python/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/capiscio/capiscio-mcp-python/compare/v2.7.1...HEAD +[2.7.1]: https://github.com/capiscio/capiscio-mcp-python/compare/v2.7.0...v2.7.1 +[2.7.0]: https://github.com/capiscio/capiscio-mcp-python/compare/v0.1.0...v2.7.0 [0.1.0]: https://github.com/capiscio/capiscio-mcp-python/releases/tag/v0.1.0