Capabilities define the features supported by MCP clients and servers. During the initialization handshake, both sides exchange capability information to determine which protocol features are available for the session.
using ModelContextProtocol.Client;using ModelContextProtocol.Protocol;var options = new McpClientOptions{ Capabilities = new ClientCapabilities { Roots = new RootsCapability { ListChanged = true // Support dynamic root list updates }, Sampling = new SamplingCapability(), Elicitation = new ElicitationCapability { Form = new FormElicitationCapability(), Url = new UrlElicitationCapability() } }};var client = await McpClient.CreateAsync(transport, options);
Indicates the client can request additional information from users:
public sealed class ElicitationCapability{ public FormElicitationCapability? Form { get; set; } public UrlElicitationCapability? Url { get; set; }}
Form elicitation - Display forms to collect structured input:
Handlers = new McpClientHandlers{ ElicitationHandler = async (request, context, ct) => { if (request.Form is not null) { // Display form UI var dialog = new FormDialog(request.Form.Title, request.Form.Description); foreach (var field in request.Form.Fields) { dialog.AddField(field.Name, field.Label, field.Type, field.Required); } var userInput = await dialog.ShowAsync(); return new ElicitResult { Form = new FormElicitationResult { Values = userInput } }; } throw new InvalidOperationException("Form elicitation not supported"); }}
URL elicitation - Open URLs for user authentication/authorization:
if (request.Url is not null){ // Open URL in browser var url = request.Url.Url; Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true }); return new ElicitResult { Url = new UrlElicitationResult() };}
public sealed class ToolsCapability{ public bool? ListChanged { get; set; }}
Properties:
ListChanged - If true, server can send notifications/tools/list_changed
Usage:
public class DynamicToolServer(McpServer server){ private readonly List<McpServerTool> _tools = new(); public async Task RegisterToolAsync(McpServerTool tool) { _tools.Add(tool); // Notify clients if capability is set if (server.ServerOptions.Capabilities?.Tools?.ListChanged == true) { await server.SendToolListChangedNotificationAsync(); } }}
// Check if server supports toolsif (client.ServerCapabilities.Tools is not null){ var tools = await client.ListToolsAsync();}// Check for specific capability featuresif (client.ServerCapabilities.Resources is { Subscribe: true }){ await client.SubscribeToResourceAsync(uri);}// Check for list change notificationsif (client.ServerCapabilities.Prompts is { ListChanged: true }){ client.RegisterNotificationHandler( NotificationMethods.PromptListChangedNotification, async (notification, ct) => { var prompts = await client.ListPromptsAsync(cancellationToken: ct); // Update UI with new prompts });}// Check multiple capabilitiesif (client.ServerCapabilities is { Tools: not null, Resources: { Subscribe: true }, Logging: not null }){ // Server supports tools, resource subscriptions, and logging}
public class ServerTool(McpServer server){ [McpServerTool] public async Task<string> OperationRequiringSamplingAsync(string prompt) { // Check if client supports sampling if (server.ClientCapabilities?.Sampling is null) { return "Error: Client does not support LLM sampling"; } var result = await server.RequestSamplingAsync(new CreateMessageRequestParams { Messages = new[] { new PromptMessage { Role = PromptRole.User, Content = new TextContentBlock { Text = prompt } } }, MaxTokens = 100 }); return result.Content.Text; } [McpServerTool] public async Task<string> GetUserRootsAsync() { if (server.ClientCapabilities?.Roots is null) { return "Client does not expose filesystem roots"; } var roots = await server.RequestRootsAsync(); return string.Join("\n", roots.Roots.Select(r => $"{r.Name}: {r.Uri}")); }}
var options = new McpClientOptions{ // Request specific version ProtocolVersion = "2025-11-25" // Or use null for flexible negotiation (default) // ProtocolVersion = null};var client = await McpClient.CreateAsync(transport, options);
Behavior:
Explicit version ("2025-11-25") - Requires exact match, fails if server doesn’t support it
Null (default) - Accepts any supported version, uses latest common version
builder.Services.AddMcpServer(options =>{ // Advertise specific version options.ProtocolVersion = "2025-11-25"; // Or use null to match client's request if supported // options.ProtocolVersion = null;});
Behavior:
Explicit version - Always advertises this version
Null (default) - Responds with client’s requested version if supported, otherwise latest
// From clientstring? version = client.NegotiatedProtocolVersion;Console.WriteLine($"Using protocol version: {version}");// From server (in handlers)string? version = server.NegotiatedProtocolVersion;
// Session resumption requires 2025-11-25+if (client.NegotiatedProtocolVersion == "2025-11-25"){ // Can use session resumption var sessionId = client.SessionId; // Save for later resumption}// Check for version in implementationif (string.Compare( server.NegotiatedProtocolVersion, "2025-11-25", StringComparison.Ordinal) >= 0){ // Use 2025-11-25 features}
// Goodif (client.ServerCapabilities.Resources?.Subscribe == true){ await client.SubscribeToResourceAsync(uri);}else{ // Fall back to polling while (true) { var resource = await client.ReadResourceAsync(uri); await Task.Delay(1000); }}// Bad - may throw if server doesn't support subscriptionsawait client.SubscribeToResourceAsync(uri);
Use null protocol version for maximum compatibility
// Good - works with all server versionsvar options = new McpClientOptions{ ProtocolVersion = null // or omit};// Only specify version when you need specific featuresvar options = new McpClientOptions{ ProtocolVersion = "2025-11-25" // Required for session resumption};
Enable ListChanged for dynamic servers
options.Capabilities = new ServerCapabilities{ Tools = new ToolsCapability { ListChanged = true }, Prompts = new PromptsCapability { ListChanged = true }, Resources = new ResourcesCapability { Subscribe = true, ListChanged = true }};
This allows efficient updates without polling.
Provide clear error messages when capabilities are missing
[McpServerTool]public async Task<string> RequiresSamplingAsync(){ if (server.ClientCapabilities?.Sampling is null) { return "This operation requires a client with LLM sampling capability. " + "Please use a client that supports the 'sampling' capability."; } // Proceed with sampling}