diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2e0a2f5..b6bed11 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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:
diff --git a/.gitignore b/.gitignore
index 1c7489e..879b5d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,4 +62,5 @@ ehthumbs.db
Desktop.ini
# Recycle Bin used on file shares
-$RECYCLE.BIN/
\ No newline at end of file
+$RECYCLE.BIN/
+/mcpdotnet.v3.ncrunchsolution
diff --git a/README.MD b/README.MD
index db3f88e..c063c0b 100644
--- a/README.MD
+++ b/README.MD
@@ -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:
diff --git a/samples/TestServerWithHosting/Program.cs b/samples/TestServerWithHosting/Program.cs
index 4f0770c..f1bdf7c 100644
--- a/samples/TestServerWithHosting/Program.cs
+++ b/samples/TestServerWithHosting/Program.cs
@@ -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();
diff --git a/samples/TestServerWithHosting/Tools/EchoTool.cs b/samples/TestServerWithHosting/Tools/EchoTool.cs
new file mode 100644
index 0000000..4b3ba0d
--- /dev/null
+++ b/samples/TestServerWithHosting/Tools/EchoTool.cs
@@ -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;
+ }
+}
diff --git a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs
new file mode 100644
index 0000000..2ed764e
--- /dev/null
+++ b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs
@@ -0,0 +1,47 @@
+using McpDotNet.Protocol.Types;
+using McpDotNet.Server;
+
+namespace TestServerWithHosting.Tools;
+
+///
+/// This tool uses depenency injection and async method
+///
+[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 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
+ };
+ }
+}
diff --git a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.cs b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Handler.cs
similarity index 68%
rename from src/mcpdotnet/Configuration/McpServerBuilderExtensions.cs
rename to src/mcpdotnet/Configuration/McpServerBuilderExtensions.Handler.cs
index b97171a..22bdd8f 100644
--- a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.cs
+++ b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Handler.cs
@@ -1,5 +1,4 @@
using McpDotNet.Configuration;
-using McpDotNet.Protocol.Transport;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.DependencyInjection;
@@ -7,28 +6,16 @@ using Microsoft.Extensions.DependencyInjection;
namespace McpDotNet;
///
-/// Extension to configure the MCP server
+/// Extension to configure the MCP server with handlers
///
-public static class McpServerBuilderExtensions
+public static partial class McpServerBuilderExtensions
{
- ///
- /// Adds a server transport that uses stdin/stdout for communication.
- ///
- /// The builder instance.
- ///
- public static IMcpServerBuilder WithStdioServerTransport(this IMcpServerBuilder builder)
- {
- builder.Services.AddSingleton();
- return builder;
- }
-
///
/// Sets the handler for list tools requests.
///
/// The builder instance.
/// The handler.
- ///
- public static IMcpServerBuilder WithListToolsHandler(this IMcpServerBuilder builder, Func> handler)
+ public static IMcpServerBuilder WithListToolsHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler)
{
builder.Services.Configure(s => s.ListToolsHandler = handler);
return builder;
@@ -39,8 +26,7 @@ public static class McpServerBuilderExtensions
///
/// The builder instance.
/// The handler.
- ///
- public static IMcpServerBuilder WithCallToolHandler(this IMcpServerBuilder builder, Func> handler)
+ public static IMcpServerBuilder WithCallToolHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler)
{
builder.Services.Configure(s => s.CallToolHandler = handler);
return builder;
@@ -51,8 +37,7 @@ public static class McpServerBuilderExtensions
///
/// The builder instance.
/// The handler.
- ///
- public static IMcpServerBuilder WithListPromptsHandler(this IMcpServerBuilder builder, Func> handler)
+ public static IMcpServerBuilder WithListPromptsHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler)
{
builder.Services.Configure(s => s.ListPromptsHandler = handler);
return builder;
@@ -63,8 +48,7 @@ public static class McpServerBuilderExtensions
///
/// The builder instance.
/// The handler.
- ///
- public static IMcpServerBuilder WithGetPromptHandler(this IMcpServerBuilder builder, Func> handler)
+ public static IMcpServerBuilder WithGetPromptHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler)
{
builder.Services.Configure(s => s.GetPromptHandler = handler);
return builder;
@@ -75,8 +59,7 @@ public static class McpServerBuilderExtensions
///
/// The builder instance.
/// The handler.
- ///
- public static IMcpServerBuilder WithListResourcesHandler(this IMcpServerBuilder builder, Func> handler)
+ public static IMcpServerBuilder WithListResourcesHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler)
{
builder.Services.Configure(s => s.ListResourcesHandler = handler);
return builder;
@@ -87,8 +70,7 @@ public static class McpServerBuilderExtensions
///
/// The builder instance.
/// The handler.
- ///
- public static IMcpServerBuilder WithReadResourceHandler(this IMcpServerBuilder builder, Func> handler)
+ public static IMcpServerBuilder WithReadResourceHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler)
{
builder.Services.Configure(s => s.ReadResourceHandler = handler);
return builder;
@@ -99,8 +81,7 @@ public static class McpServerBuilderExtensions
///
/// The builder instance.
/// The handler.
- ///
- public static IMcpServerBuilder WithGetCompletionHandler(this IMcpServerBuilder builder, Func> handler)
+ public static IMcpServerBuilder WithGetCompletionHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler)
{
builder.Services.Configure(s => s.GetCompletionHandler = handler);
return builder;
@@ -111,8 +92,7 @@ public static class McpServerBuilderExtensions
///
/// The builder instance.
/// The handler.
- ///
- public static IMcpServerBuilder WithSubscribeToResourcesHandler(this IMcpServerBuilder builder, Func handler)
+ public static IMcpServerBuilder WithSubscribeToResourcesHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler)
{
builder.Services.Configure(s => s.SubscribeToResourcesHandler = handler);
return builder;
@@ -123,8 +103,7 @@ public static class McpServerBuilderExtensions
///
/// The builder instance.
/// The handler.
- ///
- public static IMcpServerBuilder WithUnsubscribeFromResourcesHandler(this IMcpServerBuilder builder, Func handler)
+ public static IMcpServerBuilder WithUnsubscribeFromResourcesHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler)
{
builder.Services.Configure(s => s.UnsubscribeFromResourcesHandler = handler);
return builder;
diff --git a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs
new file mode 100644
index 0000000..c2feb80
--- /dev/null
+++ b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs
@@ -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;
+
+///
+/// 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)
+ {
+ ArgumentNullException.ThrowIfNull(toolTypes);
+ if (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));
+
+ // 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;
+ }
+
+ ///
+ /// 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