From aff8b1c984f5de32fea2aafb11d8955b047b4c6d Mon Sep 17 00:00:00 2001 From: aryanma Date: Thu, 5 Feb 2026 11:24:27 -0800 Subject: [PATCH 1/3] fix(mcp): filter credentials per-server in _embed_credentials --- src/dedalus_labs/lib/mcp/request.py | 29 ++++++++++++++++++++--------- src/dedalus_labs/lib/mcp/wire.py | 16 ++++++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/dedalus_labs/lib/mcp/request.py b/src/dedalus_labs/lib/mcp/request.py index fa7943a..fa23ee1 100644 --- a/src/dedalus_labs/lib/mcp/request.py +++ b/src/dedalus_labs/lib/mcp/request.py @@ -24,7 +24,7 @@ from ..crypto import encrypt_credentials, fetch_encryption_key, fetch_encryption_key_sync from .protocols import CredentialProtocol -from .wire import serialize_mcp_servers +from .wire import serialize_mcp_servers, slug_to_connection_name logger = logging.getLogger(__name__) @@ -165,35 +165,46 @@ def _encrypt_credentials( return EncryptedCredentials(**encrypted) +def _credentials_for_server( + name: str, + all_creds: Dict[str, str], +) -> Optional[Dict[str, str]]: + """Return the subset of *all_creds* that belongs to *name*, or None.""" + conn = slug_to_connection_name(name) + blob = all_creds.get(conn) + return {conn: blob} if blob else None + + def _embed_credentials( servers: List[MCPServerItem], encrypted: EncryptedCredentials, ) -> List[MCPServerSpec]: """Embed encrypted credentials into each server spec. - Converts slug strings to full specs and adds credentials to all servers. + Each server receives only its own credentials, matched by connection name + via :func:`~dedalus_labs.lib.mcp.wire.slug_to_connection_name`. Args: servers: Serialized MCP servers (slug strings or spec dicts). encrypted: EncryptedCredentials instance. Returns: - List of MCPServerSpec dicts with credentials embedded. + List of MCPServerSpec dicts with per-server credentials embedded. """ - creds_dict = encrypted.to_dict() + all_creds = encrypted.to_dict() result: List[MCPServerSpec] = [] for server in servers: if isinstance(server, str): + creds = _credentials_for_server(server, all_creds) if server.startswith(("http://", "https://")): - result.append({"url": server, "name": server, "credentials": creds_dict}) + result.append({"url": server, "name": server, "credentials": creds}) else: - result.append({"slug": server, "name": server, "credentials": creds_dict}) + result.append({"slug": server, "name": server, "credentials": creds}) elif isinstance(server, dict): - # Existing spec -> add name (if missing) and credentials name = server.get("name") or server.get("slug") or server.get("url") or "" - spec: MCPServerSpec = {**server, "name": name, "credentials": creds_dict} - result.append(spec) + creds = _credentials_for_server(name, all_creds) + result.append({**server, "name": name, "credentials": creds}) return result diff --git a/src/dedalus_labs/lib/mcp/wire.py b/src/dedalus_labs/lib/mcp/wire.py index ec67840..ab416f7 100644 --- a/src/dedalus_labs/lib/mcp/wire.py +++ b/src/dedalus_labs/lib/mcp/wire.py @@ -34,6 +34,7 @@ # Helpers "build_connection_record", "collect_unique_connections", + "slug_to_connection_name", ] @@ -326,6 +327,21 @@ def collect_unique_connections(servers: Sequence[MCPServerProtocol]) -> List[Any return unique +def slug_to_connection_name(slug: str) -> str: + """Derive the canonical connection name from a server slug. + + Slugs use ``org/server`` format; connection names use dashes. + + Args: + slug: Server slug, URL, or name string. + + Returns: + Connection name with slashes replaced by dashes. + + """ + return slug.replace("/", "-") + + # --------------------------------------------------------------------------- # Credential Matching # --------------------------------------------------------------------------- From 7dc03e960df690017b3b41d73f61cd7254d54964 Mon Sep 17 00:00:00 2001 From: Windsor Date: Sat, 28 Feb 2026 11:07:14 -0800 Subject: [PATCH 2/3] test(mcp): add request credential embedding regression tests --- src/dedalus_labs/lib/mcp/request.py | 6 +- src/dedalus_labs/lib/mcp/wire.py | 6 +- tests/test_mcp_request.py | 104 ++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 tests/test_mcp_request.py diff --git a/src/dedalus_labs/lib/mcp/request.py b/src/dedalus_labs/lib/mcp/request.py index fa23ee1..fefb30c 100644 --- a/src/dedalus_labs/lib/mcp/request.py +++ b/src/dedalus_labs/lib/mcp/request.py @@ -16,15 +16,15 @@ import copy import logging -from dataclasses import dataclass from typing import Any, Dict, List, Optional, Sequence +from dataclasses import dataclass -from dedalus_labs.types.shared_params.mcp_server_spec import MCPServerSpec from dedalus_labs.types.shared_params.mcp_servers import MCPServerItem +from dedalus_labs.types.shared_params.mcp_server_spec import MCPServerSpec +from .wire import serialize_mcp_servers, slug_to_connection_name from ..crypto import encrypt_credentials, fetch_encryption_key, fetch_encryption_key_sync from .protocols import CredentialProtocol -from .wire import serialize_mcp_servers, slug_to_connection_name logger = logging.getLogger(__name__) diff --git a/src/dedalus_labs/lib/mcp/wire.py b/src/dedalus_labs/lib/mcp/wire.py index ab416f7..5678018 100644 --- a/src/dedalus_labs/lib/mcp/wire.py +++ b/src/dedalus_labs/lib/mcp/wire.py @@ -11,11 +11,11 @@ from __future__ import annotations -from typing import Any, Dict, List, Optional, Sequence, Tuple, Union, cast - -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from typing import Any, Dict, List, Tuple, Union, Optional, Sequence, cast from typing_extensions import TypeAlias +from pydantic import Field, BaseModel, ConfigDict, field_validator, model_validator + from .protocols import MCPServerProtocol, CredentialProtocol, is_mcp_server __all__ = [ diff --git a/tests/test_mcp_request.py b/tests/test_mcp_request.py new file mode 100644 index 0000000..a7632a9 --- /dev/null +++ b/tests/test_mcp_request.py @@ -0,0 +1,104 @@ +# ============================================================================== +# © 2025 Dedalus Labs, Inc. and affiliates +# Licensed under MIT +# github.com/dedalus-labs/dedalus-sdk-python/LICENSE +# ============================================================================== + +"""Tests for MCP request credential embedding.""" + +from __future__ import annotations + +from typing import Any, Dict + +import pytest + +from dedalus_labs.lib.mcp import request as mcp_request +from dedalus_labs.lib.mcp.wire import slug_to_connection_name + + +def _fake_encrypted_credentials() -> mcp_request.EncryptedCredentials: + return mcp_request.EncryptedCredentials( + **{ + "dedalus-labs-gmail-mcp": "enc-gmail", + "dedalus-labs-slack-mcp": "enc-slack", + } + ) + + +class TestSlugToConnectionName: + def test_slug_to_connection_name(self) -> None: + assert slug_to_connection_name("dedalus-labs/gmail-mcp") == "dedalus-labs-gmail-mcp" + + +class TestPrepareMCPRequest: + def test_sync_embeds_per_server_credentials(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(mcp_request, "fetch_encryption_key_sync", lambda _http, _url: object()) + monkeypatch.setattr(mcp_request, "_encrypt_credentials", lambda _creds, _key: _fake_encrypted_credentials()) + + data: Dict[str, Any] = { + "mcp_servers": [ + "dedalus-labs/gmail-mcp", + "dedalus-labs/slack-mcp", + "dedalus-labs/calendar-mcp", + ], + "credentials": [object(), object()], + } + + result = mcp_request.prepare_mcp_request_sync(data, "https://auth.example.com", object()) + + assert "credentials" not in result + assert result["mcp_servers"] == [ + { + "slug": "dedalus-labs/gmail-mcp", + "name": "dedalus-labs/gmail-mcp", + "credentials": {"dedalus-labs-gmail-mcp": "enc-gmail"}, + }, + { + "slug": "dedalus-labs/slack-mcp", + "name": "dedalus-labs/slack-mcp", + "credentials": {"dedalus-labs-slack-mcp": "enc-slack"}, + }, + { + "slug": "dedalus-labs/calendar-mcp", + "name": "dedalus-labs/calendar-mcp", + "credentials": None, + }, + ] + + @pytest.mark.asyncio + async def test_async_embeds_per_server_credentials(self, monkeypatch: pytest.MonkeyPatch) -> None: + async def fake_fetch(_http: Any, _url: str) -> object: + return object() + + monkeypatch.setattr(mcp_request, "fetch_encryption_key", fake_fetch) + monkeypatch.setattr(mcp_request, "_encrypt_credentials", lambda _creds, _key: _fake_encrypted_credentials()) + + data: Dict[str, Any] = { + "mcp_servers": [ + "dedalus-labs/gmail-mcp", + "dedalus-labs/slack-mcp", + "dedalus-labs/calendar-mcp", + ], + "credentials": [object(), object()], + } + + result = await mcp_request.prepare_mcp_request(data, "https://auth.example.com", object()) + + assert "credentials" not in result + assert result["mcp_servers"] == [ + { + "slug": "dedalus-labs/gmail-mcp", + "name": "dedalus-labs/gmail-mcp", + "credentials": {"dedalus-labs-gmail-mcp": "enc-gmail"}, + }, + { + "slug": "dedalus-labs/slack-mcp", + "name": "dedalus-labs/slack-mcp", + "credentials": {"dedalus-labs-slack-mcp": "enc-slack"}, + }, + { + "slug": "dedalus-labs/calendar-mcp", + "name": "dedalus-labs/calendar-mcp", + "credentials": None, + }, + ] From 3c1d4159616c0d6cffacd66b0353e236c20d467f Mon Sep 17 00:00:00 2001 From: Windsor Date: Sat, 28 Feb 2026 11:33:10 -0800 Subject: [PATCH 3/3] refactor(test): tighten typing in MCP request regression tests --- tests/test_mcp_request.py | 152 +++++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 67 deletions(-) diff --git a/tests/test_mcp_request.py b/tests/test_mcp_request.py index a7632a9..5361d36 100644 --- a/tests/test_mcp_request.py +++ b/tests/test_mcp_request.py @@ -8,7 +8,8 @@ from __future__ import annotations -from typing import Any, Dict +from typing import TypedDict +from dataclasses import dataclass import pytest @@ -16,13 +17,69 @@ from dedalus_labs.lib.mcp.wire import slug_to_connection_name -def _fake_encrypted_credentials() -> mcp_request.EncryptedCredentials: - return mcp_request.EncryptedCredentials( - **{ - "dedalus-labs-gmail-mcp": "enc-gmail", - "dedalus-labs-slack-mcp": "enc-slack", - } - ) +@dataclass(frozen=True) +class _FakeConnection: + name: str + + +@dataclass(frozen=True) +class _FakeCredential: + connection: _FakeConnection + encrypted_blob: str + + def values_for_encryption(self) -> dict[str, str]: + return {"blob": self.encrypted_blob} + + +class _RequestPayload(TypedDict): + mcp_servers: list[str] + credentials: list[_FakeCredential] + + +class _FakeHTTPClient: + pass + + +class _FakePublicKey: + pass + + +def _build_payload() -> _RequestPayload: + return { + "mcp_servers": [ + "dedalus-labs/gmail-mcp", + "dedalus-labs/slack-mcp", + "dedalus-labs/calendar-mcp", + ], + "credentials": [ + _FakeCredential(_FakeConnection("dedalus-labs-gmail-mcp"), "enc-gmail"), + _FakeCredential(_FakeConnection("dedalus-labs-slack-mcp"), "enc-slack"), + ], + } + + +def _expected_mcp_servers() -> list[dict[str, str | dict[str, str] | None]]: + return [ + { + "slug": "dedalus-labs/gmail-mcp", + "name": "dedalus-labs/gmail-mcp", + "credentials": {"dedalus-labs-gmail-mcp": "enc-gmail"}, + }, + { + "slug": "dedalus-labs/slack-mcp", + "name": "dedalus-labs/slack-mcp", + "credentials": {"dedalus-labs-slack-mcp": "enc-slack"}, + }, + { + "slug": "dedalus-labs/calendar-mcp", + "name": "dedalus-labs/calendar-mcp", + "credentials": None, + }, + ] + + +def _fake_encrypt_credentials(_public_key: _FakePublicKey, values: dict[str, str]) -> str: + return values["blob"] class TestSlugToConnectionName: @@ -32,73 +89,34 @@ def test_slug_to_connection_name(self) -> None: class TestPrepareMCPRequest: def test_sync_embeds_per_server_credentials(self, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(mcp_request, "fetch_encryption_key_sync", lambda _http, _url: object()) - monkeypatch.setattr(mcp_request, "_encrypt_credentials", lambda _creds, _key: _fake_encrypted_credentials()) + def _fake_fetch_sync(_http: _FakeHTTPClient, _url: str) -> _FakePublicKey: + return _FakePublicKey() - data: Dict[str, Any] = { - "mcp_servers": [ - "dedalus-labs/gmail-mcp", - "dedalus-labs/slack-mcp", - "dedalus-labs/calendar-mcp", - ], - "credentials": [object(), object()], - } + monkeypatch.setattr(mcp_request, "fetch_encryption_key_sync", _fake_fetch_sync) + monkeypatch.setattr(mcp_request, "encrypt_credentials", _fake_encrypt_credentials) - result = mcp_request.prepare_mcp_request_sync(data, "https://auth.example.com", object()) + result = mcp_request.prepare_mcp_request_sync( + _build_payload(), + "https://auth.example.com", + _FakeHTTPClient(), + ) assert "credentials" not in result - assert result["mcp_servers"] == [ - { - "slug": "dedalus-labs/gmail-mcp", - "name": "dedalus-labs/gmail-mcp", - "credentials": {"dedalus-labs-gmail-mcp": "enc-gmail"}, - }, - { - "slug": "dedalus-labs/slack-mcp", - "name": "dedalus-labs/slack-mcp", - "credentials": {"dedalus-labs-slack-mcp": "enc-slack"}, - }, - { - "slug": "dedalus-labs/calendar-mcp", - "name": "dedalus-labs/calendar-mcp", - "credentials": None, - }, - ] + assert result["mcp_servers"] == _expected_mcp_servers() @pytest.mark.asyncio async def test_async_embeds_per_server_credentials(self, monkeypatch: pytest.MonkeyPatch) -> None: - async def fake_fetch(_http: Any, _url: str) -> object: - return object() - - monkeypatch.setattr(mcp_request, "fetch_encryption_key", fake_fetch) - monkeypatch.setattr(mcp_request, "_encrypt_credentials", lambda _creds, _key: _fake_encrypted_credentials()) + async def _fake_fetch_async(_http: _FakeHTTPClient, _url: str) -> _FakePublicKey: + return _FakePublicKey() - data: Dict[str, Any] = { - "mcp_servers": [ - "dedalus-labs/gmail-mcp", - "dedalus-labs/slack-mcp", - "dedalus-labs/calendar-mcp", - ], - "credentials": [object(), object()], - } + monkeypatch.setattr(mcp_request, "fetch_encryption_key", _fake_fetch_async) + monkeypatch.setattr(mcp_request, "encrypt_credentials", _fake_encrypt_credentials) - result = await mcp_request.prepare_mcp_request(data, "https://auth.example.com", object()) + result = await mcp_request.prepare_mcp_request( + _build_payload(), + "https://auth.example.com", + _FakeHTTPClient(), + ) assert "credentials" not in result - assert result["mcp_servers"] == [ - { - "slug": "dedalus-labs/gmail-mcp", - "name": "dedalus-labs/gmail-mcp", - "credentials": {"dedalus-labs-gmail-mcp": "enc-gmail"}, - }, - { - "slug": "dedalus-labs/slack-mcp", - "name": "dedalus-labs/slack-mcp", - "credentials": {"dedalus-labs-slack-mcp": "enc-slack"}, - }, - { - "slug": "dedalus-labs/calendar-mcp", - "name": "dedalus-labs/calendar-mcp", - "credentials": None, - }, - ] + assert result["mcp_servers"] == _expected_mcp_servers()