Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ModelContextProtocol.Core/Protocol/EmptyResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ namespace ModelContextProtocol.Protocol;
public sealed class EmptyResult : Result
{
[JsonIgnore]
internal static EmptyResult Instance { get; } = new();
internal static EmptyResult Instance { get; } = new() { ResultType = "complete" };
}
2 changes: 1 addition & 1 deletion src/ModelContextProtocol.Core/Protocol/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ private protected Result()
/// </summary>
/// <remarks>
/// <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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to lock into these semantics. The spec says that client should treat null as complete but that is a behavior that shouldn't leak into the model IMO.

/// 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.
/// When set to <c>"task"</c>, the result is a <see cref="CreateTaskResult"/> indicating that the server
Expand Down
40 changes: 38 additions & 2 deletions src/ModelContextProtocol.Core/Server/McpServerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ private void ConfigureInitialize(McpServerOptions options)
Instructions = options.ServerInstructions,
ServerInfo = options.ServerInfo ?? DefaultImplementation,
Capabilities = ServerCapabilities ?? new(),
ResultType = "complete",
};
},
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
Expand Down Expand Up @@ -414,6 +415,7 @@ private void ConfigureDiscover(McpServerOptions options)
// their "do not cache" behavior while satisfying the wire requirement.
TimeToLive = TimeSpan.Zero,
CacheScope = CacheScope.Private,
ResultType = "complete",
});
},
McpJsonUtilities.JsonContext.Default.DiscoverRequestParams,
Expand Down Expand Up @@ -458,7 +460,7 @@ private void ConfigureSubscriptions(McpServerOptions options)

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

return new EmptyResult();
return EmptyResult.Instance;
}

// Filter the requested notifications against what the server actually supports.
Expand Down Expand Up @@ -498,7 +500,7 @@ private void ConfigureSubscriptions(McpServerOptions options)
_activeSubscriptions.TryRemove(jsonRpcRequest.Id, out _);
}

return new EmptyResult();
return EmptyResult.Instance;
},
McpJsonUtilities.JsonContext.Default.SubscriptionsListenRequestParams,
McpJsonUtilities.JsonContext.Default.EmptyResult);
Expand Down Expand Up @@ -1664,6 +1666,21 @@ private void SetHandler<TParams, TResult>(
};
}

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,
(request, jsonRpcRequest, cancellationToken) =>
InvokeHandlerAsync(handler, request, jsonRpcRequest, cancellationToken),
Expand All @@ -1678,6 +1695,25 @@ private void SetTaskAugmentedHandler<TParams, TResult>(
JsonTypeInfo<CreateTaskResult> taskResultTypeInfo)
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,
(request, jsonRpcRequest, cancellationToken) =>
InvokeHandlerAsync(handler, request, jsonRpcRequest, cancellationToken),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public async Task LegacyClient_CanCallServerDiscover()

var discoverResult = JsonSerializer.Deserialize<DiscoverResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(discoverResult);
Assert.Equal("complete", discoverResult.ResultType);
Assert.NotEmpty(discoverResult.SupportedVersions);
Assert.Contains(LatestStableVersion, discoverResult.SupportedVersions);
Assert.Equal(nameof(July2026ProtocolConnectionTests), discoverResult.ServerInfo.Name);
Expand All @@ -85,6 +86,7 @@ public async Task ServerDiscover_IncludesJuly2026ProtocolVersion()

var discoverResult = JsonSerializer.Deserialize<DiscoverResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(discoverResult);
Assert.Equal("complete", discoverResult.ResultType);
Assert.Contains(McpHttpHeaders.July2026ProtocolVersion, discoverResult.SupportedVersions);
}
}
21 changes: 21 additions & 0 deletions tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.Equal(expectedAssemblyName.Name, result.ServerInfo.Name);
Assert.Equal(expectedAssemblyName.Version?.ToString() ?? "1.0.0", result.ServerInfo.Version);
Assert.Equal("2024", result.ProtocolVersion);
Expand All @@ -304,6 +305,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.NotNull(result.Capabilities.Extensions);
Assert.True(result.Capabilities.Extensions.ContainsKey("io.myext"));
});
Expand All @@ -323,6 +325,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.NotNull(result.Capabilities.Experimental);
Assert.True(result.Capabilities.Experimental.ContainsKey("customFeature"));
});
Expand Down Expand Up @@ -354,6 +357,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);

// Use reflection to verify every public property on ServerCapabilities is non-null.
// This catches cases where new capability properties are added but not copied
Expand Down Expand Up @@ -400,6 +404,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<CompleteResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion);
Assert.Equal("complete", result.ResultType);
Assert.Equal(["test"], result.Completion.Values);
Assert.Equal(2, result.Completion.Total);
Assert.True(result.Completion.HasMore);
Expand Down Expand Up @@ -443,6 +448,7 @@ await transport.SendMessageAsync(new JsonRpcRequest
Assert.NotNull(response);
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion);
Assert.Equal("complete", result.ResultType);
Assert.Equal(["cat"], result.Completion.Values);
Assert.Equal(1, result.Completion.Total);

Expand Down Expand Up @@ -486,6 +492,7 @@ await transport.SendMessageAsync(new JsonRpcRequest
Assert.NotNull(response);
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion);
Assert.Equal("complete", result.ResultType);
Assert.Empty(result.Completion.Values);

await transport.DisposeAsync();
Expand Down Expand Up @@ -535,6 +542,7 @@ await transport.SendMessageAsync(new JsonRpcRequest
Assert.NotNull(response);
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion);
Assert.Equal("complete", result.ResultType);
Assert.Equal(["us-east-1", "us-west-2"], result.Completion.Values);
Assert.Equal(2, result.Completion.Total);

Expand Down Expand Up @@ -590,6 +598,7 @@ await transport.SendMessageAsync(new JsonRpcRequest
Assert.NotNull(response);
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion);
Assert.Equal("complete", result.ResultType);
// Custom handler values + auto-populated values should be combined
Assert.Equal(["custom-value", "dog", "cat"], result.Completion.Values);
Assert.Equal(3, result.Completion.Total);
Expand Down Expand Up @@ -637,6 +646,7 @@ await transport.SendMessageAsync(new JsonRpcRequest
Assert.NotNull(response);
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion);
Assert.Equal("complete", result.ResultType);
Assert.Equal(["a", "b"], result.Completion.Values);

await transport.DisposeAsync();
Expand Down Expand Up @@ -675,6 +685,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.ResourceTemplates);
Assert.Equal("complete", result.ResultType);
Assert.NotEmpty(result.ResourceTemplates);
Assert.Equal("test", result.ResourceTemplates[0].UriTemplate);
});
Expand Down Expand Up @@ -704,6 +715,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<ListResourcesResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Resources);
Assert.Equal("complete", result.ResultType);
Assert.NotEmpty(result.Resources);
Assert.Equal("test", result.Resources[0].Uri);
});
Expand Down Expand Up @@ -739,6 +751,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<ReadResourceResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Contents);
Assert.Equal("complete", result.ResultType);
Assert.NotEmpty(result.Contents);

TextResourceContents textResource = Assert.IsType<TextResourceContents>(result.Contents[0]);
Expand Down Expand Up @@ -776,6 +789,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<ListPromptsResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Prompts);
Assert.Equal("complete", result.ResultType);
Assert.NotEmpty(result.Prompts);
Assert.Equal("test", result.Prompts[0].Name);
});
Expand Down Expand Up @@ -805,6 +819,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<GetPromptResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.Equal("test", result.Description);
});
}
Expand Down Expand Up @@ -839,6 +854,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<ListToolsResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.NotEmpty(result.Tools);
Assert.Equal("test", result.Tools[0].Name);
});
Expand Down Expand Up @@ -874,6 +890,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.NotEmpty(result.Content);
Assert.Equal("test", Assert.IsType<TextContentBlock>(result.Content[0]).Text);
});
Expand Down Expand Up @@ -907,6 +924,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.True(result.IsError);
Assert.NotEmpty(result.Content);
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
Expand Down Expand Up @@ -935,6 +953,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal("complete", result.ResultType);
Assert.True(result.IsError);
Assert.NotEmpty(result.Content);
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
Expand Down Expand Up @@ -970,6 +989,7 @@ await Can_Handle_Requests(
{
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
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.NotEmpty(result.Content);
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
Expand Down Expand Up @@ -1230,6 +1250,7 @@ await transport.SendClientMessageAsync(new JsonRpcNotification
Assert.NotNull(response.Result);
var initResult = JsonSerializer.Deserialize<InitializeResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(initResult);
Assert.Equal("complete", initResult.ResultType);
Assert.NotNull(initResult.ServerInfo);

await transport.DisposeAsync();
Expand Down
Loading