Repository navigation
OSPP2: Add a stateless MCP tools entry to Agent Gateway - #7424
Conversation
Aias00
left a comment
There was a problem hiding this comment.
Draft review note: the overall MCP entry boundary is promising, but the preflight path currently appears to require both JSON and SSE Accept values regardless of response mode. Please confirm JSON-only clients are intentionally rejected, and keep this in draft until the protocol contract and failing K8s check are resolved.
|
Thank you for reviewing this, @Aias00. Yes, rejecting JSON-only clients is intentional for this entry. The 2026-07-28 Streamable HTTP client contract requires clients to advertise and support both JSON and SSE. I have clarified this distinction in the README and added 26 regression cases across both response modes, including checks that rejection happens before body subscription, identity resolution or tool invocation. Core/Admin (326 tests) and the example (28 tests) passed, with Checkstyle and RAT. I also merged current master normally, including the ZooKeeper rollout-race fix already merged in #7427, and pushed |
Aias00
left a comment
There was a problem hiding this comment.
Draft review note: the JSON-plus-SSE Accept requirement is now explicit and covered by tests, so the earlier ambiguity is resolved. Please keep the PR in draft until that protocol choice and the broader Agent Gateway MCP contract are ready for stabilization.
|
Thank you for the clarification, @Aias00. I have expanded the README into an explicit tools-only implementation contract covering the fixed version and legacy-client boundary, mirrored headers, authorization, result/error semantics, configuration limits, and cancellation. The protocol choice and strict Accept policy remain unchanged; this documentation is not a claim that the wider contract is finalized. Commit No production behavior, aggregation, prompts/resources or statistics were added. I will keep the PR in draft until the protocol choice and wider contract are ready for stabilization. Thank you for your guidance. |
Aias00
left a comment
There was a problem hiding this comment.
Reviewed current head 3882f857feda.
The main code paths have strong bounds, ownership, and authorization checks, and the earlier Accept-contract concern is resolved. I am leaving COMMENT because this is a broad new protocol entry with documented remaining MCP scope gaps rather than a small final-approval change.
Architecture note: No blocking defect found in the new MCP entry, request parser, dispatcher, context handling, registry, and starter wiring. This broad new protocol surface still needs live integration coverage beyond the reviewed unit tests.
Validation: exact-head targeted Maven reactor tests passed on Java 17.
./mvnw -pl shenyu-admin,shenyu-common,shenyu-plugin/shenyu-plugin-agent-gateway,shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway -am test -Dtest=RuleServiceTest,AgentGatewayRuleHandleValidatorTest,AgentGatewayMcpConfigTest,AgentGatewayPluginTest,AgentGatewayRuleHandleParserTest,AgentMcpDispatcherTest,AgentMcpHttpHandlerTest,AgentMcpRequestParserTest,AgentMcpPrincipalSecurityResolverTest,AgentToolCapabilityTest,AgentToolDefinitionTest,AgentToolRegistryTest,AgentGatewayPluginConfigurationTest -Dsurefire.failIfNoSpecifiedTests=false -Drat.skip=true -Djacoco.skip=true -DskipRemoteResources=true -Dmaven.javadoc.skip=trueSupplementary example-module tests passed (28 tests, no failures).
./mvnw -f shenyu-examples/pom.xml -pl shenyu-examples-plugin -am test -Dtest=AgentGatewayExampleConfigurationTest,ExampleMcpAuthenticationFilterTest,OrderStatusToolTest -Dsurefire.failIfNoSpecifiedTests=false -Drat.skip=true -Djacoco.skip=true -DskipRemoteResources=true -Dmaven.javadoc.skip=true| public ShenyuPlugin agentGatewayPlugin(final ObjectProvider<AgentToolProvider> providers, final ObjectProvider<AgentMcpSecurityResolver> resolvers) { | ||
| AgentToolRegistry registry = new AgentToolRegistry(providers.orderedStream().toList()); | ||
| AgentMcpSecurityResolver resolver = resolvers.getIfAvailable(() -> exchange -> Mono.empty()); | ||
| return new AgentGatewayPlugin(new AgentMcpHttpHandler(new AgentMcpDispatcher(registry, "shenyu-agent-gateway", "2.7.2-SNAPSHOT"), resolver)); |
There was a problem hiding this comment.
[NIT] The server version "2.7.2-SNAPSHOT" is hardcoded here, and again in AgentGatewayPlugin.java:54 and AgentMcpDispatcherTest.java:3812. This value is advertised as serverInfo.version in server/discover and will silently drift from the real release version after a cut. Consider deriving it from the project version (a build-time constant or ProjectVersion.getVersion() if available) or a single named constant, so discovery does not lie post-release.
| return Mono.error(failure(request, 403, -32602, "Tool is not available")); | ||
| } | ||
| JsonObject arguments = params.has("arguments") ? JsonParser.parseString(params.get("arguments").toString()).getAsJsonObject() : new JsonObject(); | ||
| JsonObject capabilities = JsonParser.parseString(params.path("_meta").path("io.modelcontextprotocol/clientCapabilities").toString()).getAsJsonObject(); |
There was a problem hiding this comment.
[NIT] params.path("_meta").path("io.modelcontextprotocol/clientCapabilities") was already validated as an object by AgentMcpRequestParser (non-object clientCapabilities is rejected with -32602). Re-serializing with .toString() and re-parsing here is redundant on the hot path. You can reuse request.getParams().getAsJsonObject("_meta").getAsJsonObject("io.modelcontextprotocol/clientCapabilities") directly, which is already an independent JsonObject copy from AgentMcpRequest.
Aias00
left a comment
There was a problem hiding this comment.
Review: OSPP2 stateless MCP tools entry for Agent Gateway
I read the full diff (49 files, +4811/−33) and the surrounding contract in the existing AgentGatewayPlugin. This is a careful, security-first implementation. I am approving it; all comments below are non-blocking.
What is done well (specifics)
- Identity is never trusted from the wire.
AgentMcpSecurityResolveris a deployment-supplied adapter; the default Starter wiring usesexchange -> Mono.empty(), so an MCP rule with no trusted adapter fails closed with 401 (AgentGatewayPluginConfigurationTest.shouldDenyMcpWhenNoSecurityAdapterIsRegistered). Forged_meta.subject/_meta.allowedToolsandX-Subject/Mcp-Session-Idheaders do not grant anything — verified byAgentMcpDispatcherTest.shouldIgnoreForgedMetadata…andAgentMcpHttpHandlerTest.shouldDenyMissingIdentityDespiteForgedHeadersAndMetadata. - Permission intersection is correct and frozen per request.
AgentMcpExecutionContextcomputesallowedTools = ruleTools ∩ securityGrantsonce, andAgentToolRegistryre-checks that same predicate atinvoketime. An empty intersection yields an emptytools/list(200) but a 403tools/call— the documented "unknown and invisible names have the same public denial" contract. - Strict, layered input validation.
AgentMcpRequestParserenforcesSTRICT_DUPLICATE_DETECTION,maxNestingDepth(64),FAIL_ON_TRAILING_TOKENS, mandatory UTF-8, single JSON-RPC request (no batch/notification/response hybrids), string-or-integral id, typed_meta, and exact mirrored-header matching (MCP-Protocol-Version,Mcp-Method,Mcp-Name, including base64 sentinel decode). This header/body cross-check is the right defense against a client smuggling the method/name through an unchecked field. - Error sanitization is enforced by tests, not just claimed. Business/tool diagnostics (
api-key=secret,private order exists,provider-private-diagnostic) are never forwarded;AgentToolArgumentExceptionis mapped to a genericisError=trueresult, and unexpected exceptions become a 500 with no detail. Multiple tests assert the secret string is absent from the wire. - Bounded, cancellable execution. One timeout budget covers identity → body read → parse → provider → encode → write;
Instantdeadline plus Reactor.timeoutboth apply; cancellation propagates to the provider subscription and is isolated per call even when 32 concurrent calls share an identical client RPC id (AgentMcpDispatcherTest.shouldKeep32ConcurrentCallsWithSameRpcIdIndependent,shouldCancelOneCallWithoutAffectingSurvivorWithTheSameRpcId). Netty buffers are released on normal completion, overflow and cancellation. - Immutability / snapshot isolation. Every parsed request, definition, invocation argument and result is deep-copied;
AgentToolRegistryandAgentGatewayMcpConfigare unmodifiable; config is captured at subscription, not assembly (AgentGatewayPluginTest.shouldSelectConfigurationAtSubscriptionNotAssembly). In-flight requests keep their snapshot while new requests see a revocation. - Tests are thorough and meaningful. Beyond the 26 two-mode Accept cases and the regression suite you cite, the unit tests deliberately exercise the failure branches (missing identity, forged metadata, hidden/unknown tools, cursor rejection, empty completion, capability gating, deadline cancellation) rather than only the happy path. CI is fully green across all 44 checks.
Non-blocking suggestions
- Hardcoded server version (minor).
"2.7.2-SNAPSHOT"is duplicated inAgentGatewayPlugin.java:54,AgentGatewayPluginConfiguration.java:53andAgentMcpDispatcherTest.java:3812, and is advertised asserverInfo.versioninserver/discover. After a release this silently lies. Prefer a single named constant or the project version. (Left as an inline nit.) - Redundant clientCapabilities re-parse (minor).
AgentMcpDispatcher.java:126re-serializes and re-parsesclientCapabilitieseven thoughAgentMcpRequestParseralready validated it as an object; reuse the existingJsonObjectcopy. (Left as an inline nit.) - RELEASE-NOTES. This adds a new
trafficType: "mcp"value to theagentGatewayrule handle and a new opt-in Starter assembly whose default behavior is "MCP denied until a trustedAgentMcpSecurityResolverbean is supplied." Please add a short## Unreleased → ### Behavior Changesentry so downstream users are not surprised that enabling the plugin no longer auto-allows MCP. (Additive public API inshenyu-commonis source-compatible, so this is documentation only.) - PR description vs state. The body still says "The PR remains draft pending the protocol-contract discussion." The actual PR is open (not draft) and CI is green. The 406 Accept policy is a deliberate, documented, defensible choice and is the kind of contract decision best finalized in the issue/ML thread rather than as a code blocker — but please update the description so reviewers are not misled about readiness.
Conclusion
The implementation is correct, well-tested and follows the existing module's defensive style. I have no blocking concerns. Approved. Please address the two nits and the RELEASE-NOTES point when convenient; the protocol-contract discussion can continue in parallel.
Aias00
left a comment
There was a problem hiding this comment.
Re-review after rebase (head defa9ef)
I re-read the incremental diff against my prior reviewed head (3882f85). The large OLD..NEW diff was rebase noise from merging master (unrelated changes: admin lock removal, e2e logging-rabbitmq, k8s controller, registry-eureka, websocket sync, etc.). The PR's true content (base...head) is unchanged from the feature I already approved, plus the following improvements that resolve my earlier comments:
Nits resolved
- Hardcoded server version —
AgentGatewayPlugin.java/AgentGatewayPluginConfiguration.java/AgentMcpDispatcherTestno longer hardcode"2.7.2-SNAPSHOT". Version is now externalized toMETA-INF/shenyu/agent-gateway-version.properties(version=${project.version}, with scoped Maven resource filtering inpom.xml) and read viaAgentGatewayConstants.MCP_SERVER_VERSION, which falls back toVersionUtils.getVersion(...).server/discoverwill now advertise the real build version. Test asserts the advertised version equalsMCP_SERVER_VERSION. - Redundant clientCapabilities re-parse —
AgentMcpDispatcher.javanow callsrequest.getClientCapabilities()instead of re-toString()-ing and re-parsing.AgentMcpRequeststores a typed Gson snapshot built once in the constructor (toGson). A new test (shouldSnapshotCapabilitiesWithoutLosingNestedTypesOrLargeIntegers) verifies nested types,null, and large integers beyondLong.MAX(92233720368547758081234) are preserved without precision loss — strictly better than the previous text round-trip.
Non-blocking suggestions resolved
- RELEASE-NOTES.md now documents the
trafficType: "mcp"opt-in and the fail-closed 401 behavior when no trustedAgentMcpSecurityResolveris supplied. - README wording no longer claims "the PR remains draft"; it frames protocol-version/compatibility decisions as open for community discussion.
Verification
- CI: no failures across recent runs (license-header and others pass).
mergeStateStatus: BLOCKEDis only because the prior approval was dismissed on the new push;mergeable: MERGEABLE(no conflicts).- Security invariants I originally verified remain intact: identity never derives from wire metadata,
allowedTools = ruleTools ∩ grantsfrozen per request, strict layered parsing, error desensitization, single timeout budget with cancellation propagation.
The capability-snapshot change is a clean refactor with a stronger test than before; no regressions introduced. Re-approving.
(Note: the two original inline COMMENTED nits are now resolved by these changes and can be considered closed.)
Background
Agent Gateway establishes request context for LLM traffic. This change adds an explicitly matched, stateless MCP tools entry in the same plugin, alongside the existing AI Proxy and legacy MCP endpoints. It reuses the foundation already merged in #7133 and depends only on merged master code.
The entry implements the tools-only subset of the fixed 2026-07-28 snapshot. Protocol and compatibility decisions can continue in the community discussion; the PR is now non-draft.
Changes
server/discover,tools/listandtools/call, with JSON or one final SSE response.defa9ef2b: derive discovery's server version from artifact/build metadata, reuse a prevalidated capability snapshot without a JSON text round trip, document default-deny MCP behavior in RELEASE-NOTES, and refresh readiness/verification information. The Jackson-to-Gson capability copy preserves nested types and large integers.Accept contract
Clients must advertise both
application/jsonandtext/event-streamwith positive quality values.responseModeselects successful output; it does not enable JSON-only compatibility. The 2026-07-28 client requirement requires support for both types; HTTP 406 and explicit-type enforcement are this entry's strict server policy. Preflight errors remain ordinary JSON. The 26 two-mode Accept cases cover rejection before body subscription, identity resolution or invocation.Verification
Local verification corresponds to
defa9ef2b, following the normal merge of masterec198d442ina9b9fce92; published history was not rewritten.shenyu-examples-pluginpassed.order_status; actual request IDs, wire results, subsequent model requests and final replies were correlated. Ten key runtime classes loaded from this PR's workspace. Artifact redaction found no remaining secret matches.git diff --checkpassed. These results are targeted verification, not full-project or complete MCP conformance.shenyu-examples/README.md, byte-for-byte unchanged from master. The failed report is retained; this PR does not modify that file. WebSocket negative-test warnings, legacy protocol-negotiation warnings and the initial harness classpath rejection are also retained.defa9ef2b: all seven workflows completed successfully on 2026-10-08. The Actions API reports 39 successful jobs and three skipped jobs (build_local_repo,test_group,compat_java21); GitHub's combined checks UI shows 40 successful checks and three skipped. The previous approval was dismissed after the new push; the updated head awaits review.Scope and remaining gaps
No initialization/session lifecycle, GET recovery stream, progress, Tasks, remote aggregation, prompts/resources, filtering or statistics are added. Providers return business JSON objects; native remote MCP results require a separate adapter. Cancellation reaches the current reactive subscription, without promising interruption of blocking provider code or rollback of external side effects.
Historical unmodified conformance results at
7c94ab887(7169291ec, requirements 2026-07-28) remain recorded and were not rerun here: JSON 109 success/63 failure/4 warning/7 skipped/1 informational; SSE 110 success/63 failure/4 warning/7 skipped. Prompts/resources are paused pending a use case, and other extensions are evaluated separately rather than implemented solely to clear those failures.