Skip to content

Commit dcdd00b

Browse files
halter73Copilot
andcommitted
Remove MRTR Core tests redundant with MapMcpTests theory rows
Phase 1 of this restoration brought back three Core MRTR happy-path tests that duplicate scenarios already covered by `MapMcpTests.Mrtr` theory rows (across StreamableHttp / SSE / Stateless transports with both `experimentalClient: true` and `experimentalClient: false`): * `CallToolAsync_BothExperimental_ElicitCompletesViaMrtr` → covered by `Mrtr_MultiRoundTrip_Completes(experimentalClient: true)` * `CallToolAsync_ConcurrentElicitAndSample_PropagatesError` → covered by `Mrtr_ParallelAwaits(experimentalClient: true)` * `CallToolAsync_ElicitThenIncompleteResultException_WorksEndToEnd` → covered by `Mrtr_MixedExceptionAndAwaitStyle(experimentalClient: true)` The MapMcpTests versions assert the same MrtrContext gate ("Concurrent server-to-client requests are not supported"), the same MrtrUsed message tracker assertions, and run against multiple transports. The Core stdio mirrors added no transport-independent coverage. Also removed two now-unused tool definitions in the test fixture: `concurrent-tool`, `incomplete-result-tool`, `elicit-then-incomplete-result-tool`. Retained the rest of MrtrIntegrationTests: * `ClientHandlerException_DuringMrtrInputResolution_SurfacesToCaller` (draft client retry-loop error propagation — not covered by the legacy `Mrtr_Backcompat_ClientHandlerThrows_PropagatesError` which exercises a different code path) * `SendMessageAsync_WithJsonRpcRequest_ThrowsAlways` (client API contract) * `LegacyRequestOnMrtrSession_LogsWarning` (fake-stream protocol-compliance assertion) * `IncompleteResultOnNonMrtrSession_LogsWarning` (fake-stream protocol-compliance assertion) * `IncompleteResultRetry_OmittingRequestState_StripsStaleStateFromRetryParams` (regression test) The server-side lifecycle tests (MrtrHandlerLifecycleTests), message filter tests (MrtrMessageFilterTests), and per-session limit pattern tests (MrtrSessionLimitTests) are intentionally kept — they exercise the internal MRTR machinery (continuation cancellation, handler drain, outgoing filter visibility) which is not naturally testable at the HTTP transport layer. Test counts: Core 2024 → 2021 (-3); AspNetCore unchanged at 435. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ef6d853 commit dcdd00b

1 file changed

Lines changed: 1 addition & 214 deletions

File tree

tests/ModelContextProtocol.Tests/Client/MrtrIntegrationTests.cs

Lines changed: 1 addition & 214 deletions
Original file line numberDiff line numberDiff line change
@@ -52,98 +52,10 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
5252
Name = "elicitation-tool",
5353
Description = "A tool that requests elicitation from the client"
5454
}),
55-
McpServerTool.Create(
56-
async (McpServer server, CancellationToken ct) =>
57-
{
58-
// Attempt concurrent ElicitAsync + SampleAsync ΓÇö MrtrContext prevents this.
59-
var t1 = server.ElicitAsync(new ElicitRequestParams
60-
{
61-
Message = "Concurrent elicit",
62-
RequestedSchema = new()
63-
}, ct).AsTask();
64-
65-
var t2 = server.SampleAsync(new CreateMessageRequestParams
66-
{
67-
Messages = [new SamplingMessage { Role = Role.User, Content = [new TextContentBlock { Text = "Concurrent sample" }] }],
68-
MaxTokens = 100
69-
}, ct).AsTask();
70-
71-
await Task.WhenAll(t1, t2);
72-
return "done";
73-
},
74-
new McpServerToolCreateOptions
75-
{
76-
Name = "concurrent-tool",
77-
Description = "A tool that attempts concurrent elicitation and sampling"
78-
}),
79-
McpServerTool.Create(
80-
(McpServer server) =>
81-
{
82-
// Low-level MRTR: throw InputRequiredException directly instead of using ElicitAsync.
83-
// This should NOT be logged at Error level ΓÇö it's normal MRTR control flow.
84-
throw new InputRequiredException(new InputRequiredResult
85-
{
86-
InputRequests = new Dictionary<string, InputRequest>
87-
{
88-
["input_1"] = InputRequest.ForElicitation(new ElicitRequestParams
89-
{
90-
Message = "low-level elicit",
91-
RequestedSchema = new()
92-
})
93-
}
94-
});
95-
},
96-
new McpServerToolCreateOptions
97-
{
98-
Name = "incomplete-result-tool",
99-
Description = "A tool that throws InputRequiredException for low-level MRTR"
100-
}),
101-
McpServerTool.Create(
102-
async (McpServer server, RequestContext<CallToolRequestParams> context, CancellationToken ct) =>
103-
{
104-
var requestState = context.Params!.RequestState;
105-
var inputResponses = context.Params!.InputResponses;
106-
107-
// Final round: we have the requestState from the InputRequiredException
108-
if (requestState == "got-name" && inputResponses is not null
109-
&& inputResponses.TryGetValue("age", out var ageResponse))
110-
{
111-
var age = ageResponse.Deserialize(InputResponse.ElicitResultJsonTypeInfo)?.Content?.FirstOrDefault().Value;
112-
// Decode the name from requestState ΓÇö in a real scenario, requestState
113-
// would carry the accumulated state, but here we just verify the flow works.
114-
return $"age={age}";
115-
}
116-
117-
// First round: use high-level ElicitAsync (handler suspends)
118-
var nameResult = await server.ElicitAsync(new ElicitRequestParams
119-
{
120-
Message = "What is your name?",
121-
RequestedSchema = new()
122-
}, ct);
123-
124-
var name = nameResult.Content?.FirstOrDefault().Value;
125-
126-
// Second round: switch to low-level InputRequiredException (handler dies)
127-
throw new InputRequiredException(
128-
inputRequests: new Dictionary<string, InputRequest>
129-
{
130-
["age"] = InputRequest.ForElicitation(new ElicitRequestParams
131-
{
132-
Message = $"How old are you, {name}?",
133-
RequestedSchema = new()
134-
})
135-
},
136-
requestState: "got-name");
137-
},
138-
new McpServerToolCreateOptions
139-
{
140-
Name = "elicit-then-incomplete-result-tool",
141-
Description = "A tool that uses high-level ElicitAsync then throws InputRequiredException"
142-
}),
14355
McpServerTool.Create(
14456
async (McpServer server) =>
14557
{
146-
// Attempt to send a JsonRpcRequest via SendMessageAsync ΓÇö should always throw
58+
// Attempt to send a JsonRpcRequest via SendMessageAsync should always throw
14759
// since requests must go through SendRequestAsync for response correlation.
14860
try
14961
{
@@ -172,131 +84,6 @@ await server.SendMessageAsync(new JsonRpcRequest
17284
]);
17385
}
17486

175-
[Fact]
176-
public async Task CallToolAsync_BothExperimental_ElicitCompletesViaMrtr()
177-
{
178-
// Simplest MRTR success: experimental server + experimental client, one elicitation round.
179-
StartServer();
180-
var clientOptions = new McpClientOptions { ProtocolVersion = "DRAFT-2026-v1" };
181-
clientOptions.Handlers.ElicitationHandler = (request, ct) =>
182-
new ValueTask<ElicitResult>(new ElicitResult
183-
{
184-
Action = "accept",
185-
Content = new Dictionary<string, JsonElement>
186-
{
187-
["name"] = JsonSerializer.SerializeToElement("Alice", McpJsonUtilities.DefaultOptions)
188-
}
189-
});
190-
191-
await using var client = await CreateMcpClientForServer(clientOptions);
192-
Assert.Equal("DRAFT-2026-v1", client.NegotiatedProtocolVersion);
193-
194-
var result = await client.CallToolAsync("elicitation-tool",
195-
new Dictionary<string, object?> { ["message"] = "What is your name?" },
196-
cancellationToken: TestContext.Current.CancellationToken);
197-
198-
var text = Assert.IsType<TextContentBlock>(Assert.Single(result.Content)).Text;
199-
Assert.Equal("accept:Alice", text);
200-
Assert.True(result.IsError is not true);
201-
_messageTracker.AssertMrtrUsed();
202-
}
203-
204-
[Fact]
205-
public async Task CallToolAsync_ConcurrentElicitAndSample_PropagatesError()
206-
{
207-
// MrtrContext only allows one pending request at a time. When a tool handler
208-
// calls ElicitAsync and SampleAsync concurrently via Task.WhenAll, the second
209-
// call sees the TCS already completed and throws InvalidOperationException.
210-
// That exception is caught by the tool error handler and returned as IsError.
211-
StartServer();
212-
var clientOptions = new McpClientOptions { ProtocolVersion = "DRAFT-2026-v1" };
213-
214-
// The first concurrent call (ElicitAsync) produces an InputRequiredResult.
215-
// The client resolves it via this handler, which unblocks the first task.
216-
// Then Task.WhenAll surfaces the InvalidOperationException from the second task.
217-
clientOptions.Handlers.ElicitationHandler = (request, ct) =>
218-
{
219-
return new ValueTask<ElicitResult>(new ElicitResult { Action = "accept" });
220-
};
221-
clientOptions.Handlers.SamplingHandler = (request, progress, ct) =>
222-
{
223-
return new ValueTask<CreateMessageResult>(new CreateMessageResult
224-
{
225-
Content = [new TextContentBlock { Text = "sampled" }],
226-
Model = "test-model"
227-
});
228-
};
229-
230-
await using var client = await CreateMcpClientForServer(clientOptions);
231-
232-
var result = await client.CallToolAsync("concurrent-tool",
233-
cancellationToken: TestContext.Current.CancellationToken);
234-
235-
Assert.True(result.IsError);
236-
var errorText = Assert.IsType<TextContentBlock>(Assert.Single(result.Content)).Text;
237-
Assert.Contains("concurrent-tool", errorText);
238-
_messageTracker.AssertMrtrUsed();
239-
}
240-
241-
[Fact]
242-
public async Task CallToolAsync_ElicitThenIncompleteResultException_WorksEndToEnd()
243-
{
244-
// Verify that a handler can mix high-level MRTR (ElicitAsync) with low-level MRTR
245-
// (InputRequiredException) in a single logical flow. The handler:
246-
// 1. Calls ElicitAsync (high-level: handler suspends, InputRequiredResult returned)
247-
// 2. Gets the response, then throws InputRequiredException (low-level: handler dies)
248-
// 3. On the next retry, a fresh handler invocation processes requestState + inputResponses
249-
StartServer();
250-
int elicitationCallCount = 0;
251-
252-
var clientOptions = new McpClientOptions { ProtocolVersion = "DRAFT-2026-v1" };
253-
clientOptions.Handlers.ElicitationHandler = (request, ct) =>
254-
{
255-
elicitationCallCount++;
256-
if (request?.Message == "What is your name?")
257-
{
258-
return new ValueTask<ElicitResult>(new ElicitResult
259-
{
260-
Action = "accept",
261-
Content = new Dictionary<string, JsonElement>
262-
{
263-
["name"] = JsonDocument.Parse("\"Alice\"").RootElement.Clone()
264-
}
265-
});
266-
}
267-
268-
// Second elicitation from the InputRequiredException path
269-
return new ValueTask<ElicitResult>(new ElicitResult
270-
{
271-
Action = "accept",
272-
Content = new Dictionary<string, JsonElement>
273-
{
274-
["age"] = JsonDocument.Parse("\"30\"").RootElement.Clone()
275-
}
276-
});
277-
};
278-
279-
await using var client = await CreateMcpClientForServer(clientOptions);
280-
281-
var result = await client.CallToolAsync(
282-
"elicit-then-incomplete-result-tool",
283-
cancellationToken: TestContext.Current.CancellationToken);
284-
285-
// Verify the final result came through correctly
286-
var content = Assert.Single(result.Content);
287-
Assert.Equal("age=30", Assert.IsType<TextContentBlock>(content).Text);
288-
Assert.NotEqual(true, result.IsError);
289-
290-
// Two elicitations: one from ElicitAsync, one from InputRequiredException's inputRequests
291-
Assert.Equal(2, elicitationCallCount);
292-
293-
// Verify no error-level logs for InputRequiredException
294-
Assert.DoesNotContain(MockLoggerProvider.LogMessages, m =>
295-
m.LogLevel == LogLevel.Error &&
296-
m.Exception is InputRequiredException);
297-
_messageTracker.AssertMrtrUsed();
298-
}
299-
30087
[Fact]
30188
public async Task ClientHandlerException_DuringMrtrInputResolution_SurfacesToCaller()
30289
{

0 commit comments

Comments
 (0)