diff --git a/packages/mcp/AGENTS.md b/packages/mcp/AGENTS.md index e5b14ecd..964105cd 100644 --- a/packages/mcp/AGENTS.md +++ b/packages/mcp/AGENTS.md @@ -93,15 +93,16 @@ are separate follow-up work). bind-safety interlock: it checks the inbound request's `Host` / `Origin`, not the bind interface. FastMCP auto-enables a loopback-only allowlist on the `127.0.0.1` construction host, so behind a proxy that forwards the public `Host` it answers -`421 Misdirected Request`. `core/transport_security.py:build_transport_security` +`421 Misdirected Request`. `transport_security.py:build_transport_security` widens it by deriving the allowed host from `resource_server_url` (the public origin the `remote` profile already declares) plus loopback, and `build_pipefy_mcp_server` passes the result to FastMCP. `PIPEFY_MCP_ALLOWED_HOSTS` / `PIPEFY_MCP_ALLOWED_ORIGINS` (JSON) extend it for extra hostnames or a stricter Origin posture. Unset (no resource-server URL and no override) leaves FastMCP's loopback-only default in force, -so the local subprocess case is unaffected. Being configuration derived at -composition (mirroring `build_resource_server_auth`), it lives in the composition -tier, not in `settings.py`, which keeps the mcp SDK out of the config boundary. +so the local subprocess case is unaffected. Transport security is its own concern +module (a peer of `auth/`, consumed by the composition root); being configuration +derived at composition (mirroring `build_resource_server_auth`), it stays out of +`settings.py`, which keeps the mcp SDK out of the config boundary. ## Tool registration diff --git a/packages/mcp/pyproject.toml b/packages/mcp/pyproject.toml index f4bdec9a..3a5f0a57 100644 --- a/packages/mcp/pyproject.toml +++ b/packages/mcp/pyproject.toml @@ -76,12 +76,13 @@ lint.extend-select = ["TID251"] root_package = "pipefy_mcp" [[tool.importlinter.contracts]] -name = "Inward-only layers: composition root > adapters > core > auth > settings" +name = "Inward-only layers: composition root > adapters > core > transport_security > auth > settings" type = "layers" layers = [ "pipefy_mcp.server", "pipefy_mcp.tools", "pipefy_mcp.core", + "pipefy_mcp.transport_security", "pipefy_mcp.auth", "pipefy_mcp.settings", ] diff --git a/packages/mcp/src/pipefy_mcp/auth/__init__.py b/packages/mcp/src/pipefy_mcp/auth/__init__.py index 7e75491e..1470b03d 100644 --- a/packages/mcp/src/pipefy_mcp/auth/__init__.py +++ b/packages/mcp/src/pipefy_mcp/auth/__init__.py @@ -1,17 +1,17 @@ -"""Identity for the HTTP transport: inbound bearer validation and session identity.""" +"""Identity for the HTTP transport: inbound bearer validation and outbound session identity.""" -from pipefy_mcp.auth.request_identity import require_request_bearer +from pipefy_mcp.auth.inbound_identity import require_request_bearer +from pipefy_mcp.auth.outbound_identity import ( + AuthSource, + RequestScopedIdentity, + StartupIdentity, +) from pipefy_mcp.auth.resource_server import ( JwtTokenVerifier, ResourceServer, ResourceServerAuth, build_resource_server_auth, ) -from pipefy_mcp.auth.session_identity import ( - AuthSource, - RequestScopedIdentity, - StartupIdentity, -) __all__ = [ "AuthSource", diff --git a/packages/mcp/src/pipefy_mcp/auth/request_identity.py b/packages/mcp/src/pipefy_mcp/auth/inbound_identity.py similarity index 98% rename from packages/mcp/src/pipefy_mcp/auth/request_identity.py rename to packages/mcp/src/pipefy_mcp/auth/inbound_identity.py index a8cc1c7e..59a9f360 100644 --- a/packages/mcp/src/pipefy_mcp/auth/request_identity.py +++ b/packages/mcp/src/pipefy_mcp/auth/inbound_identity.py @@ -5,7 +5,7 @@ that caller rather than as one identity resolved at startup. This module reads the caller's validated bearer off the request the tool handler received, so the runtime can snapshot it into a per-session credential (see -:meth:`pipefy_mcp.auth.session_identity.RequestScopedIdentity.resolve`). +:meth:`pipefy_mcp.auth.outbound_identity.RequestScopedIdentity.resolve`). The bearer comes from the request the handler passes in (``ctx.request_context.request``), not from ``AuthContextMiddleware``'s diff --git a/packages/mcp/src/pipefy_mcp/auth/session_identity.py b/packages/mcp/src/pipefy_mcp/auth/outbound_identity.py similarity index 96% rename from packages/mcp/src/pipefy_mcp/auth/session_identity.py rename to packages/mcp/src/pipefy_mcp/auth/outbound_identity.py index 21504d08..0058dfb1 100644 --- a/packages/mcp/src/pipefy_mcp/auth/session_identity.py +++ b/packages/mcp/src/pipefy_mcp/auth/outbound_identity.py @@ -1,6 +1,6 @@ """Outbound identity for a request's SDK session: who each session acts as. -The counterpart to :mod:`pipefy_mcp.auth.request_identity` (which extracts the +The counterpart to :mod:`pipefy_mcp.auth.inbound_identity` (which extracts the *inbound* bearer a caller presents): these types resolve the *outbound* ``httpx.Auth`` the per-request SDK session binds. The two profiles pick a different variant at the composition root, and both speak one ``resolve`` contract @@ -22,7 +22,7 @@ from starlette.requests import Request from pipefy_mcp._docs import DOCS_SETUP_REF -from pipefy_mcp.auth.request_identity import require_request_bearer +from pipefy_mcp.auth.inbound_identity import require_request_bearer from pipefy_mcp.settings import Settings diff --git a/packages/mcp/src/pipefy_mcp/core/runtime.py b/packages/mcp/src/pipefy_mcp/core/runtime.py index 14012576..3eb9f42a 100644 --- a/packages/mcp/src/pipefy_mcp/core/runtime.py +++ b/packages/mcp/src/pipefy_mcp/core/runtime.py @@ -12,8 +12,8 @@ StartupIdentity, build_resource_server_auth, ) -from pipefy_mcp.core.transport_security import build_transport_security from pipefy_mcp.settings import ResourceServerSettings, Settings +from pipefy_mcp.transport_security import build_transport_security def _resource_server(rs: ResourceServerSettings) -> ResourceServer | None: diff --git a/packages/mcp/src/pipefy_mcp/core/tool_middleware.py b/packages/mcp/src/pipefy_mcp/core/tool_middleware.py index 6599c94f..4392dd61 100644 --- a/packages/mcp/src/pipefy_mcp/core/tool_middleware.py +++ b/packages/mcp/src/pipefy_mcp/core/tool_middleware.py @@ -33,7 +33,7 @@ from mcp import types from mcp.server.lowlevel.server import request_ctx -from pipefy_mcp.auth.request_identity import CallerIdentity, caller_identity +from pipefy_mcp.auth.inbound_identity import CallerIdentity, caller_identity from pipefy_mcp.core.tool_error_envelope import tool_error if TYPE_CHECKING: diff --git a/packages/mcp/src/pipefy_mcp/core/transport_security.py b/packages/mcp/src/pipefy_mcp/transport_security.py similarity index 100% rename from packages/mcp/src/pipefy_mcp/core/transport_security.py rename to packages/mcp/src/pipefy_mcp/transport_security.py diff --git a/packages/mcp/tests/auth/test_request_identity.py b/packages/mcp/tests/auth/test_inbound_identity.py similarity index 98% rename from packages/mcp/tests/auth/test_request_identity.py rename to packages/mcp/tests/auth/test_inbound_identity.py index 49b33fc6..b3741cfa 100644 --- a/packages/mcp/tests/auth/test_request_identity.py +++ b/packages/mcp/tests/auth/test_inbound_identity.py @@ -16,7 +16,7 @@ from mcp.server.auth.provider import AccessToken from starlette.requests import Request -from pipefy_mcp.auth.request_identity import ( +from pipefy_mcp.auth.inbound_identity import ( CallerIdentity, caller_identity, require_request_bearer, diff --git a/packages/mcp/tests/observability/test_tool_log_middleware.py b/packages/mcp/tests/observability/test_tool_log_middleware.py index a340d486..2a94a327 100644 --- a/packages/mcp/tests/observability/test_tool_log_middleware.py +++ b/packages/mcp/tests/observability/test_tool_log_middleware.py @@ -15,7 +15,7 @@ import pytest from mcp import UrlElicitationRequiredError, types -from pipefy_mcp.auth.request_identity import CallerIdentity +from pipefy_mcp.auth.inbound_identity import CallerIdentity from pipefy_mcp.core.tool_middleware import ToolCallContext from pipefy_mcp.observability.tool_log_middleware import tool_log_middleware diff --git a/packages/mcp/tests/test_server.py b/packages/mcp/tests/test_server.py index aeeb1c00..d00abbb3 100644 --- a/packages/mcp/tests/test_server.py +++ b/packages/mcp/tests/test_server.py @@ -14,7 +14,6 @@ from pipefy_sdk import PipefySettings from pipefy_mcp.core.tool_middleware import ToolCallContext, short_circuit_error -from pipefy_mcp.core.transport_security import build_transport_security from pipefy_mcp.observability.tool_log_middleware import tool_log_middleware from pipefy_mcp.server import ( _make_lifespan, @@ -25,6 +24,7 @@ ) from pipefy_mcp.settings import McpSettings, Settings, resolve_mcp_settings from pipefy_mcp.tools.registry import PIPEFY_TOOL_NAMES +from pipefy_mcp.transport_security import build_transport_security _MINIMAL_PIPEFY_SETTINGS = Settings( pipefy=PipefySettings(base_url="https://api.pipefy.com"), diff --git a/packages/mcp/tests/core/test_transport_security.py b/packages/mcp/tests/test_transport_security.py similarity index 98% rename from packages/mcp/tests/core/test_transport_security.py rename to packages/mcp/tests/test_transport_security.py index a093c3a3..80cdc0a8 100644 --- a/packages/mcp/tests/core/test_transport_security.py +++ b/packages/mcp/tests/test_transport_security.py @@ -3,8 +3,8 @@ import pytest from pipefy_mcp.auth import ResourceServer -from pipefy_mcp.core.transport_security import build_transport_security from pipefy_mcp.settings import McpSettings +from pipefy_mcp.transport_security import build_transport_security _LOOPBACK_FORMS = { "127.0.0.1",