feat(gateway_mode): Add gateway mode feature with direct_proxy support for pass-through MCP operations#2723
Conversation
2fe6fb4 to
daef63b
Compare
9f74738 to
ea275e4
Compare
| # Create naive datetime from UTC (not local time) to test the tzinfo addition | ||
| naive_time = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(seconds=10) |
There was a problem hiding this comment.
There are many test cases where datetime.now() is used which pulls in local time. This may work if the developer is in UTC tz or around and tests may pass by fluke. Otherwise time can end up going backward. I'm fixing in this PR but I think a proper fix would really be to avoid adding test cases with plain datetime.now() if the implementation standardizes on UTC.
ea275e4 to
210b781
Compare
|
Thanks for implementing the direct proxy feature, @010gvr! The concept is sound — not all use cases benefit from caching MCP server responses. The migration is clean and the schema validation looks good. A few significant concerns:
Item 1 (RBAC) is the most critical — this is a privilege escalation risk in multi-tenant deployments. |
5147612 to
2ad4124
Compare
Code Review:
|
2ad4124 to
01b1144
Compare
|
@crivetimihai I addressed most of them in 01b1144. Reg (1) though, the code block in |
59a2943 to
c032997
Compare
Implements a new `direct_proxy` gateway mode that enables pass-through proxying of MCP operations directly to remote servers without database caching. This complements the existing `cache` mode (default) and provides flexibility for different use cases. :warning: The direct proxy assumes a stateless MCP server is built using Streamable HTTP transport. Further, RBAC architecture need to be analyzed. - **New field**: Added `gateway_mode` column to `gateways` table with values `cache` (default) or `direct_proxy` - **Migration**: Created idempotent Alembic migration - **Models**: Updated `Gateway` ORM model in `db.py` with new field and documentation - **GatewayCreate**: Added `gateway_mode` field with validation pattern `^(cache|direct_proxy)$` - **GatewayUpdate**: Added optional `gateway_mode` field for updates - **GatewayRead**: Added `gateway_mode` field to response schema **gateway_service.py** - Added `gateway_mode` field handling in gateway creation and updates - Passes gateway mode through initialization flow **tool_service.py** - Implements direct proxy mode detection via `X-Gateway-Id` header - When in `direct_proxy` mode: - Bypasses database tool lookup/tool call - Creates minimal tool payload for direct proxying - Skips access control checks (delegated to remote server) - Skips metrics recording (no tool_id in direct mode) - Enhanced error handling for `extract_using_jq` failures - Added comprehensive logging for streamablehttp requests - Fixed `Accept` header to include both `application/json` and `text/event-stream` for MCP compatibility **resource_service.py** - Implements direct proxy mode for resource operations - Proxies `resources/read` and `resources/list` requests directly to remote MCP servers when in direct_proxy mode - Uses MCP SDK for proper protocol handling **streamablehttp_transport.py** - Proxy methods for all MCP methods - Fast lookups, access control enforced, metrics recorded - Existing behavior unchanged - All MCP operations proxied directly to remote server - No database caching of tools/resources/prompts - Client sends `X-Context-Forge-Gateway-Id` header to specify gateway - Gateway must have `gateway_mode: "direct_proxy"` - Minimal overhead, real-time data from remote server - Access control delegated to remote server - Existing tests should pass (default cache mode unchanged) This is a backward-compatible feature addition with sensible defaults. Signed-off-by: 010gvr <010gvr@gmail.com>
Signed-off-by: 010gvr <010gvr@gmail.com>
Signed-off-by: 010gvr <010gvr@gmail.com>
Signed-off-by: 010gvr <010gvr@gmail.com>
Signed-off-by: 010gvr <010gvr@gmail.com>
Signed-off-by: 010gvr <010gvr@gmail.com>
Signed-off-by: 010gvr <010gvr@gmail.com>
Signed-off-by: 010gvr <010gvr@gmail.com>
Signed-off-by: 010gvr <010gvr@gmail.com>
Signed-off-by: 010gvr <010gvr@gmail.com>
Signed-off-by: 010gvr <010gvr@gmail.com>
Signed-off-by: 010gvr <010gvr@gmail.com>
The migration pointed to an old parent revision (04cda6733305) causing multiple alembic heads. Updated to current head (c1c2c3c4c5c6). Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Add check_gateway_access() calls to two code paths that were missing access control before proxying requests in direct_proxy mode: - streamablehttp_transport.py call_tool(): was forwarding tool calls without verifying the user had access to the target gateway - resource_service.py read_resource(): was proxying resource reads without verifying gateway access permissions Also adds a defensive access check inside invoke_tool_direct() to prevent RBAC bypass if called from new contexts, fixes the migration docstring Revises mismatch, removes stale attribution comment, and defines GATEWAY_ID_HEADER constant for the header name. Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
c032997 to
4599b23
Compare
…test coverage - Add MCPGATEWAY_DIRECT_PROXY_ENABLED (default: false) and MCPGATEWAY_DIRECT_PROXY_TIMEOUT (default: 30s) config settings across config.py, .env.example, Helm values, docker-compose, admin UI - Add extract_gateway_id_from_headers() helper in gateway_access.py, replacing 6 repeated header-scanning loops with unified constant - Add registration-time guards in gateway_service (register + update) that reject direct_proxy mode when the feature flag is disabled - Add runtime guards in tool_service, resource_service, and streamablehttp_transport that fall through to cache mode when disabled - Replace 5 hardcoded timeout=30.0 with configurable setting - Remove all pragma: no cover markers from direct_proxy code paths - Add 41 new unit tests covering config defaults, header helper, gateway service guards, invoke_tool_direct, invoke_tool header branch, call_tool direct_proxy, and read_resource direct_proxy - Add documentation section in configuration.md and config.schema.json Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
4599b23 to
af61f72
Compare
- call_tool: add feature-flag gate (settings.mcpgateway_direct_proxy_enabled) matching list_tools/list_resources/read_resource - call_tool: return error on proxy failure instead of falling through to cache mode (fail-closed) - call_tool: sanitize error message to avoid leaking exception details - read_resource: return empty string directly on access denial instead of raising HTTPException that gets swallowed by outer handler - read_resource: remove _meta from session.read_resource() call (MCP SDK only accepts uri parameter) - GatewayCreate: change gateway_mode from Optional[str] to str with before-validator defaulting None to 'cache' (prevents DB integrity error) - admin: expose direct_proxy_timeout in Connection Timeouts section - list_tools docstring: fix refresh_strategy -> gateway_mode - Fix jq test assertions to match TextContent return type Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Review Fixes (abca2c6)Addressed review feedback across two commits. Summary of all changes on this branch: Security Fixes
Consistency Fixes
Structural Fix
Test Fixes
Items Reviewed but Not Changed
All 11,547 unit tests pass. |
Covers meta extraction, gateway fallback paths, passthrough headers, jq filter error handling, and isError attribute fallback. Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Direct Proxy Mode — Feature Flag & ConfigurationThis feature is now controlled by two environment variables:
How it works:
Safety guarantees:
|
… prefix
Two bugs discovered during E2E testing:
1. session.initialize() was commented out in all 5 direct_proxy code
paths, causing 422 errors from remote MCP servers (MCP protocol
requires initialization before operations).
2. invoke_tool_direct sent the gateway-prefixed slug name (e.g.
"fast-test-get-system-time") to the remote server, which only
knows the original name ("get_system_time"). Fixed by looking up
the tool's original_name from the DB, with a slug-prefix stripping
fallback for tools not yet cached locally.
Signed-off-by: Venkat Ramachandran <venkat@contextforge.ai>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
|
@crivetimihai Just leaving a comment here to revisit in the future if needed. The last commit be00451 has put back the session |
…pport for pass-through MCP operations (#2723) * feat: add direct_proxy gateway mode for pass-through MCP operations Implements a new `direct_proxy` gateway mode that enables pass-through proxying of MCP operations directly to remote servers without database caching. This complements the existing `cache` mode (default) and provides flexibility for different use cases. :warning: The direct proxy assumes a stateless MCP server is built using Streamable HTTP transport. Further, RBAC architecture need to be analyzed. - **New field**: Added `gateway_mode` column to `gateways` table with values `cache` (default) or `direct_proxy` - **Migration**: Created idempotent Alembic migration - **Models**: Updated `Gateway` ORM model in `db.py` with new field and documentation - **GatewayCreate**: Added `gateway_mode` field with validation pattern `^(cache|direct_proxy)$` - **GatewayUpdate**: Added optional `gateway_mode` field for updates - **GatewayRead**: Added `gateway_mode` field to response schema **gateway_service.py** - Added `gateway_mode` field handling in gateway creation and updates - Passes gateway mode through initialization flow **tool_service.py** - Implements direct proxy mode detection via `X-Gateway-Id` header - When in `direct_proxy` mode: - Bypasses database tool lookup/tool call - Creates minimal tool payload for direct proxying - Skips access control checks (delegated to remote server) - Skips metrics recording (no tool_id in direct mode) - Enhanced error handling for `extract_using_jq` failures - Added comprehensive logging for streamablehttp requests - Fixed `Accept` header to include both `application/json` and `text/event-stream` for MCP compatibility **resource_service.py** - Implements direct proxy mode for resource operations - Proxies `resources/read` and `resources/list` requests directly to remote MCP servers when in direct_proxy mode - Uses MCP SDK for proper protocol handling **streamablehttp_transport.py** - Proxy methods for all MCP methods - Fast lookups, access control enforced, metrics recorded - Existing behavior unchanged - All MCP operations proxied directly to remote server - No database caching of tools/resources/prompts - Client sends `X-Context-Forge-Gateway-Id` header to specify gateway - Gateway must have `gateway_mode: "direct_proxy"` - Minimal overhead, real-time data from remote server - Access control delegated to remote server - Existing tests should pass (default cache mode unchanged) This is a backward-compatible feature addition with sensible defaults. Signed-off-by: 010gvr <010gvr@gmail.com> * chore: lint code Signed-off-by: 010gvr <010gvr@gmail.com> * test(gateway_mode): Schema + Gateway Service tests Signed-off-by: 010gvr <010gvr@gmail.com> * chore(lint): More lint fixes Signed-off-by: 010gvr <010gvr@gmail.com> * chore(lint): More lint fixes Signed-off-by: 010gvr <010gvr@gmail.com> * chore(lint): flake8 fix Signed-off-by: 010gvr <010gvr@gmail.com> * chore(rebase): Rebase with main Signed-off-by: 010gvr <010gvr@gmail.com> * chore(tests): Use integration tests Signed-off-by: 010gvr <010gvr@gmail.com> * fix: User access check to Gateway for all direct_proxy mode Signed-off-by: 010gvr <010gvr@gmail.com> * fix(headers): Helper for auth header propagation Signed-off-by: 010gvr <010gvr@gmail.com> * fix: inconsistent initialize() Signed-off-by: 010gvr <010gvr@gmail.com> * fix(tests): coverage fixes Signed-off-by: 010gvr <010gvr@gmail.com> * fix(migration): update alembic down_revision to current head The migration pointed to an old parent revision (04cda6733305) causing multiple alembic heads. Updated to current head (c1c2c3c4c5c6). Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * fix(security): add missing RBAC checks in direct_proxy mode Add check_gateway_access() calls to two code paths that were missing access control before proxying requests in direct_proxy mode: - streamablehttp_transport.py call_tool(): was forwarding tool calls without verifying the user had access to the target gateway - resource_service.py read_resource(): was proxying resource reads without verifying gateway access permissions Also adds a defensive access check inside invoke_tool_direct() to prevent RBAC bypass if called from new contexts, fixes the migration docstring Revises mismatch, removes stale attribution comment, and defines GATEWAY_ID_HEADER constant for the header name. Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * feat(direct_proxy): add feature flag, configurable timeout, and full test coverage - Add MCPGATEWAY_DIRECT_PROXY_ENABLED (default: false) and MCPGATEWAY_DIRECT_PROXY_TIMEOUT (default: 30s) config settings across config.py, .env.example, Helm values, docker-compose, admin UI - Add extract_gateway_id_from_headers() helper in gateway_access.py, replacing 6 repeated header-scanning loops with unified constant - Add registration-time guards in gateway_service (register + update) that reject direct_proxy mode when the feature flag is disabled - Add runtime guards in tool_service, resource_service, and streamablehttp_transport that fall through to cache mode when disabled - Replace 5 hardcoded timeout=30.0 with configurable setting - Remove all pragma: no cover markers from direct_proxy code paths - Add 41 new unit tests covering config defaults, header helper, gateway service guards, invoke_tool_direct, invoke_tool header branch, call_tool direct_proxy, and read_resource direct_proxy - Add documentation section in configuration.md and config.schema.json Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * fix(direct_proxy): address review feedback for security and consistency - call_tool: add feature-flag gate (settings.mcpgateway_direct_proxy_enabled) matching list_tools/list_resources/read_resource - call_tool: return error on proxy failure instead of falling through to cache mode (fail-closed) - call_tool: sanitize error message to avoid leaking exception details - read_resource: return empty string directly on access denial instead of raising HTTPException that gets swallowed by outer handler - read_resource: remove _meta from session.read_resource() call (MCP SDK only accepts uri parameter) - GatewayCreate: change gateway_mode from Optional[str] to str with before-validator defaulting None to 'cache' (prevents DB integrity error) - admin: expose direct_proxy_timeout in Connection Timeouts section - list_tools docstring: fix refresh_strategy -> gateway_mode - Fix jq test assertions to match TextContent return type Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * test(direct_proxy): add coverage tests for 20 missing diff lines Covers meta extraction, gateway fallback paths, passthrough headers, jq filter error handling, and isError attribute fallback. Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * fix(direct_proxy): restore session.initialize() and resolve tool name prefix Two bugs discovered during E2E testing: 1. session.initialize() was commented out in all 5 direct_proxy code paths, causing 422 errors from remote MCP servers (MCP protocol requires initialization before operations). 2. invoke_tool_direct sent the gateway-prefixed slug name (e.g. "fast-test-get-system-time") to the remote server, which only knows the original name ("get_system_time"). Fixed by looking up the tool's original_name from the DB, with a slug-prefix stripping fallback for tools not yet cached locally. Signed-off-by: Venkat Ramachandran <venkat@contextforge.ai> Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> --------- Signed-off-by: 010gvr <010gvr@gmail.com> Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> Signed-off-by: Venkat Ramachandran <venkat@contextforge.ai> Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
…pport for pass-through MCP operations (#2723) * feat: add direct_proxy gateway mode for pass-through MCP operations Implements a new `direct_proxy` gateway mode that enables pass-through proxying of MCP operations directly to remote servers without database caching. This complements the existing `cache` mode (default) and provides flexibility for different use cases. :warning: The direct proxy assumes a stateless MCP server is built using Streamable HTTP transport. Further, RBAC architecture need to be analyzed. - **New field**: Added `gateway_mode` column to `gateways` table with values `cache` (default) or `direct_proxy` - **Migration**: Created idempotent Alembic migration - **Models**: Updated `Gateway` ORM model in `db.py` with new field and documentation - **GatewayCreate**: Added `gateway_mode` field with validation pattern `^(cache|direct_proxy)$` - **GatewayUpdate**: Added optional `gateway_mode` field for updates - **GatewayRead**: Added `gateway_mode` field to response schema **gateway_service.py** - Added `gateway_mode` field handling in gateway creation and updates - Passes gateway mode through initialization flow **tool_service.py** - Implements direct proxy mode detection via `X-Gateway-Id` header - When in `direct_proxy` mode: - Bypasses database tool lookup/tool call - Creates minimal tool payload for direct proxying - Skips access control checks (delegated to remote server) - Skips metrics recording (no tool_id in direct mode) - Enhanced error handling for `extract_using_jq` failures - Added comprehensive logging for streamablehttp requests - Fixed `Accept` header to include both `application/json` and `text/event-stream` for MCP compatibility **resource_service.py** - Implements direct proxy mode for resource operations - Proxies `resources/read` and `resources/list` requests directly to remote MCP servers when in direct_proxy mode - Uses MCP SDK for proper protocol handling **streamablehttp_transport.py** - Proxy methods for all MCP methods - Fast lookups, access control enforced, metrics recorded - Existing behavior unchanged - All MCP operations proxied directly to remote server - No database caching of tools/resources/prompts - Client sends `X-Context-Forge-Gateway-Id` header to specify gateway - Gateway must have `gateway_mode: "direct_proxy"` - Minimal overhead, real-time data from remote server - Access control delegated to remote server - Existing tests should pass (default cache mode unchanged) This is a backward-compatible feature addition with sensible defaults. Signed-off-by: 010gvr <010gvr@gmail.com> * chore: lint code Signed-off-by: 010gvr <010gvr@gmail.com> * test(gateway_mode): Schema + Gateway Service tests Signed-off-by: 010gvr <010gvr@gmail.com> * chore(lint): More lint fixes Signed-off-by: 010gvr <010gvr@gmail.com> * chore(lint): More lint fixes Signed-off-by: 010gvr <010gvr@gmail.com> * chore(lint): flake8 fix Signed-off-by: 010gvr <010gvr@gmail.com> * chore(rebase): Rebase with main Signed-off-by: 010gvr <010gvr@gmail.com> * chore(tests): Use integration tests Signed-off-by: 010gvr <010gvr@gmail.com> * fix: User access check to Gateway for all direct_proxy mode Signed-off-by: 010gvr <010gvr@gmail.com> * fix(headers): Helper for auth header propagation Signed-off-by: 010gvr <010gvr@gmail.com> * fix: inconsistent initialize() Signed-off-by: 010gvr <010gvr@gmail.com> * fix(tests): coverage fixes Signed-off-by: 010gvr <010gvr@gmail.com> * fix(migration): update alembic down_revision to current head The migration pointed to an old parent revision (04cda6733305) causing multiple alembic heads. Updated to current head (c1c2c3c4c5c6). Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * fix(security): add missing RBAC checks in direct_proxy mode Add check_gateway_access() calls to two code paths that were missing access control before proxying requests in direct_proxy mode: - streamablehttp_transport.py call_tool(): was forwarding tool calls without verifying the user had access to the target gateway - resource_service.py read_resource(): was proxying resource reads without verifying gateway access permissions Also adds a defensive access check inside invoke_tool_direct() to prevent RBAC bypass if called from new contexts, fixes the migration docstring Revises mismatch, removes stale attribution comment, and defines GATEWAY_ID_HEADER constant for the header name. Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * feat(direct_proxy): add feature flag, configurable timeout, and full test coverage - Add MCPGATEWAY_DIRECT_PROXY_ENABLED (default: false) and MCPGATEWAY_DIRECT_PROXY_TIMEOUT (default: 30s) config settings across config.py, .env.example, Helm values, docker-compose, admin UI - Add extract_gateway_id_from_headers() helper in gateway_access.py, replacing 6 repeated header-scanning loops with unified constant - Add registration-time guards in gateway_service (register + update) that reject direct_proxy mode when the feature flag is disabled - Add runtime guards in tool_service, resource_service, and streamablehttp_transport that fall through to cache mode when disabled - Replace 5 hardcoded timeout=30.0 with configurable setting - Remove all pragma: no cover markers from direct_proxy code paths - Add 41 new unit tests covering config defaults, header helper, gateway service guards, invoke_tool_direct, invoke_tool header branch, call_tool direct_proxy, and read_resource direct_proxy - Add documentation section in configuration.md and config.schema.json Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * fix(direct_proxy): address review feedback for security and consistency - call_tool: add feature-flag gate (settings.mcpgateway_direct_proxy_enabled) matching list_tools/list_resources/read_resource - call_tool: return error on proxy failure instead of falling through to cache mode (fail-closed) - call_tool: sanitize error message to avoid leaking exception details - read_resource: return empty string directly on access denial instead of raising HTTPException that gets swallowed by outer handler - read_resource: remove _meta from session.read_resource() call (MCP SDK only accepts uri parameter) - GatewayCreate: change gateway_mode from Optional[str] to str with before-validator defaulting None to 'cache' (prevents DB integrity error) - admin: expose direct_proxy_timeout in Connection Timeouts section - list_tools docstring: fix refresh_strategy -> gateway_mode - Fix jq test assertions to match TextContent return type Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * test(direct_proxy): add coverage tests for 20 missing diff lines Covers meta extraction, gateway fallback paths, passthrough headers, jq filter error handling, and isError attribute fallback. Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * fix(direct_proxy): restore session.initialize() and resolve tool name prefix Two bugs discovered during E2E testing: 1. session.initialize() was commented out in all 5 direct_proxy code paths, causing 422 errors from remote MCP servers (MCP protocol requires initialization before operations). 2. invoke_tool_direct sent the gateway-prefixed slug name (e.g. "fast-test-get-system-time") to the remote server, which only knows the original name ("get_system_time"). Fixed by looking up the tool's original_name from the DB, with a slug-prefix stripping fallback for tools not yet cached locally. Signed-off-by: Venkat Ramachandran <venkat@contextforge.ai> Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> --------- Signed-off-by: 010gvr <010gvr@gmail.com> Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> Signed-off-by: Venkat Ramachandran <venkat@contextforge.ai> Co-authored-by: Mihai Criveti <crivetimihai@gmail.com> Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>
…pport for pass-through MCP operations (IBM#2723) * feat: add direct_proxy gateway mode for pass-through MCP operations Implements a new `direct_proxy` gateway mode that enables pass-through proxying of MCP operations directly to remote servers without database caching. This complements the existing `cache` mode (default) and provides flexibility for different use cases. :warning: The direct proxy assumes a stateless MCP server is built using Streamable HTTP transport. Further, RBAC architecture need to be analyzed. - **New field**: Added `gateway_mode` column to `gateways` table with values `cache` (default) or `direct_proxy` - **Migration**: Created idempotent Alembic migration - **Models**: Updated `Gateway` ORM model in `db.py` with new field and documentation - **GatewayCreate**: Added `gateway_mode` field with validation pattern `^(cache|direct_proxy)$` - **GatewayUpdate**: Added optional `gateway_mode` field for updates - **GatewayRead**: Added `gateway_mode` field to response schema **gateway_service.py** - Added `gateway_mode` field handling in gateway creation and updates - Passes gateway mode through initialization flow **tool_service.py** - Implements direct proxy mode detection via `X-Gateway-Id` header - When in `direct_proxy` mode: - Bypasses database tool lookup/tool call - Creates minimal tool payload for direct proxying - Skips access control checks (delegated to remote server) - Skips metrics recording (no tool_id in direct mode) - Enhanced error handling for `extract_using_jq` failures - Added comprehensive logging for streamablehttp requests - Fixed `Accept` header to include both `application/json` and `text/event-stream` for MCP compatibility **resource_service.py** - Implements direct proxy mode for resource operations - Proxies `resources/read` and `resources/list` requests directly to remote MCP servers when in direct_proxy mode - Uses MCP SDK for proper protocol handling **streamablehttp_transport.py** - Proxy methods for all MCP methods - Fast lookups, access control enforced, metrics recorded - Existing behavior unchanged - All MCP operations proxied directly to remote server - No database caching of tools/resources/prompts - Client sends `X-Context-Forge-Gateway-Id` header to specify gateway - Gateway must have `gateway_mode: "direct_proxy"` - Minimal overhead, real-time data from remote server - Access control delegated to remote server - Existing tests should pass (default cache mode unchanged) This is a backward-compatible feature addition with sensible defaults. Signed-off-by: 010gvr <010gvr@gmail.com> * chore: lint code Signed-off-by: 010gvr <010gvr@gmail.com> * test(gateway_mode): Schema + Gateway Service tests Signed-off-by: 010gvr <010gvr@gmail.com> * chore(lint): More lint fixes Signed-off-by: 010gvr <010gvr@gmail.com> * chore(lint): More lint fixes Signed-off-by: 010gvr <010gvr@gmail.com> * chore(lint): flake8 fix Signed-off-by: 010gvr <010gvr@gmail.com> * chore(rebase): Rebase with main Signed-off-by: 010gvr <010gvr@gmail.com> * chore(tests): Use integration tests Signed-off-by: 010gvr <010gvr@gmail.com> * fix: User access check to Gateway for all direct_proxy mode Signed-off-by: 010gvr <010gvr@gmail.com> * fix(headers): Helper for auth header propagation Signed-off-by: 010gvr <010gvr@gmail.com> * fix: inconsistent initialize() Signed-off-by: 010gvr <010gvr@gmail.com> * fix(tests): coverage fixes Signed-off-by: 010gvr <010gvr@gmail.com> * fix(migration): update alembic down_revision to current head The migration pointed to an old parent revision (04cda6733305) causing multiple alembic heads. Updated to current head (c1c2c3c4c5c6). Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * fix(security): add missing RBAC checks in direct_proxy mode Add check_gateway_access() calls to two code paths that were missing access control before proxying requests in direct_proxy mode: - streamablehttp_transport.py call_tool(): was forwarding tool calls without verifying the user had access to the target gateway - resource_service.py read_resource(): was proxying resource reads without verifying gateway access permissions Also adds a defensive access check inside invoke_tool_direct() to prevent RBAC bypass if called from new contexts, fixes the migration docstring Revises mismatch, removes stale attribution comment, and defines GATEWAY_ID_HEADER constant for the header name. Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * feat(direct_proxy): add feature flag, configurable timeout, and full test coverage - Add MCPGATEWAY_DIRECT_PROXY_ENABLED (default: false) and MCPGATEWAY_DIRECT_PROXY_TIMEOUT (default: 30s) config settings across config.py, .env.example, Helm values, docker-compose, admin UI - Add extract_gateway_id_from_headers() helper in gateway_access.py, replacing 6 repeated header-scanning loops with unified constant - Add registration-time guards in gateway_service (register + update) that reject direct_proxy mode when the feature flag is disabled - Add runtime guards in tool_service, resource_service, and streamablehttp_transport that fall through to cache mode when disabled - Replace 5 hardcoded timeout=30.0 with configurable setting - Remove all pragma: no cover markers from direct_proxy code paths - Add 41 new unit tests covering config defaults, header helper, gateway service guards, invoke_tool_direct, invoke_tool header branch, call_tool direct_proxy, and read_resource direct_proxy - Add documentation section in configuration.md and config.schema.json Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * fix(direct_proxy): address review feedback for security and consistency - call_tool: add feature-flag gate (settings.mcpgateway_direct_proxy_enabled) matching list_tools/list_resources/read_resource - call_tool: return error on proxy failure instead of falling through to cache mode (fail-closed) - call_tool: sanitize error message to avoid leaking exception details - read_resource: return empty string directly on access denial instead of raising HTTPException that gets swallowed by outer handler - read_resource: remove _meta from session.read_resource() call (MCP SDK only accepts uri parameter) - GatewayCreate: change gateway_mode from Optional[str] to str with before-validator defaulting None to 'cache' (prevents DB integrity error) - admin: expose direct_proxy_timeout in Connection Timeouts section - list_tools docstring: fix refresh_strategy -> gateway_mode - Fix jq test assertions to match TextContent return type Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * test(direct_proxy): add coverage tests for 20 missing diff lines Covers meta extraction, gateway fallback paths, passthrough headers, jq filter error handling, and isError attribute fallback. Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * fix(direct_proxy): restore session.initialize() and resolve tool name prefix Two bugs discovered during E2E testing: 1. session.initialize() was commented out in all 5 direct_proxy code paths, causing 422 errors from remote MCP servers (MCP protocol requires initialization before operations). 2. invoke_tool_direct sent the gateway-prefixed slug name (e.g. "fast-test-get-system-time") to the remote server, which only knows the original name ("get_system_time"). Fixed by looking up the tool's original_name from the DB, with a slug-prefix stripping fallback for tools not yet cached locally. Signed-off-by: Venkat Ramachandran <venkat@contextforge.ai> Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> --------- Signed-off-by: 010gvr <010gvr@gmail.com> Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> Signed-off-by: Venkat Ramachandran <venkat@contextforge.ai> Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
🔗 Related Issue
Closes #2171 and #2344
📝 Summary
Implements a new
direct_proxygateway mode that enables pass-through proxying of MCP operations directly to remote servers without database caching. This complements the existingcachemode (default) and provides flexibility for different use cases.New field: Added
gateway_modecolumn togatewaystable with valuescache(default) ordirect_proxyMigration: Created idempotent Alembic migration
Models: Updated
GatewayORM model indb.pywith new field and documentationGatewayCreate: Added
gateway_modefield with validation pattern^(cache|direct_proxy)$GatewayUpdate: Added optional
gateway_modefield for updatesGatewayRead: Added
gateway_modefield to response schemagateway_service.py
gateway_modefield handling in gateway creation and updatestool_service.py
X-Gateway-Idheaderdirect_proxymode:extract_using_jqfailuresresource_service.py
resources/readandresources/listrequests directly to remote MCP servers when in direct_proxy modestreamablehttp_transport.py
tools/list,tools/call,resources/listandresources/readFeatures
X-Context-Forge-Gateway-Idheader to specify gatewaygateway_mode: "direct_proxy"This is a backward-compatible feature addition with sensible defaults.
🏷️ Type of Change
🧪 Verification
make lintmake testmake coverage✅ Checklist
make black isort pre-commit)📓 Notes (optional)
TODOs: