Improve tool handling (#28)

* Improve tool handling

* Rework stuff with new hosting options

* Update readme

* Fix build

* Try run client tests

* Fix build

* Fix issues on linux

* Try to run testserver on linux

* Fix test issue

* Fix code smells
This commit is contained in:
Toni Wenzel 2025-03-07 16:48:55 +01:00 • committed by GitHub
commit ec9bdfcf5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1036 additions and 235 deletions

View file

@ -3,10 +3,8 @@ name: .NET Build, Test and publish
on:
push:
branches: [ "main", "master" ]
paths: [ 'src/**' ]
pull_request:
branches: [ "main", "master" ]
paths: [ 'src/**' ]
release:
types: [published,edited]
@ -24,7 +22,12 @@ jobs:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Fetch all history for all tags and branches
run: git fetch --prune --unshallow
run: |
if [ -f "$(git rev-parse --git-dir)/shallow" ]; then
git fetch --prune --unshallow
else
echo "Repository is already complete."
fi
- name: Setup .NET
uses: actions/setup-dotnet@v2
@ -33,6 +36,17 @@ jobs:
8.0.x
9.0.x
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies for tests
run: npm install @modelcontextprotocol/server-everything
- name: Install dependencies for tests
run: npm install @modelcontextprotocol/server-memory
- name: Run Cake script
uses: cake-build/cake-action@v1
with:

3
.gitignore vendored
View file

@ -62,4 +62,5 @@ ehthumbs.db
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
$RECYCLE.BIN/
/mcpdotnet.v3.ncrunchsolution

View file

@ -106,7 +106,33 @@ Remember you can connect to any MCP server, not just ones created using mcpdotne
## Getting Started (Server)
Here is an example of how to create an MCP server with a single tool. You can also refer to the TestServer in the tests folder, which implements a wider range of capabilities.
Here is an example of how to create an MCP server and register all tools from the current application.
```csharp
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMcpServer()
.WithStdioServerTransport()
.WithTools()
var app = builder.Build();
await app.RunAsync();
```
The `EchoTool` class is a simple example of a tool implementation:
```csharp
[McpToolType]
public static class EchoTool
{
[McpTool(Description = "Echoes the input back to the client.")]
public static string Echo([McpParameter(true)] string message)
{
return "hello " + message;
}
}
```
Here is an example of how to create an MCP server manually with a single tool. You can also refer to the TestServer in the tests folder, which implements a wider range of capabilities.
```csharp
McpServerOptions options = new McpServerOptions()
@ -171,6 +197,7 @@ while (true)
}
```
## Roadmap
- Expand documentation with detailed guides for:

View file

@ -19,6 +19,7 @@ try
builder.Services.AddSerilog();
builder.Services.AddMcpServer()
.WithStdioServerTransport()
.WithTools()
.WithCallToolHandler((r, ct) => Task.FromResult(new McpDotNet.Protocol.Types.CallToolResponse()));
var app = builder.Build();

View file

@ -0,0 +1,13 @@
using McpDotNet.Server;
namespace TestServerWithHosting.Tools;
[McpToolType]
public static class EchoTool
{
[McpTool(Description = "Echoes the input back to the client.")]
public static string Echo([McpParameter(true)] string message)
{
return "hello " + message;
}
}

View file

@ -0,0 +1,47 @@
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
namespace TestServerWithHosting.Tools;
/// <summary>
/// This tool uses depenency injection and async method
/// </summary>
[McpToolType]
public class SampleLlmTool
{
private readonly IMcpServer _server;
public SampleLlmTool(IMcpServer server)
{
_server = server ?? throw new ArgumentNullException(nameof(server));
}
[McpTool(Name = "sampleLLM", Description = "Samples from an LLM using MCP's sampling feature")]
public async Task<string> SampleLLM([McpParameter(true, Description = "The prompt to send to the LLM")] string prompt, [McpParameter(true, Description = "Maximum number of tokens to generate")] int maxTokens, CancellationToken cancellationToken)
{
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
var sampleResult = await _server.RequestSamplingAsync(samplingParams, cancellationToken);
return $"LLM sampling result: {sampleResult.Content.Text}";
}
private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
{
return new CreateMessageRequestParams()
{
Messages = [new SamplingMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = $"Resource {uri} context: {context}"
}
}],
SystemPrompt = "You are a helpful test server.",
MaxTokens = maxTokens,
Temperature = 0.7f,
IncludeContext = ContextInclusion.ThisServer
};
}
}

View file

@ -0,0 +1,111 @@
using McpDotNet.Configuration;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.DependencyInjection;
namespace McpDotNet;
/// <summary>
/// Extension to configure the MCP server with handlers
/// </summary>
public static partial class McpServerBuilderExtensions
{
/// <summary>
/// Sets the handler for list tools requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithListToolsHandler(this IMcpServerBuilder builder, Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.ListToolsHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for call tool requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithCallToolHandler(this IMcpServerBuilder builder, Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.CallToolHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for list prompts requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithListPromptsHandler(this IMcpServerBuilder builder, Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.ListPromptsHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for get prompt requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithGetPromptHandler(this IMcpServerBuilder builder, Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.GetPromptHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for list resources requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithListResourcesHandler(this IMcpServerBuilder builder, Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.ListResourcesHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for read resources requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithReadResourceHandler(this IMcpServerBuilder builder, Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.ReadResourceHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for get resources requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithGetCompletionHandler(this IMcpServerBuilder builder, Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.GetCompletionHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for subscribe to resources messages.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithSubscribeToResourcesHandler(this IMcpServerBuilder builder, Func<RequestContext<string>, CancellationToken, Task> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.SubscribeToResourcesHandler = handler);
return builder;
}
/// <summary>
/// Sets or sets the handler for subscribe to resources messages.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithUnsubscribeFromResourcesHandler(this IMcpServerBuilder builder, Func<RequestContext<string>, CancellationToken, Task> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.UnsubscribeFromResourcesHandler = handler);
return builder;
}
}

View file

@ -0,0 +1,249 @@
using System.Reflection;
using System.Text.Json;
using McpDotNet.Configuration;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.DependencyInjection;
namespace McpDotNet;
/// <summary>
/// Extension to configure the MCP server with tools
/// </summary>
public static partial class McpServerBuilderExtensions
{
/// <summary>
/// Adds a tool to the server.
/// </summary>
/// <typeparam name="TTool">The tool type.</typeparam>
/// <param name="builder">The builder instance.</param>
public static IMcpServerBuilder WithTool<TTool>(this IMcpServerBuilder builder)
{
return WithTools(builder, typeof(TTool));
}
/// <summary>
/// Adds all tools marked with <see cref="McpToolTypeAttribute"/> from the current assembly to the server.
/// </summary>
/// <param name="builder">The builder instance.</param>
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder)
{
return WithToolsFromAssembly(builder, Assembly.GetCallingAssembly());
}
/// <summary>
/// Adds tools to the server.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="toolTypes">Types with marked methods to add as tools to the server.</param>
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params Type[] toolTypes)
{
ArgumentNullException.ThrowIfNull(toolTypes);
if (toolTypes.Length == 0)
throw new ArgumentException("At least one tool type must be provided.", nameof(toolTypes));
var tools = new List<Tool>();
Dictionary<string, Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>> callbacks = [];
foreach (var type in toolTypes)
{
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
foreach (var method in methods)
{
var attribute = method.GetCustomAttribute<McpToolAttribute>();
if (attribute != null)
{
var tool = CreateTool(method, attribute);
tools.Add(tool);
callbacks.Add(tool.Name, async (request, cancellationToken) => await CallTool(request, method, cancellationToken));
// register type because method is not static and so we need an instance
if (!method.IsStatic)
builder.Services.AddScoped(type);
}
}
}
builder.WithListToolsHandler((_, _) => Task.FromResult(new ListToolsResult() { Tools = tools }));
builder.WithCallToolHandler(async (request, cancellationToken) =>
{
if (request.Params != null && callbacks.TryGetValue(request.Params.Name, out var callback))
return await callback(request, cancellationToken);
throw new McpServerException($"Unknown tool: {request.Params?.Name}");
});
return builder;
}
/// <summary>
/// Adds types marked with the <see cref="McpToolTypeAttribute"/> attribute from the given assembly as tools to the server.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="assembly">The assembly to load the types from. Null to get the current assembly</param>
public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder builder, Assembly? assembly = null)
{
assembly ??= Assembly.GetCallingAssembly();
List<Type> toolTypes = [];
foreach (var type in assembly.GetTypes())
{
bool hasToolTypeAttribute = type.GetCustomAttribute<McpToolTypeAttribute>() != null;
if (!hasToolTypeAttribute)
continue;
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
foreach (var method in methods)
{
var attribute = method.GetCustomAttribute<McpToolAttribute>();
if (attribute != null)
{
toolTypes.Add(type);
break;
}
}
}
if (toolTypes.Count == 0)
throw new ArgumentException("No types with marked methods found in the assembly.", nameof(assembly));
return WithTools(builder, toolTypes.ToArray());
}
private static Tool CreateTool(MethodInfo method, McpToolAttribute attribute)
{
Dictionary<string, JsonSchemaProperty> properties = [];
List<string>? requiredProperties = null;
foreach (var parameter in method.GetParameters())
{
if (parameter.ParameterType == typeof(CancellationToken))
continue;
var parameterAttribute = parameter.GetCustomAttribute<McpParameterAttribute>();
properties.Add(parameter.Name ?? "NoName", new JsonSchemaProperty()
{
Type = GetParameterType(parameter.ParameterType),
Description = parameterAttribute?.Description
});
if (parameterAttribute?.Required == true)
{
requiredProperties ??= [];
requiredProperties.Add(parameter.Name ?? "NoName");
}
}
return new Tool()
{
Name = attribute.Name ?? method.Name,
Description = attribute.Description,
InputSchema = new JsonSchema()
{
Type = "object",
Properties = properties,
Required = requiredProperties
},
};
}
private static string GetParameterType(Type parameterType)
{
return parameterType switch
{
Type t when t == typeof(string) => "string",
Type t when t == typeof(int) || t == typeof(double) || t == typeof(float) => "number",
Type t when t == typeof(bool) => "boolean",
Type t when t.IsArray => "array",
Type t when t == typeof(DateTime) => "string",
_ => "object"
};
}
private static async Task<CallToolResponse> CallTool(RequestContext<CallToolRequestParams> request, MethodInfo method, CancellationToken cancellationToken)
{
var methodParameters = method.GetParameters();
List<object?> parameters = ResolveParameters(request, methodParameters, cancellationToken);
if (cancellationToken.IsCancellationRequested)
return new CallToolResponse { Content = [new Content { Text = "Operation was cancelled" }] };
try
{
using var scope = request.Server.ServiceProvider?.CreateScope();
var objectInstance = CreateObjectInstance(method, scope?.ServiceProvider);
var result = method.Invoke(objectInstance, parameters.ToArray());
if (result is Task task)
{
await task.ConfigureAwait(false);
var resultProperty = task.GetType().GetProperty("Result");
result = resultProperty?.GetValue(task);
}
if (result is string resultString)
return new CallToolResponse { Content = [new Content() { Text = resultString, Type = "text" }] };
if (result is string[] resultStringArray)
return new CallToolResponse { Content = resultStringArray.Select(s => new Content() { Text = s, Type = "text" }).ToList() };
if (result is null)
return new CallToolResponse { Content = [new Content() { Text = "null" }] };
if (result is JsonElement jsonElement)
return new CallToolResponse { Content = [new Content() { Text = jsonElement.GetRawText(), Type = "text" }] };
return new CallToolResponse { Content = [new Content() { Text = result.ToString(), Type = "text" }] };
}
catch (TargetInvocationException e)
{
throw new McpServerException(e.Message, e);
}
}
private static List<object?> ResolveParameters(RequestContext<CallToolRequestParams> request, ParameterInfo[] methodParameters, CancellationToken cancellationToken)
{
var parameters = new List<object?>(methodParameters.Length);
foreach (var parameter in methodParameters)
{
if (parameter.ParameterType == typeof(CancellationToken))
{
parameters.Add(cancellationToken);
}
else if (request.Params?.Arguments != null && request.Params.Arguments.TryGetValue(parameter.Name ?? "NoName", out var value))
{
if (value is JsonElement element)
value = JsonSerializer.Deserialize(element.GetRawText(), parameter.ParameterType);
parameters.Add(Convert.ChangeType(value, parameter.ParameterType));
}
else
{
var parameterAttribute = parameter.GetCustomAttribute<McpParameterAttribute>();
if (parameterAttribute?.Required == true)
throw new McpServerException($"Missing required argument '{parameter.Name}'.");
parameters.Add(parameter.HasDefaultValue ? parameter.DefaultValue : null);
}
}
return parameters;
}
private static object? CreateObjectInstance(MethodInfo method, IServiceProvider? serviceProvider)
{
if (method.IsStatic)
return null;
if (serviceProvider != null)
return ActivatorUtilities.CreateInstance(serviceProvider, method.DeclaringType!);
return Activator.CreateInstance(method.DeclaringType!);
}
}

View file

@ -0,0 +1,21 @@
using McpDotNet.Configuration;
using McpDotNet.Protocol.Transport;
using Microsoft.Extensions.DependencyInjection;
namespace McpDotNet;
/// <summary>
/// Extension to configure the MCP server with transports
/// </summary>
public static partial class McpServerBuilderExtensions
{
/// <summary>
/// Adds a server transport that uses stdin/stdout for communication.
/// </summary>
/// <param name="builder">The builder instance.</param>
public static IMcpServerBuilder WithStdioServerTransport(this IMcpServerBuilder builder)
{
builder.Services.AddSingleton<IServerTransport, StdioServerTransport>();
return builder;
}
}

View file

@ -23,6 +23,11 @@ public interface IMcpServer : IAsyncDisposable
/// </summary>
Implementation? ClientInfo { get; }
/// <summary>
/// Gets the service provider for the server.
/// </summary>
IServiceProvider? ServiceProvider { get; }
/// <summary>
/// Starts the server and begins listening for client requests.
/// </summary>
@ -31,47 +36,47 @@ public interface IMcpServer : IAsyncDisposable
/// <summary>
/// Gets or sets the handler for list tools requests.
/// </summary>
Func<ListToolsRequestParams?, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
/// <summary>
/// Gets or sets the handler for call tool requests.
/// </summary>
Func<CallToolRequestParams?, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
/// <summary>
/// Gets or sets the handler for list prompts requests.
/// </summary>
Func<ListPromptsRequestParams?, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
/// <summary>
/// Gets or sets the handler for get prompt requests.
/// </summary>
Func<GetPromptRequestParams?, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
/// <summary>
/// Gets or sets the handler for list resources requests.
/// </summary>
Func<ListResourcesRequestParams?, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
/// <summary>
/// Gets or sets the handler for read resources requests.
/// </summary>
Func<ReadResourceRequestParams?, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
/// <summary>
/// Gets or sets the handler for get resources requests.
/// </summary>
Func<CompleteRequestParams?, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
/// <summary>
/// Gets or sets the handler for subscribe to resources messages.
/// </summary>
Func<string, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
Func<RequestContext<string>, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
/// <summary>
/// Gets or sets the handler for subscribe to resources messages.
/// </summary>
Func<string, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
Func<RequestContext<string>, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
/// <summary>

View file

@ -0,0 +1,29 @@
namespace McpDotNet.Server;
/// <summary>
/// Attribute to mark a method parameter
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public class McpParameterAttribute : Attribute
{
/// <summary>
/// Defines if the parameter is required.
/// </summary>
public bool Required { get; set; }
/// <summary>
/// A description of the parameter.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Attribute to mark a method as an MCP tool.
/// </summary>
/// <param name="required">True if the parameter is mandatory.</param>
/// <param name="description">A description of the tool.</param>
public McpParameterAttribute(bool required = false, string? description = null)
{
Required = required;
Description = description;
}
}

View file

@ -12,8 +12,8 @@ namespace McpDotNet.Server;
/// <inheritdoc />
internal class McpServer : McpJsonRpcEndpoint, IMcpServer
{
private IServerTransport _serverTransport;
private McpServerOptions _options;
private readonly IServerTransport _serverTransport;
private readonly McpServerOptions _options;
private volatile bool _isInitializing;
private readonly ILogger<McpServer> _logger;
@ -24,142 +24,24 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
/// <param name="options">Configuration options for this server, including capabilities.
/// Make sure to accurately reflect exactly what capabilities the server supports and does not support.</param>
/// <param name="loggerFactory">Logger factory to use for logging</param>
/// <param name="serviceProvider">Optional service provider to use for dependency injection</param>
/// <exception cref="McpServerException"></exception>
public McpServer(IServerTransport transport, McpServerOptions options, ILoggerFactory loggerFactory)
public McpServer(IServerTransport transport, McpServerOptions options, ILoggerFactory loggerFactory, IServiceProvider? serviceProvider)
: base(transport, loggerFactory)
{
_serverTransport = transport;
_options = options;
_logger = loggerFactory.CreateLogger<McpServer>();
ServerInstructions = options.ServerInstructions;
ServiceProvider = serviceProvider;
if (options.Capabilities?.Tools != null)
{
SetRequestHandler<ListToolsRequestParams, ListToolsResult>("tools/list",
async (request) =>
{
if (ListToolsHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.ListToolsHandlerNotConfigured(EndpointName);
throw new McpServerException("ListTools handler not configured");
}
return await ListToolsHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None);
});
SetRequestHandler<CallToolRequestParams, CallToolResponse>("tools/call",
async (request) =>
{
if (CallToolHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.CallToolHandlerNotConfigured(EndpointName);
throw new McpServerException("CallTool handler not configured");
}
return await CallToolHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None);
});
}
if (options.Capabilities?.Prompts != null)
{
SetRequestHandler<ListPromptsRequestParams, ListPromptsResult>("prompts/list",
async (request) =>
{
if (ListPromptsHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.ListPromptsHandlerNotConfigured(EndpointName);
throw new McpServerException("ListPrompts handler not configured");
}
return await ListPromptsHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None);
});
SetRequestHandler<GetPromptRequestParams, GetPromptResult>("prompts/get",
async (request) =>
{
if (GetPromptHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.GetPromptHandlerNotConfigured(EndpointName);
throw new McpServerException("GetPrompt handler not configured");
}
return await GetPromptHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None);
});
}
if (options.Capabilities?.Resources != null)
{
SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>("resources/list",
async (request) =>
{
if (ListResourcesHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.ListResourcesHandlerNotConfigured(EndpointName);
throw new McpServerException("ListResources handler not configured");
}
return await ListResourcesHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None);
});
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>("resources/read",
async (request) =>
{
if (ReadResourceHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.ReadResourceHandlerNotConfigured(EndpointName);
throw new McpServerException("ReadResource handler not configured");
}
return await ReadResourceHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None);
});
}
SetRequestHandler<CompleteRequestParams, CompleteResult>("completion/complete",
async (request) =>
{
if (GetCompletionHandler == null)
{
// This capability is not optional, so return an empty result if there is no handler
return new CompleteResult()
{
Completion = new()
{
Values = [],
Total = 0,
HasMore = false
}
};
}
return await GetCompletionHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None);
});
SetRequestHandler<InitializeRequestParams, InitializeResult>("initialize",
(request) =>
{
ClientCapabilities = request?.Capabilities ?? new();
ClientInfo = request?.ClientInfo;
return Task.FromResult(new InitializeResult()
{
ProtocolVersion = options.ProtocolVersion,
Instructions = ServerInstructions,
ServerInfo = _options.ServerInfo,
Capabilities = options.Capabilities ?? new ServerCapabilities(),
});
});
SetRequestHandler<JsonNode, PingResult>("ping",
(request) =>
{
return Task.FromResult(new PingResult());
});
OnNotification("notifications/initialized", (notification) =>
{
IsInitialized = true;
return Task.CompletedTask;
});
SetToolsHandler(options);
SetPromptsHandler(options);
SetResourcesHandler(options);
SetCompletionHandler();
SetInitializeHandler(options);
SetPingHandler();
SetNotificationHandler();
}
public ClientCapabilities? ClientCapabilities { get; set; }
@ -171,31 +53,34 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
public string? ServerInstructions { get; set; }
/// <inheritdoc />
public Func<ListToolsRequestParams?, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
public IServiceProvider? ServiceProvider { get; }
/// <inheritdoc />
public Func<CallToolRequestParams?, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
public Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
/// <inheritdoc />
public Func<ListPromptsRequestParams?, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
public Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
/// <inheritdoc />
public Func<GetPromptRequestParams?, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
public Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
/// <inheritdoc />
public Func<ListResourcesRequestParams?, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
public Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
/// <inheritdoc />
public Func<ReadResourceRequestParams?, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
public Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
/// <inheritdoc />
public Func<CompleteRequestParams?, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
public Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
/// <inheritdoc />
public Func<string, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
public Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
/// <inheritdoc />
public Func<string, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
public Func<RequestContext<string>, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
/// <inheritdoc />
public Func<RequestContext<string>, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
/// <inheritdoc />
public override string EndpointName
@ -277,4 +162,158 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
cancellationToken
).ConfigureAwait(false);
}
private void SetNotificationHandler()
{
OnNotification("notifications/initialized", (notification) =>
{
IsInitialized = true;
return Task.CompletedTask;
});
}
private void SetPingHandler()
{
SetRequestHandler<JsonNode, PingResult>("ping",
(request) =>
{
return Task.FromResult(new PingResult());
});
}
private void SetInitializeHandler(McpServerOptions options)
{
SetRequestHandler<InitializeRequestParams, InitializeResult>("initialize",
(request) =>
{
ClientCapabilities = request?.Capabilities ?? new();
ClientInfo = request?.ClientInfo;
return Task.FromResult(new InitializeResult()
{
ProtocolVersion = options.ProtocolVersion,
Instructions = ServerInstructions,
ServerInfo = _options.ServerInfo,
Capabilities = options.Capabilities ?? new ServerCapabilities(),
});
});
}
private void SetCompletionHandler()
{
SetRequestHandler<CompleteRequestParams, CompleteResult>("completion/complete",
async (request) =>
{
if (GetCompletionHandler == null)
{
// This capability is not optional, so return an empty result if there is no handler
return new CompleteResult()
{
Completion = new()
{
Values = [],
Total = 0,
HasMore = false
}
};
}
return await GetCompletionHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None);
});
}
private void SetResourcesHandler(McpServerOptions options)
{
if (options.Capabilities?.Resources != null)
{
SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>("resources/list",
async (request) =>
{
if (ListResourcesHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.ListResourcesHandlerNotConfigured(EndpointName);
throw new McpServerException("ListResources handler not configured");
}
return await ListResourcesHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None);
});
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>("resources/read",
async (request) =>
{
if (ReadResourceHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.ReadResourceHandlerNotConfigured(EndpointName);
throw new McpServerException("ReadResource handler not configured");
}
return await ReadResourceHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None);
});
}
}
private void SetPromptsHandler(McpServerOptions options)
{
if (options.Capabilities?.Prompts != null)
{
SetRequestHandler<ListPromptsRequestParams, ListPromptsResult>("prompts/list",
async (request) =>
{
if (ListPromptsHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.ListPromptsHandlerNotConfigured(EndpointName);
throw new McpServerException("ListPrompts handler not configured");
}
return await ListPromptsHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None);
});
SetRequestHandler<GetPromptRequestParams, GetPromptResult>("prompts/get",
async (request) =>
{
if (GetPromptHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.GetPromptHandlerNotConfigured(EndpointName);
throw new McpServerException("GetPrompt handler not configured");
}
return await GetPromptHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None);
});
}
}
private void SetToolsHandler(McpServerOptions options)
{
if (options.Capabilities?.Tools != null)
{
SetRequestHandler<ListToolsRequestParams, ListToolsResult>("tools/list",
async (request) =>
{
if (ListToolsHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.ListToolsHandlerNotConfigured(EndpointName);
throw new McpServerException("ListTools handler not configured");
}
return await ListToolsHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None);
});
SetRequestHandler<CallToolRequestParams, CallToolResponse>("tools/call",
async (request) =>
{
if (CallToolHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.CallToolHandlerNotConfigured(EndpointName);
throw new McpServerException("CallTool handler not configured");
}
return await CallToolHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None);
});
}
}
}

View file

@ -10,47 +10,47 @@ public class McpServerDelegates
/// <summary>
/// Gets or sets the handler for list tools requests.
/// </summary>
public Func<ListToolsRequestParams?, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
public Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
/// <summary>
/// Gets or sets the handler for call tool requests.
/// </summary>
public Func<CallToolRequestParams?, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
public Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
/// <summary>
/// Gets or sets the handler for list prompts requests.
/// </summary>
public Func<ListPromptsRequestParams?, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
public Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
/// <summary>
/// Gets or sets the handler for get prompt requests.
/// </summary>
public Func<GetPromptRequestParams?, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
public Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
/// <summary>
/// Gets or sets the handler for list resources requests.
/// </summary>
public Func<ListResourcesRequestParams?, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
public Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
/// <summary>
/// Gets or sets the handler for read resources requests.
/// </summary>
public Func<ReadResourceRequestParams?, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
public Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
/// <summary>
/// Gets or sets the handler for get resources requests.
/// </summary>
public Func<CompleteRequestParams?, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
public Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
/// <summary>
/// Gets or sets the handler for subscribe to resources messages.
/// </summary>
public Func<string, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
public Func<RequestContext<string>, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
/// <summary>
/// Gets or sets the handler for subscribe to resources messages.
/// </summary>
public Func<string, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
public Func<RequestContext<string>, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
/// <summary>
/// Applies the delegates to the server.

View file

@ -20,6 +20,7 @@ public class McpServerFactory : IMcpServerFactory
private readonly McpServerOptions _options;
private readonly ILoggerFactory _loggerFactory;
private readonly McpServerDelegates? _serverDelegates;
private readonly IServiceProvider? _serviceProvider;
/// <summary>
/// Initializes a new instance of the <see cref="McpServerFactory"/> class.
@ -27,14 +28,16 @@ public class McpServerFactory : IMcpServerFactory
/// <param name="serverTransport">Transport to use for the server</param>
/// <param name="options">Configuration options for this server, including capabilities.
/// Make sure to accurately reflect exactly what capabilities the server supports and does not support.</param>
/// <param name="serviceProvider">Optional service provider to create new instances.</param>
/// <param name="loggerFactory">Logger factory to use for logging</param>
/// <param name="serverDelegates"></param>
public McpServerFactory(IServerTransport serverTransport, McpServerOptions options, ILoggerFactory loggerFactory, IOptions<McpServerDelegates>? serverDelegates = null)
public McpServerFactory(IServerTransport serverTransport, McpServerOptions options, ILoggerFactory loggerFactory, IOptions<McpServerDelegates>? serverDelegates = null, IServiceProvider? serviceProvider = null)
{
_serverTransport = serverTransport ?? throw new ArgumentNullException(nameof(serverTransport));
_options = options ?? throw new ArgumentNullException(nameof(options));
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
_serverDelegates = serverDelegates?.Value;
_serviceProvider = serviceProvider;
}
/// <summary>
@ -44,7 +47,7 @@ public class McpServerFactory : IMcpServerFactory
/// </summary>
public IMcpServer CreateServer()
{
var server = new McpServer(_serverTransport, _options, _loggerFactory);
var server = new McpServer(_serverTransport, _options, _loggerFactory, _serviceProvider);
_serverDelegates?.Apply(server);

View file

@ -0,0 +1,27 @@
namespace McpDotNet.Server;
/// <summary>
/// Attribute to mark a method as an MCP tool.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class McpToolAttribute : Attribute
{
/// The name of the tool. If not provided, the class name will be used.
public string? Name { get; set; }
/// <summary>
/// A description of the tool.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Attribute to mark a method as an MCP tool.
/// </summary>
/// <param name="name">The name of the tool. If not provided, the class name will be used.</param>
/// <param name="description">A description of the tool.</param>
public McpToolAttribute(string? name = null, string? description = null)
{
Name = name;
Description = description;
}
}

View file

@ -0,0 +1,9 @@
namespace McpDotNet.Server;
/// <summary>
/// Attribute to mark a type as container for MCP tools.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class McpToolTypeAttribute : Attribute
{
}

View file

@ -0,0 +1,10 @@
namespace McpDotNet.Server;
/// <summary>
/// Container for the request context.
/// </summary>
/// <typeparam name="TParams">Type of the request parameters</typeparam>
public record RequestContext<TParams>(IMcpServer Server, TParams? Params)
{
}

View file

@ -21,7 +21,7 @@ internal abstract class McpJsonRpcEndpoint
private readonly ITransport _transport;
private readonly ConcurrentDictionary<RequestId, TaskCompletionSource<IJsonRpcMessage>> _pendingRequests;
private readonly ConcurrentDictionary<string, List<Func<JsonRpcNotification, Task>>> _notificationHandlers;
private readonly Dictionary<string, Func<JsonRpcRequest, Task<object>>> _requestHandlers = [];
private readonly Dictionary<string, Func<JsonRpcRequest, Task<object?>>> _requestHandlers = [];
private int _nextRequestId;
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger<McpClient> _logger;
@ -349,7 +349,7 @@ internal abstract class McpJsonRpcEndpoint
/// <typeparam name="TResponse">Type of response payload (not full RPC response</typeparam>
/// <param name="method">Method identifier to register for</param>
/// <param name="handler">Handler to be called when a request with specified method identifier is received</param>
protected void SetRequestHandler<TRequest, TResponse>(string method, Func<TRequest?, Task<TResponse>> handler) where TResponse : class
protected void SetRequestHandler<TRequest, TResponse>(string method, Func<TRequest?, Task<TResponse>> handler)
{
ArgumentNullException.ThrowIfNull(handler);

View file

@ -17,7 +17,7 @@ public class IntegrationTests
{
Id = "everything",
Name = "Everything",
TransportType = McpDotNet.Protocol.Transport.TransportTypes.StdIo,
TransportType = "stdio",
TransportOptions = new Dictionary<string, string>
{
["command"] = "npx",

View file

@ -27,7 +27,6 @@
<ItemGroup>
<ProjectReference Include="..\..\src\McpDotNet.Extensions.AI\McpDotNet.Extensions.AI.csproj" />
<ProjectReference Include="..\..\src\mcpdotnet\mcpdotnet.csproj" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.3.0-preview.1.25114.11" />
</ItemGroup>

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