Adopt MCP 2026-07-28 with a modern-only gateway - #104
Lili Xu (llxu1) wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
🟡 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-secretspecifically inadapter. 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
Acceptas required and the migration guide requires bothapplication/jsonandtext/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
propertiessilently skips everyx-mcp-headernested underanyOf/oneOf/allOf,$ref, or arrayitems. The portal rejects such annotations, but a non-portal client can register and call that definition without the requiredMcp-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.
| @secure() | ||
| @description('Shared first-party identity-forwarding secret; never supplied to adapter pods.') | ||
| param gatewaySecret string = '' |
There was a problem hiding this comment.
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.
| serviceAccountName: mcpgateway-sa | ||
| containers: | ||
| - name: mcpgateway-container | ||
| image: ghcr.io/microsoft/mcp-gateway:latest | ||
| image: ${GATEWAY_IMAGE} | ||
| ports: |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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.
| 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") |
There was a problem hiding this comment.
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.
| @@ -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); | |||
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
|
Review fixes pushed in 2a7a705. Addressed all eight inline review comments, plus the three additional findings in the review summary:
Validation:
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>
Summary
Adopt MCP
2026-07-28with one modern transport path, reducing the compatibility and routing complexity discussed in #98.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/sessionsare unchanged.See the migration guide.
Validation
Final branch rebased onto current
main:7 + 8 = 15at 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
nanoidadvisory also present onmain; this PR does not introduce or resolve it. The portal build also retains its bundle-size advisory.Refs #98.