Skip to content

Adopt MCP 2026-07-28 with a modern-only gateway - #104

Open
Lili Xu (llxu1) wants to merge 3 commits into
mainfrom
feature/mcp-2026-07-28-modern-only
Open

Lili Xu (llxu1) wants to merge 3 commits into
mainfrom
feature/mcp-2026-07-28-modern-only

Conversation

@llxu1

Copy link
Copy Markdown
Contributor

Summary

Adopt MCP 2026-07-28 with one modern transport path, reducing the compatibility and routing complexity discussed in #98.

  • Route each MCP request independently to a ready backend, without transport-session persistence or legacy handshake fallback.
  • Require modern protocol headers and request metadata, preserve downstream errors, isolate forwarded credentials, and stream SSE incrementally with cancellation.
  • Update the portal and Python samples for modern discovery and tool calls, including schema-derived headers and private catalog behavior. Preserve current main's workload-identity and secure-proxy protections.
  • Support explicit deployment images, configurable Kubernetes namespaces, and HTTPS ingress with streaming-friendly settings.
  • Add user-facing migration, usage, deployment, and troubleshooting documentation plus reusable local/cloud E2E tests. No internal plans, implementation reports, private setup helpers, or session traces are included.

Breaking Change

The public MCP endpoints accept only 2026-07-28. Legacy clients and adapters must stay on a compatible previous gateway image until upgraded; this release does not translate older protocols. Application/agent sessions under /sessions are unchanged.

See the migration guide.

Validation

Final branch rebased onto current main:

  • .NET Release suite: 242 passed, 8 skipped, 0 failed.
  • Portal: 8 tests passed; clean dependency install and production build/typecheck passed.
  • Python: 26 secure-proxy tests and 1 streaming-fixture test passed, including redirect rejection and header redaction with FastMCP 4.
  • Fresh local gateway, Tools, and proxy images: 34 E2E checks passed, including authorization, protocol errors, private catalogs, a 30-second SSE quiet interval, and downstream cancellation. Two gateway and two Tools replicas were running.
  • Deployment script syntax, Bicep compilation/generated ARM alignment, OpenAPI JSON, documentation links/examples, and diff checks passed.
  • Portal browser smoke checks connected and returned 7 + 8 = 15 at desktop/mobile sizes with no measured horizontal overflow. DOM-dispatched clicks were used because the integrated browser's pointer-stability check timed out; this is not a full pointer/browser compatibility test.

Earlier in this change, 32 cloud E2E checks passed through HTTPS and Entra authentication on an isolated AKS deployment in westus2. Its resource groups, test app registration, and private test files were removed. Cloud validation preceded the final rebase; the final integrated images were retested locally, not redeployed to Azure.

Remaining Caveats

  • The 8 skipped .NET tests remain skipped.
  • npm reports an existing high-severity nanoid advisory also present on main; this PR does not introduce or resolve it. The portal build also retains its bundle-size advisory.
  • Actual VS Code MCP-client automation, full interactive continuation, production load/failover, and production rollback/restore exercises are not covered by these checks.

Refs #98.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved deployment, credential-forwarding, protocol-validation, schema-traversal, and undeclared httpx2 dependency findings remain.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adopts MCP 2026-07-28 as a modern-only, stateless gateway with updated clients, samples, deployments, documentation, and E2E coverage.

Changes:

  • Replaces session-aware routing and legacy handling with per-request routing and streaming.
  • Updates portal, Python samples, Tools, and protocol metadata validation.
  • Adds configurable deployment, namespace, HTTPS, migration, and testing support.
File summaries
File Summary
sample-servers/mcp-proxy/tests/test_proxy_config.py Tests proxy configuration and redirect handling.
sample-servers/mcp-proxy/src/proxy_config.py Configures the Python proxy HTTP client.
sample-servers/mcp-proxy/src/main.py Updates proxy server startup.
sample-servers/mcp-proxy/requirements.txt Updates proxy dependencies.
sample-servers/mcp-proxy/README.md Documents modern proxy usage.
sample-servers/mcp-example/requirements.txt Updates the example server dependency.
README.md Documents modern-only MCP usage and deployment.
portal/test/mcpStream.test.mjs Tests MCP stream parsing and cancellation.
portal/test/mcp.test.mjs Tests portal MCP requests.
portal/src/styles.css Updates portal styling.
portal/src/components/PageHeader.tsx Updates the portal page header.
portal/src/components/McpTestPanel.tsx Updates the MCP testing interface.
portal/src/components/Layout.tsx Updates portal layout behavior.
portal/src/api/mcpStream.ts Handles incremental MCP SSE responses.
portal/src/api/mcp.ts Builds modern MCP metadata and requests.
portal/src/api/client.ts Updates portal API client behavior.
portal/README.md Updates portal setup guidance.
portal/package.json Updates portal package metadata.
portal/package-lock.json Locks portal dependency versions.
openapi/mcp-gateway.openapi.json Updates the MCP Gateway API contract.
dotnet/Microsoft.McpGateway.Tools/test/StorageToolDefinitionProviderTests.cs Tests dynamic tool definition loading.
dotnet/Microsoft.McpGateway.Tools/test/McpToolHeaderValidatorTests.cs Tests schema-derived header validation.
dotnet/Microsoft.McpGateway.Tools/src/Services/StorageToolDefinitionProvider.cs Loads dynamic tool definitions.
dotnet/Microsoft.McpGateway.Tools/src/Services/McpToolHeaderValidator.cs Validates schema-derived tool headers.
dotnet/Microsoft.McpGateway.Tools/src/Services/HttpToolExecutor.cs Executes configured HTTP tools.
dotnet/Microsoft.McpGateway.Tools/src/Program.cs Configures the Tools MCP service.
dotnet/Microsoft.McpGateway.Service/test/SessionStoreTests.cs Tests session storage behavior.
dotnet/Microsoft.McpGateway.Service/test/McpEndpointMiddlewareTests.cs Tests MCP endpoint middleware.
dotnet/Microsoft.McpGateway.Service/test/McpAuthenticationTests.cs Tests MCP authentication behavior.
dotnet/Microsoft.McpGateway.Service/test/HttpProxyTests.cs Tests proxy forwarding behavior.
dotnet/Microsoft.McpGateway.Service/test/AdapterSessionRoutingHandlerTests.cs Tests adapter session routing.
dotnet/Microsoft.McpGateway.Service/test/AdapterReverseProxyControllerTests.cs Tests stateless adapter routing.
dotnet/Microsoft.McpGateway.Service/src/Session/ISessionRoutingHandler.cs Defines session routing abstractions.
dotnet/Microsoft.McpGateway.Service/src/Session/IAdapterSessionStore.cs Defines adapter session storage.
dotnet/Microsoft.McpGateway.Service/src/Session/DistributedMemorySessionStore.cs Implements distributed memory session storage.
dotnet/Microsoft.McpGateway.Service/src/Session/AdapterSessionRoutingHandler.cs Handles adapter session routing.
dotnet/Microsoft.McpGateway.Service/src/Routing/AdapterKubernetesNodeInfoProvider.cs Provides Kubernetes adapter node information.
dotnet/Microsoft.McpGateway.Service/src/Program.cs Updates service registration and routing.
dotnet/Microsoft.McpGateway.Service/src/McpSubPathAwareAuthenticationHandler.cs Updates subpath authentication behavior.
dotnet/Microsoft.McpGateway.Service/src/McpEndpointMiddleware.cs Validates public MCP endpoint requests.
dotnet/Microsoft.McpGateway.Service/src/HttpProxy.cs Forwards and streams downstream responses.
dotnet/Microsoft.McpGateway.Service/src/Controllers/AdapterReverseProxyController.cs Implements stateless backend selection.
dotnet/Microsoft.McpGateway.Management/src/Foundry/AgentToolRegistry.cs Updates agent and tool registration.
dotnet/Microsoft.McpGateway.Management/src/Deployment/KubernetesAdapterDeploymentManager.cs Manages Kubernetes adapter deployments.
dotnet/Microsoft.McpGateway.Management/src/Deployment/GatewayKubernetesOptions.cs Defines gateway Kubernetes options.
dotnet/Microsoft.McpGateway.Management/src/Contracts/McpProtocol.cs Defines the supported MCP protocol version.
dotnet/Directory.Packages.props Updates shared .NET package versions.
docs/mcp-2026-07-28.md Adds migration and compatibility guidance.
deployment/k8s/local-deployment.yml Updates local Kubernetes deployment resources.
deployment/k8s/kustomization.yaml Configures Kubernetes resource composition.
deployment/k8s/cloud-deployment-template.yml Defines cloud Kubernetes deployment resources.
deployment/infra/README.md Documents infrastructure deployment.
deployment/infra/azure-deployment.json Updates the generated Azure deployment template.
deployment/infra/azure-deployment.bicep Configures Azure and embedded Kubernetes deployment.
deployment/e2e/test-mcp.mjs Exercises modern protocol and streaming scenarios.
deployment/e2e/stream-server/test_main.py Tests the streaming fixture server.
deployment/e2e/stream-server/requirements.txt Defines streaming fixture dependencies.
deployment/e2e/stream-server/main.py Implements the streaming fixture server.
deployment/e2e/stream-server/Dockerfile Builds the streaming fixture image.
deployment/e2e/README.md Documents local and cloud E2E validation.
deployment/e2e/local/proxy-network-policy.yaml Defines local proxy network policy.
deployment/e2e/local/kustomization.yaml Configures local E2E resources.
deployment/Deploy-McpGateway.ps1 Supports staged deployment and explicit images.
Review details

Files not reviewed (1)

  • portal/package-lock.json: Generated file

Suppressed comments (3)

deployment/Deploy-McpGateway.ps1:320

  • The staged deployment path still queries gateway-secret specifically in adapter. If the manifest is used with the newly supported non-default namespace, reruns cannot find the existing secret, generate a different one, and can break identity forwarding or fail because the secret is not in the namespace being deployed. Use the same configured namespace for secret lookup as for manifest rendering.
    $existing = az aks command invoke --resource-group $ResourceGroupName --name $aksName --command 'kubectl -n adapter get secret gateway-secret -o json --ignore-not-found' --output json | ConvertFrom-Json

dotnet/Microsoft.McpGateway.Service/src/McpEndpointMiddleware.cs:70

  • The new endpoint contract marks Accept as required and the migration guide requires both application/json and text/event-stream, but this middleware never validates it. Requests with only the version/method headers are forwarded to arbitrary adapters, so compliance depends on each backend instead of the modern-only gateway contract.
        if (method[0] is "tools/call" or "resources/read" or "prompts/get")
        {
            var name = context.Request.Headers["Mcp-Name"];
            if (name.Count != 1 || string.IsNullOrWhiteSpace(name[0]))
            {
                await RejectAsync(context, -32020, "A single Mcp-Name header is required for this method.").ConfigureAwait(false);
                return;
            }
        }

        await next(context).ConfigureAwait(false);

dotnet/Microsoft.McpGateway.Tools/src/Services/McpToolHeaderValidator.cs:26

  • Returning when the current schema node has no properties silently skips every x-mcp-header nested under anyOf/oneOf/allOf, $ref, or array items. The portal rejects such annotations, but a non-portal client can register and call that definition without the required Mcp-Param-* check, so execution validation is not equivalent to the client-side traversal. Traverse the supported schema compositions or reject annotations outside an unambiguous object property.
  • Files reviewed: 62/63 changed files
  • Comments generated: 8
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +45 to +47
@secure()
@description('Shared first-party identity-forwarding secret; never supplied to adapter pods.')
param gatewaySecret string = ''

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 2a7a705.

The embedded deployment now reuses the existing namespace-scoped gateway secret, or generates and persists a cryptographically random secret when none exists. It rejects empty stored secrets, failed lookups, and explicit values that conflict with the stored secret. Both deployment paths render Base64-backed Kubernetes secret data.

Bicep compilation and embedded Bash syntax checks passed. The offline staged-deployment tests cover secret generation/reuse and failure handling; no live Azure deployment was performed.

Comment on lines 31 to 35
serviceAccountName: mcpgateway-sa
containers:
- name: mcpgateway-container
image: ghcr.io/microsoft/mcp-gateway:latest
image: ${GATEWAY_IMAGE}
ports:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 2a7a705.

Added a consistent namespace parameter throughout the manifest, including the Namespace resource, workloads, secrets, RBAC subjects, network policies, and Kubernetes__Namespace runtime setting. The Bicep workload-identity subjects use the same value, and the staged script reads the recorded infrastructure namespace for secret lookup and rendering, rejecting conflicting overrides. The embedded deployment now includes the matching checkout manifest at Bicep compilation time.

The 11 offline deployment scenarios and ARM consistency checks pass, including non-default, numeric, and boolean-like namespace names.

Comment thread dotnet/Microsoft.McpGateway.Service/src/HttpProxy.cs
Comment on lines +53 to +57
var method = context.Request.Headers["Mcp-Method"];
if (method.Count != 1 || string.IsNullOrWhiteSpace(method[0]))
{
await RejectAsync(context, -32020, "A single Mcp-Method header is required.").ConfigureAwait(false);
return;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 2a7a705.

Added a shared McpProtocolMiddleware at the public gateway and Tools boundaries. It validates a single JSON-RPC 2.0 body, method/header agreement, required params._meta protocol version and capabilities object, and decoded Mcp-Name agreement with the method's name or URI. The request stream is rewound before forwarding, with regression coverage for unknown-length/chunked bodies. The guard also requires both response media types in Accept and a JSON content type.

The mismatch, metadata, encoding, and body-preservation tests passed as part of the full .NET Release suite: 327 passed, eight existing platform skips.

Comment on lines +53 to +60
var method = context.Request.Headers["Mcp-Method"];
if (method.Count != 1 || string.IsNullOrWhiteSpace(method[0]))
{
await RejectAsync(context, -32020, "A single Mcp-Method header is required.").ConfigureAwait(false);
return;
}

if (method[0] is "tools/call" or "resources/read" or "prompts/get")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 2a7a705.

The shared protocol guard now explicitly rejects initialize and notifications/initialized, including when the version header claims MCP 2026-07-28. Body/header agreement is checked as well, so a different header method cannot conceal a legacy handshake in the body.

Regression tests cover both public MCP routes and the guarded SDK HTTP transport; they passed.

Comment on lines 76 to +87
@@ -81,7 +84,7 @@
var toolExecutor = c.Services!.GetRequiredService<IToolExecutor>();
return toolExecutor?.ExecuteToolAsync(c, ct) ?? throw new InvalidOperationException("Tool executor not properly registered.");
})
.WithHttpTransport();
.WithHttpTransport(options => options.Stateless = true);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 2a7a705.

Tools now runs the same explicit modern-only protocol middleware before MapMcp, in addition to the SDK protocol-version and stateless settings. This rejects missing/down-level versions, removed handshake methods, and invalid body/header agreement before the SDK transport handles the request.

Eight live loopback tests using the middleware with the real SDK HTTP transport passed, including modern discovery and rejection of legacy versions, handshakes, GET, and DELETE.

from urllib.parse import urlparse

import httpx
import httpx2

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 2a7a705.

Kept the intentional httpx2 import and explicitly pinned the real distribution as httpx2==2.12.0 in the runtime requirements. FastMCP 4.0.3 delegates to fastmcp-slim 4.0.3, whose published client metadata requires httpx2>=2.5.0, so retaining that client package keeps the factory aligned with FastMCP.

Clean-install/test verification remains blocked locally: the configured package mirror lacks FastMCP 4.0.3, and the public PyPI wheel host failed TLS handshakes. No Python test pass is claimed; the existing proxy CI job installs the checked-in development requirements and runs the tests.

from pathlib import Path

import httpx
import httpx2

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 2a7a705.

The test and runtime imports remain consistently on httpx2, now explicitly pinned to 2.12.0 in the runtime requirements. The development requirements include that file, so the test environment receives the same declared dependency.

Local clean-install/test validation is still blocked by the mirror missing FastMCP 4.0.3 and TLS failures downloading from public PyPI. This is documented rather than reported as a test pass.

Share modern-only protocol validation, isolate forwarded credentials, validate mirrored schema headers, and declare the HTTP client dependency. Propagate deployment namespaces and safely initialize or reuse gateway secrets with regression coverage.
@llxu1

Copy link
Copy Markdown
Contributor Author

Review fixes pushed in 2a7a705.

Addressed all eight inline review comments, plus the three additional findings in the review summary:

  • Reuse the infrastructure namespace for staged secret lookup and manifest rendering.
  • Enforce both required Accept media types at the gateway and Tools boundaries.
  • Reject mirrored-header annotations outside unambiguous primitive object properties, including annotations in schema compositions, referenced definitions, and array items. The 39 schema-header tests pass.

Validation:

  • Full .NET Release suite: 327 passed, eight existing Windows/platform skips. Includes eight live loopback SDK transport tests.
  • 11 offline deployment scenarios, ARM consistency checks, embedded Bash syntax, and local Bicep compilation passed.
  • Diff whitespace checks passed.
  • Python clean-install/tests remain blocked by package-mirror availability and public PyPI TLS download failures; no Python test pass is claimed.

No Azure resources were deployed or changed for this follow-up. Deployment scenarios were mocked, not live cloud validation.

`McpProtocol.DecodeHeader` threw `ArgumentOutOfRangeException` for the exact
value `=?base64?=`. At 10 characters it satisfies both the `=?base64?` prefix
and `?=` suffix tests with the delimiters overlapping, so `value[9..^2]`
resolved to a negative length. The catch filter only covered `FormatException`
and `DecoderFallbackException`, so the exception escaped unhandled and the
gateway answered `500` instead of `400`.

`McpEndpointMiddleware` runs before `UseAuthentication`, so an unauthenticated
caller could trigger it with a single `Mcp-Name` header;
`McpToolHeaderValidator` reached the same path via `Mcp-Param-*`.

Guard the payload slice so an overlapping sentinel is rejected as a malformed
header, consistent with other invalid sentinels, and name the delimiters so the
length invariant is explicit. The empty payload `=?base64??=` still decodes to
an empty string.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
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