Skip to content

Commit ed7413c

Browse files
committed
feat(core): add title field support and SSO ADFS integration
Add title column to tools, resources, and prompts tables following MCP 2025-11-25 spec precedence (title → annotations.title → name). Implement _resolve_tool_title() helper with comprehensive doctests. SSO enhancements: - Add ADFS integration with tutorial documentation - Expand SSO service test coverage (775+ new test lines) - Add integration tests for ADFS flows Performance improvements: - Optimize gateway listings with eager-loaded tool counts - Add selectinload for tools relationship in admin queries Testing: - Add unit tests for demo_a2a_agent.py script - Expand gateway_service and sso_service test coverage - Add ADFS integration test suite Documentation: - Add comprehensive ADFS SSO tutorial - Update A2A agent documentation - Remove duplicate config.schema.json files Database: - Migration a7f3c9e1b2d4: add title column (idempotent) - Update schemas to include title field validation Signed-off-by: Jonathan Springer <jps@s390x.com>
1 parent 13e3f52 commit ed7413c

22 files changed

+3493
-23873
lines changed

.secrets.baseline

Lines changed: 3219 additions & 23707 deletions
Large diffs are not rendered by default.

mcpgateway/alembic/versions/a2a_push_notification_configs_8f2e1c9b0d3a.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
from alembic import op
1616
import sqlalchemy as sa
1717

18-
revision: str = "8f2e1c9b0d3a"
19-
down_revision: Union[str, Sequence[str], None] = "3f7e9d1a2b4c"
18+
revision: str = "8f2e1c9b0d3a" # pragma: allowlist secret
19+
down_revision: Union[str, Sequence[str], None] = "3f7e9d1a2b4c" # pragma: allowlist secret
2020
branch_labels: Union[str, Sequence[str], None] = None
2121
depends_on: Union[str, Sequence[str], None] = None
2222

mcpgateway/alembic/versions/a2a_v1_domain_models_3f7e9d1a2b4c.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
from alembic import op
1818
import sqlalchemy as sa
1919

20-
revision: str = "3f7e9d1a2b4c"
21-
down_revision: Union[str, Sequence[str], None] = "a7f3c9e1b2d4" # pragma: allowlist secret
20+
revision: str = "3f7e9d1a2b4c" # pragma: allowlist secret
21+
down_revision: Union[str, Sequence[str], None] = "c2d3e4f5a6b7" # pragma: allowlist secret
2222
branch_labels: Union[str, Sequence[str], None] = None
2323
depends_on: Union[str, Sequence[str], None] = None
2424

mcpgateway/alembic/versions/a7f3c9e1b2d4_add_title_to_tools_resources_prompts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import sqlalchemy as sa
1515

1616
# revision identifiers, used by Alembic.
17-
revision: str = "a7f3c9e1b2d4"
17+
revision: str = "a7f3c9e1b2d4" # pragma: allowlist secret
1818
down_revision: Union[str, Sequence[str], None] = "225bde88217e"
1919
branch_labels: Union[str, Sequence[str], None] = None
2020
depends_on: Union[str, Sequence[str], None] = None

mcpgateway/alembic/versions/ffe4494639d3_add_a2a_task_events_table.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
import sqlalchemy as sa
1515

1616
# revision identifiers, used by Alembic.
17-
revision: str = "ffe4494639d3"
18-
down_revision: Union[str, Sequence[str], None] = "8f2e1c9b0d3a"
17+
revision: str = "ffe4494639d3" # pragma: allowlist secret
18+
down_revision: Union[str, Sequence[str], None] = "8f2e1c9b0d3a" # pragma: allowlist secret
1919
branch_labels: Union[str, Sequence[str], None] = None
2020
depends_on: Union[str, Sequence[str], None] = None
2121

mcpgateway/db.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,8 @@ class Permissions:
12571257
TOOLS_DELETE = "tools.delete"
12581258
TOOLS_EXECUTE = "tools.execute"
12591259

1260+
TOOLS_MANAGE_PLUGINS = "tools.manage_plugins"
1261+
12601262
# Resource permissions
12611263
RESOURCES_CREATE = "resources.create"
12621264
RESOURCES_READ = "resources.read"

mcpgateway/services/a2a_protocol.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@
1010
from __future__ import annotations
1111

1212
# Standard
13+
import binascii
1314
from dataclasses import dataclass
1415
import logging
1516
from typing import Any, Dict, Mapping, Optional
1617
import uuid
1718

19+
# Third-Party
20+
from cryptography.exceptions import InvalidTag
21+
import orjson
22+
1823
# First-Party
1924
from mcpgateway.utils.services_auth import decode_auth
2025
from mcpgateway.utils.url_auth import apply_query_param_auth, sanitize_url_for_logging
@@ -347,14 +352,36 @@ def prepare_a2a_invocation(
347352
if auth_type in {"basic", "bearer", "authheaders", "api_key"} and auth_value:
348353
if isinstance(auth_value, str):
349354
if auth_type == "api_key":
350-
headers.setdefault("Authorization", f"Bearer {auth_value}")
355+
# For api_key, try to decode from base64 first, but fall back to using raw value
356+
try:
357+
decoded = decode_auth(auth_value)
358+
if isinstance(decoded, Mapping):
359+
# Extract the actual key value from the decoded dict
360+
api_key = next(iter(decoded.values())) if decoded else auth_value
361+
headers.setdefault("Authorization", f"Bearer {api_key}")
362+
else:
363+
# Fallback if decode returns a string directly
364+
headers.setdefault("Authorization", f"Bearer {decoded}")
365+
except (InvalidTag, binascii.Error, orjson.JSONDecodeError, IndexError, ValueError):
366+
# If decoding fails (corrupted data, wrong key, invalid encoding, truncated input,
367+
# or invalid nonce/cipher parameters), use the raw value as the API key
368+
#
369+
# TODO: Is this a logic failure? Perhaps the gateway should
370+
# ensure all API Key style auths are encoded -- or vice versa
371+
#
372+
headers.setdefault("Authorization", f"Bearer {auth_value}")
351373
else:
352374
decoded = decode_auth(auth_value)
353375
if not isinstance(decoded, Mapping):
354376
raise ValueError("Decoded A2A authentication payload must be a mapping")
355377
headers.update({str(key): str(value) for key, value in decoded.items()})
356378
elif isinstance(auth_value, Mapping):
357-
headers.update({str(key): str(value) for key, value in auth_value.items()})
379+
if auth_type == "api_key":
380+
# Extract the actual key value from the mapping
381+
api_key = next(iter(auth_value.values()), "") if auth_value else ""
382+
headers.setdefault("Authorization", f"Bearer {api_key}")
383+
else:
384+
headers.update({str(key): str(value) for key, value in auth_value.items()})
358385

359386
auth_query_params_decrypted: Dict[str, str] = {}
360387
target_endpoint_url = endpoint_url

tests/integration/test_a2a_sdk_integration.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#!/usr/bin/env python3
21
# -*- coding: utf-8 -*-
32
"""Integration tests for A2A agent support using an in-memory ASGI fixture.
43

tests/unit/mcpgateway/services/test_a2a_protocol.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
prepare_a2a_invocation,
2626
)
2727

28-
2928
# ── is_v1_a2a_protocol ──────────────────────────────────────────────────────
3029

3130

@@ -552,7 +551,7 @@ def test_prepare_a2a_invocation_skips_query_param_decrypt_failures(monkeypatch):
552551
parameters={"query": "hello"},
553552
interaction_type="query",
554553
auth_type="query_param",
555-
auth_query_params={"api_key": "bad"},
554+
auth_query_params={"api_key": "bad"}, # pragma: allowlist secret
556555
)
557556

558557
assert prepared.endpoint_url == "https://example.com/"
@@ -640,8 +639,8 @@ def test_prepare_a2a_invocation_rejects_non_mapping_decoded_auth(monkeypatch):
640639

641640
def test_prepare_a2a_invocation_query_param_auth_applies(monkeypatch):
642641
"""Successful query param auth decryption applies params to URL."""
643-
monkeypatch.setattr("mcpgateway.services.a2a_protocol.decode_auth", lambda _val: {"api_key": "real-key"})
644-
monkeypatch.setattr("mcpgateway.services.a2a_protocol.apply_query_param_auth", lambda url, params: url + "?api_key=real-key")
642+
monkeypatch.setattr("mcpgateway.services.a2a_protocol.decode_auth", lambda _val: {"api_key": "real-key"}) # pragma: allowlist secret
643+
monkeypatch.setattr("mcpgateway.services.a2a_protocol.apply_query_param_auth", lambda url, params: url + "?api_key=real-key") # pragma: allowlist secret
645644

646645
prepared = prepare_a2a_invocation(
647646
agent_type="generic",
@@ -650,7 +649,7 @@ def test_prepare_a2a_invocation_query_param_auth_applies(monkeypatch):
650649
parameters={"query": "hi"},
651650
interaction_type="query",
652651
auth_type="query_param",
653-
auth_query_params={"api_key": "encrypted"},
652+
auth_query_params={"api_key": "encrypted"}, # pragma: allowlist secret
654653
)
655654

656655
assert "api_key=real-key" in prepared.endpoint_url
@@ -778,8 +777,8 @@ def test_prepare_a2a_invocation_v_prefixed_protocol_uses_v1_format():
778777

779778
def test_prepare_a2a_invocation_preserves_encrypted_auth_fields(monkeypatch):
780779
"""Query-param auth preserves encrypted blobs and sets base_endpoint_url."""
781-
monkeypatch.setattr("mcpgateway.services.a2a_protocol.decode_auth", lambda _val: {"api_key": "decrypted-key"})
782-
monkeypatch.setattr("mcpgateway.services.a2a_protocol.apply_query_param_auth", lambda url, params: url + "?api_key=decrypted-key")
780+
monkeypatch.setattr("mcpgateway.services.a2a_protocol.decode_auth", lambda _val: {"api_key": "decrypted-key"}) # pragma: allowlist secret
781+
monkeypatch.setattr("mcpgateway.services.a2a_protocol.apply_query_param_auth", lambda url, params: url + "?api_key=decrypted-key") # pragma: allowlist secret
783782

784783
prepared = prepare_a2a_invocation(
785784
agent_type="generic",
@@ -788,10 +787,10 @@ def test_prepare_a2a_invocation_preserves_encrypted_auth_fields(monkeypatch):
788787
parameters={"query": "hello"},
789788
interaction_type="query",
790789
auth_type="query_param",
791-
auth_query_params={"api_key": "encrypted_blob"},
790+
auth_query_params={"api_key": "encrypted_blob"}, # pragma: allowlist secret
792791
)
793792

794-
assert prepared.auth_query_params_encrypted == {"api_key": "encrypted_blob"}
793+
assert prepared.auth_query_params_encrypted == {"api_key": "encrypted_blob"} # pragma: allowlist secret
795794
assert prepared.base_endpoint_url == "https://example.com/"
796795

797796

tests/unit/mcpgateway/services/test_rust_a2a_runtime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ async def test_invoke_sends_encrypted_auth_fields(self):
228228
uses_jsonrpc=True,
229229
base_endpoint_url="https://agent.test/",
230230
auth_value_encrypted="enc-bearer-blob",
231-
auth_query_params_encrypted={"api_key": "enc-qp-blob"},
231+
auth_query_params_encrypted={"api_key": "enc-qp-blob"}, # pragma: allowlist secret
232232
)
233233

234234
mock_response = MagicMock()
@@ -245,7 +245,7 @@ async def test_invoke_sends_encrypted_auth_fields(self):
245245
call_kwargs = mock_client.post.call_args
246246
payload = call_kwargs.kwargs["json"]
247247
assert payload["auth_headers_encrypted"] == "enc-bearer-blob"
248-
assert payload["auth_query_params_encrypted"] == {"api_key": "enc-qp-blob"}
248+
assert payload["auth_query_params_encrypted"] == {"api_key": "enc-qp-blob"} # pragma: allowlist secret
249249

250250
@pytest.mark.asyncio
251251
async def test_invoke_uses_base_endpoint_url_when_available(self):

0 commit comments

Comments
 (0)