Fix missing resultType on complete result responses (#1684)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com>
This commit is contained in:
Pranav Senthilnathan 2026-07-12 21:24:21 -04:00 • committed by GitHub
commit 374e76eecd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 118 additions and 4 deletions

View file

@ -9,5 +9,5 @@ namespace ModelContextProtocol.Protocol;
public sealed class EmptyResult : Result public sealed class EmptyResult : Result
{ {
[JsonIgnore] [JsonIgnore]
internal static EmptyResult Instance { get; } = new(); internal static EmptyResult Instance { get; } = new() { ResultType = "complete" };
} }

View file

@ -27,7 +27,7 @@ public abstract class Result
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para> /// <para>
/// When absent or set to <c>"complete"</c>, the result is a normal completed response. /// When set to <c>"complete"</c>, the result is a normal completed response.
/// When set to <c>"input_required"</c>, the result is an <see cref="InputRequiredResult"/> indicating /// When set to <c>"input_required"</c>, the result is an <see cref="InputRequiredResult"/> indicating
/// that additional input is needed before the request can be completed. /// that additional input is needed before the request can be completed.
/// When set to <c>"task"</c>, the result is a <see cref="CreateTaskResult"/> indicating that the server /// When set to <c>"task"</c>, the result is a <see cref="CreateTaskResult"/> indicating that the server

View file

@ -621,6 +621,7 @@ internal sealed partial class McpServerImpl : McpServer
Instructions = options.ServerInstructions, Instructions = options.ServerInstructions,
ServerInfo = options.ServerInfo ?? DefaultImplementation, ServerInfo = options.ServerInfo ?? DefaultImplementation,
Capabilities = ServerCapabilities ?? new(), Capabilities = ServerCapabilities ?? new(),
ResultType = "complete",
}; };
}, },
McpJsonUtilities.JsonContext.Default.InitializeRequestParams, McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
@ -651,6 +652,7 @@ internal sealed partial class McpServerImpl : McpServer
// their "do not cache" behavior while satisfying the wire requirement. // their "do not cache" behavior while satisfying the wire requirement.
TimeToLive = TimeSpan.Zero, TimeToLive = TimeSpan.Zero,
CacheScope = CacheScope.Private, CacheScope = CacheScope.Private,
ResultType = "complete",
}); });
}, },
McpJsonUtilities.JsonContext.Default.DiscoverRequestParams, McpJsonUtilities.JsonContext.Default.DiscoverRequestParams,
@ -703,7 +705,7 @@ internal sealed partial class McpServerImpl : McpServer
await SendSubscriptionAckAsync(statelessSubscription, cancellationToken).ConfigureAwait(false); await SendSubscriptionAckAsync(statelessSubscription, cancellationToken).ConfigureAwait(false);
return new EmptyResult(); return EmptyResult.Instance;
} }
// Filter the requested notifications against what the server actually supports. // Filter the requested notifications against what the server actually supports.
@ -743,7 +745,7 @@ internal sealed partial class McpServerImpl : McpServer
_activeSubscriptions.TryRemove(jsonRpcRequest.Id, out _); _activeSubscriptions.TryRemove(jsonRpcRequest.Id, out _);
} }
return new EmptyResult(); return EmptyResult.Instance;
}, },
McpJsonUtilities.JsonContext.Default.SubscriptionsListenRequestParams, McpJsonUtilities.JsonContext.Default.SubscriptionsListenRequestParams,
McpJsonUtilities.JsonContext.Default.EmptyResult); McpJsonUtilities.JsonContext.Default.EmptyResult);
@ -1916,6 +1918,21 @@ internal sealed partial class McpServerImpl : McpServer
}; };
} }
if (typeof(Result).IsAssignableFrom(typeof(TResult)))
{
var innerHandler = handler;
handler = async (request, cancellationToken) =>
{
var result = await innerHandler(request, cancellationToken).ConfigureAwait(false);
if (result is Result protocolResult && protocolResult.ResultType is null)
{
protocolResult.ResultType = "complete";
}
return result;
};
}
_requestHandlers.Set(method, _requestHandlers.Set(method,
(request, jsonRpcRequest, cancellationToken) => (request, jsonRpcRequest, cancellationToken) =>
InvokeHandlerAsync(handler, request, jsonRpcRequest, cancellationToken), InvokeHandlerAsync(handler, request, jsonRpcRequest, cancellationToken),
@ -1930,6 +1947,25 @@ internal sealed partial class McpServerImpl : McpServer
JsonTypeInfo<CreateTaskResult> taskResultTypeInfo) JsonTypeInfo<CreateTaskResult> taskResultTypeInfo)
where TResult : Result where TResult : Result
{ {
var innerHandler = handler;
handler = async (request, cancellationToken) =>
{
var result = await innerHandler(request, cancellationToken).ConfigureAwait(false);
if (result.IsTask)
{
if (result.TaskCreated is { ResultType: null } taskCreated)
{
taskCreated.ResultType = "task";
}
}
else if (result.Result is { ResultType: null } immediateResult)
{
immediateResult.ResultType = "complete";
}
return result;
};
_requestHandlers.SetTaskAugmented(method, _requestHandlers.SetTaskAugmented(method,
(request, jsonRpcRequest, cancellationToken) => (request, jsonRpcRequest, cancellationToken) =>
InvokeHandlerAsync(handler, request, jsonRpcRequest, cancellationToken), InvokeHandlerAsync(handler, request, jsonRpcRequest, cancellationToken),

View file

@ -83,6 +83,7 @@ public class July2026ProtocolConnectionTests : ClientServerTestBase
var discoverResult = JsonSerializer.Deserialize<DiscoverResult>(response.Result, McpJsonUtilities.DefaultOptions); var discoverResult = JsonSerializer.Deserialize<DiscoverResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(discoverResult); Assert.NotNull(discoverResult);
Assert.Equal("complete", discoverResult.ResultType);
Assert.Equal([McpProtocolVersions.July2026ProtocolVersion], discoverResult.SupportedVersions); Assert.Equal([McpProtocolVersions.July2026ProtocolVersion], discoverResult.SupportedVersions);
} }
} }

View file

@ -58,6 +58,21 @@ public class McpServerTaskTests : ClientServerTestBase
}; };
} }
if (toolName == "async-tool-default-resulttype")
{
// Intentionally leaves ResultType unset so the server boundary is responsible for
// filling in "task" on the wire.
var taskId = store.CreateTask();
return new CreateTaskResult
{
TaskId = taskId,
Status = McpTaskStatus.Working,
CreatedAt = DateTimeOffset.UtcNow,
LastUpdatedAt = DateTimeOffset.UtcNow,
PollIntervalMs = 50,
};
}
if (toolName == "input-required-tool") if (toolName == "input-required-tool")
{ {
var taskId = store.CreateTask(McpTaskStatus.InputRequired); var taskId = store.CreateTask(McpTaskStatus.InputRequired);
@ -292,6 +307,19 @@ public class McpServerTaskTests : ClientServerTestBase
Assert.Equal("task", augmented.TaskCreated!.ResultType); Assert.Equal("task", augmented.TaskCreated!.ResultType);
} }
[Fact]
public async Task CreateTaskResult_WithoutExplicitResultType_ServerFillsTask()
{
await using var client = await CreateMcpClientForServer();
var augmented = await client.CallToolRawAsync(
new CallToolRequestParams { Name = "async-tool-default-resulttype" },
TestContext.Current.CancellationToken);
Assert.True(augmented.IsTask);
Assert.Equal("task", augmented.TaskCreated!.ResultType);
}
[Fact] [Fact]
public async Task GetTaskAsync_ImmediatelyAfterCreate_Resolves() public async Task GetTaskAsync_ImmediatelyAfterCreate_Resolves()
{ {

View file

@ -283,6 +283,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.Equal(expectedAssemblyName.Name, result.ServerInfo.Name); Assert.Equal(expectedAssemblyName.Name, result.ServerInfo.Name);
Assert.Equal(expectedAssemblyName.Version?.ToString() ?? "1.0.0", result.ServerInfo.Version); Assert.Equal(expectedAssemblyName.Version?.ToString() ?? "1.0.0", result.ServerInfo.Version);
Assert.Equal("2024-11-05", result.ProtocolVersion); Assert.Equal("2024-11-05", result.ProtocolVersion);
@ -385,6 +386,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.NotNull(result.Capabilities.Extensions); Assert.NotNull(result.Capabilities.Extensions);
Assert.True(result.Capabilities.Extensions.ContainsKey("io.myext")); Assert.True(result.Capabilities.Extensions.ContainsKey("io.myext"));
}); });
@ -404,6 +406,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.NotNull(result.Capabilities.Experimental); Assert.NotNull(result.Capabilities.Experimental);
Assert.True(result.Capabilities.Experimental.ContainsKey("customFeature")); Assert.True(result.Capabilities.Experimental.ContainsKey("customFeature"));
}); });
@ -435,6 +438,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
// Use reflection to verify every public property on ServerCapabilities is non-null. // Use reflection to verify every public property on ServerCapabilities is non-null.
// This catches cases where new capability properties are added but not copied // This catches cases where new capability properties are added but not copied
@ -481,6 +485,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<CompleteResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<CompleteResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion); Assert.NotNull(result?.Completion);
Assert.Equal("complete", result.ResultType);
Assert.Equal(["test"], result.Completion.Values); Assert.Equal(["test"], result.Completion.Values);
Assert.Equal(2, result.Completion.Total); Assert.Equal(2, result.Completion.Total);
Assert.True(result.Completion.HasMore); Assert.True(result.Completion.HasMore);
@ -524,6 +529,7 @@ public class McpServerTests : LoggedTest
Assert.NotNull(response); Assert.NotNull(response);
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion); Assert.NotNull(result?.Completion);
Assert.Equal("complete", result.ResultType);
Assert.Equal(["cat"], result.Completion.Values); Assert.Equal(["cat"], result.Completion.Values);
Assert.Equal(1, result.Completion.Total); Assert.Equal(1, result.Completion.Total);
@ -567,6 +573,7 @@ public class McpServerTests : LoggedTest
Assert.NotNull(response); Assert.NotNull(response);
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion); Assert.NotNull(result?.Completion);
Assert.Equal("complete", result.ResultType);
Assert.Empty(result.Completion.Values); Assert.Empty(result.Completion.Values);
await transport.DisposeAsync(); await transport.DisposeAsync();
@ -616,6 +623,7 @@ public class McpServerTests : LoggedTest
Assert.NotNull(response); Assert.NotNull(response);
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion); Assert.NotNull(result?.Completion);
Assert.Equal("complete", result.ResultType);
Assert.Equal(["us-east-1", "us-west-2"], result.Completion.Values); Assert.Equal(["us-east-1", "us-west-2"], result.Completion.Values);
Assert.Equal(2, result.Completion.Total); Assert.Equal(2, result.Completion.Total);
@ -671,6 +679,7 @@ public class McpServerTests : LoggedTest
Assert.NotNull(response); Assert.NotNull(response);
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion); Assert.NotNull(result?.Completion);
Assert.Equal("complete", result.ResultType);
// Custom handler values + auto-populated values should be combined // Custom handler values + auto-populated values should be combined
Assert.Equal(["custom-value", "dog", "cat"], result.Completion.Values); Assert.Equal(["custom-value", "dog", "cat"], result.Completion.Values);
Assert.Equal(3, result.Completion.Total); Assert.Equal(3, result.Completion.Total);
@ -718,6 +727,7 @@ public class McpServerTests : LoggedTest
Assert.NotNull(response); Assert.NotNull(response);
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion); Assert.NotNull(result?.Completion);
Assert.Equal("complete", result.ResultType);
Assert.Equal(["a", "b"], result.Completion.Values); Assert.Equal(["a", "b"], result.Completion.Values);
await transport.DisposeAsync(); await transport.DisposeAsync();
@ -756,6 +766,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.ResourceTemplates); Assert.NotNull(result?.ResourceTemplates);
Assert.Equal("complete", result.ResultType);
Assert.NotEmpty(result.ResourceTemplates); Assert.NotEmpty(result.ResourceTemplates);
Assert.Equal("test", result.ResourceTemplates[0].UriTemplate); Assert.Equal("test", result.ResourceTemplates[0].UriTemplate);
}); });
@ -785,6 +796,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<ListResourcesResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<ListResourcesResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Resources); Assert.NotNull(result?.Resources);
Assert.Equal("complete", result.ResultType);
Assert.NotEmpty(result.Resources); Assert.NotEmpty(result.Resources);
Assert.Equal("test", result.Resources[0].Uri); Assert.Equal("test", result.Resources[0].Uri);
}); });
@ -820,6 +832,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<ReadResourceResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<ReadResourceResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Contents); Assert.NotNull(result?.Contents);
Assert.Equal("complete", result.ResultType);
Assert.NotEmpty(result.Contents); Assert.NotEmpty(result.Contents);
TextResourceContents textResource = Assert.IsType<TextResourceContents>(result.Contents[0]); TextResourceContents textResource = Assert.IsType<TextResourceContents>(result.Contents[0]);
@ -857,6 +870,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<ListPromptsResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<ListPromptsResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Prompts); Assert.NotNull(result?.Prompts);
Assert.Equal("complete", result.ResultType);
Assert.NotEmpty(result.Prompts); Assert.NotEmpty(result.Prompts);
Assert.Equal("test", result.Prompts[0].Name); Assert.Equal("test", result.Prompts[0].Name);
}); });
@ -886,6 +900,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<GetPromptResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<GetPromptResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.Equal("test", result.Description); Assert.Equal("test", result.Description);
}); });
} }
@ -920,6 +935,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<ListToolsResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<ListToolsResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.NotEmpty(result.Tools); Assert.NotEmpty(result.Tools);
Assert.Equal("test", result.Tools[0].Name); Assert.Equal("test", result.Tools[0].Name);
}); });
@ -955,6 +971,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.NotEmpty(result.Content); Assert.NotEmpty(result.Content);
Assert.Equal("test", Assert.IsType<TextContentBlock>(result.Content[0]).Text); Assert.Equal("test", Assert.IsType<TextContentBlock>(result.Content[0]).Text);
}); });
@ -966,6 +983,34 @@ public class McpServerTests : LoggedTest
await Succeeds_Even_If_No_Handler_Assigned(new ServerCapabilities { Tools = new() }, RequestMethods.ToolsCall, "CallTool handler not configured"); await Succeeds_Even_If_No_Handler_Assigned(new ServerCapabilities { Tools = new() }, RequestMethods.ToolsCall, "CallTool handler not configured");
} }
[Fact]
public async Task Can_Handle_SetLoggingLevel_Requests()
{
await Can_Handle_Requests(
new ServerCapabilities
{
Logging = new()
},
method: RequestMethods.LoggingSetLevel,
configureOptions: options =>
{
// logging/setLevel is a legacy (2025-06-18) method whose result must serialize as an
// empty object {}. The custom handler returns a bare result and the server must not
// add a resultType, otherwise the MCP conformance suite rejects the response.
options.Handlers.SetLoggingLevelHandler = async (request, ct) => new EmptyResult();
},
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<EmptyResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Null(result.ResultType);
// The wire response must be exactly {} with no additional properties.
var obj = Assert.IsType<JsonObject>(response);
Assert.Empty(obj);
});
}
[Fact] [Fact]
public async Task Can_Handle_Call_Tool_Requests_With_McpException() public async Task Can_Handle_Call_Tool_Requests_With_McpException()
{ {
@ -988,6 +1033,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.True(result.IsError); Assert.True(result.IsError);
Assert.NotEmpty(result.Content); Assert.NotEmpty(result.Content);
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]); var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
@ -1016,6 +1062,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.True(result.IsError); Assert.True(result.IsError);
Assert.NotEmpty(result.Content); Assert.NotEmpty(result.Content);
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]); var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
@ -1051,6 +1098,7 @@ public class McpServerTests : LoggedTest
{ {
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.True(result.IsError, "Input validation errors should be returned as tool execution errors (IsError=true), not protocol errors"); Assert.True(result.IsError, "Input validation errors should be returned as tool execution errors (IsError=true), not protocol errors");
Assert.NotEmpty(result.Content); Assert.NotEmpty(result.Content);
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]); var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
@ -1311,6 +1359,7 @@ public class McpServerTests : LoggedTest
Assert.NotNull(response.Result); Assert.NotNull(response.Result);
var initResult = JsonSerializer.Deserialize<InitializeResult>(response.Result, McpJsonUtilities.DefaultOptions); var initResult = JsonSerializer.Deserialize<InitializeResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(initResult); Assert.NotNull(initResult);
Assert.Equal("complete", initResult.ResultType);
Assert.NotNull(initResult.ServerInfo); Assert.NotNull(initResult.ServerInfo);
await transport.DisposeAsync(); await transport.DisposeAsync();