using System.Globalization; using System.Reflection; using System.Text.Json; using McpDotNet.Configuration; using McpDotNet.Protocol.Types; using McpDotNet.Server; using Microsoft.Extensions.DependencyInjection; namespace McpDotNet; /// /// Extension to configure the MCP server with tools /// public static partial class McpServerBuilderExtensions { /// /// Adds a tool to the server. /// /// The tool type. /// The builder instance. public static IMcpServerBuilder WithTool(this IMcpServerBuilder builder) { return WithTools(builder, typeof(TTool)); } /// /// Adds all tools marked with from the current assembly to the server. /// /// The builder instance. public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder) { return WithToolsFromAssembly(builder, Assembly.GetCallingAssembly()); } /// /// Adds tools to the server. /// /// The builder instance. /// Types with marked methods to add as tools to the server. public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params Type[] toolTypes) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (toolTypes is null || toolTypes.Length == 0) throw new ArgumentException("At least one tool type must be provided.", nameof(toolTypes)); var tools = new List(); Dictionary, CancellationToken, Task>> 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(); if (attribute != null) { var tool = CreateTool(method, attribute); tools.Add(tool); callbacks.Add(tool.Name, async (request, cancellationToken) => await CallTool(request, method, cancellationToken).ConfigureAwait(false)); // 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).ConfigureAwait(false); throw new McpServerException($"Unknown tool: {request.Params?.Name}"); }); return builder; } /// /// Adds types marked with the attribute from the given assembly as tools to the server. /// /// The builder instance. /// The assembly to load the types from. Null to get the current assembly public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder builder, Assembly? assembly = null) { assembly ??= Assembly.GetCallingAssembly(); List toolTypes = []; foreach (var type in assembly.GetTypes()) { bool hasToolTypeAttribute = type.GetCustomAttribute() != null; if (!hasToolTypeAttribute) continue; var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); foreach (var method in methods) { var attribute = method.GetCustomAttribute(); 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 properties = []; List? requiredProperties = null; foreach (var parameter in method.GetParameters()) { if (parameter.ParameterType == typeof(CancellationToken)) continue; var parameterAttribute = parameter.GetCustomAttribute(); 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 CallTool(RequestContext request, MethodInfo method, CancellationToken cancellationToken) { var methodParameters = method.GetParameters(); List 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 ResolveParameters(RequestContext request, ParameterInfo[] methodParameters, CancellationToken cancellationToken) { var parameters = new List(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, CultureInfo.InvariantCulture)); } else { var parameterAttribute = parameter.GetCustomAttribute(); 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!); } }