Add 3-arg RequestContext constructor and obsolete 2-arg to eliminate null-forgiving operator usage (#1462)

This commit is contained in:
Copilot 2026-03-27 12:33:23 -07:00 • committed by GitHub
commit 7dcdbafe9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 190 additions and 219 deletions

View file

@ -317,7 +317,7 @@ Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filt
{
var logger = context.Services?.GetService<ILogger<Program>>();
logger?.LogInformation($"Processing request from {context.Params?.ProgressToken}");
logger?.LogInformation($"Processing request from {context.Params.ProgressToken}");
var result = await next(context, cancellationToken);
logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools");
return result;
@ -339,7 +339,7 @@ Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filt
catch (Exception ex)
{
var logger = context.Services?.GetService<ILogger<Program>>();
logger?.LogError(ex, "Error while processing CallTool request for {ProgressToken}", context.Params?.ProgressToken);
logger?.LogError(ex, "Error while processing CallTool request for {ProgressToken}", context.Params.ProgressToken);
return new CallToolResult
{

View file

@ -13,7 +13,7 @@ public class LoggingTools
int duration = 10,
int steps = 10)
{
var progressToken = context.Params?.ProgressToken;
var progressToken = context.Params.ProgressToken;
var stepDuration = duration / steps;
// <snippet_LoggingConfiguration >

View file

@ -77,7 +77,7 @@ builder.Services.AddMcpServer()
int startIndex = 0;
// Parse cursor to determine starting position
if (ctx.Params?.Cursor is { } cursor)
if (ctx.Params.Cursor is { } cursor)
{
startIndex = int.Parse(cursor);
}

View file

@ -15,7 +15,7 @@ public class LongRunningTools
int duration = 10,
int steps = 5)
{
var progressToken = context.Params?.ProgressToken;
var progressToken = context.Params.ProgressToken;
var stepDuration = duration / steps;
for (int i = 1; i <= steps; i++)

View file

@ -212,7 +212,7 @@ builder.Services.AddMcpServer()
.WithResources<MyResources>()
.WithSubscribeToResourcesHandler(async (ctx, ct) =>
{
if (ctx.Params?.Uri is { } uri)
if (ctx.Params.Uri is { } uri)
{
// Track the subscription (e.g., in a concurrent dictionary)
subscriptions[ctx.Server.SessionId].TryAdd(uri, 0);
@ -221,7 +221,7 @@ builder.Services.AddMcpServer()
})
.WithUnsubscribeFromResourcesHandler(async (ctx, ct) =>
{
if (ctx.Params?.Uri is { } uri)
if (ctx.Params.Uri is { } uri)
{
subscriptions[ctx.Server.SessionId].TryRemove(uri, out _);
}

View file

@ -36,3 +36,4 @@ When APIs are marked as obsolete, a diagnostic is emitted to warn users that the
| :------------ | :----- | :---------- |
| `MCP9001` | In place | The `EnumSchema` and `LegacyTitledEnumSchema` APIs are deprecated as of specification version 2025-11-25. Use the current schema APIs instead. |
| `MCP9002` | Removed | The `AddXxxFilter` extension methods on `IMcpServerBuilder` (e.g., `AddListToolsFilter`, `AddCallToolFilter`, `AddIncomingMessageFilter`) were superseded by `WithRequestFilters()` and `WithMessageFilters()`. |
| `MCP9003` | In place | The `RequestContext<TParams>(McpServer, JsonRpcRequest)` constructor is obsolete. Use the overload that accepts a `parameters` argument: `RequestContext<TParams>(McpServer, JsonRpcRequest, TParams)`. |

View file

@ -130,7 +130,7 @@ builder.Services
{
throw new McpException("Cannot add subscription for server with null SessionId");
}
if (ctx.Params?.Uri is { } uri)
if (ctx.Params.Uri is { } uri)
{
subscriptions[ctx.Server.SessionId].TryAdd(uri, 0);
@ -154,7 +154,7 @@ builder.Services
{
throw new McpException("Cannot remove subscription for server with null SessionId");
}
if (ctx.Params?.Uri is { } uri)
if (ctx.Params.Uri is { } uri)
{
subscriptions[ctx.Server.SessionId].TryRemove(uri, out _);
}
@ -212,11 +212,6 @@ builder.Services
})
.WithSetLoggingLevelHandler(async (ctx, ct) =>
{
if (ctx.Params?.Level is null)
{
throw new McpProtocolException("Missing required argument 'level'", McpErrorCode.InvalidParams);
}
// The SDK updates the LoggingLevel field of the IMcpServer
await ctx.Server.SendNotificationAsync("notifications/message", new

View file

@ -19,7 +19,7 @@ public class SimpleResourceType
int index = id - 1;
if ((uint)index >= ResourceGenerator.Resources.Count)
{
throw new NotSupportedException($"Unknown resource: {requestContext.Params?.Uri}");
throw new NotSupportedException($"Unknown resource: {requestContext.Params.Uri}");
}
var resource = ResourceGenerator.Resources[index];

View file

@ -15,7 +15,7 @@ public class LongRunningTool
int duration = 10,
int steps = 5)
{
var progressToken = context.Params?.ProgressToken;
var progressToken = context.Params.ProgressToken;
var stepDuration = duration / steps;
for (int i = 1; i <= steps + 1; i++)

View file

@ -25,4 +25,8 @@ internal static class Obsoletions
// MCP9002 was used for the AddXxxFilter extension methods on IMcpServerBuilder that were superseded by
// WithMessageFilters() and WithRequestFilters(). The APIs were removed; do not reuse this diagnostic ID.
public const string RequestContextParamsConstructor_DiagnosticId = "MCP9003";
public const string RequestContextParamsConstructor_Message = "Use the constructor overload that accepts a parameters argument.";
public const string RequestContextParamsConstructor_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcp9003";
}

View file

@ -29,7 +29,7 @@ internal sealed class RequestHandlers : Dictionary<string, Func<JsonRpcRequest,
/// </remarks>
public void Set<TParams, TResult>(
string method,
Func<TParams?, JsonRpcRequest, CancellationToken, ValueTask<TResult>> handler,
Func<TParams, JsonRpcRequest, CancellationToken, ValueTask<TResult>> handler,
JsonTypeInfo<TParams> requestTypeInfo,
JsonTypeInfo<TResult> responseTypeInfo)
{
@ -40,7 +40,7 @@ internal sealed class RequestHandlers : Dictionary<string, Func<JsonRpcRequest,
this[method] = async (request, cancellationToken) =>
{
TParams? typedRequest = JsonSerializer.Deserialize(request.Params, requestTypeInfo);
TParams typedRequest = JsonSerializer.Deserialize(request.Params, requestTypeInfo)!;
object? result = await handler(typedRequest, request, cancellationToken).ConfigureAwait(false);
return JsonSerializer.SerializeToNode(result, responseTypeInfo);
};

View file

@ -386,14 +386,14 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
TextContent tc => new()
{
Contents = [new TextResourceContents { Uri = request.Params!.Uri, MimeType = ProtocolResourceTemplate.MimeType, Text = tc.Text }],
Contents = [new TextResourceContents { Uri = request.Params.Uri, MimeType = ProtocolResourceTemplate.MimeType, Text = tc.Text }],
},
DataContent dc => new()
{
Contents = [new BlobResourceContents
{
Uri = request.Params!.Uri,
Uri = request.Params.Uri,
MimeType = dc.MediaType,
Blob = EncodingUtilities.GetUtf8Bytes(dc.Base64Data.Span)
}],
@ -401,7 +401,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
string text => new()
{
Contents = [new TextResourceContents { Uri = request.Params!.Uri, MimeType = ProtocolResourceTemplate.MimeType, Text = text }],
Contents = [new TextResourceContents { Uri = request.Params.Uri, MimeType = ProtocolResourceTemplate.MimeType, Text = text }],
},
IEnumerable<ResourceContents> contents => new()
@ -416,14 +416,14 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
{
TextContent tc => new TextResourceContents
{
Uri = request.Params!.Uri,
Uri = request.Params.Uri,
MimeType = ProtocolResourceTemplate.MimeType,
Text = tc.Text
},
DataContent dc => new BlobResourceContents
{
Uri = request.Params!.Uri,
Uri = request.Params.Uri,
MimeType = dc.MediaType,
Blob = EncodingUtilities.GetUtf8Bytes(dc.Base64Data.Span)
},
@ -436,7 +436,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
{
Contents = strings.Select<string, ResourceContents>(text => new TextResourceContents
{
Uri = request.Params!.Uri,
Uri = request.Params.Uri,
MimeType = ProtocolResourceTemplate.MimeType,
Text = text
}).ToList(),

View file

@ -954,7 +954,7 @@ internal sealed partial class McpServerImpl : McpServer
// If a handler was provided, now delegate to it.
if (setLoggingLevelHandler is not null)
{
return InvokeHandlerAsync(setLoggingLevelHandler, request, jsonRpcRequest, cancellationToken);
return InvokeHandlerAsync(setLoggingLevelHandler, request!, jsonRpcRequest, cancellationToken);
}
// Otherwise, consider it handled.
@ -966,17 +966,17 @@ internal sealed partial class McpServerImpl : McpServer
private ValueTask<TResult> InvokeHandlerAsync<TParams, TResult>(
McpRequestHandler<TParams, TResult> handler,
TParams? args,
TParams args,
JsonRpcRequest jsonRpcRequest,
CancellationToken cancellationToken = default)
{
return _servicesScopePerRequest ?
InvokeScopedAsync(handler, args, jsonRpcRequest, cancellationToken) :
handler(new(new DestinationBoundMcpServer(this, jsonRpcRequest.Context?.RelatedTransport), jsonRpcRequest) { Params = args }, cancellationToken);
handler(new(new DestinationBoundMcpServer(this, jsonRpcRequest.Context?.RelatedTransport), jsonRpcRequest, args), cancellationToken);
async ValueTask<TResult> InvokeScopedAsync(
McpRequestHandler<TParams, TResult> handler,
TParams? args,
TParams args,
JsonRpcRequest jsonRpcRequest,
CancellationToken cancellationToken)
{
@ -984,10 +984,9 @@ internal sealed partial class McpServerImpl : McpServer
try
{
return await handler(
new RequestContext<TParams>(new DestinationBoundMcpServer(this, jsonRpcRequest.Context?.RelatedTransport), jsonRpcRequest)
new RequestContext<TParams>(new DestinationBoundMcpServer(this, jsonRpcRequest.Context?.RelatedTransport), jsonRpcRequest, args)
{
Services = scope?.ServiceProvider ?? Services,
Params = args
},
cancellationToken).ConfigureAwait(false);
}

View file

@ -13,19 +13,34 @@ namespace ModelContextProtocol.Server;
/// </remarks>
public sealed class RequestContext<TParams> : MessageContext
{
/// <summary>
/// Initializes a new instance of the <see cref="RequestContext{TParams}"/> class with the specified server, JSON-RPC request, and request parameters.
/// </summary>
/// <param name="server">The server with which this instance is associated.</param>
/// <param name="jsonRpcRequest">The JSON-RPC request associated with this context.</param>
/// <param name="parameters">The parameters associated with this request.</param>
/// <exception cref="ArgumentNullException"><paramref name="server"/> or <paramref name="jsonRpcRequest"/> is <see langword="null"/>.</exception>
public RequestContext(McpServer server, JsonRpcRequest jsonRpcRequest, TParams parameters)
: base(server, jsonRpcRequest)
{
Params = parameters;
}
/// <summary>
/// Initializes a new instance of the <see cref="RequestContext{TParams}"/> class with the specified server and JSON-RPC request.
/// </summary>
/// <param name="server">The server with which this instance is associated.</param>
/// <param name="jsonRpcRequest">The JSON-RPC request associated with this context.</param>
/// <exception cref="ArgumentNullException"><paramref name="server"/> or <paramref name="jsonRpcRequest"/> is <see langword="null"/>.</exception>
[Obsolete(Obsoletions.RequestContextParamsConstructor_Message, DiagnosticId = Obsoletions.RequestContextParamsConstructor_DiagnosticId, UrlFormat = Obsoletions.RequestContextParamsConstructor_Url)]
public RequestContext(McpServer server, JsonRpcRequest jsonRpcRequest)
: base(server, jsonRpcRequest)
{
Params = default!;
}
/// <summary>Gets or sets the parameters associated with this request.</summary>
public TParams? Params { get; set; }
public TParams Params { get; set; }
/// <summary>
/// Gets or sets the primitive that matched the request.

View file

@ -39,7 +39,7 @@ public class Program
// For the test_reconnection tool, enable polling mode after the tool runs.
// This stores the result and closes the SSE stream, so the client
// must reconnect via GET with Last-Event-ID to retrieve the result.
if (request.Params?.Name == "test_reconnection")
if (request.Params.Name == "test_reconnection")
{
await request.EnablePollingAsync(TimeSpan.FromMilliseconds(500), cancellationToken);
}
@ -54,7 +54,7 @@ public class Program
{
throw new McpException("Cannot add subscription for server with null SessionId");
}
if (ctx.Params?.Uri is { } uri)
if (ctx.Params.Uri is { } uri)
{
var sessionSubscriptions = subscriptions.GetOrAdd(ctx.Server.SessionId, _ => new());
sessionSubscriptions.TryAdd(uri, 0);
@ -68,7 +68,7 @@ public class Program
{
throw new McpException("Cannot remove subscription for server with null SessionId");
}
if (ctx.Params?.Uri is { } uri)
if (ctx.Params.Uri is { } uri)
{
subscriptions[ctx.Server.SessionId].TryRemove(uri, out _);
}
@ -91,11 +91,6 @@ public class Program
})
.WithSetLoggingLevelHandler(async (ctx, ct) =>
{
if (ctx.Params?.Level is null)
{
throw new McpProtocolException("Missing required argument 'level'", McpErrorCode.InvalidParams);
}
// The SDK updates the LoggingLevel field of the McpServer
// Send a log notification to confirm the level was set
await ctx.Server.SendNotificationAsync("notifications/message", new LoggingMessageNotificationParams

View file

@ -132,7 +132,7 @@ public class ConformanceTools
RequestContext<CallToolRequestParams> context,
CancellationToken cancellationToken)
{
var progressToken = context.Params?.ProgressToken;
var progressToken = context.Params.ProgressToken;
if (progressToken is not null)
{

View file

@ -204,9 +204,9 @@ internal static class Program
};
options.Handlers.CallToolHandler = async (request, cancellationToken) =>
{
if (request.Params?.Name == "echo")
if (request.Params.Name == "echo")
{
if (request.Params?.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message))
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message))
{
throw new McpProtocolException("Missing required argument 'message'", McpErrorCode.InvalidParams);
}
@ -215,16 +215,16 @@ internal static class Program
Content = [new TextContentBlock { Text = $"Echo: {message}" }]
};
}
else if (request.Params?.Name == "echoSessionId")
else if (request.Params.Name == "echoSessionId")
{
return new CallToolResult
{
Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }]
};
}
else if (request.Params?.Name == "trigger-sampling-request")
else if (request.Params.Name == "trigger-sampling-request")
{
if (request.Params?.Arguments is null ||
if (request.Params.Arguments is null ||
!request.Params.Arguments.TryGetValue("prompt", out var prompt) ||
!request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens))
{
@ -238,16 +238,16 @@ internal static class Program
Content = [new TextContentBlock { Text = $"LLM sampling result: {sampleResult.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text}" }]
};
}
else if (request.Params?.Name == "echoCliArg")
else if (request.Params.Name == "echoCliArg")
{
return new CallToolResult
{
Content = [new TextContentBlock { Text = cliArg ?? "null" }]
};
}
else if (request.Params?.Name == "longRunning")
else if (request.Params.Name == "longRunning")
{
if (request.Params?.Arguments is null || !request.Params.Arguments.TryGetValue("durationMs", out var durationMsValue))
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("durationMs", out var durationMsValue))
{
throw new McpProtocolException("Missing required argument 'durationMs'", McpErrorCode.InvalidParams);
}
@ -258,9 +258,9 @@ internal static class Program
Content = [new TextContentBlock { Text = $"Long-running operation completed after {durationMs}ms" }]
};
}
else if (request.Params?.Name == "crash")
else if (request.Params.Name == "crash")
{
if (request.Params?.Arguments is null || !request.Params.Arguments.TryGetValue("exitCode", out var exitCodeValue))
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("exitCode", out var exitCodeValue))
{
throw new McpProtocolException("Missing required argument 'exitCode'", McpErrorCode.InvalidParams);
}
@ -271,7 +271,7 @@ internal static class Program
}
else
{
throw new McpProtocolException($"Unknown tool: {request.Params?.Name}", McpErrorCode.InvalidParams);
throw new McpProtocolException($"Unknown tool: {request.Params.Name}", McpErrorCode.InvalidParams);
}
};
}
@ -335,7 +335,7 @@ internal static class Program
options.Handlers.GetPromptHandler = async (request, cancellationToken) =>
{
List<PromptMessage> messages = [];
if (request.Params?.Name == "simple-prompt")
if (request.Params.Name == "simple-prompt")
{
messages.Add(new PromptMessage
{
@ -343,7 +343,7 @@ internal static class Program
Content = new TextContentBlock { Text = "This is a simple prompt without arguments." },
});
}
else if (request.Params?.Name == "args-prompt")
else if (request.Params.Name == "args-prompt")
{
string city = request.Params.Arguments?["city"].ToString() ?? "unknown";
string state = request.Params.Arguments?["state"].ToString() ?? "";
@ -354,7 +354,7 @@ internal static class Program
Content = new TextContentBlock { Text = $"What's weather in {location}?" },
});
}
else if (request.Params?.Name == "completable-prompt")
else if (request.Params.Name == "completable-prompt")
{
string department = request.Params.Arguments?["department"].ToString() ?? "unknown";
string name = request.Params.Arguments?["name"].ToString() ?? "unknown";
@ -366,7 +366,7 @@ internal static class Program
}
else
{
throw new McpProtocolException($"Unknown prompt: {request.Params?.Name}", McpErrorCode.InvalidParams);
throw new McpProtocolException($"Unknown prompt: {request.Params.Name}", McpErrorCode.InvalidParams);
}
return new GetPromptResult
@ -382,11 +382,6 @@ internal static class Program
{
options.Handlers.SetLoggingLevelHandler = async (request, cancellationToken) =>
{
if (request.Params?.Level is null)
{
throw new McpProtocolException("Missing required argument 'level'", McpErrorCode.InvalidParams);
}
_minimumLoggingLevel = request.Params.Level;
return new EmptyResult();
@ -452,7 +447,7 @@ internal static class Program
options.Handlers.ListResourcesHandler = async (request, cancellationToken) =>
{
int startIndex = 0;
if (request.Params?.Cursor is not null)
if (request.Params.Cursor is not null)
{
try
{
@ -481,7 +476,7 @@ internal static class Program
options.Handlers.ReadResourceHandler = async (request, cancellationToken) =>
{
if (request.Params?.Uri is null)
if (request.Params.Uri is null)
{
throw new McpProtocolException("Missing required argument 'uri'", McpErrorCode.InvalidParams);
}
@ -518,7 +513,7 @@ internal static class Program
options.Handlers.SubscribeToResourcesHandler = async (request, cancellationToken) =>
{
if (request?.Params?.Uri is null)
if (request?.Params.Uri is null)
{
throw new McpProtocolException("Missing required argument 'uri'", McpErrorCode.InvalidParams);
}
@ -535,7 +530,7 @@ internal static class Program
options.Handlers.UnsubscribeFromResourcesHandler = async (request, cancellationToken) =>
{
if (request?.Params?.Uri is null)
if (request?.Params.Uri is null)
{
throw new McpProtocolException("Missing required argument 'uri'", McpErrorCode.InvalidParams);
}
@ -563,7 +558,7 @@ internal static class Program
options.Handlers.CompleteHandler = async (request, cancellationToken) =>
{
string[]? values;
switch (request.Params?.Ref)
switch (request.Params.Ref)
{
case ResourceTemplateReference rtr:
var resourceId = rtr.Uri?.Split('/').LastOrDefault();
@ -571,7 +566,7 @@ internal static class Program
return new CompleteResult { Completion = new() { Values = [] } };
// Filter resource IDs that start with the input value
values = sampleResourceIds.Where(id => id.StartsWith(request.Params!.Argument.Value)).ToArray();
values = sampleResourceIds.Where(id => id.StartsWith(request.Params.Argument.Value)).ToArray();
return new CompleteResult { Completion = new() { Values = values, HasMore = false, Total = values.Length } };
case PromptReference pr:
@ -583,7 +578,7 @@ internal static class Program
return new CompleteResult { Completion = new() { Values = values, HasMore = false, Total = values.Length } };
default:
throw new McpProtocolException($"Unknown reference type: '{request.Params?.Ref.Type}'", McpErrorCode.InvalidParams);
throw new McpProtocolException($"Unknown reference type: '{request.Params.Ref.Type}'", McpErrorCode.InvalidParams);
}
};
}

View file

@ -280,7 +280,7 @@ public class Program
ReadResourceHandler = async (request, cancellationToken) =>
{
if (request.Params?.Uri is null)
if (request.Params.Uri is null)
{
throw new McpProtocolException("Missing required argument 'uri'", McpErrorCode.InvalidParams);
}

View file

@ -37,7 +37,7 @@ public class McpClientMetaTests : ClientServerTestBase
async (RequestContext<CallToolRequestParams> context) =>
{
// Access the foo property of _meta field from the request parameters
var metaFoo = context.Params?.Meta?["foo"]?.ToString();
var metaFoo = context.Params.Meta?["foo"]?.ToString();
// Assert that the meta foo is correctly passed
Assert.NotNull(metaFoo);
@ -73,7 +73,7 @@ public class McpClientMetaTests : ClientServerTestBase
(RequestContext<ReadResourceRequestParams> context) =>
{
// Access the foo property of _meta field from the request parameters
var metaFoo = context.Params?.Meta?["foo"]?.ToString();
var metaFoo = context.Params.Meta?["foo"]?.ToString();
// Assert that the meta foo is correctly passed
Assert.NotNull(metaFoo);
@ -109,7 +109,7 @@ public class McpClientMetaTests : ClientServerTestBase
(RequestContext<GetPromptRequestParams> context) =>
{
// Access the foo property of _meta field from the request parameters
var metaFoo = context.Params?.Meta?["foo"]?.ToString();
var metaFoo = context.Params.Meta?["foo"]?.ToString();
// Assert that the meta foo is correctly passed
Assert.NotNull(metaFoo);

View file

@ -29,7 +29,7 @@ public class McpClientPromptTests : ClientServerTestBase
[McpServerPrompt, Description("Echoes back the metadata it receives")]
public static ChatMessage MetadataEcho(RequestContext<GetPromptRequestParams> context) =>
new(ChatRole.User, context.Params?.Meta?.ToJsonString() ?? "{}");
new(ChatRole.User, context.Params.Meta?.ToJsonString() ?? "{}");
}
[Fact]

Some files were not shown because too many files have changed in this diff Show more