From a23c4908c446f89ea24e055a727fa9701d6c8270 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Thu, 21 May 2026 12:25:37 -0400 Subject: [PATCH 1/4] fix: guard_sync wraps TimeoutError as GuardError with descriptive message TimeoutError from concurrent.futures was leaking to users when guard_sync was called outside MCP context. Now catches it and raises GuardError with a helpful message explaining the likely cause. --- capiscio_mcp/guard.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/capiscio_mcp/guard.py b/capiscio_mcp/guard.py index 215cca6..f9681ac 100644 --- a/capiscio_mcp/guard.py +++ b/capiscio_mcp/guard.py @@ -688,7 +688,17 @@ async def run_eval(): # We're in an async context, use run_coroutine_threadsafe import concurrent.futures future = asyncio.run_coroutine_threadsafe(run_eval(), loop) - result = future.result(timeout=30.0) + try: + result = future.result(timeout=30.0) + except (TimeoutError, concurrent.futures.TimeoutError): + raise GuardError( + reason=DenyReason.INTERNAL_ERROR, + detail=( + "Guard evaluation timed out. This usually means the " + "guard is being called outside of an MCP request context " + "or the gRPC core server is not responding." + ), + ) else: # No event loop, create one result = asyncio.run(run_eval()) From 01bfe202c9d3ba979d7264e44d3408c5a1dd5206 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Thu, 21 May 2026 12:40:21 -0400 Subject: [PATCH 2/4] chore: bump version to 2.7.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3ecccce..bb7b9ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "capiscio-mcp" -version = "2.7.1" +version = "2.7.2" description = "Trust badges for MCP tool calls - RFC-006 & RFC-007 implementation" readme = "README.md" requires-python = ">=3.10" From b36cbeba7d17e681b1a6c9ca0fe4cfc93da14ae2 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Thu, 21 May 2026 12:43:46 -0400 Subject: [PATCH 3/4] fix: cancel pending future on timeout to prevent resource leak --- capiscio_mcp/guard.py | 1 + 1 file changed, 1 insertion(+) diff --git a/capiscio_mcp/guard.py b/capiscio_mcp/guard.py index f9681ac..eb54054 100644 --- a/capiscio_mcp/guard.py +++ b/capiscio_mcp/guard.py @@ -691,6 +691,7 @@ async def run_eval(): try: result = future.result(timeout=30.0) except (TimeoutError, concurrent.futures.TimeoutError): + future.cancel() raise GuardError( reason=DenyReason.INTERNAL_ERROR, detail=( From 2b7aad2efc45989e5556d9504405c83c4e75b479 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Thu, 21 May 2026 23:00:19 -0400 Subject: [PATCH 4/4] fix: include timeout value in GuardError message Extract timeout to named constant and interpolate into error detail so callers can see how long guard_sync waited. --- capiscio_mcp/guard.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/capiscio_mcp/guard.py b/capiscio_mcp/guard.py index eb54054..cf9c040 100644 --- a/capiscio_mcp/guard.py +++ b/capiscio_mcp/guard.py @@ -688,16 +688,17 @@ async def run_eval(): # We're in an async context, use run_coroutine_threadsafe import concurrent.futures future = asyncio.run_coroutine_threadsafe(run_eval(), loop) + _GUARD_SYNC_TIMEOUT = 30.0 try: - result = future.result(timeout=30.0) + result = future.result(timeout=_GUARD_SYNC_TIMEOUT) except (TimeoutError, concurrent.futures.TimeoutError): future.cancel() raise GuardError( reason=DenyReason.INTERNAL_ERROR, detail=( - "Guard evaluation timed out. This usually means the " - "guard is being called outside of an MCP request context " - "or the gRPC core server is not responding." + f"Guard evaluation timed out after {_GUARD_SYNC_TIMEOUT}s. " + "This usually means the guard is being called outside of " + "an MCP request context or the gRPC core server is not responding." ), ) else: