Skip to content

Conversation

@andrei-tyk
Copy link
Contributor

@andrei-tyk andrei-tyk commented Jan 28, 2026

Description

Related Issue

Motivation and Context

How This Has Been Tested

Screenshots (if appropriate)

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactoring or add test (improvements in base code or adds test coverage to functionality)

Checklist

  • I ensured that the documentation is up to date
  • I explained why this PR updates go.mod in detail with reasoning why it's required
  • I would like a code coverage CI quality gate exception and have explained why

Ticket Details

TT-16492
Status In Dev
Summary MCP Request Handling & JSON-RPC Routing

Generated at: 2026-01-30 15:35:49

andrei-tyk and others added 22 commits January 27, 2026 11:04
…hnologies/tyk into TT-16492-mcp-request-handling-json-rpc-routing
…ting

# Conflicts:
#	apidef/oas/linter_test.go
#	apidef/oas/mcp_primitive.go
#	apidef/oas/mcp_primitive_test.go
#	apidef/oas/mcp_test.go
#	apidef/oas/middleware.go
…to TT-16492-mcp-request-handling-json-rpc-routing
@github-actions
Copy link
Contributor

github-actions bot commented Jan 28, 2026

API Changes

--- prev.txt	2026-01-30 15:36:27.217940239 +0000
+++ current.txt	2026-01-30 15:36:16.876973654 +0000
@@ -3766,6 +3766,12 @@
     resources, prompts). It embeds Operation to reuse all standard middleware
     (rate limiting, transforms, caching, etc.).
 
+func (m *MCPPrimitive) ExtractToExtendedPaths(ep *apidef.ExtendedPathsSet, path string, method string)
+    ExtractToExtendedPaths extracts middleware config, delegating to embedded
+    Operation but allowing MCPPrimitive-specific overrides. Methods without
+    overrides are promoted to Operation. Methods with empty overrides (like
+    extractTransformResponseBodyTo) are effectively disabled for MCP primitives.
+
 type MCPPrimitives map[string]*MCPPrimitive
     MCPPrimitives maps primitive names to their middleware configurations.
     For tools: key is tool name (e.g., "get-weather"). For resources: key is
@@ -8367,8 +8373,10 @@
 	SelfLooping
 	// RequestStartTime holds the time when the request entered the middleware chain
 	RequestStartTime
-	// MCPRouting indicates the request came via MCP JSON-RPC routing
-	MCPRouting
+	// JsonRPCRouting indicates the request came via JSON-RPC routing (MCP, A2A, etc.)
+	JsonRPCRouting
+	// JSONRPCRequest stores parsed JSON-RPC request data for protocol routing (MCP, A2A, etc.)
+	JSONRPCRequest
 )
 # Package: ./dlpython
 
@@ -9474,6 +9482,13 @@
 
 func (a *APISpec) Expired() bool
 
+func (a *APISpec) FindAllVEMChainSpecs(r *http.Request, rxPaths []URLSpec, mode URLStatus) []*URLSpec
+    FindAllVEMChainSpecs returns all URLSpecs matching the given status from
+    the VEM chain. For MCP APIs with JSON-RPC routing, this returns specs from
+    all VEMs in the chain (operation VEM + tool VEM), allowing middleware to be
+    applied at each stage. For non-MCP APIs, it returns only the spec matching
+    the current path.
+
 func (a *APISpec) FindSpecMatchesStatus(r *http.Request, rxPaths []URLSpec, mode URLStatus) (*URLSpec, bool)
     FindSpecMatchesStatus checks if a URL spec has a specific status and returns
     the URLSpec for it.
@@ -10781,6 +10796,28 @@
 	Timestamp time.Time
 }
 
+type JSONRPCError struct {
+	Code    int         `json:"code"`
+	Message string      `json:"message"`
+	Data    interface{} `json:"data,omitempty"`
+}
+    JSONRPCError represents a JSON-RPC 2.0 error object.
+
+type JSONRPCErrorResponse struct {
+	JSONRPC string       `json:"jsonrpc"`
+	Error   JSONRPCError `json:"error"`
+	ID      interface{}  `json:"id"`
+}
+    JSONRPCErrorResponse represents a JSON-RPC 2.0 error response.
+
+type JSONRPCRequest struct {
+	JSONRPC string          `json:"jsonrpc"`
+	Method  string          `json:"method"`
+	Params  json.RawMessage `json:"params,omitempty"`
+	ID      interface{}     `json:"id,omitempty"`
+}
+    JSONRPCRequest represents a JSON-RPC 2.0 request structure.
+
 type JSVM struct {
 	Spec    *APISpec
 	VM      *otto.Otto `json:"-"`
@@ -10962,6 +10999,25 @@
     Init initializes the LogMessageEventHandler instance with the given
     configuration.
 
+type MCPJSONRPCMiddleware struct {
+	*BaseMiddleware
+}
+    MCPJSONRPCMiddleware handles JSON-RPC 2.0 request detection and routing
+    for MCP APIs. When a client sends a JSON-RPC request to an MCP endpoint,
+    the middleware detects it, extracts the method and primitive name, routes to
+    the correct VEM, and enables the middleware chain to execute before proxying
+    to upstream.
+
+func (m *MCPJSONRPCMiddleware) EnabledForSpec() bool
+    EnabledForSpec returns true if this middleware should be enabled for the API
+    spec. It requires the API to be an MCP API with JSON-RPC 2.0 protocol.
+
+func (m *MCPJSONRPCMiddleware) Name() string
+    Name returns the middleware name.
+
+func (m *MCPJSONRPCMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int)
+    ProcessRequest handles JSON-RPC request detection and routing.
+
 type MethodNotAllowedHandler struct{}
 
 func (m MethodNotAllowedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
@@ -11250,6 +11306,14 @@
 
 func (k *OrganizationMonitor) SetOrgSentinel(orgChan chan bool, orgId string)
 
+type ParamExtractionResult struct {
+	Value        string
+	ErrorMessage string
+	IsValid      bool
+}
+    ParamExtractionResult represents the result of extracting a parameter from
+    JSON-RPC params.
+
 type PersistGraphQLOperationMiddleware struct {
 	*BaseMiddleware
 }

@probelabs
Copy link

probelabs bot commented Jan 28, 2026

This pull request introduces a significant enhancement to the gateway's Model Context Protocol (MCP) capabilities by adding a native MCPJSONRPCMiddleware. This middleware intercepts and processes JSON-RPC 2.0 requests, enabling fine-grained policy enforcement (e.g., rate limiting, access control) on individual MCP primitives like tools, resources, and prompts.

Previously, MCP traffic was largely opaque to the gateway's middleware chain. This change addresses that by parsing the JSON-RPC payload, rewriting the request to an internal-only Virtual Endpoint (VEM), and then running it through the standard middleware pipeline. This also introduces the concept of a "VEM Chain" to apply layered policies (e.g., operation-level then primitive-level), which resolves a critical bug where operation-level and global API rate limits were not being enforced on MCP APIs.

Files Changed Analysis

The changes are substantial, with over 3,200 additions across 19 files, centered around the new middleware, its extensive test suite, and a more generic protocol-handling framework.

  • Core Feature Addition: The bulk of the new code is in gateway/mw_mcp_jsonrpc.go and its comprehensive test file gateway/mw_mcp_jsonrpc_test.go, which together introduce the JSON-RPC routing logic.
  • Framework Abstraction: New files like internal/agentprotocol/vem.go, internal/httpctx/jsonrpc.go, and internal/mcp/jsonrpc.go create a generic and extensible framework for handling agent-based protocols, replacing the older, hardcoded MCP context handling (internal/httpctx/mcp.go was removed).
  • Rate Limiting & Middleware Fixes: gateway/mw_api_rate_limit.go and gateway/mw_modify_headers.go are modified to support the "VEM chain," ensuring that both primitive-specific and broader operation-level/global policies are applied correctly.
  • VEM Generation & Security: gateway/mcp_vem.go and gateway/api_definition.go are updated to generate more sophisticated VEMs, including operation-level VEMs and catch-all BlackList rules for proper allow-list enforcement. The security model is enhanced to prevent direct external access to these internal VEMs.

Architecture & Impact Assessment

  • What this PR accomplishes: It deeply integrates the MCP JSON-RPC protocol into the Tyk Gateway, transforming it from a simple passthrough mechanism into a fully managed endpoint with per-primitive and per-operation policy enforcement. It also fixes a critical bug where rate limits were not being applied to MCP traffic.
  • Key technical changes introduced:
    • A new MCPJSONRPCMiddleware that parses the JSON-RPC body and rewrites the request path to a corresponding internal VEM (e.g., /mcp-tool:get-weather).
    • A "VEM Chain" concept that allows for layered middleware application (e.g., an operation-level rate limit on tools/call and a stricter tool-level limit on get-weather).
    • A context-based security model (JsonRPCRouting flag) that prevents direct external HTTP access to these internal VEMs.
    • Generation of catch-all BlackList VEMs when an allow-list is active, ensuring unregistered or disallowed primitives are blocked by default.
  • Affected system components: The Gateway's request processing pipeline, API definition loading, VEM generation logic, and middleware (especially rate-limiting and header modification) for MCP-enabled APIs.

JSON-RPC Request Handling Flow

sequenceDiagram
    participant Client
    participant Tyk Gateway
    participant MCPJSONRPCMiddleware
    participant RateLimitMiddleware
    participant VersionCheckMiddleware
    participant Upstream Service

    Client->>Tyk Gateway: POST /mcp (JSON-RPC: method='tools/call', params={name:'get-weather'})
    Tyk Gateway->>MCPJSONRPCMiddleware: ProcessRequest
    MCPJSONRPCMiddleware->>MCPJSONRPCMiddleware: Parse body, build VEM chain ['/mcp-operation:tools/call', '/mcp-tool:get-weather']
    MCPJSONRPCMiddleware->>Tyk Gateway: Rewrite URL to '/mcp-tool:get-weather' & set JsonRPCRouting=true
    Tyk Gateway->>RateLimitMiddleware: Process request for VEM chain
    RateLimitMiddleware->>RateLimitMiddleware: Check rate limits for both VEMs in the chain
    RateLimitMiddleware->>VersionCheckMiddleware: Forward request
    VersionCheckMiddleware->>VersionCheckMiddleware: Validate access to internal VEM via JsonRPCRouting flag
    VersionCheckMiddleware->>Upstream Service: Proxy original request body
    Upstream Service-->>Client: JSON-RPC Response
Loading

Scope Discovery & Context Expansion

  • The introduction of the internal/agentprotocol package is a forward-looking architectural decision. It establishes a centralized registry for protocol-specific VEM prefixes, creating a reusable pattern for securely integrating other agent-based protocols in the future.
  • The security of the feature hinges on the httpctx.IsJsonRPCRouting(r) check within gateway/api_definition.go. This acts as the gatekeeper, ensuring sensitive internal VEM paths are only accessible after being processed and flagged by a trusted routing middleware like MCPJSONRPCMiddleware.
  • The fix in gateway/mw_api_rate_limit.go by introducing getAllVEMChainSessions is a critical part of the scope. It correctly enforces layered rate limits, preventing a scenario where a generous tool-specific limit could bypass a stricter, overarching API or operation-level limit.
Metadata
  • Review Effort: 4 / 5
  • Primary Label: feature

Powered by Visor from Probelabs

Last updated: 2026-01-30T15:39:04.132Z | Triggered by: pr_updated | Commit: 3f6f2c4

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

@probelabs
Copy link

probelabs bot commented Jan 28, 2026

✅ Security Check Passed

No security issues found – changes LGTM.

Architecture Issues (2)

Severity Location Issue
🟠 Error gateway/mw_api_rate_limit.go:80-122
The `getAllVEMChainSessions` function manually iterates through VEM paths and all API specs to find matching rate limits. This duplicates the purpose of the new `APISpec.FindAllVEMChainSpecs` function, which was introduced as a centralized way to handle this logic. The `TransformHeaders` middleware was correctly refactored to use `FindAllVEMChainSpecs`, but this one was not. This inconsistency complicates maintenance and violates the new architectural pattern.
💡 SuggestionRefactor `getAllVEMChainSessions` to use `APISpec.FindAllVEMChainSpecs(r, versionPaths, RateLimit)` to retrieve all relevant rate limit specs, and then build the sessions from that result. This would align it with the pattern used in `mw_modify_headers.go`, improving consistency and reducing code duplication.
🟠 Error apidef/oas/operation.go:298-302
The change in `extractPathsAndOperations` maps middleware from all OAS operations onto the single `listenPath` for MCP APIs. This is a coarse-grained approach that can lead to unpredictable behavior if multiple operations define conflicting middleware (e.g., different rate limits). This logic conflicts with the more precise mechanism introduced in `gateway/mcp_vem.go` (`generateMCPOperationVEMs`), which correctly maps middleware from a specific OAS path (e.g., `/tools/call`) to a specific operation VEM (e.g., `/mcp-operation:tools/call`). The VEM-based approach is superior and should be the sole mechanism for applying operation-level middleware.
💡 SuggestionRemove the logic that forces the `extractPath` to be the `listenPath` for all MCP operations within `extractPathsAndOperations`. The VEM generation logic in `gateway/mcp_vem.go` is the correct and sufficient place to handle the mapping of operation middleware to specific VEM paths.

Performance Issues (3)

Severity Location Issue
🟠 Error gateway/mw_mcp_jsonrpc.go:333-335
The middleware parses the JSON request body twice. First, it unmarshals the entire payload into a `JSONRPCRequest` struct in `readAndParseJSONRPC`. Then, for requests like `tools/call` or `resources/read`, it unmarshals the `params` field (a `json.RawMessage`) a second time into a `map[string]interface{}` within `extractParamWithDetails`. This redundant parsing adds unnecessary CPU and memory overhead to every request.
💡 SuggestionDefine specific structs for the `params` of each method (e.g., `ToolsCallParams`, `ResourcesReadParams`). Then, in the `routeRequest` function, after identifying the method, unmarshal the `rpcReq.Params` into the corresponding struct just once. This avoids the second unmarshal into a generic map and is more type-safe.
🟡 Warning gateway/mw_mcp_jsonrpc.go:400-418
The `matchResourceURI` function uses a linear scan (O(N)) over all registered primitives to find a matching wildcard pattern for a given resource URI. For APIs with a large number of resource primitives defined with wildcards, this iteration on every `resources/read` request can become a performance bottleneck.
💡 SuggestionFor more scalable wildcard path matching, consider using a more efficient data structure like a Trie (prefix tree). The resource patterns could be pre-compiled into a Trie when the API definition is loaded. Matching a URI against the Trie would be significantly faster, with a complexity related to the length of the URI rather than the number of patterns.
🟡 Warning gateway/model_apispec.go:167-176
The `FindAllVEMChainSpecs` function iterates over the entire `rxPaths` slice for each VEM in the request's VEM chain. This results in a nested loop with O(M*P) complexity, where M is the VEM chain length and P is the total number of path specs. For APIs with many defined paths, this linear scan on every request can degrade performance.
💡 SuggestionTo optimize this lookup, consider pre-processing the `rxPaths` into a `map[string][]URLSpec` during API loading, where the key is the literal path. This would allow `FindAllVEMChainSpecs` to perform a near O(1) map lookup for each `vemPath` instead of an O(P) slice scan, significantly improving performance for APIs with many paths.

Quality Issues (1)

Severity Location Issue
🟡 Warning gateway/mw_mcp_jsonrpc_test.go:1475-1790
Test names and comments are misleading as they describe testing for a bug that this PR is intended to fix. Tests like `TestMCPJSONRPCMiddleware_OperationRateLimitNotEnforced` and `TestMCPJSONRPCMiddleware_OperationRateLimitNotEnforced_ExactUserScenario` were likely created to reproduce a bug, but now that the bug is fixed, they serve as regression tests. Their names and comments should be updated to reflect that they verify the correct, fixed behavior.
💡 SuggestionRename the tests to accurately describe the behavior they now verify (e.g., `TestMCPJSONRPCMiddleware_OperationAndGlobalRateLimitsAreEnforced`) and remove comments that suggest the test is for an existing bug or is expected to fail. This will improve test suite clarity and maintainability.

Powered by Visor from Probelabs

Last updated: 2026-01-30T15:39:07.387Z | Triggered by: pr_updated | Commit: 3f6f2c4

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

…to TT-16492-mcp-request-handling-json-rpc-routing

# Conflicts:
#	gateway/api_definition.go
…eneration

# Conflicts:
#	gateway/api_definition.go
#	gateway/model_apispec.go
…to TT-16492-mcp-request-handling-json-rpc-routing

# Conflicts:
#	gateway/model_apispec.go
Base automatically changed from TT-16491-mcp-definition-gateway-load-vem-generation to master January 30, 2026 06:41
…ting

# Conflicts:
#	ctx/ctx.go
#	gateway/api_definition.go
#	gateway/mcp_vem_test.go
#	internal/mcp/mcp.go
}

if a.Proxy.ListenPath != "/" {
if a.Proxy.ListenPath != "/" && !agentprotocol.IsProtocolVEMPath(matchPath) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Q: would it be beneficial to do this only if the API is JSON-RPC and the payload is JSON-RPC

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored


// maxJSONRPCRequestSize is the maximum allowed size for JSON-RPC request bodies.
// This limit protects against DoS attacks when the global MaxRequestBodySize is not configured.
const maxJSONRPCRequestSize = 1 << 20 // 1MB
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we use the GW config for the request size limit

Copy link
Collaborator

Choose a reason for hiding this comment

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

also we have at MCP definition level this config

Copy link
Contributor Author

Choose a reason for hiding this comment

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

adapted

return nil, middleware.StatusRespond
}
if !found {
if m.mcpAllowListEnabled() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

not sure why we need to do custom Allow middleware check here, since the JSON-RPC methods are transformed in Paths, and the request to the internal VEM should behave same as a normal path request and all middleware should run and be applied (including Allow)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

recatored

})

// Enable JSON-RPC routing (allows access to internal VEM endpoints)
httpctx.SetJsonRPCRouting(r, true)
Copy link
Collaborator

Choose a reason for hiding this comment

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

@andrei-tyk to check why is this still here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored

}
}

func (m *MCPJSONRPCMiddleware) mcpAllowListEnabled() bool {
Copy link
Collaborator

Choose a reason for hiding this comment

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

@andrei-tyk to check here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored


// matchResourceURI matches a resource URI against configured patterns.
// It first tries an exact match, then falls back to wildcard matching.
func (m *MCPJSONRPCMiddleware) matchResourceURI(uri string, primitives map[string]string) (vemPath string, found bool) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

our paths already have support for regexes. Once the resource mcpPrimitive is stored as a path, we should have regex handling by defualt

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we need some separate logic for them as mcp have regex like characters in them and the OAS logic has support for {id} params


// shouldPassthrough returns true if the method should be passed through to upstream
// without requiring a configured VEM (e.g., discovery operations, notifications).
func (m *MCPJSONRPCMiddleware) shouldPassthrough(method string) bool {
Copy link
Collaborator

Choose a reason for hiding this comment

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

we should passthrough by default not just for notifications

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored and fixed

// MCP JSON-RPC method names as defined in the Model Context Protocol specification.
const (
// Tool methods
MethodToolsCall = "tools/call"
Copy link
Collaborator

Choose a reason for hiding this comment

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

we need to intercept just tools/call resource/read and prompts/get

Copy link
Contributor Author

Choose a reason for hiding this comment

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

cleaned it up

andrei-tyk and others added 19 commits January 30, 2026 12:15
- Add constants for JSON-RPC parameter keys, primitive prefixes, and error messages
- Replace magic strings throughout gateway/mw_mcp_jsonrpc.go
- Update test to use constant for error message assertion
- Use consistent key format in mcp_vem.go
- Add extractAndValidateParam helper to eliminate repeated validation pattern
- Replace switch statement with methodPrefixMap for cleaner buildUnregisteredVEMPath
- Simplify parameter extraction in routeRequest method
- Extract validateJSONRPCRequest to check request type
- Extract readAndParseJSONRPC to handle parsing and validation
- Extract setupJSONRPCRouting to configure context and routing
- Simplify ProcessRequest to show high-level flow
- Improve variable naming in matchResourceURI for better readability
- Add detailed precedence rules for matchResourceURI wildcard matching
- Expand passthrough comment to explain discovery, notifications, and operations
- Clarify which requests are handled by upstream MCP server
Operation-level rate limits were ignored after JSON-RPC routing to VEM paths.
Fixed by extracting operation middleware using listenPath for MCP APIs and
checking rate limits on both VEM and original paths.
…on-rpc-routing' into TT-16492-mcp-request-handling-json-rpc-routing
@sonarqubecloud
Copy link

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