Address asymmetry in McpServerHandlers/McpClientHandlers and make all filter properties settable (#1337)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
parent
4f7095453c
commit
a2ee0c805a
6 changed files with 432 additions and 293 deletions
|
|
@ -22,7 +22,7 @@ namespace ModelContextProtocol.Client;
|
||||||
/// is done based on an ordinal, case-sensitive string comparison.
|
/// is done based on an ordinal, case-sensitive string comparison.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class McpClientHandlers
|
public sealed class McpClientHandlers
|
||||||
{
|
{
|
||||||
/// <summary>Gets or sets notification handlers to register with the client.</summary>
|
/// <summary>Gets or sets notification handlers to register with the client.</summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace ModelContextProtocol.Server;
|
||||||
public sealed class McpMessageFilters
|
public sealed class McpMessageFilters
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for all incoming JSON-RPC messages.
|
/// Gets or sets the filters for all incoming JSON-RPC messages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -21,10 +21,18 @@ public sealed class McpMessageFilters
|
||||||
/// the next handler in the pipeline, the default handlers will not be executed.
|
/// the next handler in the pipeline, the default handlers will not be executed.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpMessageFilter> IncomingFilters { get; } = [];
|
public IList<McpMessageFilter> IncomingFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for all outgoing JSON-RPC messages.
|
/// Gets or sets the filters for all outgoing JSON-RPC messages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -38,5 +46,13 @@ public sealed class McpMessageFilters
|
||||||
/// server-to-client messages.
|
/// server-to-client messages.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpMessageFilter> OutgoingFilters { get; } = [];
|
public IList<McpMessageFilter> OutgoingFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace ModelContextProtocol.Server;
|
||||||
public sealed class McpRequestFilters
|
public sealed class McpRequestFilters
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for the list-tools handler pipeline.
|
/// Gets or sets the filters for the list-tools handler pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -22,20 +22,36 @@ public sealed class McpRequestFilters
|
||||||
/// Tools from both sources will be combined when returning results to clients.
|
/// Tools from both sources will be combined when returning results to clients.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpRequestFilter<ListToolsRequestParams, ListToolsResult>> ListToolsFilters { get; } = [];
|
public IList<McpRequestFilter<ListToolsRequestParams, ListToolsResult>> ListToolsFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for the call-tool handler pipeline.
|
/// Gets or sets the filters for the call-tool handler pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// These filters wrap handlers that are invoked when a client makes a call to a tool that isn't found in the <see cref="McpServerTool"/> collection.
|
/// These filters wrap handlers that are invoked when a client makes a call to a tool that isn't found in the <see cref="McpServerTool"/> collection.
|
||||||
/// The filters can modify, log, or perform additional operations on requests and responses for
|
/// The filters can modify, log, or perform additional operations on requests and responses for
|
||||||
/// <see cref="RequestMethods.ToolsCall"/> requests. The handler should implement logic to execute the requested tool and return appropriate results.
|
/// <see cref="RequestMethods.ToolsCall"/> requests. The handler should implement logic to execute the requested tool and return appropriate results.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpRequestFilter<CallToolRequestParams, CallToolResult>> CallToolFilters { get; } = [];
|
public IList<McpRequestFilter<CallToolRequestParams, CallToolResult>> CallToolFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for the list-prompts handler pipeline.
|
/// Gets or sets the filters for the list-prompts handler pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -49,20 +65,36 @@ public sealed class McpRequestFilters
|
||||||
/// Prompts from both sources will be combined when returning results to clients.
|
/// Prompts from both sources will be combined when returning results to clients.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpRequestFilter<ListPromptsRequestParams, ListPromptsResult>> ListPromptsFilters { get; } = [];
|
public IList<McpRequestFilter<ListPromptsRequestParams, ListPromptsResult>> ListPromptsFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for the get-prompt handler pipeline.
|
/// Gets or sets the filters for the get-prompt handler pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// These filters wrap handlers that are invoked when a client requests details for a specific prompt that isn't found in the <see cref="McpServerPrompt"/> collection.
|
/// These filters wrap handlers that are invoked when a client requests details for a specific prompt that isn't found in the <see cref="McpServerPrompt"/> collection.
|
||||||
/// The filters can modify, log, or perform additional operations on requests and responses for
|
/// The filters can modify, log, or perform additional operations on requests and responses for
|
||||||
/// <see cref="RequestMethods.PromptsGet"/> requests. The handler should implement logic to fetch or generate the requested prompt and return appropriate results.
|
/// <see cref="RequestMethods.PromptsGet"/> requests. The handler should implement logic to fetch or generate the requested prompt and return appropriate results.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpRequestFilter<GetPromptRequestParams, GetPromptResult>> GetPromptFilters { get; } = [];
|
public IList<McpRequestFilter<GetPromptRequestParams, GetPromptResult>> GetPromptFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for the list-resource-templates handler pipeline.
|
/// Gets or sets the filters for the list-resource-templates handler pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// These filters wrap handlers that return a list of available resource templates when requested by a client.
|
/// These filters wrap handlers that return a list of available resource templates when requested by a client.
|
||||||
|
|
@ -70,10 +102,18 @@ public sealed class McpRequestFilters
|
||||||
/// <see cref="RequestMethods.ResourcesTemplatesList"/> requests. It supports pagination through the cursor mechanism,
|
/// <see cref="RequestMethods.ResourcesTemplatesList"/> requests. It supports pagination through the cursor mechanism,
|
||||||
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more resource templates.
|
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more resource templates.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpRequestFilter<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>> ListResourceTemplatesFilters { get; } = [];
|
public IList<McpRequestFilter<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>> ListResourceTemplatesFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for the list-resources handler pipeline.
|
/// Gets or sets the filters for the list-resources handler pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// These filters wrap handlers that return a list of available resources when requested by a client.
|
/// These filters wrap handlers that return a list of available resources when requested by a client.
|
||||||
|
|
@ -81,20 +121,36 @@ public sealed class McpRequestFilters
|
||||||
/// <see cref="RequestMethods.ResourcesList"/> requests. It supports pagination through the cursor mechanism,
|
/// <see cref="RequestMethods.ResourcesList"/> requests. It supports pagination through the cursor mechanism,
|
||||||
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more resources.
|
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more resources.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpRequestFilter<ListResourcesRequestParams, ListResourcesResult>> ListResourcesFilters { get; } = [];
|
public IList<McpRequestFilter<ListResourcesRequestParams, ListResourcesResult>> ListResourcesFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for the read-resource handler pipeline.
|
/// Gets or sets the filters for the read-resource handler pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// These filters wrap handlers that are invoked when a client requests the content of a specific resource identified by its URI.
|
/// These filters wrap handlers that are invoked when a client requests the content of a specific resource identified by its URI.
|
||||||
/// The filters can modify, log, or perform additional operations on requests and responses for
|
/// The filters can modify, log, or perform additional operations on requests and responses for
|
||||||
/// <see cref="RequestMethods.ResourcesRead"/> requests. The handler should implement logic to locate and retrieve the requested resource.
|
/// <see cref="RequestMethods.ResourcesRead"/> requests. The handler should implement logic to locate and retrieve the requested resource.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpRequestFilter<ReadResourceRequestParams, ReadResourceResult>> ReadResourceFilters { get; } = [];
|
public IList<McpRequestFilter<ReadResourceRequestParams, ReadResourceResult>> ReadResourceFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for the complete-handler pipeline.
|
/// Gets or sets the filters for the complete-handler pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// These filters wrap handlers that provide auto-completion suggestions for prompt arguments or resource references in the Model Context Protocol.
|
/// These filters wrap handlers that provide auto-completion suggestions for prompt arguments or resource references in the Model Context Protocol.
|
||||||
|
|
@ -102,10 +158,18 @@ public sealed class McpRequestFilters
|
||||||
/// <see cref="RequestMethods.CompletionComplete"/> requests. The handler processes auto-completion requests, returning a list of suggestions based on the
|
/// <see cref="RequestMethods.CompletionComplete"/> requests. The handler processes auto-completion requests, returning a list of suggestions based on the
|
||||||
/// reference type and current argument value.
|
/// reference type and current argument value.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpRequestFilter<CompleteRequestParams, CompleteResult>> CompleteFilters { get; } = [];
|
public IList<McpRequestFilter<CompleteRequestParams, CompleteResult>> CompleteFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for the subscribe-to-resources handler pipeline.
|
/// Gets or sets the filters for the subscribe-to-resources handler pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -119,10 +183,18 @@ public sealed class McpRequestFilters
|
||||||
/// whenever a relevant resource is created, updated, or deleted.
|
/// whenever a relevant resource is created, updated, or deleted.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpRequestFilter<SubscribeRequestParams, EmptyResult>> SubscribeToResourcesFilters { get; } = [];
|
public IList<McpRequestFilter<SubscribeRequestParams, EmptyResult>> SubscribeToResourcesFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for the unsubscribe-from-resources handler pipeline.
|
/// Gets or sets the filters for the unsubscribe-from-resources handler pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -136,10 +208,18 @@ public sealed class McpRequestFilters
|
||||||
/// to the client for the specified resources.
|
/// to the client for the specified resources.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpRequestFilter<UnsubscribeRequestParams, EmptyResult>> UnsubscribeFromResourcesFilters { get; } = [];
|
public IList<McpRequestFilter<UnsubscribeRequestParams, EmptyResult>> UnsubscribeFromResourcesFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for the set-logging-level handler pipeline.
|
/// Gets or sets the filters for the set-logging-level handler pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -153,5 +233,13 @@ public sealed class McpRequestFilters
|
||||||
/// at or above the specified level to the client as notifications/message notifications.
|
/// at or above the specified level to the client as notifications/message notifications.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<McpRequestFilter<SetLevelRequestParams, EmptyResult>> SetLoggingLevelFilters { get; } = [];
|
public IList<McpRequestFilter<SetLevelRequestParams, EmptyResult>> SetLoggingLevelFilters
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,28 @@ namespace ModelContextProtocol.Server;
|
||||||
public sealed class McpServerFilters
|
public sealed class McpServerFilters
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for incoming and outgoing JSON-RPC messages.
|
/// Gets or sets the filters for incoming and outgoing JSON-RPC messages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public McpMessageFilters Message { get; } = new();
|
public McpMessageFilters Message
|
||||||
|
{
|
||||||
|
get => field ??= new();
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filters for request-specific MCP handler pipelines.
|
/// Gets or sets the filters for request-specific MCP handler pipelines.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public McpRequestFilters Request { get; } = new();
|
public McpRequestFilters Request
|
||||||
|
{
|
||||||
|
get => field ??= new();
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,17 +97,33 @@ public sealed class McpServerOptions
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the container of handlers used by the server for processing protocol messages.
|
/// Gets or sets the container of handlers used by the server for processing protocol messages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public McpServerHandlers Handlers { get; } = new();
|
public McpServerHandlers Handlers
|
||||||
|
{
|
||||||
|
get => field ??= new();
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the filter collections for MCP server handlers.
|
/// Gets or sets the filter collections for MCP server handlers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This property provides access to filter collections that can be used to modify the behavior
|
/// This property provides access to filter collections that can be used to modify the behavior
|
||||||
/// of various MCP server handlers. The first filter added is the outermost (first to execute),
|
/// of various MCP server handlers. The first filter added is the outermost (first to execute),
|
||||||
/// and each subsequent filter wraps closer to the handler.
|
/// and each subsequent filter wraps closer to the handler.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public McpServerFilters Filters { get; } = new();
|
public McpServerFilters Filters
|
||||||
|
{
|
||||||
|
get => field ??= new();
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a collection of tools served by the server.
|
/// Gets or sets a collection of tools served by the server.
|
||||||
|
|
|
||||||
|
|
@ -90,313 +90,316 @@ public class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
const int pageSize = 10;
|
const int pageSize = 10;
|
||||||
options.Handlers.ListToolsHandler = async (request, cancellationToken) =>
|
options.Handlers = new()
|
||||||
{
|
{
|
||||||
return new ListToolsResult
|
ListToolsHandler = async (request, cancellationToken) =>
|
||||||
{
|
{
|
||||||
Tools =
|
return new ListToolsResult
|
||||||
[
|
{
|
||||||
new Tool
|
Tools =
|
||||||
{
|
[
|
||||||
Name = "echo",
|
new Tool
|
||||||
Description = "Echoes the input back to the client.",
|
|
||||||
InputSchema = JsonElement.Parse("""
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The input to echo back."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["message"]
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
},
|
|
||||||
new Tool
|
|
||||||
{
|
|
||||||
Name = "echoSessionId",
|
|
||||||
Description = "Echoes the session id back to the client.",
|
|
||||||
InputSchema = JsonElement.Parse("""
|
|
||||||
{
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
},
|
|
||||||
new Tool
|
|
||||||
{
|
|
||||||
Name = "sampleLLM",
|
|
||||||
Description = "Samples from an LLM using MCP's sampling feature.",
|
|
||||||
InputSchema = JsonElement.Parse("""
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"prompt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The prompt to send to the LLM"
|
|
||||||
},
|
|
||||||
"maxTokens": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of tokens to generate"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["prompt", "maxTokens"]
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
},
|
|
||||||
new Tool
|
|
||||||
{
|
|
||||||
Name = "longRunning",
|
|
||||||
Description = "Simulates a long-running operation that supports task-based execution.",
|
|
||||||
InputSchema = JsonElement.Parse("""
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"durationMs": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Duration of the operation in milliseconds"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["durationMs"]
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Execution = new ToolExecution
|
|
||||||
{
|
{
|
||||||
TaskSupport = ToolTaskSupport.Optional
|
Name = "echo",
|
||||||
|
Description = "Echoes the input back to the client.",
|
||||||
|
InputSchema = JsonElement.Parse("""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The input to echo back."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["message"]
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
},
|
||||||
|
new Tool
|
||||||
|
{
|
||||||
|
Name = "echoSessionId",
|
||||||
|
Description = "Echoes the session id back to the client.",
|
||||||
|
InputSchema = JsonElement.Parse("""
|
||||||
|
{
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
},
|
||||||
|
new Tool
|
||||||
|
{
|
||||||
|
Name = "sampleLLM",
|
||||||
|
Description = "Samples from an LLM using MCP's sampling feature.",
|
||||||
|
InputSchema = JsonElement.Parse("""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"prompt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The prompt to send to the LLM"
|
||||||
|
},
|
||||||
|
"maxTokens": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum number of tokens to generate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["prompt", "maxTokens"]
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
},
|
||||||
|
new Tool
|
||||||
|
{
|
||||||
|
Name = "longRunning",
|
||||||
|
Description = "Simulates a long-running operation that supports task-based execution.",
|
||||||
|
InputSchema = JsonElement.Parse("""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"durationMs": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Duration of the operation in milliseconds"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["durationMs"]
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Execution = new ToolExecution
|
||||||
|
{
|
||||||
|
TaskSupport = ToolTaskSupport.Optional
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
CallToolHandler = async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
if (request.Params is null)
|
||||||
|
{
|
||||||
|
throw new McpProtocolException("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 McpProtocolException("Missing required argument 'message'", McpErrorCode.InvalidParams);
|
||||||
}
|
}
|
||||||
]
|
return new CallToolResult
|
||||||
};
|
{
|
||||||
};
|
Content = [new TextContentBlock { Text = $"Echo: {message}" }]
|
||||||
|
};
|
||||||
options.Handlers.CallToolHandler = async (request, cancellationToken) =>
|
|
||||||
{
|
|
||||||
if (request.Params is null)
|
|
||||||
{
|
|
||||||
throw new McpProtocolException("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 McpProtocolException("Missing required argument 'message'", McpErrorCode.InvalidParams);
|
|
||||||
}
|
}
|
||||||
return new CallToolResult
|
else if (request.Params.Name == "echoSessionId")
|
||||||
{
|
{
|
||||||
Content = [new TextContentBlock { Text = $"Echo: {message}" }]
|
return new CallToolResult
|
||||||
|
{
|
||||||
|
Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (request.Params.Name == "sampleLLM")
|
||||||
|
{
|
||||||
|
if (request.Params.Arguments is null ||
|
||||||
|
!request.Params.Arguments.TryGetValue("prompt", out var prompt) ||
|
||||||
|
!request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens))
|
||||||
|
{
|
||||||
|
throw new McpProtocolException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams);
|
||||||
|
}
|
||||||
|
var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())),
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return new CallToolResult
|
||||||
|
{
|
||||||
|
Content = [new TextContentBlock { Text = $"LLM sampling result: {sampleResult.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text}" }]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (request.Params.Name == "longRunning")
|
||||||
|
{
|
||||||
|
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("durationMs", out var durationMsValue))
|
||||||
|
{
|
||||||
|
throw new McpProtocolException("Missing required argument 'durationMs'", McpErrorCode.InvalidParams);
|
||||||
|
}
|
||||||
|
int durationMs = Convert.ToInt32(durationMsValue.ToString());
|
||||||
|
await Task.Delay(durationMs, cancellationToken);
|
||||||
|
return new CallToolResult
|
||||||
|
{
|
||||||
|
Content = [new TextContentBlock { Text = $"Long-running operation completed after {durationMs}ms" }]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new McpProtocolException($"Unknown tool: '{request.Params.Name}'", McpErrorCode.InvalidParams);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
ListResourceTemplatesHandler = async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
return new ListResourceTemplatesResult
|
||||||
|
{
|
||||||
|
ResourceTemplates = [
|
||||||
|
new ResourceTemplate
|
||||||
|
{
|
||||||
|
UriTemplate = "test://dynamic/resource/{id}",
|
||||||
|
Name = "Dynamic Resource",
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
else if (request.Params.Name == "echoSessionId")
|
|
||||||
|
ListResourcesHandler = async (request, cancellationToken) =>
|
||||||
{
|
{
|
||||||
return new CallToolResult
|
int startIndex = 0;
|
||||||
|
var requestParams = request.Params ?? new();
|
||||||
|
|
||||||
|
if (requestParams.Cursor is not null)
|
||||||
{
|
{
|
||||||
Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }]
|
try
|
||||||
|
{
|
||||||
|
var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(requestParams.Cursor));
|
||||||
|
startIndex = Convert.ToInt32(startIndexAsString);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new McpProtocolException($"Invalid cursor: '{requestParams.Cursor}'", e, McpErrorCode.InvalidParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int endIndex = Math.Min(startIndex + pageSize, resources.Count);
|
||||||
|
string? nextCursor = null;
|
||||||
|
|
||||||
|
if (endIndex < resources.Count)
|
||||||
|
{
|
||||||
|
nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ListResourcesResult
|
||||||
|
{
|
||||||
|
NextCursor = nextCursor,
|
||||||
|
Resources = resources.GetRange(startIndex, endIndex - startIndex)
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
else if (request.Params.Name == "sampleLLM")
|
|
||||||
|
ReadResourceHandler = async (request, cancellationToken) =>
|
||||||
{
|
{
|
||||||
if (request.Params.Arguments is null ||
|
if (request.Params?.Uri is null)
|
||||||
!request.Params.Arguments.TryGetValue("prompt", out var prompt) ||
|
|
||||||
!request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens))
|
|
||||||
{
|
{
|
||||||
throw new McpProtocolException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams);
|
throw new McpProtocolException("Missing required argument 'uri'", McpErrorCode.InvalidParams);
|
||||||
}
|
}
|
||||||
var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())),
|
|
||||||
cancellationToken: cancellationToken);
|
|
||||||
|
|
||||||
return new CallToolResult
|
if (request.Params.Uri.StartsWith("test://dynamic/resource/"))
|
||||||
{
|
{
|
||||||
Content = [new TextContentBlock { Text = $"LLM sampling result: {sampleResult.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text}" }]
|
var id = request.Params.Uri.Split('/').LastOrDefault();
|
||||||
};
|
if (string.IsNullOrEmpty(id))
|
||||||
}
|
{
|
||||||
else if (request.Params.Name == "longRunning")
|
throw new McpProtocolException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams);
|
||||||
{
|
}
|
||||||
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("durationMs", out var durationMsValue))
|
|
||||||
{
|
return new ReadResourceResult
|
||||||
throw new McpProtocolException("Missing required argument 'durationMs'", McpErrorCode.InvalidParams);
|
{
|
||||||
|
Contents = [
|
||||||
|
new TextResourceContents
|
||||||
|
{
|
||||||
|
Uri = request.Params.Uri,
|
||||||
|
MimeType = "text/plain",
|
||||||
|
Text = $"Dynamic resource {id}: This is a plaintext resource"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
int durationMs = Convert.ToInt32(durationMsValue.ToString());
|
|
||||||
await Task.Delay(durationMs, cancellationToken);
|
|
||||||
return new CallToolResult
|
|
||||||
{
|
|
||||||
Content = [new TextContentBlock { Text = $"Long-running operation completed after {durationMs}ms" }]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new McpProtocolException($"Unknown tool: '{request.Params.Name}'", McpErrorCode.InvalidParams);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
options.Handlers.ListResourceTemplatesHandler = async (request, cancellationToken) =>
|
ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) ??
|
||||||
{
|
throw new McpProtocolException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.ResourceNotFound);
|
||||||
return new ListResourceTemplatesResult
|
|
||||||
{
|
|
||||||
ResourceTemplates = [
|
|
||||||
new ResourceTemplate
|
|
||||||
{
|
|
||||||
UriTemplate = "test://dynamic/resource/{id}",
|
|
||||||
Name = "Dynamic Resource",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
options.Handlers.ListResourcesHandler = async (request, cancellationToken) =>
|
|
||||||
{
|
|
||||||
int startIndex = 0;
|
|
||||||
var requestParams = request.Params ?? new();
|
|
||||||
|
|
||||||
if (requestParams.Cursor is not null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(requestParams.Cursor));
|
|
||||||
startIndex = Convert.ToInt32(startIndexAsString);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
throw new McpProtocolException($"Invalid cursor: '{requestParams.Cursor}'", e, McpErrorCode.InvalidParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int endIndex = Math.Min(startIndex + pageSize, resources.Count);
|
|
||||||
string? nextCursor = null;
|
|
||||||
|
|
||||||
if (endIndex < resources.Count)
|
|
||||||
{
|
|
||||||
nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ListResourcesResult
|
|
||||||
{
|
|
||||||
NextCursor = nextCursor,
|
|
||||||
Resources = resources.GetRange(startIndex, endIndex - startIndex)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
options.Handlers.ReadResourceHandler = async (request, cancellationToken) =>
|
|
||||||
{
|
|
||||||
if (request.Params?.Uri is null)
|
|
||||||
{
|
|
||||||
throw new McpProtocolException("Missing required argument 'uri'", McpErrorCode.InvalidParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Params.Uri.StartsWith("test://dynamic/resource/"))
|
|
||||||
{
|
|
||||||
var id = request.Params.Uri.Split('/').LastOrDefault();
|
|
||||||
if (string.IsNullOrEmpty(id))
|
|
||||||
{
|
|
||||||
throw new McpProtocolException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ReadResourceResult
|
return new ReadResourceResult
|
||||||
{
|
{
|
||||||
Contents = [
|
Contents = [contents]
|
||||||
new TextResourceContents
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
ListPromptsHandler = async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
return new ListPromptsResult
|
||||||
|
{
|
||||||
|
Prompts = [
|
||||||
|
new Prompt
|
||||||
|
{
|
||||||
|
Name = "simple_prompt",
|
||||||
|
Description = "A prompt without arguments"
|
||||||
|
},
|
||||||
|
new Prompt
|
||||||
|
{
|
||||||
|
Name = "complex_prompt",
|
||||||
|
Description = "A prompt with arguments",
|
||||||
|
Arguments =
|
||||||
|
[
|
||||||
|
new PromptArgument
|
||||||
{
|
{
|
||||||
Uri = request.Params.Uri,
|
Name = "temperature",
|
||||||
MimeType = "text/plain",
|
Description = "Temperature setting",
|
||||||
Text = $"Dynamic resource {id}: This is a plaintext resource"
|
Required = true
|
||||||
|
},
|
||||||
|
new PromptArgument
|
||||||
|
{
|
||||||
|
Name = "style",
|
||||||
|
Description = "Output style",
|
||||||
|
Required = false
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) ??
|
GetPromptHandler = async (request, cancellationToken) =>
|
||||||
throw new McpProtocolException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.ResourceNotFound);
|
|
||||||
|
|
||||||
return new ReadResourceResult
|
|
||||||
{
|
{
|
||||||
Contents = [contents]
|
if (request.Params is null)
|
||||||
};
|
{
|
||||||
};
|
throw new McpProtocolException("Missing required parameter 'name'", McpErrorCode.InvalidParams);
|
||||||
|
}
|
||||||
|
|
||||||
options.Handlers.ListPromptsHandler = async (request, cancellationToken) =>
|
List<PromptMessage> messages = [];
|
||||||
{
|
|
||||||
return new ListPromptsResult
|
if (request.Params.Name == "simple_prompt")
|
||||||
{
|
{
|
||||||
Prompts = [
|
messages.Add(new PromptMessage
|
||||||
new Prompt
|
|
||||||
{
|
{
|
||||||
Name = "simple_prompt",
|
Role = Role.User,
|
||||||
Description = "A prompt without arguments"
|
Content = new TextContentBlock { Text = "This is a simple prompt without arguments." },
|
||||||
},
|
});
|
||||||
new Prompt
|
}
|
||||||
|
else if (request.Params.Name == "complex_prompt")
|
||||||
|
{
|
||||||
|
string temperature = request.Params.Arguments?["temperature"].ToString() ?? "unknown";
|
||||||
|
string style = request.Params.Arguments?["style"].ToString() ?? "unknown";
|
||||||
|
messages.Add(new PromptMessage
|
||||||
{
|
{
|
||||||
Name = "complex_prompt",
|
Role = Role.User,
|
||||||
Description = "A prompt with arguments",
|
Content = new TextContentBlock { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" },
|
||||||
Arguments =
|
});
|
||||||
[
|
messages.Add(new PromptMessage
|
||||||
new PromptArgument
|
|
||||||
{
|
|
||||||
Name = "temperature",
|
|
||||||
Description = "Temperature setting",
|
|
||||||
Required = true
|
|
||||||
},
|
|
||||||
new PromptArgument
|
|
||||||
{
|
|
||||||
Name = "style",
|
|
||||||
Description = "Output style",
|
|
||||||
Required = false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
options.Handlers.GetPromptHandler = async (request, cancellationToken) =>
|
|
||||||
{
|
|
||||||
if (request.Params is null)
|
|
||||||
{
|
|
||||||
throw new McpProtocolException("Missing required parameter 'name'", McpErrorCode.InvalidParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<PromptMessage> messages = [];
|
|
||||||
|
|
||||||
if (request.Params.Name == "simple_prompt")
|
|
||||||
{
|
|
||||||
messages.Add(new PromptMessage
|
|
||||||
{
|
|
||||||
Role = Role.User,
|
|
||||||
Content = new TextContentBlock { Text = "This is a simple prompt without arguments." },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (request.Params.Name == "complex_prompt")
|
|
||||||
{
|
|
||||||
string temperature = request.Params.Arguments?["temperature"].ToString() ?? "unknown";
|
|
||||||
string style = request.Params.Arguments?["style"].ToString() ?? "unknown";
|
|
||||||
messages.Add(new PromptMessage
|
|
||||||
{
|
|
||||||
Role = Role.User,
|
|
||||||
Content = new TextContentBlock { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" },
|
|
||||||
});
|
|
||||||
messages.Add(new PromptMessage
|
|
||||||
{
|
|
||||||
Role = Role.User,
|
|
||||||
Content = new TextContentBlock { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" },
|
|
||||||
});
|
|
||||||
messages.Add(new PromptMessage
|
|
||||||
{
|
|
||||||
Role = Role.User,
|
|
||||||
Content = new ImageContentBlock
|
|
||||||
{
|
{
|
||||||
Data = System.Text.Encoding.UTF8.GetBytes(MCP_TINY_IMAGE),
|
Role = Role.User,
|
||||||
MimeType = "image/png"
|
Content = new TextContentBlock { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" },
|
||||||
}
|
});
|
||||||
});
|
messages.Add(new PromptMessage
|
||||||
}
|
{
|
||||||
else
|
Role = Role.User,
|
||||||
{
|
Content = new ImageContentBlock
|
||||||
throw new McpProtocolException($"Unknown prompt: {request.Params.Name}", McpErrorCode.InvalidParams);
|
{
|
||||||
}
|
Data = System.Text.Encoding.UTF8.GetBytes(MCP_TINY_IMAGE),
|
||||||
|
MimeType = "image/png"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new McpProtocolException($"Unknown prompt: {request.Params.Name}", McpErrorCode.InvalidParams);
|
||||||
|
}
|
||||||
|
|
||||||
return new GetPromptResult
|
return new GetPromptResult
|
||||||
{
|
{
|
||||||
Messages = messages
|
Messages = messages
|
||||||
};
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue