Clean up (use of) McpException and McpTransportException (#321)

* Clean up (use of) McpException

* Replace McpTransportException with InvalidOperationException

* Add cancellation test
This commit is contained in:
Stephen Toub 2025-04-18 12:25:29 -04:00 • committed by GitHub
commit 7d77be68cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 231 additions and 219 deletions

View file

@ -101,7 +101,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
BindParameter = (pi, args) =>
GetRequestContext(args)?.Services?.GetService(pi.ParameterType) ??
(pi.HasDefaultValue ? null :
throw new ArgumentException("No service of the requested type was found.")),
throw new InvalidOperationException("No service of the requested type was found.")),
};
}
@ -113,7 +113,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
BindParameter = (pi, args) =>
(GetRequestContext(args)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
(pi.HasDefaultValue ? null :
throw new ArgumentException("No service of the requested type was found.")),
throw new InvalidOperationException("No service of the requested type was found.")),
};
}

View file

@ -122,7 +122,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
BindParameter = (pi, args) =>
GetRequestContext(args)?.Services?.GetService(pi.ParameterType) ??
(pi.HasDefaultValue ? null :
throw new ArgumentException("No service of the requested type was found.")),
throw new InvalidOperationException("No service of the requested type was found.")),
};
}
@ -134,7 +134,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
BindParameter = (pi, args) =>
(GetRequestContext(args)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
(pi.HasDefaultValue ? null :
throw new ArgumentException("No service of the requested type was found.")),
throw new InvalidOperationException("No service of the requested type was found.")),
};
}
@ -265,10 +265,14 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
}
catch (Exception e) when (e is not OperationCanceledException)
{
return new CallToolResponse()
string errorMessage = e is McpException ?
$"An error occurred invoking '{request.Params?.Name}': {e.Message}" :
$"An error occurred invoking '{request.Params?.Name}'.";
return new()
{
IsError = true,
Content = [new() { Text = $"An error occurred invoking '{request.Params?.Name}'.", Type = "text" }],
Content = [new() { Text = errorMessage, Type = "text" }],
};
}

View file

@ -196,7 +196,9 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
}
var completeHandler = completionsCapability.CompleteHandler ??
throw new McpException("Completions capability was enabled, but Complete handler was not specified.");
throw new InvalidOperationException(
$"{nameof(ServerCapabilities)}.{nameof(ServerCapabilities.Completions)} was enabled, " +
$"but {nameof(CompletionsCapability.CompleteHandler)} was not specified.");
// This capability is not optional, so return an empty result if there is no handler.
RequestHandlers.Set(
@ -219,7 +221,9 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
if ((listResourcesHandler is not { } && listResourceTemplatesHandler is not { }) ||
resourcesCapability.ReadResourceHandler is not { } readResourceHandler)
{
throw new McpException("Resources capability was enabled, but ListResources and/or ReadResource handlers were not specified.");
throw new InvalidOperationException(
$"{nameof(ServerCapabilities)}.{nameof(ServerCapabilities.Resources)} was enabled, " +
$"but {nameof(ResourcesCapability.ListResourcesHandler)} or {nameof(ResourcesCapability.ReadResourceHandler)} was not specified.");
}
listResourcesHandler ??= static async (_, _) => new ListResourcesResult();
@ -252,7 +256,9 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
var unsubscribeHandler = resourcesCapability.UnsubscribeFromResourcesHandler;
if (subscribeHandler is null || unsubscribeHandler is null)
{
throw new McpException("Resources capability was enabled with subscribe support, but SubscribeToResources and/or UnsubscribeFromResources handlers were not specified.");
throw new InvalidOperationException(
$"{nameof(ServerCapabilities)}.{nameof(ServerCapabilities.Resources)}.{nameof(ResourcesCapability.Subscribe)} is set, " +
$"but {nameof(ResourcesCapability.SubscribeToResourcesHandler)} or {nameof(ResourcesCapability.UnsubscribeFromResourcesHandler)} was not specified.");
}
RequestHandlers.Set(
@ -277,7 +283,10 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
if (listPromptsHandler is null != getPromptHandler is null)
{
throw new McpException("ListPrompts and GetPrompt handlers should be specified together.");
throw new InvalidOperationException(
$"{nameof(PromptsCapability)}.{nameof(promptsCapability.ListPromptsHandler)} or " +
$"{nameof(PromptsCapability)}.{nameof(promptsCapability.GetPromptHandler)} was specified without the other. " +
$"Both or neither must be provided.");
}
// Handle prompts provided via DI.
@ -310,7 +319,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
return originalGetPromptHandler(request, cancellationToken);
}
throw new McpException($"Unknown prompt '{request.Params?.Name}'");
throw new McpException($"Unknown prompt: '{request.Params?.Name}'", McpErrorCode.InvalidParams);
}
return prompt.GetAsync(request, cancellationToken);
@ -344,7 +353,9 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
// Make sure the handlers are provided if the capability is enabled.
if (listPromptsHandler is null || getPromptHandler is null)
{
throw new McpException("ListPrompts and/or GetPrompt handlers were not specified but the Prompts capability was enabled.");
throw new InvalidOperationException(
$"{nameof(ServerCapabilities)}.{nameof(ServerCapabilities.Prompts)} was enabled, " +
$"but {nameof(PromptsCapability.ListPromptsHandler)} or {nameof(PromptsCapability.GetPromptHandler)} was not specified.");
}
}
@ -370,7 +381,10 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
if (listToolsHandler is null != callToolHandler is null)
{
throw new McpException("ListTools and CallTool handlers should be specified together.");
throw new InvalidOperationException(
$"{nameof(ToolsCapability)}.{nameof(ToolsCapability.ListToolsHandler)} or " +
$"{nameof(ToolsCapability)}.{nameof(ToolsCapability.CallToolHandler)} was specified without the other. " +
$"Both or neither must be provided.");
}
// Handle tools provided via DI.
@ -403,7 +417,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
return originalCallToolHandler(request, cancellationToken);
}
throw new McpException($"Unknown tool '{request.Params?.Name}'");
throw new McpException($"Unknown tool: '{request.Params?.Name}'", McpErrorCode.InvalidParams);
}
return tool.InvokeAsync(request, cancellationToken);
@ -437,7 +451,9 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
// Make sure the handlers are provided if the capability is enabled.
if (listToolsHandler is null || callToolHandler is null)
{
throw new McpException("ListTools and/or CallTool handlers were not specified but the Tools capability was enabled.");
throw new InvalidOperationException(
$"{nameof(ServerCapabilities)}.{nameof(ServerCapabilities.Tools)} was enabled, " +
$"but {nameof(ToolsCapability.ListToolsHandler)} or {nameof(ToolsCapability.CallToolHandler)} was not specified.");
}
}

View file

@ -36,7 +36,7 @@ public static class McpServerExtensions
if (server.ClientCapabilities?.Sampling is null)
{
throw new ArgumentException("Client connected to the server does not support sampling.", nameof(server));
throw new InvalidOperationException("Client does not support sampling.");
}
return server.SendRequestAsync(
@ -166,7 +166,7 @@ public static class McpServerExtensions
if (server.ClientCapabilities?.Sampling is null)
{
throw new ArgumentException("Client connected to the server does not support sampling.", nameof(server));
throw new InvalidOperationException("Client does not support sampling.");
}
return new SamplingChatClient(server);
@ -204,7 +204,7 @@ public static class McpServerExtensions
if (server.ClientCapabilities?.Roots is null)
{
throw new ArgumentException("Client connected to the server does not support roots.", nameof(server));
throw new InvalidOperationException("Client does not support roots.");
}
return server.SendRequestAsync(