Skip to content

[TT-16823] Filter MCP discovery with OAS middleware rules#8246

Merged
andrei-tyk merged 4 commits into
masterfrom
TT-16823-filter-middleware-by-allow-block
May 27, 2026
Merged

[TT-16823] Filter MCP discovery with OAS middleware rules#8246
andrei-tyk merged 4 commits into
masterfrom
TT-16823-filter-middleware-by-allow-block

Conversation

@andrei-tyk

@andrei-tyk andrei-tyk commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Applies OAS MCP middleware allow/block rules to MCP discovery responses.
  • Composes middleware and session/policy discovery filters as intersection for allow lists and union for block lists.
  • Filters initialize capabilities when method-level JSON-RPC access rules deny backing methods such as sampling/create.

Root Cause

MCP primitive invocation rules were enforced for tools/call, resources/read, and prompts/get, but discovery responses only considered session MCPAccessRights and ignored OAS middleware rules. As a result, tools/list could advertise primitives that tools/call later denied.

Validation

  • GOCACHE=/private/tmp/tyk-go-cache go test -count=1 ./internal/mcp
  • GOCACHE=/private/tmp/tyk-go-cache go test -count=1 ./gateway -run 'MCPListFilter|MCPAccessControl|JSONRPCAccessControl'
  • Pre-commit/pre-push hooks: golangci-lint, import lint, schema lint, go build, gateway test compile
  • Live curl validation: tools/list returned only permitted tools; denied tools/call returned JSON-RPC 403; allowed tools/call succeeded

Ticket Details

TT-16823
Status Ready for Testing
Summary Filter Middleware Allow Block Lists in the capability handshake

Generated at: 2026-05-27 10:48:54

@probelabs

probelabs Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

This PR introduces filtering for Model Context Protocol (MCP) discovery responses based on rules defined in the OpenAPI Specification (OAS), ensuring clients only discover primitives (tools, prompts, resources) they are permitted to access.

Files Changed Analysis

The changes are concentrated in the gateway and internal/mcp packages.

  • gateway/mcp_discovery_rules.go is a new file that centralizes the logic for extracting and composing access control rules from both OAS middleware definitions and user session policies.
  • internal/mcp/list_filter.go has been significantly updated to handle multiple rule sets, applying an intersection for allow-lists and a union for block-lists. It also adds logic to filter the initialize capability response based on access to underlying JSON-RPC methods.
  • gateway/res_handler_mcp_list_filter.go and gateway/sse_hook_mcp_list_filter.go are modified to use this new composite rule logic for both standard HTTP and streaming (SSE) responses, ensuring consistent filtering.
  • internal/mcp/jsonrpc.go adds constants for new MCP methods like initialize and sampling/create.
  • The corresponding test files have been expanded to cover the new functionality, including OAS rule extraction, rule composition, and capability filtering.

Architecture & Impact Assessment

What this PR accomplishes

This PR rectifies an inconsistency where MCP discovery endpoints (tools/list, etc.) could advertise primitives that would later be denied during invocation (tools/call). By filtering discovery responses at the source, the gateway provides clients with an accurate and secure view of their permissions, preventing attempts to use inaccessible primitives.

Key technical changes introduced

  1. OAS-based Rule Definition: The gateway now reads MCP primitive and JSON-RPC method allow/block rules from the x-tyk-gateway.middleware section of an API's OAS definition.
  2. Rule Set Composition: A new composition strategy is implemented for when both OAS and session/policy rules are present. Allow-lists are combined with an intersection (an item must be in all allow-lists), while block-lists are combined with a union (an item is blocked if in any block-list).
  3. initialize Capability Filtering: The initialize response is now dynamically filtered. If access to any of the JSON-RPC methods backing a capability (e.g., tools/list for the tools capability) is denied, the entire capability is removed from the response.

Affected system components

  • MCP Gateway: The primary component affected, as it now enforces an additional layer of access control on discovery endpoints.
  • Access Control Subsystem: The logic is extended to source rules from the API's OAS definition in addition to the user's session policy.
  • Response Handlers: Both the standard HTTP (MCPListFilterResponseHandler) and streaming SSE (MCPListFilterSSEHook) response handlers are updated to apply the new filtering logic.

Flow Diagram

sequenceDiagram
    participant Client
    participant Gateway
    participant Upstream

    Client->>Gateway: POST /mcp (e.g., tools/list)
    Gateway->>Upstream: POST /mcp (e.g., tools/list)
    Upstream-->>Gateway: 200 OK (Full list of tools)
    activate Gateway
    Note right of Gateway: Intercept response
    Gateway->>Gateway: 1. Extract Session/Policy rules
    Gateway->>Gateway: 2. Extract OAS Middleware rules
    Gateway->>Gateway: 3. Compose rule sets (AND for allow, OR for block)
    Gateway->>Gateway: 4. Filter response body using composed rules
    deactivate Gateway
    Gateway-->>Client: 200 OK (Filtered list of tools)
Loading

Scope Discovery & Context Expansion

This change establishes a new pattern for access control where rules can be defined statically on the API specification, empowering API designers to enforce fine-grained access directly within their OpenAPI definitions. This reduces reliance on separate, dynamic policy configurations for static rules.

  • The core logic is consolidated in gateway/mcp_discovery_rules.go and internal/mcp/list_filter.go, which are now the central files for understanding MCP access control.
  • The function effectiveMCPListRuleSets in gateway/mcp_discovery_rules.go is the main entry point for combining the different rule sources.
  • The function CheckAccessControlRuleSets in internal/mcp/list_filter.go implements the core composition logic, returning a denial if any single rule set denies access.
  • This feature directly impacts how API developers will secure MCP services, making the OAS definition a source of truth for security rules.
Metadata
  • Review Effort: 4 / 5
  • Primary Label: feature

Powered by Visor from Probelabs

Last updated: 2026-05-27T10:50:47.116Z | Triggered by: pr_updated | Commit: 0ab181e

💡 TIP: You can chat with Visor using /visor ask <your question>

@probelabs

probelabs Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Security Issues (2)

Severity Location Issue
🟡 Warning gateway/res_handler_mcp_list_filter.go:67-70
The MCP discovery filter fails open, passing the original unfiltered response to the client if an error occurs during response parsing or filtering. This could lead to information disclosure, as the client might see primitives (tools, prompts, etc.) that should be hidden by access control rules.
💡 SuggestionImplement a fail-closed mechanism for the discovery filter. If filtering cannot be successfully applied due to parsing or other errors, the gateway should return an empty list of primitives or a server-side error to the client. This prevents accidental leakage of information about protected resources.
🟡 Warning gateway/sse_hook_mcp_list_filter.go:76-78
The SSE hook for MCP discovery filtering fails open. If `filterSSEData` returns an error (indicated by `ok` being false), the original, unfiltered event is passed through to the client. This could leak the existence of primitives that should be hidden by access control rules.
💡 SuggestionModify the SSE hook to fail-closed. If an event cannot be filtered successfully, it should be dropped or replaced with an error event. Passing through the original event bypasses the intended security control under error conditions.

Security Issues (2)

Severity Location Issue
🟡 Warning gateway/res_handler_mcp_list_filter.go:67-70
The MCP discovery filter fails open, passing the original unfiltered response to the client if an error occurs during response parsing or filtering. This could lead to information disclosure, as the client might see primitives (tools, prompts, etc.) that should be hidden by access control rules.
💡 SuggestionImplement a fail-closed mechanism for the discovery filter. If filtering cannot be successfully applied due to parsing or other errors, the gateway should return an empty list of primitives or a server-side error to the client. This prevents accidental leakage of information about protected resources.
🟡 Warning gateway/sse_hook_mcp_list_filter.go:76-78
The SSE hook for MCP discovery filtering fails open. If `filterSSEData` returns an error (indicated by `ok` being false), the original, unfiltered event is passed through to the client. This could leak the existence of primitives that should be hidden by access control rules.
💡 SuggestionModify the SSE hook to fail-closed. If an event cannot be filtered successfully, it should be dropped or replaced with an error event. Passing through the original event bypasses the intended security control under error conditions.
\n\n ### Architecture Issues (2)
Severity Location Issue
🟡 Warning gateway/sse_hook_mcp_list_filter.go:101-113
The filtering logic in `filterSSEData` is nearly identical to the logic in `MCPListFilterResponseHandler.HandleResponse` (in `gateway/res_handler_mcp_list_filter.go`). Both implementations first check if the response is for a list method, apply list-based filtering, and then fall back to handling the `initialize` method to filter capabilities. This duplication can lead to maintenance issues where one implementation is updated but the other is missed.
💡 SuggestionRefactor the core decision-making logic into a shared function. This function could take the `APISpec`, `SessionState`, and the parsed JSON-RPC `result` map as input, and return a new, filtered `result` map along with a boolean indicating if changes were made. Both `filterSSEData` and `HandleResponse` could then call this shared function after parsing their respective inputs (SSE data or HTTP body). They would then only be responsible for re-encoding the returned result. This would centralize the filtering logic, reduce duplication, and improve maintainability.
🟡 Warning gateway/sse_hook_mcp_list_filter.go:108-113
The logic to filter `initialize` capabilities in the SSE hook is implicit. It assumes that any response for which `mcp.InferListConfigFromResult` returns `nil` is an `initialize` response. This is less robust than the corresponding HTTP handler, which can explicitly check `state.Method == mcp.MethodInitialize`. If other non-list JSON-RPC responses are ever sent over the SSE stream, they might be incorrectly processed or, in the current implementation, passed through without filtering, which might be unintended.
💡 SuggestionTo make the logic more explicit, consider checking for the existence of the `"capabilities"` key within the `result` map before attempting to apply capability filtering. This would ensure that the filtering logic is only applied to responses that are structurally similar to an `initialize` response, making the code's intent clearer and more resilient to future changes.

Performance Issues (3)

Severity Location Issue
🟠 Error internal/mcp/list_filter.go:85-97
The filtering logic repeatedly compiles regular expressions on each call. The underlying `CheckAccessControlRules` function, which is called for every item in a list and for every rule pattern, uses `regexp.MatchString`. This function compiles the regex on every invocation, leading to significant CPU overhead, especially for large lists or complex rules.
💡 SuggestionCompile regular expressions once and reuse them. A good approach would be to use a package-level cache (e.g., an LRU cache) for compiled `*regexp.Regexp` objects, keyed by the pattern string. This would avoid the cost of compilation in hot paths like list filtering.
🟠 Error gateway/sse_hook_mcp_list_filter.go:100-113
The `filterSSEData` method recalculates access control rule sets by calling `effectiveMCPListRuleSets` or `effectiveJSONRPCMethodRuleSets` for every Server-Sent Event (SSE). For a streaming connection, this is highly inefficient because the rules, particularly those from the OAS API definition, are static for the connection's lifetime.
💡 SuggestionCompute the rule sets once within the `NewMCPListFilterSSEHook` constructor and store them on the `MCPListFilterSSEHook` struct. The `filterSSEData` method should then reuse these pre-computed rules for filtering each event, avoiding redundant calculations.
🟡 Warning gateway/sse_hook_mcp_list_filter.go:120-137
The `hasMCPDiscoveryFiltering` function determines if filtering is needed by calling functions like `oasPrimitiveRules` and `oasJSONRPCMethodRules`. These functions build complete `AccessControlRules` objects, which are then immediately discarded after an `IsEmpty()` check. This is an inefficient pattern for a simple existence check.
💡 SuggestionOptimize the existence check by directly inspecting the source of the rules. For OAS rules, check if the corresponding maps in the API specification (e.g., `spec.OAS.GetTykMiddleware().McpTools`) have a non-zero length. This avoids the overhead of allocating and populating intermediate `AccessControlRules` objects.

Quality Issues (1)

Severity Location Issue
🟡 Warning internal/mcp/list_filter.go:177-185
The current implementation removes an entire capability from the `initialize` response if any of its associated backing methods are denied. For example, if `tools/list` is denied but `tools/call` is allowed, the entire `tools` capability is hidden. This might be too restrictive, as a client could still use `tools/call` with known tool names. This could lead to unexpected behavior where clients that are permitted to use a feature (like calling a specific tool) are told the server doesn't support that capability at all.
💡 SuggestionConsider refining the filtering logic for capabilities. Instead of removing the entire capability, you could either: 1. Check if only *essential* methods are denied (e.g., for tools, `tools/call` might be essential, but `tools/list` might not be). 2. Modify the capability object in the response to reflect that certain actions are disabled, if the MCP specification allows for it. This would provide more granular information to the client.

Powered by Visor from Probelabs

Last updated: 2026-05-27T10:50:12.447Z | Triggered by: pr_updated | Commit: 0ab181e

💡 TIP: You can chat with Visor using /visor ask <your question>

@andrei-tyk
andrei-tyk marked this pull request as ready for review May 25, 2026 10:41
@andrei-tyk andrei-tyk changed the title [codex] Filter MCP discovery with OAS middleware rules [TT-16823] Filter MCP discovery with OAS middleware rules May 25, 2026

@edsonmichaque edsonmichaque left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@andrei-tyk
andrei-tyk enabled auto-merge (squash) May 27, 2026 07:57
@github-actions

Copy link
Copy Markdown
Contributor

🚨 Jira Linter Failed

Commit: 0ab181e
Failed at: 2026-05-27 10:48:56 UTC

The Jira linter failed to validate your PR. Please check the error details below:

🔍 Click to view error details
failed to validate Jira issue: jira ticket TT-16823 has status 'Ready for Testing' but must be one of: In Dev, In Code Review, Ready For Dev, Dod Check

Next Steps

  • Ensure your branch name contains a valid Jira ticket ID (e.g., ABC-123)
  • Verify your PR title matches the branch's Jira ticket ID
  • Check that the Jira ticket exists and is accessible

This comment will be automatically deleted once the linter passes.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
0 New issues
0 Accepted issues

Measures
0 Security Hotspots
86.4% Coverage on New Code
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

@andrei-tyk
andrei-tyk merged commit 505350c into master May 27, 2026
79 of 90 checks passed
@andrei-tyk
andrei-tyk deleted the TT-16823-filter-middleware-by-allow-block branch May 27, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants