From 31569f8ea990369153ec08cdaa9cd8759be64b4b Mon Sep 17 00:00:00 2001 From: Chris Guidry Date: Thu, 8 Jan 2026 17:08:22 -0500 Subject: [PATCH] Prefix Redis keys with docket name for ACL isolation The Redis keys fastmcp uses directly (not via docket) weren't prefixed with the docket name, making ACL-based isolation impossible. Now uses docket.key() to prefix all task metadata keys consistently. Bumps pydocket requirement to >=0.16.4 for the new key() method. Co-Authored-By: Claude Opus 4.5 --- pyproject.toml | 2 +- src/fastmcp/server/tasks/handlers.py | 24 ++++++++++++++--------- src/fastmcp/server/tasks/protocol.py | 20 +++++++++++-------- src/fastmcp/server/tasks/subscriptions.py | 6 ++---- uv.lock | 8 ++++---- 5 files changed, 34 insertions(+), 26 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8a5391cd71..3010b826eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "mcp>=1.24.0,<2.0", "openapi-pydantic>=0.5.1", "platformdirs>=4.0.0", - "pydocket>=0.16.3", + "pydocket>=0.16.4", "rich>=13.9.4", "cyclopts>=4.0.0", "authlib>=1.6.5", diff --git a/src/fastmcp/server/tasks/handlers.py b/src/fastmcp/server/tasks/handlers.py index 5693304497..3f528a0b59 100644 --- a/src/fastmcp/server/tasks/handlers.py +++ b/src/fastmcp/server/tasks/handlers.py @@ -72,13 +72,15 @@ async def handle_tool_as_task( tool = await server.get_tool(tool_name) # Store task key mapping and creation timestamp in Redis for protocol handlers - redis_key = f"fastmcp:task:{session_id}:{server_task_id}" - created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at" + task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}") + created_at_key = docket.key( + f"fastmcp:task:{session_id}:{server_task_id}:created_at" + ) ttl_seconds = int( docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS ) async with docket.redis() as redis: - await redis.set(redis_key, task_key, ex=ttl_seconds) + await redis.set(task_meta_key, task_key, ex=ttl_seconds) await redis.set(created_at_key, created_at, ex=ttl_seconds) # Send notifications/tasks/created per SEP-1686 (mandatory) @@ -181,13 +183,15 @@ async def handle_prompt_as_task( prompt = await server.get_prompt(prompt_name) # Store task key mapping and creation timestamp in Redis for protocol handlers - redis_key = f"fastmcp:task:{session_id}:{server_task_id}" - created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at" + task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}") + created_at_key = docket.key( + f"fastmcp:task:{session_id}:{server_task_id}:created_at" + ) ttl_seconds = int( docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS ) async with docket.redis() as redis: - await redis.set(redis_key, task_key, ex=ttl_seconds) + await redis.set(task_meta_key, task_key, ex=ttl_seconds) await redis.set(created_at_key, created_at, ex=ttl_seconds) # Send notifications/tasks/created per SEP-1686 (mandatory) @@ -285,13 +289,15 @@ async def handle_resource_as_task( task_key = build_task_key(session_id, server_task_id, "resource", str(uri)) # Store task key mapping and creation timestamp in Redis for protocol handlers - redis_key = f"fastmcp:task:{session_id}:{server_task_id}" - created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at" + task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}") + created_at_key = docket.key( + f"fastmcp:task:{session_id}:{server_task_id}:created_at" + ) ttl_seconds = int( docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS ) async with docket.redis() as redis: - await redis.set(redis_key, task_key, ex=ttl_seconds) + await redis.set(task_meta_key, task_key, ex=ttl_seconds) await redis.set(created_at_key, created_at, ex=ttl_seconds) # Send notifications/tasks/created per SEP-1686 (mandatory) diff --git a/src/fastmcp/server/tasks/protocol.py b/src/fastmcp/server/tasks/protocol.py index 902947862b..8ada3749e1 100644 --- a/src/fastmcp/server/tasks/protocol.py +++ b/src/fastmcp/server/tasks/protocol.py @@ -77,10 +77,12 @@ async def tasks_get_handler(server: FastMCP, params: dict[str, Any]) -> GetTaskR ) # Look up full task key and creation timestamp from Redis - redis_key = f"fastmcp:task:{session_id}:{client_task_id}" - created_at_key = f"fastmcp:task:{session_id}:{client_task_id}:created_at" + task_meta_key = docket.key(f"fastmcp:task:{session_id}:{client_task_id}") + created_at_key = docket.key( + f"fastmcp:task:{session_id}:{client_task_id}:created_at" + ) async with docket.redis() as redis: - task_key_bytes = await redis.get(redis_key) + task_key_bytes = await redis.get(task_meta_key) created_at_bytes = await redis.get(created_at_key) task_key = None if task_key_bytes is None else task_key_bytes.decode("utf-8") @@ -176,9 +178,9 @@ async def tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any: ) # Look up full task key from Redis - redis_key = f"fastmcp:task:{session_id}:{client_task_id}" + task_meta_key = docket.key(f"fastmcp:task:{session_id}:{client_task_id}") async with docket.redis() as redis: - task_key_bytes = await redis.get(redis_key) + task_key_bytes = await redis.get(task_meta_key) task_key = None if task_key_bytes is None else task_key_bytes.decode("utf-8") @@ -309,10 +311,12 @@ async def tasks_cancel_handler( ) # Look up full task key and creation timestamp from Redis - redis_key = f"fastmcp:task:{session_id}:{client_task_id}" - created_at_key = f"fastmcp:task:{session_id}:{client_task_id}:created_at" + task_meta_key = docket.key(f"fastmcp:task:{session_id}:{client_task_id}") + created_at_key = docket.key( + f"fastmcp:task:{session_id}:{client_task_id}:created_at" + ) async with docket.redis() as redis: - task_key_bytes = await redis.get(redis_key) + task_key_bytes = await redis.get(task_meta_key) created_at_bytes = await redis.get(created_at_key) task_key = None if task_key_bytes is None else task_key_bytes.decode("utf-8") diff --git a/src/fastmcp/server/tasks/subscriptions.py b/src/fastmcp/server/tasks/subscriptions.py index dae18675b5..9bbf0fad16 100644 --- a/src/fastmcp/server/tasks/subscriptions.py +++ b/src/fastmcp/server/tasks/subscriptions.py @@ -101,8 +101,7 @@ async def _send_status_notification( key_parts = parse_task_key(task_key) session_id = key_parts["session_id"] - # Retrieve createdAt timestamp from Redis - created_at_key = f"fastmcp:task:{session_id}:{task_id}:created_at" + created_at_key = docket.key(f"fastmcp:task:{session_id}:{task_id}:created_at") async with docket.redis() as redis: created_at_bytes = await redis.get(created_at_key) @@ -175,8 +174,7 @@ async def _send_progress_notification( key_parts = parse_task_key(task_key) session_id = key_parts["session_id"] - # Retrieve createdAt timestamp from Redis - created_at_key = f"fastmcp:task:{session_id}:{task_id}:created_at" + created_at_key = docket.key(f"fastmcp:task:{session_id}:{task_id}:created_at") async with docket.redis() as redis: created_at_bytes = await redis.get(created_at_key) diff --git a/uv.lock b/uv.lock index 789db41258..afb7d2323d 100644 --- a/uv.lock +++ b/uv.lock @@ -752,7 +752,7 @@ requires-dist = [ { name = "platformdirs", specifier = ">=4.0.0" }, { name = "py-key-value-aio", extras = ["disk", "keyring", "memory"], specifier = ">=0.3.0,<0.4.0" }, { name = "pydantic", extras = ["email"], specifier = ">=2.11.7" }, - { name = "pydocket", specifier = ">=0.16.3" }, + { name = "pydocket", specifier = ">=0.16.4" }, { name = "pyperclip", specifier = ">=1.9.0" }, { name = "python-dotenv", specifier = ">=1.1.0" }, { name = "rich", specifier = ">=13.9.4" }, @@ -1775,7 +1775,7 @@ wheels = [ [[package]] name = "pydocket" -version = "0.16.3" +version = "0.16.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle" }, @@ -1792,9 +1792,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/c5/61dcfce4d50b66a3f09743294d37fab598b81bb0975054b7f732da9243ec/pydocket-0.16.3.tar.gz", hash = "sha256:78e9da576de09e9f3f410d2471ef1c679b7741ddd21b586c97a13872b69bd265", size = 297080, upload-time = "2025-12-23T23:37:33.32Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/c6/eb7f3af72fa5c04b52a3f9390ff0c948987441987f9526dd992d2a6b3524/pydocket-0.16.4.tar.gz", hash = "sha256:d034d1ac75877560d86329fb3643e7b862fcbcdac407d876a62f5d9e386e8753", size = 297949, upload-time = "2026-01-08T21:58:31.637Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/94/93b7f5981aa04f922e0d9ce7326a4587866ec7e39f7c180ffcf408e66ee8/pydocket-0.16.3-py3-none-any.whl", hash = "sha256:e2b50925356e7cd535286255195458ac7bba15f25293356651b36d223db5dd7c", size = 67087, upload-time = "2025-12-23T23:37:31.829Z" }, + { url = "https://files.pythonhosted.org/packages/74/c5/e6ffed3902ead6cb906758749c42211f7f24ea6d8fdd772f531f5a81c9fa/pydocket-0.16.4-py3-none-any.whl", hash = "sha256:cdcdf74b987c2cd5d03c7353d15f8dd2ac9bd43f2a91f7441748d5a8ebd617c9", size = 67374, upload-time = "2026-01-08T21:58:30.01Z" }, ] [[package]]