using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using Moq; using System.ComponentModel; using System.Reflection; namespace ModelContextProtocol.Tests.Server; public class McpServerPromptTests { [Fact] public void Create_InvalidArgs_Throws() { Assert.Throws("function", () => McpServerPrompt.Create((AIFunction)null!)); Assert.Throws("method", () => McpServerPrompt.Create((MethodInfo)null!)); Assert.Throws("method", () => McpServerPrompt.Create((MethodInfo)null!, _ => new object())); Assert.Throws("createTargetFunc", () => McpServerPrompt.Create(typeof(McpServerPromptTests).GetMethod(nameof(Create_InvalidArgs_Throws))!, null!)); Assert.Throws("method", () => McpServerPrompt.Create((Delegate)null!)); } [Fact] public async Task SupportsIMcpServer() { Mock mockServer = new(); McpServerPrompt prompt = McpServerPrompt.Create((IMcpServer server) => { Assert.Same(mockServer.Object, server); return new ChatMessage(ChatRole.User, "Hello"); }); Assert.DoesNotContain("server", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []); var result = await prompt.GetAsync( new RequestContext(mockServer.Object), TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.NotNull(result.Messages); Assert.Single(result.Messages); Assert.Equal("Hello", result.Messages[0].Content.Text); } [Fact] public async Task SupportsServiceFromDI() { MyService expectedMyService = new(); ServiceCollection sc = new(); sc.AddSingleton(expectedMyService); IServiceProvider services = sc.BuildServiceProvider(); McpServerPrompt prompt = McpServerPrompt.Create((MyService actualMyService, int? something = null) => { Assert.Same(expectedMyService, actualMyService); return new PromptMessage() { Role = Role.Assistant, Content = new() { Text = "Hello", Type = "text" } }; }, new() { Services = services }); Assert.Contains("something", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []); Assert.DoesNotContain("actualMyService", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []); await Assert.ThrowsAnyAsync(async () => await prompt.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken)); var result = await prompt.GetAsync( new RequestContext(new Mock().Object) { Services = services }, TestContext.Current.CancellationToken); Assert.Equal("Hello", result.Messages[0].Content.Text); } [Fact] public async Task SupportsOptionalServiceFromDI() { MyService expectedMyService = new(); ServiceCollection sc = new(); sc.AddSingleton(expectedMyService); IServiceProvider services = sc.BuildServiceProvider(); McpServerPrompt prompt = McpServerPrompt.Create((MyService? actualMyService = null) => { Assert.Null(actualMyService); return new PromptMessage() { Role = Role.Assistant, Content = new() { Text = "Hello", Type = "text" } }; }, new() { Services = services }); var result = await prompt.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Equal("Hello", result.Messages[0].Content.Text); } [Fact] public async Task SupportsDisposingInstantiatedDisposableTargets() { McpServerPrompt prompt1 = McpServerPrompt.Create( typeof(DisposablePromptType).GetMethod(nameof(DisposablePromptType.InstanceMethod))!, _ => new DisposablePromptType()); var result = await prompt1.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Equal("disposals:1", result.Messages[0].Content.Text); } [Fact] public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableTargets() { McpServerPrompt prompt1 = McpServerPrompt.Create( typeof(AsyncDisposablePromptType).GetMethod(nameof(AsyncDisposablePromptType.InstanceMethod))!, _ => new AsyncDisposablePromptType()); var result = await prompt1.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Equal("asyncDisposals:1", result.Messages[0].Content.Text); } [Fact] public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableAndDisposableTargets() { McpServerPrompt prompt1 = McpServerPrompt.Create( typeof(AsyncDisposableAndDisposablePromptType).GetMethod(nameof(AsyncDisposableAndDisposablePromptType.InstanceMethod))!, _ => new AsyncDisposableAndDisposablePromptType()); var result = await prompt1.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Equal("disposals:0, asyncDisposals:1", result.Messages[0].Content.Text); } [Fact] public async Task CanReturnGetPromptResult() { GetPromptResult expected = new(); McpServerPrompt prompt = McpServerPrompt.Create(() => { return expected; }); var actual = await prompt.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Same(expected, actual); } [Fact] public async Task CanReturnText() { string expected = "hello"; McpServerPrompt prompt = McpServerPrompt.Create(() => { return expected; }); var actual = await prompt.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.NotNull(actual); Assert.NotNull(actual.Messages); Assert.Single(actual.Messages); Assert.Equal(Role.User, actual.Messages[0].Role); Assert.Equal("text", actual.Messages[0].Content.Type); Assert.Equal(expected, actual.Messages[0].Content.Text); } [Fact] public async Task CanReturnPromptMessage() { PromptMessage expected = new() { Role = Role.User, Content = new() { Text = "hello", Type = "text" } }; McpServerPrompt prompt = McpServerPrompt.Create(() => { return expected; }); var actual = await prompt.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.NotNull(actual); Assert.NotNull(actual.Messages); Assert.Single(actual.Messages); Assert.Same(expected, actual.Messages[0]); } [Fact] public async Task CanReturnPromptMessages() { PromptMessage[] expected = [ new() { Role = Role.User, Content = new() { Text = "hello", Type = "text" } }, new() { Role = Role.Assistant, Content = new() { Text = "hello again", Type = "text" } } ]; McpServerPrompt prompt = McpServerPrompt.Create(() => { return expected; }); var actual = await prompt.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.NotNull(actual); Assert.NotNull(actual.Messages); Assert.Equal(2, actual.Messages.Count); Assert.Equal(Role.User, actual.Messages[0].Role); Assert.Equal("text", actual.Messages[0].Content.Type); Assert.Equal("hello", actual.Messages[0].Content.Text); Assert.Equal(Role.Assistant, actual.Messages[1].Role); Assert.Equal("text", actual.Messages[1].Content.Type); Assert.Equal("hello again", actual.Messages[1].Content.Text); } [Fact] public async Task CanReturnChatMessage() { PromptMessage expected = new() { Role = Role.User, Content = new() { Text = "hello", Type = "text" } }; McpServerPrompt prompt = McpServerPrompt.Create(() => { return expected.ToChatMessage(); }); var actual = await prompt.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.NotNull(actual); Assert.NotNull(actual.Messages); Assert.Single(actual.Messages); Assert.Equal(Role.User, actual.Messages[0].Role); Assert.Equal("text", actual.Messages[0].Content.Type); Assert.Equal("hello", actual.Messages[0].Content.Text); } [Fact] public async Task CanReturnChatMessages() { PromptMessage[] expected = [ new() { Role = Role.User, Content = new() { Text = "hello", Type = "text" } }, new() { Role = Role.Assistant, Content = new() { Text = "hello again", Type = "text" } } ]; McpServerPrompt prompt = McpServerPrompt.Create(() => { return expected.Select(p => p.ToChatMessage()); }); var actual = await prompt.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.NotNull(actual); Assert.NotNull(actual.Messages); Assert.Equal(2, actual.Messages.Count); Assert.Equal(Role.User, actual.Messages[0].Role); Assert.Equal("text", actual.Messages[0].Content.Type); Assert.Equal("hello", actual.Messages[0].Content.Text); Assert.Equal(Role.Assistant, actual.Messages[1].Role); Assert.Equal("text", actual.Messages[1].Content.Type); Assert.Equal("hello again", actual.Messages[1].Content.Text); } [Fact] public async Task ThrowsForNullReturn() { McpServerPrompt prompt = McpServerPrompt.Create(() => { return (string)null!; }); await Assert.ThrowsAsync(async () => await prompt.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken)); } [Fact] public async Task ThrowsForUnexpectedTypeReturn() { McpServerPrompt prompt = McpServerPrompt.Create(() => { return new object(); }); await Assert.ThrowsAsync(async () => await prompt.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken)); } [Fact] public async Task SupportsSchemaCreateOptions() { AIJsonSchemaCreateOptions schemaCreateOptions = new() { TransformSchemaNode = (context, node) => { node["description"] = "1234"; return node; } }; McpServerPrompt prompt = McpServerPrompt.Create(([Description("argument1")] int num, [Description("argument2")] string str) => { return new ChatMessage(ChatRole.User, "Hello"); }, new() { SchemaCreateOptions = schemaCreateOptions }); Assert.NotNull(prompt.ProtocolPrompt.Arguments); Assert.All( prompt.ProtocolPrompt.Arguments, x => Assert.Equal("1234", x.Description) ); } private sealed class MyService; private class DisposablePromptType : IDisposable { public int Disposals { get; private set; } private ChatMessage _message = new ChatMessage(ChatRole.User, ""); public void Dispose() { Disposals++; ((TextContent)_message.Contents[0]).Text = $"disposals:{Disposals}"; } public ChatMessage InstanceMethod() { if (Disposals != 0) { throw new InvalidOperationException("Dispose was called"); } return _message; } } private class AsyncDisposablePromptType : IAsyncDisposable { public int AsyncDisposals { get; private set; } private ChatMessage _message = new ChatMessage(ChatRole.User, ""); public ValueTask DisposeAsync() { AsyncDisposals++; ((TextContent)_message.Contents[0]).Text = $"asyncDisposals:{AsyncDisposals}"; return default; } public ChatMessage InstanceMethod() { if (AsyncDisposals != 0) { throw new InvalidOperationException("DisposeAsync was called"); } return _message; } } private class AsyncDisposableAndDisposablePromptType : IAsyncDisposable, IDisposable { public int Disposals { get; private set; } public int AsyncDisposals { get; private set; } private ChatMessage _message = new ChatMessage(ChatRole.User, ""); public void Dispose() { Disposals++; ((TextContent)_message.Contents[0]).Text = $"disposals:{Disposals}, asyncDisposals:{AsyncDisposals}"; } public ValueTask DisposeAsync() { AsyncDisposals++; ((TextContent)_message.Contents[0]).Text = $"disposals:{Disposals}, asyncDisposals:{AsyncDisposals}"; return default; } public ChatMessage InstanceMethod() { if (Disposals + AsyncDisposals != 0) { throw new InvalidOperationException("Dispose and/or DisposeAsync was called"); } return _message; } } }