Skip to content

test(darling): live HTTP proof of the MCP host's gates, including /core (#4128b) - #4142

Merged
erikdarlingdata merged 3 commits into
devfrom
test/4128-mcp-host-gates
Sep 24, 2026
Merged

erikdarlingdata merged 3 commits into
devfrom
test/4128-mcp-host-gates

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Closes #4128. Part (a), the web host, merged in #4132; this PR does part (b), the MCP host, and finishes the issue.

Part (b) of #4128: a live-HTTP proof of the MCP host's gates, including /core, modeled on merged PR #4132 (the web host's part (a)).

What changed

DarlingMcpHostService.TryStartServerAsync had two seams extracted, so a test builds the SAME server production wires instead of a hand-copied one that could silently drift:

  1. The services seam — internal static void ConfigureMcpServices(IServiceCollection services, DarlingPeerDirectory.Snapshot declaredPeers). Everything from builder.Services.AddMcpServer(...) through the .WithRequestFilters(...) call (every tool class registration, the stateless transport, /core's ConfigureSessionOptions narrowing) moved in, unchanged except the receiver (builder.Services there vs. the services parameter here).
  2. The pipeline seam — internal void ConfigurePipeline(WebApplication app, bool networkMode, IPAddress? networkListenIp, IPNetwork allowedCidr, string bearerToken). Everything after builder.Build() through MapMcp("/core") moved in: the Host-header/DNS-rebinding guard (both modes), the network-mode bearer-token + CIDR middleware, and both MapMcp() calls. Renamed _app.Use/_app.MapMcp to app.Use/app.MapMcp inside the method, mirroring the rename test(darling): live HTTP proof of the web host's gates (#4128a) #4132 made for the web host's ConfigurePipeline.

Production (TryStartServerAsync) calls both in the same order with the same values it always built inline; every moved line is textually identical to before the extraction.

Darling/Darling.Tests/DarlingMcpHostGateLiveTests.cs is new: builds a TestServer running the real ConfigureMcpServices + ConfigurePipeline, with a dead NpgsqlDataSource (never opened — every case here is refused, or reaches tools/list, before any tool body runs) and a null plan fetcher. Cases:

  • network mode, no token: refused (401) on both / and /core
  • a foreign Host header: refused (400) on both paths, in both modes
  • the right token from an in-CIDR remote: tools/list on / succeeds (200)
  • loopback mode, no token: tools/list on / succeeds (no token middleware installed at all in this mode)
  • /core tools/list: exactly DarlingCoreToolProfile.Closure's pinned tool-name set, with the instructions containing the /core note
  • a tools/call on /core for a tool outside that closure (delete_custom_view): the SDK's own "Unknown tool" error, proving /core is a real subset (no fallback CallToolHandler is registered), not a listing filter

MCP is spoken as plain JSON-RPC HTTP POSTs over TestServer.SendAsync (not CreateClient, since the CIDR gate reads context.Connection.RemoteIpAddress, which CreateClient's in-memory context leaves null) with a caller-owned response MemoryStream (TestServer's own response body stream doesn't support Seek).

HostHeaderGuardTests updates

  • The ReadHostSource anchor comment and every _app.MapMcp(...) literal pin for DarlingMcpHostService.cs are updated to app.MapMcp(...), matching the parameter rename.
  • McpHost_HasNoPathConditionalMiddlewareBeforeTheMcpMappings is rescoped to ConfigurePipeline's own method body (found by internal void ConfigurePipeline( first) rather than a whole-file text slice from builder.Build() to MapMcp("/core"). Reason: ConfigureMcpServices — now textually BETWEEN TryStartServerAsync's builder.Build() and ConfigurePipeline's MapMcp("/core") — contains its own unrelated context.Request.Path check inside ConfigureSessionOptions (the /core tool-set narrowing), which a whole-file slice would wrongly flag as a path-conditional gate that could skip /core. Scoping to ConfigurePipeline's body is the accurate claim: that method is the real, in-order request pipeline.
  • EachHost_RunsTheGuardInBothModes_NotOnlyWhenNetworkExposed already searched from the guard's own position (an existing test(darling): live HTTP proof of the web host's gates (#4128a) #4132-era accommodation) and needed no further change for the MCP host.

What I ran

  • dotnet build on PerformanceMonitor.Darling.Service.csproj and Darling.Tests.csproj with -p:EnableWindowsTargeting=true: both succeed, 0 warnings on the service project, 0 errors on Tests (after fixing a HashSet<string?> nullability warning).
  • dotnet Darling.Tests.dll -class Darling.Tests.DarlingMcpHostGateLiveTests on the Mac test rig (Windows-target stripped from the runtimeconfig): reached 10 test cases; iterated on failures (the CIDR gate needs an explicit RemoteIpAddress since CreateClient's context leaves it null; TestServer's response body stream doesn't support Seek) and was mid-fix on the Seek issue (switching to a caller-owned MemoryStream) when I hit the context wall.

What I did NOT verify

  • The new test class's final green run. The last edit in flight (routing the response body through a caller-owned MemoryStream instead of ctx.Response.Body.Seek) is committed and builds clean, but I have not re-run DarlingMcpHostGateLiveTests to confirm all 10 cases pass with that fix.
  • HostHeaderGuardTests — not re-run after the literal-pin edits (should still pass; the changes are purely text-anchor updates matching the actual renamed source).
  • The rest of the classes touching DarlingMcpHostService, and the FULL Darling suite — not run at all. The brief's step 5 (run the new class, HostHeaderGuardTests, every class touching DarlingMcpHostService, then the full suite once with DARLING_TEST_PG unset) is undone.

Next step for whoever picks this up: run DarlingMcpHostGateLiveTests and HostHeaderGuardTests first; if the Seek fix holds, run the full suite once and diff failures against dev's known 214 Windows-only baseline before readying this PR.

Coordinator verification (branch head 7ca8bb7)

  • Behaviour-preserving, checked mechanically. Every line removed from TryStartServerAsync reappears in ConfigureMcpServices or ConfigurePipeline, identical except the receiver (builder.Services → services, _app → app). The only other additions are the two signatures, their doc comments and the two call sites.
  • The lane stopped at its budget before re-running its last fix. My run found 7 of 10 failing, and I fixed them:
    • TestServer's response stream can't seek, so it is now read as it stands (6 cases);
    • /core's instructions travel in the initialize result, not tools/list. The case now initializes on both paths and checks that only /core leads with DarlingCoreToolProfile.CoreNote;
    • one cref to TestServer.CreateClient is now prose (DocCommentHygieneTests).
  • DarlingMcpHostGateLiveTests passes (10/10), HostHeaderGuardTests passes (46/46), and DocCommentHygieneTests passes (77/77). The full Darling suite on macOS equals dev's baseline once the cref fix is in: the only extra failures in the full run were those two DocCommentHygiene tests.
  • No security review round: the gate code moved line for line, and this PR adds tests of it (as test(darling): live HTTP proof of the web host's gates (#4128a) #4132 did).

CHANGELOG entry

  • Added a live-HTTP test proving the MCP host's Host-header, bearer-token, and CIDR gates actually refuse and admit requests as documented, including /core's narrowed tool set — by extracting the host's service registration and pipeline wiring into two reusable methods (ConfigureMcpServices, ConfigurePipeline) that a test now drives directly.

Part of #4128.

@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 24, 2026 06:13
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 24, 2026 06:13
@erikdarlingdata
erikdarlingdata merged commit 10a6da3 into dev Sep 24, 2026
18 of 20 checks passed
@erikdarlingdata
erikdarlingdata deleted the test/4128-mcp-host-gates branch September 24, 2026 06:22
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.

1 participant