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:
parent
7d69499ad5
commit
ec9bdfcf5d
28 changed files with 1036 additions and 235 deletions
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
|
|
@ -3,10 +3,8 @@ name: .NET Build, Test and publish
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main", "master" ]
|
branches: [ "main", "master" ]
|
||||||
paths: [ 'src/**' ]
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main", "master" ]
|
branches: [ "main", "master" ]
|
||||||
paths: [ 'src/**' ]
|
|
||||||
release:
|
release:
|
||||||
types: [published,edited]
|
types: [published,edited]
|
||||||
|
|
||||||
|
|
@ -24,7 +22,12 @@ jobs:
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||||
|
|
||||||
- name: Fetch all history for all tags and branches
|
- 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
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v2
|
uses: actions/setup-dotnet@v2
|
||||||
|
|
@ -33,6 +36,17 @@ jobs:
|
||||||
8.0.x
|
8.0.x
|
||||||
9.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
|
- name: Run Cake script
|
||||||
uses: cake-build/cake-action@v1
|
uses: cake-build/cake-action@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -63,3 +63,4 @@ Desktop.ini
|
||||||
|
|
||||||
# Recycle Bin used on file shares
|
# Recycle Bin used on file shares
|
||||||
$RECYCLE.BIN/
|
$RECYCLE.BIN/
|
||||||
|
/mcpdotnet.v3.ncrunchsolution
|
||||||
|
|
|
||||||
29
README.MD
29
README.MD
|
|
@ -106,7 +106,33 @@ Remember you can connect to any MCP server, not just ones created using mcpdotne
|
||||||
|
|
||||||
## Getting Started (Server)
|
## 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
|
```csharp
|
||||||
McpServerOptions options = new McpServerOptions()
|
McpServerOptions options = new McpServerOptions()
|
||||||
|
|
@ -171,6 +197,7 @@ while (true)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- Expand documentation with detailed guides for:
|
- Expand documentation with detailed guides for:
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ try
|
||||||
builder.Services.AddSerilog();
|
builder.Services.AddSerilog();
|
||||||
builder.Services.AddMcpServer()
|
builder.Services.AddMcpServer()
|
||||||
.WithStdioServerTransport()
|
.WithStdioServerTransport()
|
||||||
|
.WithTools()
|
||||||
.WithCallToolHandler((r, ct) => Task.FromResult(new McpDotNet.Protocol.Types.CallToolResponse()));
|
.WithCallToolHandler((r, ct) => Task.FromResult(new McpDotNet.Protocol.Types.CallToolResponse()));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
|
||||||
13
samples/TestServerWithHosting/Tools/EchoTool.cs
Normal file
13
samples/TestServerWithHosting/Tools/EchoTool.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
47
samples/TestServerWithHosting/Tools/SampleLlmTool.cs
Normal file
47
samples/TestServerWithHosting/Tools/SampleLlmTool.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
249
src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs
Normal file
249
src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs
Normal 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!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,11 @@ public interface IMcpServer : IAsyncDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Implementation? ClientInfo { get; }
|
Implementation? ClientInfo { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the service provider for the server.
|
||||||
|
/// </summary>
|
||||||
|
IServiceProvider? ServiceProvider { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the server and begins listening for client requests.
|
/// Starts the server and begins listening for client requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -31,47 +36,47 @@ public interface IMcpServer : IAsyncDisposable
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for list tools requests.
|
/// Gets or sets the handler for list tools requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Func<ListToolsRequestParams?, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
|
Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for call tool requests.
|
/// Gets or sets the handler for call tool requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Func<CallToolRequestParams?, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
|
Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for list prompts requests.
|
/// Gets or sets the handler for list prompts requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Func<ListPromptsRequestParams?, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
|
Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for get prompt requests.
|
/// Gets or sets the handler for get prompt requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Func<GetPromptRequestParams?, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
|
Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for list resources requests.
|
/// Gets or sets the handler for list resources requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Func<ListResourcesRequestParams?, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
|
Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for read resources requests.
|
/// Gets or sets the handler for read resources requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Func<ReadResourceRequestParams?, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
|
Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for get resources requests.
|
/// Gets or sets the handler for get resources requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Func<CompleteRequestParams?, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
|
Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for subscribe to resources messages.
|
/// Gets or sets the handler for subscribe to resources messages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Func<string, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
|
Func<RequestContext<string>, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for subscribe to resources messages.
|
/// Gets or sets the handler for subscribe to resources messages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Func<string, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
|
Func<RequestContext<string>, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
29
src/mcpdotnet/Server/McpParameterAttribute.cs
Normal file
29
src/mcpdotnet/Server/McpParameterAttribute.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,8 +12,8 @@ namespace McpDotNet.Server;
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
internal class McpServer : McpJsonRpcEndpoint, IMcpServer
|
internal class McpServer : McpJsonRpcEndpoint, IMcpServer
|
||||||
{
|
{
|
||||||
private IServerTransport _serverTransport;
|
private readonly IServerTransport _serverTransport;
|
||||||
private McpServerOptions _options;
|
private readonly McpServerOptions _options;
|
||||||
private volatile bool _isInitializing;
|
private volatile bool _isInitializing;
|
||||||
private readonly ILogger<McpServer> _logger;
|
private readonly ILogger<McpServer> _logger;
|
||||||
|
|
||||||
|
|
@ -24,142 +24,24 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
|
||||||
/// <param name="options">Configuration options for this server, including capabilities.
|
/// <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>
|
/// 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="loggerFactory">Logger factory to use for logging</param>
|
||||||
|
/// <param name="serviceProvider">Optional service provider to use for dependency injection</param>
|
||||||
/// <exception cref="McpServerException"></exception>
|
/// <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)
|
: base(transport, loggerFactory)
|
||||||
{
|
{
|
||||||
_serverTransport = transport;
|
_serverTransport = transport;
|
||||||
_options = options;
|
_options = options;
|
||||||
_logger = loggerFactory.CreateLogger<McpServer>();
|
_logger = loggerFactory.CreateLogger<McpServer>();
|
||||||
ServerInstructions = options.ServerInstructions;
|
ServerInstructions = options.ServerInstructions;
|
||||||
|
ServiceProvider = serviceProvider;
|
||||||
|
|
||||||
if (options.Capabilities?.Tools != null)
|
SetToolsHandler(options);
|
||||||
{
|
SetPromptsHandler(options);
|
||||||
SetRequestHandler<ListToolsRequestParams, ListToolsResult>("tools/list",
|
SetResourcesHandler(options);
|
||||||
async (request) =>
|
SetCompletionHandler();
|
||||||
{
|
SetInitializeHandler(options);
|
||||||
if (ListToolsHandler == null)
|
SetPingHandler();
|
||||||
{
|
SetNotificationHandler();
|
||||||
// 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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientCapabilities? ClientCapabilities { get; set; }
|
public ClientCapabilities? ClientCapabilities { get; set; }
|
||||||
|
|
@ -171,31 +53,34 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
|
||||||
public string? ServerInstructions { get; set; }
|
public string? ServerInstructions { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Func<ListToolsRequestParams?, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
|
public IServiceProvider? ServiceProvider { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Func<CallToolRequestParams?, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
|
public Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Func<ListPromptsRequestParams?, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
|
public Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Func<GetPromptRequestParams?, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
|
public Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Func<ListResourcesRequestParams?, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
|
public Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Func<ReadResourceRequestParams?, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
|
public Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Func<CompleteRequestParams?, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
|
public Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Func<string, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
|
public Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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 />
|
/// <inheritdoc />
|
||||||
public override string EndpointName
|
public override string EndpointName
|
||||||
|
|
@ -277,4 +162,158 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
|
||||||
cancellationToken
|
cancellationToken
|
||||||
).ConfigureAwait(false);
|
).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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,47 +10,47 @@ public class McpServerDelegates
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for list tools requests.
|
/// Gets or sets the handler for list tools requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<ListToolsRequestParams?, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
|
public Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for call tool requests.
|
/// Gets or sets the handler for call tool requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<CallToolRequestParams?, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
|
public Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for list prompts requests.
|
/// Gets or sets the handler for list prompts requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<ListPromptsRequestParams?, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
|
public Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for get prompt requests.
|
/// Gets or sets the handler for get prompt requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<GetPromptRequestParams?, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
|
public Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for list resources requests.
|
/// Gets or sets the handler for list resources requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<ListResourcesRequestParams?, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
|
public Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for read resources requests.
|
/// Gets or sets the handler for read resources requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<ReadResourceRequestParams?, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
|
public Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for get resources requests.
|
/// Gets or sets the handler for get resources requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<CompleteRequestParams?, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
|
public Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for subscribe to resources messages.
|
/// Gets or sets the handler for subscribe to resources messages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<string, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
|
public Func<RequestContext<string>, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for subscribe to resources messages.
|
/// Gets or sets the handler for subscribe to resources messages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<string, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
|
public Func<RequestContext<string>, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies the delegates to the server.
|
/// Applies the delegates to the server.
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ public class McpServerFactory : IMcpServerFactory
|
||||||
private readonly McpServerOptions _options;
|
private readonly McpServerOptions _options;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly McpServerDelegates? _serverDelegates;
|
private readonly McpServerDelegates? _serverDelegates;
|
||||||
|
private readonly IServiceProvider? _serviceProvider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="McpServerFactory"/> class.
|
/// 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="serverTransport">Transport to use for the server</param>
|
||||||
/// <param name="options">Configuration options for this server, including capabilities.
|
/// <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>
|
/// 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="loggerFactory">Logger factory to use for logging</param>
|
||||||
/// <param name="serverDelegates"></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));
|
_serverTransport = serverTransport ?? throw new ArgumentNullException(nameof(serverTransport));
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||||
_serverDelegates = serverDelegates?.Value;
|
_serverDelegates = serverDelegates?.Value;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -44,7 +47,7 @@ public class McpServerFactory : IMcpServerFactory
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IMcpServer CreateServer()
|
public IMcpServer CreateServer()
|
||||||
{
|
{
|
||||||
var server = new McpServer(_serverTransport, _options, _loggerFactory);
|
var server = new McpServer(_serverTransport, _options, _loggerFactory, _serviceProvider);
|
||||||
|
|
||||||
_serverDelegates?.Apply(server);
|
_serverDelegates?.Apply(server);
|
||||||
|
|
||||||
|
|
|
||||||
27
src/mcpdotnet/Server/McpToolAttribute.cs
Normal file
27
src/mcpdotnet/Server/McpToolAttribute.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/mcpdotnet/Server/McpToolTypeAttribute.cs
Normal file
9
src/mcpdotnet/Server/McpToolTypeAttribute.cs
Normal 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
|
||||||
|
{
|
||||||
|
}
|
||||||
10
src/mcpdotnet/Server/RequestContext.cs
Normal file
10
src/mcpdotnet/Server/RequestContext.cs
Normal 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)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ internal abstract class McpJsonRpcEndpoint
|
||||||
private readonly ITransport _transport;
|
private readonly ITransport _transport;
|
||||||
private readonly ConcurrentDictionary<RequestId, TaskCompletionSource<IJsonRpcMessage>> _pendingRequests;
|
private readonly ConcurrentDictionary<RequestId, TaskCompletionSource<IJsonRpcMessage>> _pendingRequests;
|
||||||
private readonly ConcurrentDictionary<string, List<Func<JsonRpcNotification, Task>>> _notificationHandlers;
|
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 int _nextRequestId;
|
||||||
private readonly JsonSerializerOptions _jsonOptions;
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
private readonly ILogger<McpClient> _logger;
|
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>
|
/// <typeparam name="TResponse">Type of response payload (not full RPC response</typeparam>
|
||||||
/// <param name="method">Method identifier to register for</param>
|
/// <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>
|
/// <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);
|
ArgumentNullException.ThrowIfNull(handler);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ public class IntegrationTests
|
||||||
{
|
{
|
||||||
Id = "everything",
|
Id = "everything",
|
||||||
Name = "Everything",
|
Name = "Everything",
|
||||||
TransportType = McpDotNet.Protocol.Transport.TransportTypes.StdIo,
|
TransportType = "stdio",
|
||||||
TransportOptions = new Dictionary<string, string>
|
TransportOptions = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["command"] = "npx",
|
["command"] = "npx",
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\McpDotNet.Extensions.AI\McpDotNet.Extensions.AI.csproj" />
|
<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" />
|
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.3.0-preview.1.25114.11" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue