From e18324d3930a285dc369831092f831d352b5d98d Mon Sep 17 00:00:00 2001 From: Gabriel Date: Mon, 13 Jul 2026 21:15:40 -0300 Subject: [PATCH 1/2] refactor(mcp): promote transport security to its own concern Move build_transport_security out of core/ into a package-root transport_security module, a peer of auth/ rather than a module under the composition tier. It imports only auth (ResourceServer) and settings and is consumed by the runtime, so it slots into the layering contract between core and auth. This is the incremental app-level step toward the target architecture, where core/ dissolves into named concerns. --- packages/mcp/AGENTS.md | 9 +++++---- packages/mcp/pyproject.toml | 3 ++- packages/mcp/src/pipefy_mcp/core/runtime.py | 2 +- .../mcp/src/pipefy_mcp/{core => }/transport_security.py | 0 packages/mcp/tests/test_server.py | 2 +- packages/mcp/tests/{core => }/test_transport_security.py | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) rename packages/mcp/src/pipefy_mcp/{core => }/transport_security.py (100%) rename packages/mcp/tests/{core => }/test_transport_security.py (98%) 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/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/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/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", From 15103ec5b73183d61a5ef1a263f0ffd033cc639f Mon Sep 17 00:00:00 2001 From: Gabriel Date: Mon, 13 Jul 2026 21:16:02 -0300 Subject: [PATCH 2/2] refactor(mcp): rename identity modules onto the inbound/outbound axis request_identity.py becomes inbound_identity.py (it extracts the inbound bearer a caller presents) and session_identity.py becomes outbound_identity.py (it resolves the outbound httpx.Auth each session binds). The names now state the axis the docstrings already framed, so the pair reads as counterparts. No behavior change; the auth facade re-exports the same contract symbols. --- packages/mcp/src/pipefy_mcp/auth/__init__.py | 14 +++++++------- .../{request_identity.py => inbound_identity.py} | 2 +- .../{session_identity.py => outbound_identity.py} | 4 ++-- .../mcp/src/pipefy_mcp/core/tool_middleware.py | 2 +- ...equest_identity.py => test_inbound_identity.py} | 2 +- .../observability/test_tool_log_middleware.py | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) rename packages/mcp/src/pipefy_mcp/auth/{request_identity.py => inbound_identity.py} (98%) rename packages/mcp/src/pipefy_mcp/auth/{session_identity.py => outbound_identity.py} (96%) rename packages/mcp/tests/auth/{test_request_identity.py => test_inbound_identity.py} (98%) 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/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/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