Skip to content

Commit 374e76e

Browse files
PranavSenthilnathanCopilotTarek Mahmoud Sayed
authored
Fix missing resultType on complete result responses (modelcontextprotocol#1684)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com>
1 parent fd38f51 commit 374e76e

6 files changed

Lines changed: 118 additions & 4 deletions

File tree

src/ModelContextProtocol.Core/Protocol/EmptyResult.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ namespace ModelContextProtocol.Protocol;
99
public sealed class EmptyResult : Result
1010
{
1111
[JsonIgnore]
12-
internal static EmptyResult Instance { get; } = new();
12+
internal static EmptyResult Instance { get; } = new() { ResultType = "complete" };
1313
}

src/ModelContextProtocol.Core/Protocol/Result.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ private protected Result()
2727
/// </summary>
2828
/// <remarks>
2929
/// <para>
30-
/// When absent or set to <c>"complete"</c>, the result is a normal completed response.
30+
/// When set to <c>"complete"</c>, the result is a normal completed response.
3131
/// When set to <c>"input_required"</c>, the result is an <see cref="InputRequiredResult"/> indicating
3232
/// that additional input is needed before the request can be completed.
3333
/// When set to <c>"task"</c>, the result is a <see cref="CreateTaskResult"/> indicating that the server

src/ModelContextProtocol.Core/Server/McpServerImpl.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,7 @@ private void ConfigureInitialize(McpServerOptions options)
621621
Instructions = options.ServerInstructions,
622622
ServerInfo = options.ServerInfo ?? DefaultImplementation,
623623
Capabilities = ServerCapabilities ?? new(),
624+
ResultType = "complete",
624625
};
625626
},
626627
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
@@ -651,6 +652,7 @@ private void ConfigureDiscover(McpServerOptions options)
651652
// their "do not cache" behavior while satisfying the wire requirement.
652653
TimeToLive = TimeSpan.Zero,
653654
CacheScope = CacheScope.Private,
655+
ResultType = "complete",
654656
});
655657
},
656658
McpJsonUtilities.JsonContext.Default.DiscoverRequestParams,
@@ -703,7 +705,7 @@ private void ConfigureSubscriptions(McpServerOptions options)
703705

704706
await SendSubscriptionAckAsync(statelessSubscription, cancellationToken).ConfigureAwait(false);
705707

706-
return new EmptyResult();
708+
return EmptyResult.Instance;
707709
}
708710

709711
// Filter the requested notifications against what the server actually supports.
@@ -743,7 +745,7 @@ private void ConfigureSubscriptions(McpServerOptions options)
743745
_activeSubscriptions.TryRemove(jsonRpcRequest.Id, out _);
744746
}
745747

746-
return new EmptyResult();
748+
return EmptyResult.Instance;
747749
},
748750
McpJsonUtilities.JsonContext.Default.SubscriptionsListenRequestParams,
749751
McpJsonUtilities.JsonContext.Default.EmptyResult);
@@ -1916,6 +1918,21 @@ private void SetHandler<TParams, TResult>(
19161918
};
19171919
}
19181920

1921+
if (typeof(Result).IsAssignableFrom(typeof(TResult)))
1922+
{
1923+
var innerHandler = handler;
1924+
handler = async (request, cancellationToken) =>
1925+
{
1926+
var result = await innerHandler(request, cancellationToken).ConfigureAwait(false);
1927+
if (result is Result protocolResult && protocolResult.ResultType is null)
1928+
{
1929+
protocolResult.ResultType = "complete";
1930+
}
1931+
1932+
return result;
1933+
};
1934+
}
1935+
19191936
_requestHandlers.Set(method,
19201937
(request, jsonRpcRequest, cancellationToken) =>
19211938
InvokeHandlerAsync(handler, request, jsonRpcRequest, cancellationToken),
@@ -1930,6 +1947,25 @@ private void SetTaskAugmentedHandler<TParams, TResult>(
19301947
JsonTypeInfo<CreateTaskResult> taskResultTypeInfo)
19311948
where TResult : Result
19321949
{
1950+
var innerHandler = handler;
1951+
handler = async (request, cancellationToken) =>
1952+
{
1953+
var result = await innerHandler(request, cancellationToken).ConfigureAwait(false);
1954+
if (result.IsTask)
1955+
{
1956+
if (result.TaskCreated is { ResultType: null } taskCreated)
1957+
{
1958+
taskCreated.ResultType = "task";
1959+
}
1960+
}
1961+
else if (result.Result is { ResultType: null } immediateResult)
1962+
{
1963+
immediateResult.ResultType = "complete";
1964+
}
1965+
1966+
return result;
1967+
};
1968+
19331969
_requestHandlers.SetTaskAugmented(method,
19341970
(request, jsonRpcRequest, cancellationToken) =>
19351971
InvokeHandlerAsync(handler, request, jsonRpcRequest, cancellationToken),

tests/ModelContextProtocol.Tests/Client/July2026ProtocolConnectionTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public async Task ServerDiscover_IncludesJuly2026ProtocolVersion()
8383

8484
var discoverResult = JsonSerializer.Deserialize<DiscoverResult>(response.Result, McpJsonUtilities.DefaultOptions);
8585
Assert.NotNull(discoverResult);
86+
Assert.Equal("complete", discoverResult.ResultType);
8687
Assert.Equal([McpProtocolVersions.July2026ProtocolVersion], discoverResult.SupportedVersions);
8788
}
8889
}

tests/ModelContextProtocol.Tests/Server/McpServerTaskTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
5858
};
5959
}
6060

61+
if (toolName == "async-tool-default-resulttype")
62+
{
63+
// Intentionally leaves ResultType unset so the server boundary is responsible for
64+
// filling in "task" on the wire.
65+
var taskId = store.CreateTask();
66+
return new CreateTaskResult
67+
{
68+
TaskId = taskId,
69+
Status = McpTaskStatus.Working,
70+
CreatedAt = DateTimeOffset.UtcNow,
71+
LastUpdatedAt = DateTimeOffset.UtcNow,
72+
PollIntervalMs = 50,
73+
};
74+
}
75+
6176
if (toolName == "input-required-tool")
6277
{
6378
var taskId = store.CreateTask(McpTaskStatus.InputRequired);
@@ -292,6 +307,19 @@ public async Task CreateTaskResult_HasResultTypeTask()
292307
Assert.Equal("task", augmented.TaskCreated!.ResultType);
293308
}
294309

310+
[Fact]
311+
public async Task CreateTaskResult_WithoutExplicitResultType_ServerFillsTask()
312+
{
313+
await using var client = await CreateMcpClientForServer();
314+
315+
var augmented = await client.CallToolRawAsync(
316+
new CallToolRequestParams { Name = "async-tool-default-resulttype" },
317+
TestContext.Current.CancellationToken);
318+
319+
Assert.True(augmented.IsTask);
320+
Assert.Equal("task", augmented.TaskCreated!.ResultType);
321+
}
322+
295323
[Fact]
296324
public async Task GetTaskAsync_ImmediatelyAfterCreate_Resolves()
297325
{

tests/ModelContextProtocol.Tests/Server/McpServerTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ await Can_Handle_Requests(
283283
{
284284
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
285285
Assert.NotNull(result);
286+
Assert.Equal("complete", result.ResultType);
286287
Assert.Equal(expectedAssemblyName.Name, result.ServerInfo.Name);
287288
Assert.Equal(expectedAssemblyName.Version?.ToString() ?? "1.0.0", result.ServerInfo.Version);
288289
Assert.Equal("2024-11-05", result.ProtocolVersion);
@@ -385,6 +386,7 @@ await Can_Handle_Requests(
385386
{
386387
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
387388
Assert.NotNull(result);
389+
Assert.Equal("complete", result.ResultType);
388390
Assert.NotNull(result.Capabilities.Extensions);
389391
Assert.True(result.Capabilities.Extensions.ContainsKey("io.myext"));
390392
});
@@ -404,6 +406,7 @@ await Can_Handle_Requests(
404406
{
405407
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
406408
Assert.NotNull(result);
409+
Assert.Equal("complete", result.ResultType);
407410
Assert.NotNull(result.Capabilities.Experimental);
408411
Assert.True(result.Capabilities.Experimental.ContainsKey("customFeature"));
409412
});
@@ -435,6 +438,7 @@ await Can_Handle_Requests(
435438
{
436439
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
437440
Assert.NotNull(result);
441+
Assert.Equal("complete", result.ResultType);
438442

439443
// Use reflection to verify every public property on ServerCapabilities is non-null.
440444
// This catches cases where new capability properties are added but not copied
@@ -481,6 +485,7 @@ await Can_Handle_Requests(
481485
{
482486
var result = JsonSerializer.Deserialize<CompleteResult>(response, McpJsonUtilities.DefaultOptions);
483487
Assert.NotNull(result?.Completion);
488+
Assert.Equal("complete", result.ResultType);
484489
Assert.Equal(["test"], result.Completion.Values);
485490
Assert.Equal(2, result.Completion.Total);
486491
Assert.True(result.Completion.HasMore);
@@ -524,6 +529,7 @@ await transport.SendMessageAsync(new JsonRpcRequest
524529
Assert.NotNull(response);
525530
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
526531
Assert.NotNull(result?.Completion);
532+
Assert.Equal("complete", result.ResultType);
527533
Assert.Equal(["cat"], result.Completion.Values);
528534
Assert.Equal(1, result.Completion.Total);
529535

@@ -567,6 +573,7 @@ await transport.SendMessageAsync(new JsonRpcRequest
567573
Assert.NotNull(response);
568574
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
569575
Assert.NotNull(result?.Completion);
576+
Assert.Equal("complete", result.ResultType);
570577
Assert.Empty(result.Completion.Values);
571578

572579
await transport.DisposeAsync();
@@ -616,6 +623,7 @@ await transport.SendMessageAsync(new JsonRpcRequest
616623
Assert.NotNull(response);
617624
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
618625
Assert.NotNull(result?.Completion);
626+
Assert.Equal("complete", result.ResultType);
619627
Assert.Equal(["us-east-1", "us-west-2"], result.Completion.Values);
620628
Assert.Equal(2, result.Completion.Total);
621629

@@ -671,6 +679,7 @@ await transport.SendMessageAsync(new JsonRpcRequest
671679
Assert.NotNull(response);
672680
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
673681
Assert.NotNull(result?.Completion);
682+
Assert.Equal("complete", result.ResultType);
674683
// Custom handler values + auto-populated values should be combined
675684
Assert.Equal(["custom-value", "dog", "cat"], result.Completion.Values);
676685
Assert.Equal(3, result.Completion.Total);
@@ -718,6 +727,7 @@ await transport.SendMessageAsync(new JsonRpcRequest
718727
Assert.NotNull(response);
719728
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
720729
Assert.NotNull(result?.Completion);
730+
Assert.Equal("complete", result.ResultType);
721731
Assert.Equal(["a", "b"], result.Completion.Values);
722732

723733
await transport.DisposeAsync();
@@ -756,6 +766,7 @@ await Can_Handle_Requests(
756766
{
757767
var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response, McpJsonUtilities.DefaultOptions);
758768
Assert.NotNull(result?.ResourceTemplates);
769+
Assert.Equal("complete", result.ResultType);
759770
Assert.NotEmpty(result.ResourceTemplates);
760771
Assert.Equal("test", result.ResourceTemplates[0].UriTemplate);
761772
});
@@ -785,6 +796,7 @@ await Can_Handle_Requests(
785796
{
786797
var result = JsonSerializer.Deserialize<ListResourcesResult>(response, McpJsonUtilities.DefaultOptions);
787798
Assert.NotNull(result?.Resources);
799+
Assert.Equal("complete", result.ResultType);
788800
Assert.NotEmpty(result.Resources);
789801
Assert.Equal("test", result.Resources[0].Uri);
790802
});
@@ -820,6 +832,7 @@ await Can_Handle_Requests(
820832
{
821833
var result = JsonSerializer.Deserialize<ReadResourceResult>(response, McpJsonUtilities.DefaultOptions);
822834
Assert.NotNull(result?.Contents);
835+
Assert.Equal("complete", result.ResultType);
823836
Assert.NotEmpty(result.Contents);
824837

825838
TextResourceContents textResource = Assert.IsType<TextResourceContents>(result.Contents[0]);
@@ -857,6 +870,7 @@ await Can_Handle_Requests(
857870
{
858871
var result = JsonSerializer.Deserialize<ListPromptsResult>(response, McpJsonUtilities.DefaultOptions);
859872
Assert.NotNull(result?.Prompts);
873+
Assert.Equal("complete", result.ResultType);
860874
Assert.NotEmpty(result.Prompts);
861875
Assert.Equal("test", result.Prompts[0].Name);
862876
});
@@ -886,6 +900,7 @@ await Can_Handle_Requests(
886900
{
887901
var result = JsonSerializer.Deserialize<GetPromptResult>(response, McpJsonUtilities.DefaultOptions);
888902
Assert.NotNull(result);
903+
Assert.Equal("complete", result.ResultType);
889904
Assert.Equal("test", result.Description);
890905
});
891906
}
@@ -920,6 +935,7 @@ await Can_Handle_Requests(
920935
{
921936
var result = JsonSerializer.Deserialize<ListToolsResult>(response, McpJsonUtilities.DefaultOptions);
922937
Assert.NotNull(result);
938+
Assert.Equal("complete", result.ResultType);
923939
Assert.NotEmpty(result.Tools);
924940
Assert.Equal("test", result.Tools[0].Name);
925941
});
@@ -955,6 +971,7 @@ await Can_Handle_Requests(
955971
{
956972
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
957973
Assert.NotNull(result);
974+
Assert.Equal("complete", result.ResultType);
958975
Assert.NotEmpty(result.Content);
959976
Assert.Equal("test", Assert.IsType<TextContentBlock>(result.Content[0]).Text);
960977
});
@@ -966,6 +983,34 @@ public async Task Can_Handle_Call_Tool_Requests_Throws_Exception_If_No_Handler_A
966983
await Succeeds_Even_If_No_Handler_Assigned(new ServerCapabilities { Tools = new() }, RequestMethods.ToolsCall, "CallTool handler not configured");
967984
}
968985

986+
[Fact]
987+
public async Task Can_Handle_SetLoggingLevel_Requests()
988+
{
989+
await Can_Handle_Requests(
990+
new ServerCapabilities
991+
{
992+
Logging = new()
993+
},
994+
method: RequestMethods.LoggingSetLevel,
995+
configureOptions: options =>
996+
{
997+
// logging/setLevel is a legacy (2025-06-18) method whose result must serialize as an
998+
// empty object {}. The custom handler returns a bare result and the server must not
999+
// add a resultType, otherwise the MCP conformance suite rejects the response.
1000+
options.Handlers.SetLoggingLevelHandler = async (request, ct) => new EmptyResult();
1001+
},
1002+
assertResult: (_, response) =>
1003+
{
1004+
var result = JsonSerializer.Deserialize<EmptyResult>(response, McpJsonUtilities.DefaultOptions);
1005+
Assert.NotNull(result);
1006+
Assert.Null(result.ResultType);
1007+
1008+
// The wire response must be exactly {} with no additional properties.
1009+
var obj = Assert.IsType<JsonObject>(response);
1010+
Assert.Empty(obj);
1011+
});
1012+
}
1013+
9691014
[Fact]
9701015
public async Task Can_Handle_Call_Tool_Requests_With_McpException()
9711016
{
@@ -988,6 +1033,7 @@ await Can_Handle_Requests(
9881033
{
9891034
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
9901035
Assert.NotNull(result);
1036+
Assert.Equal("complete", result.ResultType);
9911037
Assert.True(result.IsError);
9921038
Assert.NotEmpty(result.Content);
9931039
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
@@ -1016,6 +1062,7 @@ await Can_Handle_Requests(
10161062
{
10171063
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
10181064
Assert.NotNull(result);
1065+
Assert.Equal("complete", result.ResultType);
10191066
Assert.True(result.IsError);
10201067
Assert.NotEmpty(result.Content);
10211068
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
@@ -1051,6 +1098,7 @@ await Can_Handle_Requests(
10511098
{
10521099
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
10531100
Assert.NotNull(result);
1101+
Assert.Equal("complete", result.ResultType);
10541102
Assert.True(result.IsError, "Input validation errors should be returned as tool execution errors (IsError=true), not protocol errors");
10551103
Assert.NotEmpty(result.Content);
10561104
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
@@ -1311,6 +1359,7 @@ await transport.SendClientMessageAsync(new JsonRpcNotification
13111359
Assert.NotNull(response.Result);
13121360
var initResult = JsonSerializer.Deserialize<InitializeResult>(response.Result, McpJsonUtilities.DefaultOptions);
13131361
Assert.NotNull(initResult);
1362+
Assert.Equal("complete", initResult.ResultType);
13141363
Assert.NotNull(initResult.ServerInfo);
13151364

13161365
await transport.DisposeAsync();

0 commit comments

Comments
 (0)