Skip to content

OSPP2: Add a stateless MCP tools entry to Agent Gateway - #7424

Merged
Aias00 merged 6 commits into
apache:masterfrom
BobSong-dev:feat/agent-gateway-mcp-entry
Oct 8, 2026
Merged

Aias00 merged 6 commits into
apache:masterfrom
BobSong-dev:feat/agent-gateway-mcp-entry

Conversation

@BobSong-dev

@BobSong-dev BobSong-dev commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

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

  • Add request-local server/discover, tools/list and tools/call, with JSON or one final SSE response.
  • Validate UTF-8 JSON-RPC, mirrored headers, protocol metadata and Origin; bound request/response sizes and execution deadlines.
  • Intersect explicitly registered tools, rule permissions and trusted server-side grants. Missing identity adapters deny MCP access. Check client capabilities after authorization and before argument validation or invocation.
  • Preserve immutable request/configuration snapshots and request-local cancellation, including equal client RPC/session IDs.
  • Add strict Admin rule validation, opt-in Starter wiring and a disabled-by-default loopback/JWT/read-only order example.
  • Address review suggestions in 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/json and text/event-stream with positive quality values. responseMode selects 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 master ec198d442 in a9b9fce92; published history was not rewritten.

  • Core/Admin/legacy MCP/WebSocket: 384 executed targeted tests in a 51-module reactor, zero failures/errors/skips; Checkstyle/RAT passed in all 51 modules. Only XML suites named in this run's log were counted, excluding stale reports.
  • Example: 28 targeted tests and Checkstyle passed; an explicit RAT check of shenyu-examples-plugin passed.
  • Real Admin/H2 → WebSocket → Bootstrap: 40 entry checks and 12 legacy MCP/AI Proxy coexistence checks passed. Both JSON/SSE discovery responses advertise the actual Maven project version. Coexistence upstreams are controlled fixtures.
  • OpenCode with the real configured DeepSeek model: JSON, SSE and client-side CodeMode each successfully called 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.
  • Admin/Bootstrap's 193-module assembly build succeeded with tests explicitly skipped; git diff --check passed. These results are targeted verification, not full-project or complete MCP conformance.
  • Validation note: a separately invoked RAT check of the example parent flags its existing 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.
  • GitHub CI at 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.

@Aias00 Aias00 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.

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.

@BobSong-dev

Copy link
Copy Markdown
Contributor Author

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. responseMode only selects the successful server response format. HTTP 406 and explicit positive-quality Accept types are our strict entry policy, not a rejection status mandated by that client requirement.

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 0c35745b9. New CI is running; I am not treating the earlier CI failure as resolved until this head passes its checks. I will keep the PR in draft pending the checks and your feedback on the clarified contract. No aggregation, prompts/resources or statistics were added.

@Aias00 Aias00 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.

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.

@BobSong-dev

Copy link
Copy Markdown
Contributor Author

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 3882f857f changes only the README and HTTP tests. It adds 18 regression cases and strengthens the same-RPC-ID cancellation test with the same client-supplied session ID. Local verification passed: 262 targeted tests across a 13-module reactor, with zero failures, errors or skips; Checkstyle and RAT passed. These are local targeted results, not full-project tests or a passing result for the new GitHub CI.

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.

@BobSong-dev
BobSong-dev marked this pull request as ready for review October 8, 2026 01:28

@Aias00 Aias00 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.

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=true

Supplementary 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));

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.

[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();

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.

[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
Aias00 previously approved these changes Oct 8, 2026

@Aias00 Aias00 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.

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. AgentMcpSecurityResolver is a deployment-supplied adapter; the default Starter wiring uses exchange -> Mono.empty(), so an MCP rule with no trusted adapter fails closed with 401 (AgentGatewayPluginConfigurationTest.shouldDenyMcpWhenNoSecurityAdapterIsRegistered). Forged _meta.subject / _meta.allowedTools and X-Subject / Mcp-Session-Id headers do not grant anything — verified by AgentMcpDispatcherTest.shouldIgnoreForgedMetadata… and AgentMcpHttpHandlerTest.shouldDenyMissingIdentityDespiteForgedHeadersAndMetadata.
  • Permission intersection is correct and frozen per request. AgentMcpExecutionContext computes allowedTools = ruleTools ∩ securityGrants once, and AgentToolRegistry re-checks that same predicate at invoke time. An empty intersection yields an empty tools/list (200) but a 403 tools/call — the documented "unknown and invisible names have the same public denial" contract.
  • Strict, layered input validation. AgentMcpRequestParser enforces STRICT_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; AgentToolArgumentException is mapped to a generic isError=true result, 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; Instant deadline plus Reactor .timeout both 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; AgentToolRegistry and AgentGatewayMcpConfig are 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

  1. Hardcoded server version (minor). "2.7.2-SNAPSHOT" is duplicated in AgentGatewayPlugin.java:54, AgentGatewayPluginConfiguration.java:53 and AgentMcpDispatcherTest.java:3812, and is advertised as serverInfo.version in server/discover. After a release this silently lies. Prefer a single named constant or the project version. (Left as an inline nit.)
  2. Redundant clientCapabilities re-parse (minor). AgentMcpDispatcher.java:126 re-serializes and re-parses clientCapabilities even though AgentMcpRequestParser already validated it as an object; reuse the existing JsonObject copy. (Left as an inline nit.)
  3. RELEASE-NOTES. This adds a new trafficType: "mcp" value to the agentGateway rule handle and a new opt-in Starter assembly whose default behavior is "MCP denied until a trusted AgentMcpSecurityResolver bean is supplied." Please add a short ## Unreleased → ### Behavior Changes entry so downstream users are not surprised that enabling the plugin no longer auto-allows MCP. (Additive public API in shenyu-common is source-compatible, so this is documentation only.)
  4. 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 Aias00 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.

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

  1. Hardcoded server version — AgentGatewayPlugin.java / AgentGatewayPluginConfiguration.java / AgentMcpDispatcherTest no longer hardcode "2.7.2-SNAPSHOT". Version is now externalized to META-INF/shenyu/agent-gateway-version.properties (version=${project.version}, with scoped Maven resource filtering in pom.xml) and read via AgentGatewayConstants.MCP_SERVER_VERSION, which falls back to VersionUtils.getVersion(...). server/discover will now advertise the real build version. Test asserts the advertised version equals MCP_SERVER_VERSION.
  2. Redundant clientCapabilities re-parse — AgentMcpDispatcher.java now calls request.getClientCapabilities() instead of re-toString()-ing and re-parsing. AgentMcpRequest stores a typed Gson snapshot built once in the constructor (toGson). A new test (shouldSnapshotCapabilitiesWithoutLosingNestedTypesOrLargeIntegers) verifies nested types, null, and large integers beyond Long.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 trusted AgentMcpSecurityResolver is 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: BLOCKED is 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 ∩ grants frozen 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.)

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.

2 participants