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:
parent
d65ddce268
commit
7d77be68cf
27 changed files with 231 additions and 219 deletions
|
|
@ -176,7 +176,7 @@ builder.Services
|
|||
{
|
||||
if (ctx.Params?.Level is null)
|
||||
{
|
||||
throw new McpException("Missing required argument 'level'");
|
||||
throw new McpException("Missing required argument 'level'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
|
||||
_minimumLoggingLevel = ctx.Params.Level;
|
||||
|
|
|
|||
|
|
@ -155,10 +155,10 @@ internal sealed partial class McpClient : McpEndpoint, IMcpClient
|
|||
new JsonRpcNotification { Method = NotificationMethods.InitializedNotification },
|
||||
initializationCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested)
|
||||
catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
LogClientInitializationTimeout(EndpointName);
|
||||
throw new McpException("Initialization timed out", oce);
|
||||
throw new TimeoutException("Initialization timed out", oce);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
|
|||
49
src/ModelContextProtocol/McpErrorCode.cs
Normal file
49
src/ModelContextProtocol/McpErrorCode.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
namespace ModelContextProtocol;
|
||||
|
||||
/// <summary>
|
||||
/// Represents standard JSON-RPC error codes as defined in the MCP specification.
|
||||
/// </summary>
|
||||
public enum McpErrorCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that the JSON received could not be parsed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This error occurs when the input contains malformed JSON or incorrect syntax.
|
||||
/// </remarks>
|
||||
ParseError = -32700,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the JSON payload does not conform to the expected Request object structure.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The request is considered invalid if it lacks required fields or fails to follow the JSON-RPC protocol.
|
||||
/// </remarks>
|
||||
InvalidRequest = -32600,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the requested method does not exist or is not available on the server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This error is returned when the method name specified in the request cannot be found.
|
||||
/// </remarks>
|
||||
MethodNotFound = -32601,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that one or more parameters provided in the request are invalid.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This error is returned when the parameters do not match the expected method signature or constraints.
|
||||
/// This includes cases where required parameters are missing or not understood, such as when a name for
|
||||
/// a tool or prompt is not recognized.
|
||||
/// </remarks>
|
||||
InvalidParams = -32602,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that an internal error occurred while processing the request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This error is used when the endpoint encounters an unexpected condition that prevents it from fulfilling the request.
|
||||
/// </remarks>
|
||||
InternalError = -32603,
|
||||
}
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
namespace ModelContextProtocol;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an exception that is thrown when a Model Context Protocol (MCP) error occurs.
|
||||
/// Represents an exception that is thrown when an Model Context Protocol (MCP) error occurs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This exception is used to represent failures to do with protocol-level concerns, such as invalid JSON-RPC requests,
|
||||
/// invalid parameters, or internal errors. It is not intended to be used for application-level errors.
|
||||
/// <see cref="Exception.Message"/> or <see cref="ErrorCode"/> from a <see cref="McpException"/> may be
|
||||
/// propagated to the remote endpoint; sensitive information should not be included. If sensitive details need
|
||||
/// to be included, a different exception type should be used.
|
||||
/// </remarks>
|
||||
public class McpException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -33,8 +40,8 @@ public class McpException : Exception
|
|||
/// Initializes a new instance of the <see cref="McpException"/> class with a specified error message and JSON-RPC error code.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
/// <param name="errorCode">A JSON-RPC error code from <see cref="Protocol.Messages.ErrorCodes"/> class.</param>
|
||||
public McpException(string message, int? errorCode) : this(message, null, errorCode)
|
||||
/// <param name="errorCode">A <see cref="McpErrorCode"/>.</param>
|
||||
public McpException(string message, McpErrorCode errorCode) : this(message, null, errorCode)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -43,18 +50,15 @@ public class McpException : Exception
|
|||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
|
||||
/// <param name="errorCode">A JSON-RPC error code from <see cref="Protocol.Messages.ErrorCodes"/> class.</param>
|
||||
public McpException(string message, Exception? innerException, int? errorCode) : base(message, innerException)
|
||||
/// <param name="errorCode">A <see cref="McpErrorCode"/>.</param>
|
||||
public McpException(string message, Exception? innerException, McpErrorCode errorCode) : base(message, innerException)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the JSON-RPC error code associated with this exception.
|
||||
/// Gets the error code associated with this exception.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A standard JSON-RPC error code, or <see langword="null"/> if the exception wasn't caused by a JSON-RPC error.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// This property contains a standard JSON-RPC error code as defined in the MCP specification. Common error codes include:
|
||||
/// <list type="bullet">
|
||||
|
|
@ -65,5 +69,5 @@ public class McpException : Exception
|
|||
/// <item><description>-32603: Internal error - Internal JSON-RPC error</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public int? ErrorCode { get; }
|
||||
public McpErrorCode ErrorCode { get; } = McpErrorCode.InternalError;
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Standard JSON-RPC error codes as defined in the MCP specification.
|
||||
/// </summary>
|
||||
internal static class ErrorCodes
|
||||
{
|
||||
/// <summary>
|
||||
/// Invalid JSON was received by the server.
|
||||
/// </summary>
|
||||
public const int ParseError = -32700;
|
||||
|
||||
/// <summary>
|
||||
/// The JSON sent is not a valid Request object.
|
||||
/// </summary>
|
||||
public const int InvalidRequest = -32600;
|
||||
|
||||
/// <summary>
|
||||
/// The method does not exist / is not available.
|
||||
/// </summary>
|
||||
public const int MethodNotFound = -32601;
|
||||
|
||||
/// <summary>
|
||||
/// Invalid method parameter(s).
|
||||
/// </summary>
|
||||
public const int InvalidParams = -32602;
|
||||
|
||||
/// <summary>
|
||||
/// Internal JSON-RPC error.
|
||||
/// </summary>
|
||||
public const int InternalError = -32603;
|
||||
}
|
||||
|
|
@ -42,6 +42,6 @@ public interface IClientTransport
|
|||
/// This method is used by <see cref="McpClientFactory"/> to initialize the connection.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="McpTransportException">The transport connection could not be established.</exception>
|
||||
/// <exception cref="InvalidOperationException">The transport connection could not be established.</exception>
|
||||
Task<ITransport> ConnectAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
namespace ModelContextProtocol.Protocol.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Represents errors that occur in MCP transport operations.
|
||||
/// </summary>
|
||||
public class McpTransportException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpTransportException"/> class.
|
||||
/// </summary>
|
||||
public McpTransportException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpTransportException"/> class with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public McpTransportException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpTransportException"/> class with a specified error message
|
||||
/// and a reference to the inner exception that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception.</param>
|
||||
public McpTransportException(string message, Exception? innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -58,11 +58,11 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
|
||||
await _connectionEstablished.Task.WaitAsync(_options.ConnectionTimeout, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (ex is not McpTransportException) // propagate transport exceptions
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogTransportConnectFailed(Name, ex);
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
throw new McpTransportException("Failed to connect transport", ex);
|
||||
throw new InvalidOperationException("Failed to connect transport", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
else
|
||||
{
|
||||
JsonRpcResponse initializeResponse = JsonSerializer.Deserialize(responseContent, McpJsonUtilities.JsonContext.Default.JsonRpcResponse) ??
|
||||
throw new McpTransportException("Failed to initialize client");
|
||||
throw new InvalidOperationException("Failed to initialize client");
|
||||
|
||||
LogTransportReceivedMessage(Name, messageId);
|
||||
await WriteMessageAsync(initializeResponse, cancellationToken).ConfigureAwait(false);
|
||||
|
|
@ -136,7 +136,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
LogRejectedPost(Name, messageId);
|
||||
}
|
||||
|
||||
throw new McpTransportException("Failed to send message");
|
||||
throw new InvalidOperationException("Failed to send message");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -273,34 +273,18 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
|
||||
private void HandleEndpointEvent(string data)
|
||||
{
|
||||
try
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
LogTransportEndpointEventInvalid(Name);
|
||||
return;
|
||||
}
|
||||
|
||||
// If data is an absolute URL, the Uri will be constructed entirely from it and not the _sseEndpoint.
|
||||
_messageEndpoint = new Uri(_sseEndpoint, data);
|
||||
|
||||
// Set connected state
|
||||
SetConnected(true);
|
||||
_connectionEstablished.TrySetResult(true);
|
||||
LogTransportEndpointEventInvalid(Name);
|
||||
return;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
LogTransportEndpointEventParseFailedSensitive(Name, data, ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogTransportEndpointEventParseFailed(Name, ex);
|
||||
}
|
||||
|
||||
throw new McpTransportException("Failed to parse endpoint event", ex);
|
||||
}
|
||||
// If data is an absolute URL, the Uri will be constructed entirely from it and not the _sseEndpoint.
|
||||
_messageEndpoint = new Uri(_sseEndpoint, data);
|
||||
|
||||
// Set connected state
|
||||
SetConnected(true);
|
||||
_connectionEstablished.TrySetResult(true);
|
||||
}
|
||||
|
||||
private void CopyAdditionalHeaders(HttpRequestHeaders headers)
|
||||
|
|
|
|||
|
|
@ -23,14 +23,14 @@ internal sealed class StdioClientSessionTransport : StreamClientSessionTransport
|
|||
/// <para>
|
||||
/// For stdio-based transports, this implementation first verifies that the underlying process
|
||||
/// is still running before attempting to send the message. If the process has exited or cannot
|
||||
/// be accessed, a <see cref="McpTransportException"/> is thrown with details about the failure.
|
||||
/// be accessed, a <see cref="InvalidOperationException"/> is thrown with details about the failure.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// After verifying the process state, this method delegates to the base class implementation
|
||||
/// to handle the actual message serialization and transmission to the process's standard input stream.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="McpTransportException">
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the underlying process has exited or cannot be accessed.
|
||||
/// </exception>
|
||||
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
|
|
@ -49,7 +49,7 @@ internal sealed class StdioClientSessionTransport : StreamClientSessionTransport
|
|||
|
||||
if (hasExited)
|
||||
{
|
||||
throw new McpTransportException("Transport is not connected", processException);
|
||||
throw new InvalidOperationException("Transport is not connected", processException);
|
||||
}
|
||||
|
||||
await base.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ public sealed partial class StdioClientTransport : IClientTransport
|
|||
if (!processStarted)
|
||||
{
|
||||
LogTransportProcessStartFailed(logger, endpointName);
|
||||
throw new McpTransportException("Failed to start MCP server process");
|
||||
throw new InvalidOperationException("Failed to start MCP server process");
|
||||
}
|
||||
|
||||
LogTransportProcessStarted(logger, endpointName, process.Id);
|
||||
|
|
@ -176,7 +176,7 @@ public sealed partial class StdioClientTransport : IClientTransport
|
|||
LogTransportShutdownFailed(logger, endpointName, ex2);
|
||||
}
|
||||
|
||||
throw new McpTransportException("Failed to connect transport", ex);
|
||||
throw new InvalidOperationException("Failed to connect transport", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,28 +57,11 @@ internal class StreamClientSessionTransport : TransportBase
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For stream-based transports, this implementation serializes the JSON-RPC message to the
|
||||
/// underlying output stream. The specific serialization format includes:
|
||||
/// <list type="bullet">
|
||||
/// <item>A Content-Length header that specifies the byte length of the JSON message</item>
|
||||
/// <item>A blank line separator</item>
|
||||
/// <item>The UTF-8 encoded JSON representation of the message</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This implementation first checks if the transport is connected and throws a <see cref="McpTransportException"/>
|
||||
/// if it's not. It then extracts the message ID (if present) for logging purposes, serializes the message,
|
||||
/// and writes it to the output stream.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="McpTransportException">Thrown when the transport is not connected.</exception>
|
||||
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
throw new McpTransportException("Transport is not connected");
|
||||
throw new InvalidOperationException("Transport is not connected");
|
||||
}
|
||||
|
||||
string id = "(no id)";
|
||||
|
|
@ -99,7 +82,7 @@ internal class StreamClientSessionTransport : TransportBase
|
|||
catch (Exception ex)
|
||||
{
|
||||
LogTransportSendFailed(Name, id, ex);
|
||||
throw new McpTransportException("Failed to send message", ex);
|
||||
throw new InvalidOperationException("Failed to send message", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ public class StreamServerTransport : TransportBase
|
|||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
throw new McpTransportException("Transport is not connected");
|
||||
throw new InvalidOperationException("Transport is not connected");
|
||||
}
|
||||
|
||||
using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
|
@ -80,7 +80,7 @@ public class StreamServerTransport : TransportBase
|
|||
catch (Exception ex)
|
||||
{
|
||||
LogTransportSendFailed(Name, id, ex);
|
||||
throw new McpTransportException("Failed to send message", ex);
|
||||
throw new InvalidOperationException("Failed to send message", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ public abstract partial class TransportBase : ITransport
|
|||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
throw new McpTransportException("Transport is not connected");
|
||||
throw new InvalidOperationException("Transport is not connected");
|
||||
}
|
||||
|
||||
await _messageChannel.Writer.WriteAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
|
|
|
|||
|
|
@ -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.")),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" }],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -142,15 +142,24 @@ internal sealed partial class McpSession : IDisposable
|
|||
if (!isUserCancellation && message is JsonRpcRequest request)
|
||||
{
|
||||
LogRequestHandlerException(EndpointName, request.Method, ex);
|
||||
|
||||
JsonRpcErrorDetail detail = ex is McpException mcpe ?
|
||||
new()
|
||||
{
|
||||
Code = (int)mcpe.ErrorCode,
|
||||
Message = mcpe.Message,
|
||||
} :
|
||||
new()
|
||||
{
|
||||
Code = (int)McpErrorCode.InternalError,
|
||||
Message = "An error occurred.",
|
||||
};
|
||||
|
||||
await _transport.SendMessageAsync(new JsonRpcError
|
||||
{
|
||||
Id = request.Id,
|
||||
JsonRpc = "2.0",
|
||||
Error = new JsonRpcErrorDetail
|
||||
{
|
||||
Code = (ex as McpException)?.ErrorCode ?? ErrorCodes.InternalError,
|
||||
Message = ex.Message
|
||||
}
|
||||
Error = detail,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (ex is not OperationCanceledException)
|
||||
|
|
@ -287,7 +296,7 @@ internal sealed partial class McpSession : IDisposable
|
|||
if (!_requestHandlers.TryGetValue(request.Method, out var handler))
|
||||
{
|
||||
LogNoHandlerFoundForRequest(EndpointName, request.Method);
|
||||
throw new McpException("The method does not exist or is not available.", ErrorCodes.MethodNotFound);
|
||||
throw new McpException($"Method '{request.Method}' is not available.", McpErrorCode.MethodNotFound);
|
||||
}
|
||||
|
||||
LogRequestHandlerCalled(EndpointName, request.Method);
|
||||
|
|
@ -342,7 +351,7 @@ internal sealed partial class McpSession : IDisposable
|
|||
{
|
||||
if (!_transport.IsConnected)
|
||||
{
|
||||
throw new McpException("Transport is not connected");
|
||||
throw new InvalidOperationException("Transport is not connected");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
|
@ -399,7 +408,7 @@ internal sealed partial class McpSession : IDisposable
|
|||
if (response is JsonRpcError error)
|
||||
{
|
||||
LogSendingRequestFailed(EndpointName, request.Method, error.Error.Message, error.Error.Code);
|
||||
throw new McpException($"Request failed (remote): {error.Error.Message}", error.Error.Code);
|
||||
throw new McpException($"Request failed (remote): {error.Error.Message}", (McpErrorCode)error.Error.Code);
|
||||
}
|
||||
|
||||
if (response is JsonRpcResponse success)
|
||||
|
|
@ -443,7 +452,7 @@ internal sealed partial class McpSession : IDisposable
|
|||
|
||||
if (!_transport.IsConnected)
|
||||
{
|
||||
throw new McpException("Transport is not connected");
|
||||
throw new InvalidOperationException("Transport is not connected");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
|
@ -591,13 +600,16 @@ internal sealed partial class McpSession : IDisposable
|
|||
e = ae.InnerException;
|
||||
}
|
||||
|
||||
int? intErrorCode = (e as McpException)?.ErrorCode is int errorCode ? errorCode :
|
||||
e is JsonException ? ErrorCodes.ParseError : null;
|
||||
int? intErrorCode =
|
||||
(int?)((e as McpException)?.ErrorCode) is int errorCode ? errorCode :
|
||||
e is JsonException ? (int)McpErrorCode.ParseError :
|
||||
null;
|
||||
|
||||
tags.Add("error.type", intErrorCode == null ? e.GetType().FullName : intErrorCode.ToString());
|
||||
string? errorType = intErrorCode?.ToString() ?? e.GetType().FullName;
|
||||
tags.Add("error.type", errorType);
|
||||
if (intErrorCode is not null)
|
||||
{
|
||||
tags.Add("rpc.jsonrpc.error_code", intErrorCode.ToString());
|
||||
tags.Add("rpc.jsonrpc.error_code", errorType);
|
||||
}
|
||||
|
||||
if (activity is { IsAllDataRequested: true })
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ internal static class Program
|
|||
{
|
||||
if (request.Params?.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message))
|
||||
{
|
||||
throw new McpException("Missing required argument 'message'");
|
||||
throw new McpException("Missing required argument 'message'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
return new CallToolResponse()
|
||||
{
|
||||
|
|
@ -179,7 +179,7 @@ internal static class Program
|
|||
!request.Params.Arguments.TryGetValue("prompt", out var prompt) ||
|
||||
!request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens))
|
||||
{
|
||||
throw new McpException("Missing required arguments 'prompt' and 'maxTokens'");
|
||||
throw new McpException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
var sampleResult = await request.Server.RequestSamplingAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.GetRawText())),
|
||||
cancellationToken);
|
||||
|
|
@ -191,7 +191,7 @@ internal static class Program
|
|||
}
|
||||
else
|
||||
{
|
||||
throw new McpException($"Unknown tool: {request.Params?.Name}");
|
||||
throw new McpException($"Unknown tool: {request.Params?.Name}", McpErrorCode.InvalidParams);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -285,7 +285,7 @@ internal static class Program
|
|||
}
|
||||
else
|
||||
{
|
||||
throw new McpException($"Unknown prompt: {request.Params?.Name}");
|
||||
throw new McpException($"Unknown prompt: {request.Params?.Name}", McpErrorCode.InvalidParams);
|
||||
}
|
||||
|
||||
return new GetPromptResult()
|
||||
|
|
@ -306,7 +306,7 @@ internal static class Program
|
|||
{
|
||||
if (request.Params?.Level is null)
|
||||
{
|
||||
throw new McpException("Missing required argument 'level'");
|
||||
throw new McpException("Missing required argument 'level'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
|
||||
_minimumLoggingLevel = request.Params.Level;
|
||||
|
|
@ -388,7 +388,7 @@ internal static class Program
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new McpException("Invalid cursor.", e);
|
||||
throw new McpException($"Invalid cursor: '{request.Params.Cursor}'", e, McpErrorCode.InvalidParams);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -410,7 +410,7 @@ internal static class Program
|
|||
{
|
||||
if (request.Params?.Uri is null)
|
||||
{
|
||||
throw new McpException("Missing required argument 'uri'");
|
||||
throw new McpException("Missing required argument 'uri'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
|
||||
if (request.Params.Uri.StartsWith("test://dynamic/resource/"))
|
||||
|
|
@ -418,7 +418,7 @@ internal static class Program
|
|||
var id = request.Params.Uri.Split('/').LastOrDefault();
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
throw new McpException("Invalid resource URI");
|
||||
throw new McpException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
|
||||
return new ReadResourceResult()
|
||||
|
|
@ -435,7 +435,7 @@ internal static class Program
|
|||
}
|
||||
|
||||
ResourceContents contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri)
|
||||
?? throw new McpException("Resource not found");
|
||||
?? throw new McpException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.InvalidParams);
|
||||
|
||||
return new ReadResourceResult()
|
||||
{
|
||||
|
|
@ -447,12 +447,12 @@ internal static class Program
|
|||
{
|
||||
if (request?.Params?.Uri is null)
|
||||
{
|
||||
throw new McpException("Missing required argument 'uri'");
|
||||
throw new McpException("Missing required argument 'uri'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
if (!request.Params.Uri.StartsWith("test://static/resource/")
|
||||
&& !request.Params.Uri.StartsWith("test://dynamic/resource/"))
|
||||
{
|
||||
throw new McpException("Invalid resource URI");
|
||||
throw new McpException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
|
||||
_subscribedResources.TryAdd(request.Params.Uri, true);
|
||||
|
|
@ -464,12 +464,12 @@ internal static class Program
|
|||
{
|
||||
if (request?.Params?.Uri is null)
|
||||
{
|
||||
throw new McpException("Missing required argument 'uri'");
|
||||
throw new McpException("Missing required argument 'uri'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
if (!request.Params.Uri.StartsWith("test://static/resource/")
|
||||
&& !request.Params.Uri.StartsWith("test://dynamic/resource/"))
|
||||
{
|
||||
throw new McpException("Invalid resource URI");
|
||||
throw new McpException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
|
||||
_subscribedResources.TryRemove(request.Params.Uri, out _);
|
||||
|
|
@ -514,7 +514,7 @@ internal static class Program
|
|||
return new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } };
|
||||
}
|
||||
|
||||
throw new McpException($"Unknown reference type: {request.Params?.Ref.Type}");
|
||||
throw new McpException($"Unknown reference type: '{request.Params?.Ref.Type}'", McpErrorCode.InvalidParams);
|
||||
};
|
||||
|
||||
return new() { CompleteHandler = handler };
|
||||
|
|
|
|||
|
|
@ -157,13 +157,13 @@ public class Program
|
|||
{
|
||||
if (request.Params is null)
|
||||
{
|
||||
throw new McpException("Missing required parameter 'name'");
|
||||
throw new McpException("Missing required parameter 'name'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
if (request.Params.Name == "echo")
|
||||
{
|
||||
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message))
|
||||
{
|
||||
throw new McpException("Missing required argument 'message'");
|
||||
throw new McpException("Missing required argument 'message'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
return new CallToolResponse()
|
||||
{
|
||||
|
|
@ -176,7 +176,7 @@ public class Program
|
|||
!request.Params.Arguments.TryGetValue("prompt", out var prompt) ||
|
||||
!request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens))
|
||||
{
|
||||
throw new McpException("Missing required arguments 'prompt' and 'maxTokens'");
|
||||
throw new McpException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
var sampleResult = await request.Server.RequestSamplingAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())),
|
||||
cancellationToken);
|
||||
|
|
@ -188,7 +188,7 @@ public class Program
|
|||
}
|
||||
else
|
||||
{
|
||||
throw new McpException($"Unknown tool: {request.Params.Name}");
|
||||
throw new McpException($"Unknown tool: '{request.Params.Name}'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -220,9 +220,9 @@ public class Program
|
|||
var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(requestParams.Cursor));
|
||||
startIndex = Convert.ToInt32(startIndexAsString);
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new McpException("Invalid cursor");
|
||||
throw new McpException($"Invalid cursor: '{requestParams.Cursor}'", e, McpErrorCode.InvalidParams);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +244,7 @@ public class Program
|
|||
{
|
||||
if (request.Params?.Uri is null)
|
||||
{
|
||||
throw new McpException("Missing required argument 'uri'");
|
||||
throw new McpException("Missing required argument 'uri'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
|
||||
if (request.Params.Uri.StartsWith("test://dynamic/resource/"))
|
||||
|
|
@ -252,7 +252,7 @@ public class Program
|
|||
var id = request.Params.Uri.Split('/').LastOrDefault();
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
throw new McpException("Invalid resource URI");
|
||||
throw new McpException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
|
||||
return new ReadResourceResult()
|
||||
|
|
@ -269,7 +269,7 @@ public class Program
|
|||
}
|
||||
|
||||
ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) ??
|
||||
throw new McpException("Resource not found");
|
||||
throw new McpException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.InvalidParams);
|
||||
|
||||
return new ReadResourceResult()
|
||||
{
|
||||
|
|
@ -316,7 +316,7 @@ public class Program
|
|||
{
|
||||
if (request.Params is null)
|
||||
{
|
||||
throw new McpException("Missing required parameter 'name'");
|
||||
throw new McpException("Missing required parameter 'name'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
List<PromptMessage> messages = new();
|
||||
if (request.Params.Name == "simple_prompt")
|
||||
|
|
@ -366,7 +366,7 @@ public class Program
|
|||
}
|
||||
else
|
||||
{
|
||||
throw new McpException($"Unknown prompt: {request.Params.Name}");
|
||||
throw new McpException($"Unknown prompt: {request.Params.Name}", McpErrorCode.InvalidParams);
|
||||
}
|
||||
|
||||
return new GetPromptResult()
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue