Move functionality out of IMcpClient/Server into extensions (#60)
This commit is contained in:
parent
cdcf134e7a
commit
67320b22d5
8 changed files with 1042 additions and 708 deletions
|
|
@ -48,8 +48,7 @@ public class IntegrationTests
|
|||
public async Task IntegrateWithMeai_UsingEverythingServer_ToolsAreProperlyCalled()
|
||||
{
|
||||
var client = await GetMcpClientAsync();
|
||||
var tools = await client.ListToolsAsync();
|
||||
var mappedTools = tools.Tools.Select(t => t.ToAITool(client)).ToList();
|
||||
var mappedTools = await client.ListToolsAsync().Select(t => t.ToAITool(client)).ToListAsync();
|
||||
|
||||
IChatClient openaiClient = new OpenAIClient(_openAIKey)
|
||||
.AsChatClient("gpt-4o-mini");
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="System.Linq.AsyncEnumerable" Version="10.0.0-preview.1.25080.5" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ internal static class Program
|
|||
|
||||
private static void ConfigureTools(IMcpServer server)
|
||||
{
|
||||
server.ListToolsHandler = (request, cancellationToken) =>
|
||||
server.SetListToolsHandler((request, cancellationToken) =>
|
||||
{
|
||||
return Task.FromResult(new ListToolsResult()
|
||||
{
|
||||
|
|
@ -103,9 +103,9 @@ internal static class Program
|
|||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
server.CallToolHandler = async (request, cancellationToken) =>
|
||||
server.SetCallToolHandler(async (request, cancellationToken) =>
|
||||
{
|
||||
if (request.Params?.Name == "echo")
|
||||
{
|
||||
|
|
@ -138,12 +138,12 @@ internal static class Program
|
|||
{
|
||||
throw new McpServerException($"Unknown tool: {request.Params?.Name}");
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigurePrompts(IMcpServer server)
|
||||
{
|
||||
server.ListPromptsHandler = (request, cancellationToken) =>
|
||||
server.SetListPromptsHandler((request, cancellationToken) =>
|
||||
{
|
||||
return Task.FromResult(new ListPromptsResult()
|
||||
{
|
||||
|
|
@ -175,9 +175,9 @@ internal static class Program
|
|||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
server.GetPromptHandler = (request, cancellationToken) =>
|
||||
server.SetGetPromptHandler((request, cancellationToken) =>
|
||||
{
|
||||
List<PromptMessage> messages = new();
|
||||
if (request.Params?.Name == "simple_prompt")
|
||||
|
|
@ -234,7 +234,7 @@ internal static class Program
|
|||
{
|
||||
Messages = messages
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureResources(IMcpServer server)
|
||||
|
|
@ -279,7 +279,7 @@ internal static class Program
|
|||
|
||||
const int pageSize = 10;
|
||||
|
||||
server.ListResourcesHandler = (request, cancellationToken) =>
|
||||
server.SetListResourcesHandler((request, cancellationToken) =>
|
||||
{
|
||||
int startIndex = 0;
|
||||
request ??= new(server, new());
|
||||
|
|
@ -308,9 +308,9 @@ internal static class Program
|
|||
NextCursor = nextCursor,
|
||||
Resources = resources.GetRange(startIndex, endIndex - startIndex)
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
server.ReadResourceHandler = (request, cancellationToken) =>
|
||||
server.SetReadResourceHandler((request, cancellationToken) =>
|
||||
{
|
||||
if (request.Params?.Uri is null)
|
||||
{
|
||||
|
|
@ -323,7 +323,7 @@ internal static class Program
|
|||
{
|
||||
Contents = [contents]
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureCompletion(IMcpServer server)
|
||||
|
|
@ -335,32 +335,32 @@ internal static class Program
|
|||
{"temperature", ["0", "0.5", "0.7", "1.0"]},
|
||||
};
|
||||
|
||||
server.GetCompletionHandler = (request, cancellationToken) =>
|
||||
server.SetGetCompletionHandler((request, cancellationToken) =>
|
||||
{
|
||||
if (request.Params?.Ref?.Type == "ref/resource")
|
||||
{
|
||||
if (request.Params?.Ref?.Type == "ref/resource")
|
||||
{
|
||||
var resourceId = request.Params?.Ref?.Uri?.Split('/').LastOrDefault();
|
||||
if (string.IsNullOrEmpty(resourceId))
|
||||
return Task.FromResult(new CompleteResult() { Completion = new() { Values = [] } });
|
||||
var resourceId = request.Params?.Ref?.Uri?.Split('/').LastOrDefault();
|
||||
if (string.IsNullOrEmpty(resourceId))
|
||||
return Task.FromResult(new CompleteResult() { Completion = new() { Values = [] } });
|
||||
|
||||
// Filter resource IDs that start with the input value
|
||||
var values = sampleResourceIds.Where(id => id.StartsWith(request.Params.Argument.Value)).ToArray();
|
||||
return Task.FromResult(new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } });
|
||||
// Filter resource IDs that start with the input value
|
||||
var values = sampleResourceIds.Where(id => id.StartsWith(request.Params.Argument.Value)).ToArray();
|
||||
return Task.FromResult(new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } });
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (request.Params?.Ref?.Type == "ref/prompt")
|
||||
{
|
||||
// Handle completion for prompt arguments
|
||||
if (!exampleCompletions.TryGetValue(request.Params.Argument.Name, out var completions))
|
||||
return Task.FromResult(new CompleteResult() { Completion = new() { Values = [] } });
|
||||
if (request.Params?.Ref?.Type == "ref/prompt")
|
||||
{
|
||||
// Handle completion for prompt arguments
|
||||
if (!exampleCompletions.TryGetValue(request.Params.Argument.Name, out var completions))
|
||||
return Task.FromResult(new CompleteResult() { Completion = new() { Values = [] } });
|
||||
|
||||
var values = completions.Where(value => value.StartsWith(request.Params.Argument.Value)).ToArray();
|
||||
return Task.FromResult(new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } });
|
||||
}
|
||||
var values = completions.Where(value => value.StartsWith(request.Params.Argument.Value)).ToArray();
|
||||
return Task.FromResult(new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } });
|
||||
}
|
||||
|
||||
throw new McpServerException($"Unknown reference type: {request.Params?.Ref.Type}");
|
||||
};
|
||||
throw new McpServerException($"Unknown reference type: {request.Params?.Ref.Type}");
|
||||
});
|
||||
}
|
||||
|
||||
static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
|
||||
|
|
|
|||
|
|
@ -59,11 +59,10 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
|
|||
|
||||
// act
|
||||
var client = await _fixture.Factory.GetClientAsync(clientId);
|
||||
var tools = await client.ListToolsAsync();
|
||||
var tools = await client.ListToolsAsync().ToListAsync();
|
||||
|
||||
// assert
|
||||
Assert.NotNull(tools);
|
||||
Assert.NotEmpty(tools.Tools);
|
||||
Assert.NotEmpty(tools);
|
||||
// We could add more specific assertions about expected tools
|
||||
}
|
||||
|
||||
|
|
@ -99,14 +98,13 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
|
|||
|
||||
// act
|
||||
var client = await _fixture.Factory.GetClientAsync(clientId);
|
||||
var prompts = await client.ListPromptsAsync();
|
||||
var prompts = await client.ListPromptsAsync().ToListAsync();
|
||||
|
||||
// assert
|
||||
Assert.NotNull(prompts);
|
||||
Assert.NotEmpty(prompts.Prompts);
|
||||
Assert.NotEmpty(prompts);
|
||||
// We could add specific assertions for the known prompts
|
||||
Assert.Contains(prompts.Prompts, p => p.Name == "simple_prompt");
|
||||
Assert.Contains(prompts.Prompts, p => p.Name == "complex_prompt");
|
||||
Assert.Contains(prompts, p => p.Name == "simple_prompt");
|
||||
Assert.Contains(prompts, p => p.Name == "complex_prompt");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -265,7 +263,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
|
|||
|
||||
// Set up the sampling handler
|
||||
int samplingHandlerCalls = 0;
|
||||
client.SamplingHandler = (_, _) =>
|
||||
client.SetSamplingHandler((_, _) =>
|
||||
{
|
||||
samplingHandlerCalls++;
|
||||
return Task.FromResult(new CreateMessageResult
|
||||
|
|
@ -278,7 +276,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
|
|||
Text = "Test response"
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Call the server's sampleLLM tool which should trigger our sampling handler
|
||||
var result = await client.CallToolAsync(
|
||||
|
|
@ -311,14 +309,14 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
|
|||
// var client = await _fixture.Factory.GetClientAsync(clientId);
|
||||
|
||||
// // Set up the roots handler
|
||||
// client.RootsHandler = (request, ct) =>
|
||||
// client.SetRootsHandler((request, ct) =>
|
||||
// {
|
||||
// rootsHandlerCalls++;
|
||||
// return Task.FromResult(new ListRootsResult
|
||||
// {
|
||||
// Roots = testRoots
|
||||
// });
|
||||
// };
|
||||
// });
|
||||
|
||||
// // Connect
|
||||
// await client.ConnectAsync(CancellationToken.None);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using McpDotNet.Protocol.Types;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Protocol.Types;
|
||||
using McpDotNet.Server;
|
||||
using Moq;
|
||||
|
||||
namespace McpDotNet.Tests.Server;
|
||||
|
||||
|
|
@ -10,8 +10,7 @@ public class McpServerDelegatesTests
|
|||
public void Applies_All_Given_Delegates()
|
||||
{
|
||||
var container = new McpServerDelegates();
|
||||
var server = new Mock<IMcpServer>();
|
||||
server.SetupAllProperties();
|
||||
var server = new ExposeSetHandlersServer();
|
||||
|
||||
container.ListToolsHandler = (p, c) => Task.FromResult(new ListToolsResult());
|
||||
container.CallToolHandler = (p, c) => Task.FromResult(new CallToolResponse());
|
||||
|
|
@ -23,16 +22,33 @@ public class McpServerDelegatesTests
|
|||
container.SubscribeToResourcesHandler = (s, c) => Task.CompletedTask;
|
||||
container.UnsubscribeFromResourcesHandler = (s, c) => Task.CompletedTask;
|
||||
|
||||
container.Apply(server.Object);
|
||||
container.Apply(server);
|
||||
|
||||
Assert.Equal(container.ListToolsHandler, server.Object.ListToolsHandler);
|
||||
Assert.Equal(container.CallToolHandler, server.Object.CallToolHandler);
|
||||
Assert.Equal(container.ListPromptsHandler, server.Object.ListPromptsHandler);
|
||||
Assert.Equal(container.GetPromptHandler, server.Object.GetPromptHandler);
|
||||
Assert.Equal(container.ListResourcesHandler, server.Object.ListResourcesHandler);
|
||||
Assert.Equal(container.ReadResourceHandler, server.Object.ReadResourceHandler);
|
||||
Assert.Equal(container.GetCompletionHandler, server.Object.GetCompletionHandler);
|
||||
Assert.Equal(container.SubscribeToResourcesHandler, server.Object.SubscribeToResourcesHandler);
|
||||
Assert.Equal(container.UnsubscribeFromResourcesHandler, server.Object.UnsubscribeFromResourcesHandler);
|
||||
Assert.Equal(container.ListToolsHandler, server.Handlers[OperationNames.ListTools]);
|
||||
Assert.Equal(container.CallToolHandler, server.Handlers[OperationNames.CallTool]);
|
||||
Assert.Equal(container.ListPromptsHandler, server.Handlers[OperationNames.ListPrompts]);
|
||||
Assert.Equal(container.GetPromptHandler, server.Handlers[OperationNames.GetPrompt]);
|
||||
Assert.Equal(container.ListResourcesHandler, server.Handlers[OperationNames.ListResources]);
|
||||
Assert.Equal(container.ReadResourceHandler, server.Handlers[OperationNames.ReadResource]);
|
||||
Assert.Equal(container.GetCompletionHandler, server.Handlers[OperationNames.GetCompletion]);
|
||||
Assert.Equal(container.SubscribeToResourcesHandler, server.Handlers[OperationNames.SubscribeToResources]);
|
||||
Assert.Equal(container.UnsubscribeFromResourcesHandler, server.Handlers[OperationNames.UnsubscribeFromResources]);
|
||||
}
|
||||
|
||||
private sealed class ExposeSetHandlersServer : IMcpServer
|
||||
{
|
||||
public Dictionary<string, Delegate> Handlers = [];
|
||||
|
||||
public void SetOperationHandler(string operationName, Delegate handler) => Handlers[operationName] = handler;
|
||||
|
||||
public ValueTask DisposeAsync() => default;
|
||||
public bool IsInitialized => throw new NotImplementedException();
|
||||
public ClientCapabilities? ClientCapabilities => throw new NotImplementedException();
|
||||
public Implementation? ClientInfo => throw new NotImplementedException();
|
||||
public IServiceProvider? ServiceProvider => throw new NotImplementedException();
|
||||
public void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler) => throw new NotImplementedException();
|
||||
public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
public Task<T> SendRequestAsync<T>(JsonRpcRequest request, CancellationToken cancellationToken) where T : class => throw new NotImplementedException();
|
||||
public Task StartAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ public class McpServerTests
|
|||
await Can_Handle_Requests(null, "completion/complete",
|
||||
configureServer: server =>
|
||||
{
|
||||
server.GetCompletionHandler = (request, ct) =>
|
||||
server.SetGetCompletionHandler((request, ct) =>
|
||||
{
|
||||
return Task.FromResult(new CompleteResult
|
||||
{
|
||||
|
|
@ -284,7 +284,7 @@ public class McpServerTests
|
|||
HasMore = true
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
},
|
||||
assertResult: response =>
|
||||
{
|
||||
|
|
@ -305,13 +305,13 @@ public class McpServerTests
|
|||
await Can_Handle_Requests(new ServerCapabilities { Resources = new() }, "resources/list",
|
||||
configureServer: server =>
|
||||
{
|
||||
server.ListResourcesHandler = (request, ct) =>
|
||||
server.SetListResourcesHandler((request, ct) =>
|
||||
{
|
||||
return Task.FromResult(new ListResourcesResult
|
||||
{
|
||||
Resources = [new() { Uri = "test", Name = "Test Resource" }]
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
},
|
||||
assertResult: response =>
|
||||
|
|
@ -337,13 +337,13 @@ public class McpServerTests
|
|||
await Can_Handle_Requests(new ServerCapabilities { Resources = new() }, "resources/read",
|
||||
configureServer: server =>
|
||||
{
|
||||
server.ReadResourceHandler = (request, ct) =>
|
||||
server.SetReadResourceHandler((request, ct) =>
|
||||
{
|
||||
return Task.FromResult(new ReadResourceResult
|
||||
{
|
||||
Contents = [new() { Text = "test" }]
|
||||
});
|
||||
};
|
||||
});
|
||||
},
|
||||
assertResult: response =>
|
||||
{
|
||||
|
|
@ -368,13 +368,13 @@ public class McpServerTests
|
|||
await Can_Handle_Requests(new ServerCapabilities { Prompts = new() }, "prompts/list",
|
||||
configureServer: server =>
|
||||
{
|
||||
server.ListPromptsHandler = (request, ct) =>
|
||||
server.SetListPromptsHandler((request, ct) =>
|
||||
{
|
||||
return Task.FromResult(new ListPromptsResult
|
||||
{
|
||||
Prompts = [new() { Name = "test" }]
|
||||
});
|
||||
};
|
||||
});
|
||||
},
|
||||
assertResult: response =>
|
||||
{
|
||||
|
|
@ -399,13 +399,13 @@ public class McpServerTests
|
|||
await Can_Handle_Requests(new ServerCapabilities { Prompts = new() }, "prompts/get",
|
||||
configureServer: server =>
|
||||
{
|
||||
server.GetPromptHandler = (request, ct) =>
|
||||
server.SetGetPromptHandler((request, ct) =>
|
||||
{
|
||||
return Task.FromResult(new GetPromptResult
|
||||
{
|
||||
Description = "test"
|
||||
});
|
||||
};
|
||||
});
|
||||
},
|
||||
assertResult: response =>
|
||||
{
|
||||
|
|
@ -428,13 +428,13 @@ public class McpServerTests
|
|||
await Can_Handle_Requests(new ServerCapabilities { Tools = new() }, "tools/list",
|
||||
configureServer: server =>
|
||||
{
|
||||
server.ListToolsHandler = (request, ct) =>
|
||||
server.SetListToolsHandler((request, ct) =>
|
||||
{
|
||||
return Task.FromResult(new ListToolsResult
|
||||
{
|
||||
Tools = [new() { Name = "test" }]
|
||||
});
|
||||
};
|
||||
});
|
||||
},
|
||||
assertResult: response =>
|
||||
{
|
||||
|
|
@ -458,13 +458,13 @@ public class McpServerTests
|
|||
await Can_Handle_Requests(new ServerCapabilities { Tools = new() }, "tools/call",
|
||||
configureServer: server =>
|
||||
{
|
||||
server.CallToolHandler = (request, ct) =>
|
||||
server.SetCallToolHandler((request, ct) =>
|
||||
{
|
||||
return Task.FromResult(new CallToolResponse
|
||||
{
|
||||
Content = [new Content { Text = "test" }]
|
||||
});
|
||||
};
|
||||
});
|
||||
},
|
||||
assertResult: response =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -87,11 +87,10 @@ public class SseIntegrationTests
|
|||
|
||||
// Create client and run tests
|
||||
var client = await factory.GetClientAsync("everything");
|
||||
var tools = await client.ListToolsAsync();
|
||||
var tools = await client.ListToolsAsync().ToListAsync();
|
||||
|
||||
// assert
|
||||
Assert.NotNull(tools);
|
||||
Assert.NotEmpty(tools.Tools);
|
||||
Assert.NotEmpty(tools);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -136,7 +135,7 @@ public class SseIntegrationTests
|
|||
|
||||
// Set up the sampling handler
|
||||
int samplingHandlerCalls = 0;
|
||||
client.SamplingHandler = (_, _) =>
|
||||
client.SetSamplingHandler((_, _) =>
|
||||
{
|
||||
samplingHandlerCalls++;
|
||||
return Task.FromResult(new CreateMessageResult
|
||||
|
|
@ -149,7 +148,7 @@ public class SseIntegrationTests
|
|||
Text = "Test response"
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Call the server's sampleLLM tool which should trigger our sampling handler
|
||||
var result = await client.CallToolAsync(
|
||||
|
|
@ -253,7 +252,7 @@ public class SseIntegrationTests
|
|||
await server.WaitForConnectionAsync(TimeSpan.FromSeconds(10));
|
||||
|
||||
var receivedNotification = new TaskCompletionSource<string?>();
|
||||
client.OnNotification("test/notification", (args) =>
|
||||
client.AddNotificationHandler("test/notification", (args) =>
|
||||
{
|
||||
var msg = ((JsonElement?)args.Params)?.GetProperty("message").GetString();
|
||||
receivedNotification.SetResult(msg);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="System.Linq.AsyncEnumerable" Version="10.0.0-preview.1.25080.5" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
|
|
|||
Loading…
Reference in a new issue