using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Xunit; namespace ModelContextProtocol.Analyzers.Tests; public partial class XmlToDescriptionGeneratorTests { [Fact] public void Generator_WithSummaryOnly_GeneratesMethodDescription() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Test tool description /// [McpServerTool] public static partial string TestMethod(string input) { return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Test tool description")] public static partial string TestMethod(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithSummaryAndRemarks_CombinesInMethodDescription() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Test tool summary /// /// /// Additional remarks /// [McpServerTool] public static partial string TestMethod(string input) { return input; } } """); Assert.True(result.Success); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Test tool summary\nAdditional remarks")] public static partial string TestMethod(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithParameterDocs_GeneratesParameterDescriptions() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Test tool /// /// Input parameter description /// Count parameter description [McpServerTool] public static partial string TestMethod(string input, int count) { return input; } } """); Assert.True(result.Success); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Test tool")] public static partial string TestMethod([Description("Input parameter description")] string input, [Description("Count parameter description")] int count); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithReturnDocs_GeneratesReturnDescription() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Test tool /// /// The result of the operation [McpServerTool] public static partial string TestMethod(string input) { return input; } } """); Assert.True(result.Success); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Test tool")] [return: Description("The result of the operation")] public static partial string TestMethod(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithExistingMethodDescription_DoesNotGenerateMethodDescription() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Test tool summary /// /// Result [McpServerTool] [Description("Already has description")] public static partial string TestMethod(string input) { return input; } } """); Assert.True(result.Success); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [return: Description("Result")] public static partial string TestMethod(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithExistingParameterDescription_SkipsThatParameter() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Test tool /// /// Input description /// Count description [McpServerTool] public static partial string TestMethod(string input, [Description("Already has")] int count) { return input; } } """); Assert.True(result.Success); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Test tool")] public static partial string TestMethod([Description("Input description")] string input, int count); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithoutMcpServerToolAttribute_DoesNotGenerate() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; public partial class TestTools { /// /// Test tool /// public static partial string TestMethod(string input) { return input; } } """); Assert.True(result.Success); Assert.Empty(result.GeneratedSources); } [Fact] public void Generator_WithoutPartialKeyword_DoesNotGenerate() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public class TestTools { /// /// Test tool /// [McpServerTool] public static string TestMethod(string input) { return input; } } """, "MCP002"); Assert.True(result.Success); Assert.Empty(result.GeneratedSources); } [Fact] public void Generator_NonPartialMethodWithXmlDocs_ReportsMCP002Diagnostic() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public class TestTools { /// /// Test tool with documentation /// [McpServerTool] public static string TestMethod(string input) { return input; } } """, "MCP002"); Assert.True(result.Success); Assert.Empty(result.GeneratedSources); // Should report MCP002 diagnostic var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity); Assert.Contains("TestMethod", diagnostic.GetMessage()); Assert.Contains("partial", diagnostic.GetMessage()); } [Fact] public void Generator_NonPartialMethodWithParameterDocs_ReportsMCP002Diagnostic() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public class TestTools { /// Input parameter [McpServerTool] public static string TestMethod(string input) { return input; } } """, "MCP002"); Assert.True(result.Success); Assert.Empty(result.GeneratedSources); // Should report MCP002 diagnostic because parameter has documentation var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity); } [Fact] public void Generator_NonPartialMethodWithReturnDocs_ReportsMCP002Diagnostic() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public class TestTools { /// Return value [McpServerTool] public static string TestMethod(string input) { return input; } } """, "MCP002"); Assert.True(result.Success); Assert.Empty(result.GeneratedSources); // Should report MCP002 diagnostic because return has documentation var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity); } [Fact] public void Generator_NonPartialMethodWithoutXmlDocs_DoesNotReportDiagnostic() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public class TestTools { [McpServerTool] public static string TestMethod(string input) { return input; } } """); Assert.True(result.Success); Assert.Empty(result.GeneratedSources); // Should NOT report MCP002 diagnostic because there's no XML documentation Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MCP002"); } [Fact] public void Generator_NonPartialMethodWithEmptyXmlDocs_DoesNotReportDiagnostic() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public class TestTools { /// [McpServerTool] public static string TestMethod(string input) { return input; } } """); Assert.True(result.Success); Assert.Empty(result.GeneratedSources); // Should NOT report MCP002 diagnostic because XML documentation is empty Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MCP002"); } [Fact] public void Generator_NonPartialMethodWithExistingDescriptions_DoesNotReportDiagnostic() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public class TestTools { /// Test tool /// Input param /// Return value [McpServerTool] [Description("Already has method description")] [return: Description("Already has return description")] public static string TestMethod([Description("Already has param description")] string input) { return input; } } """); Assert.True(result.Success); Assert.Empty(result.GeneratedSources); // Should NOT report MCP002 diagnostic because all descriptions already exist Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MCP002"); } [Fact] public void Generator_NonPartialMethodWithPartialExistingDescriptions_ReportsMCP002Diagnostic() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public class TestTools { /// Test tool /// Input param [McpServerTool] [Description("Already has method description")] public static string TestMethod(string input) { return input; } } """, "MCP002"); Assert.True(result.Success); Assert.Empty(result.GeneratedSources); // Should report MCP002 diagnostic because parameter description would be generated var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity); } [Fact] public void Generator_NonPartialPromptWithXmlDocs_ReportsMCP002Diagnostic() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerPromptType] public class TestPrompts { /// /// Test prompt /// [McpServerPrompt] public static string TestPrompt(string input) { return input; } } """, "MCP002"); Assert.True(result.Success); Assert.Empty(result.GeneratedSources); // Should report MCP002 diagnostic for prompts too var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity); } [Fact] public void Generator_NonPartialResourceWithXmlDocs_ReportsMCP002Diagnostic() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerResourceType] public class TestResources { /// /// Test resource /// [McpServerResource("test://resource")] public static string TestResource(string input) { return input; } } """, "MCP002"); Assert.True(result.Success); Assert.Empty(result.GeneratedSources); // Should report MCP002 diagnostic for resources too var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity); } [Fact] public void Generator_WithSpecialCharacters_EscapesCorrectly() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Test with "quotes", \backslash, newline /// and tab characters. /// /// Parameter with "quotes" [McpServerTool] public static partial string TestEscaping(string input) { return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Test with \"quotes\", \\backslash, newline and tab characters.")] public static partial string TestEscaping([Description("Parameter with \"quotes\"")] string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithInvalidXml_GeneratesPartialAndReportsDiagnostic() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Test with [McpServerTool] public static partial string TestInvalidXml(string input) { return input; } } """, "MCP001"); // Should not throw, generates partial implementation without Description attributes Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { public static partial string TestInvalidXml(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); // Should report a warning diagnostic var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP001"); Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); Assert.Contains("invalid", diagnostic.GetMessage(), StringComparison.OrdinalIgnoreCase); } [Fact] public void Generator_DiagnosticHasValidSourceLocation() { // This test verifies that diagnostic locations are properly reconstructed // and point to valid source positions (regression test for locations from stale compilations) var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public class TestTools { /// /// Test tool /// [McpServerTool] public static string TestMethod(string input) { return input; } } """, "MCP002"); Assert.True(result.Success); // Verify the diagnostic has a valid location with correct line/column information var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); Assert.NotNull(diagnostic.Location); // Verify the location span has valid position information var lineSpan = diagnostic.Location.GetLineSpan(); Assert.True(lineSpan.IsValid, "Diagnostic line span should be valid"); // Verify reasonable location values without assuming specific line numbers Assert.True(lineSpan.StartLinePosition.Line >= 0, "Start line should be non-negative"); Assert.True(lineSpan.StartLinePosition.Character >= 0, "Start character should be non-negative"); Assert.True(lineSpan.EndLinePosition.Line >= lineSpan.StartLinePosition.Line, "End line should be >= start line"); // The span should have a non-zero length (the identifier "TestMethod" is 10 characters) Assert.True(diagnostic.Location.SourceSpan.Length > 0, "Span should have non-zero length"); Assert.Equal(10, diagnostic.Location.SourceSpan.Length); // "TestMethod".Length == 10 } [Fact] public void Generator_WithGenericType_GeneratesCorrectly() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Test generic /// [McpServerTool] public static partial string TestGeneric(string input) { return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Test generic")] public static partial string TestGeneric(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithEmptyXmlComments_GeneratesPartialWithoutDescription() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// [McpServerTool] public static partial string TestEmpty(string input) { return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { public static partial string TestEmpty(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithMultilineComments_CombinesIntoSingleLine() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// First line /// Second line /// Third line /// [McpServerTool] public static partial string TestMultiline(string input) { return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("First line Second line Third line")] public static partial string TestMultiline(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithParametersOnly_GeneratesParameterDescriptions() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// Input parameter /// Count parameter [McpServerTool] public static partial string TestMethod(string input, int count) { return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { public static partial string TestMethod([Description("Input parameter")] string input, [Description("Count parameter")] int count); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithNestedType_GeneratesCorrectly() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; public partial class OuterClass { [McpServerToolType] public partial class InnerClass { /// /// Nested tool /// [McpServerTool] public static partial string NestedMethod(string input) { return input; } } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class OuterClass { partial class InnerClass { [Description("Nested tool")] public static partial string NestedMethod(string input); } } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithManyToolsAcrossMultipleNestedTypes_GeneratesCorrectly() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test.Outer; [McpServerToolType] public partial class RootTools { /// Root level tool 1 [McpServerTool] public static partial string RootTool1(string input) => input; /// Root level tool 2 /// The count [McpServerTool] public static partial string RootTool2(string input, int count) => input; } public partial class OuterContainer { [McpServerToolType] public partial class Level2A { /// Level 2A tool [McpServerTool] public static partial string Level2ATool(string input) => input; public partial class Level3 { [McpServerToolType] public partial class Level4 { /// Deep nested tool /// The result [McpServerTool] public static partial string DeepTool(string input) => input; } } } [McpServerToolType] public partial class Level2B { /// Level 2B tool 1 [McpServerTool] public static partial string Level2BTool1(string input) => input; /// Level 2B tool 2 [McpServerTool] public static partial string Level2BTool2(string input) => input; } } namespace Test.Resources; [McpServerResourceType] public partial class ResourceProviders { /// Test resource 1 /// The path [McpServerResource("test:///{path}")] public static partial string Resource1(string path) => path; /// Test resource 2 [McpServerResource("test2:///{id}")] public static partial string Resource2(string id) => id; } [McpServerPromptType] public partial class GlobalPrompts { /// Global prompt [McpServerPrompt] public static partial string GlobalPrompt(string input) => input; } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test.Outer { partial class RootTools { [Description("Root level tool 1")] public static partial string RootTool1(string input); [Description("Root level tool 2")] public static partial string RootTool2(string input, [Description("The count")] int count); } partial class OuterContainer { partial class Level2A { [Description("Level 2A tool")] public static partial string Level2ATool(string input); } } partial class OuterContainer { partial class Level2A { partial class Level3 { partial class Level4 { [Description("Deep nested tool")] [return: Description("The result")] public static partial string DeepTool(string input); } } } } partial class OuterContainer { partial class Level2B { [Description("Level 2B tool 1")] public static partial string Level2BTool1(string input); [Description("Level 2B tool 2")] public static partial string Level2BTool2(string input); } } } namespace Test.Outer.Test.Resources { partial class GlobalPrompts { [Description("Global prompt")] public static partial string GlobalPrompt(string input); } partial class ResourceProviders { [Description("Test resource 1")] public static partial string Resource1([Description("The path")] string path); [Description("Test resource 2")] public static partial string Resource2(string id); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithRecordClass_GeneratesCorrectly() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial record TestTools { /// /// Record tool /// [McpServerTool] public static partial string RecordMethod(string input) { return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial record class TestTools { [Description("Record tool")] public static partial string RecordMethod(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithRecordStruct_GeneratesCorrectly() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial record struct TestTools { /// /// Record struct tool /// [McpServerTool] public static partial string RecordStructMethod(string input) { return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial record struct TestTools { [Description("Record struct tool")] public static partial string RecordStructMethod(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithVirtualMethod_GeneratesCorrectly() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Virtual tool /// [McpServerTool] public virtual partial string VirtualMethod(string input) { return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Virtual tool")] public virtual partial string VirtualMethod(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithAbstractMethod_GeneratesCorrectly() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public abstract partial class TestTools { /// /// Abstract tool /// [McpServerTool] public abstract partial string AbstractMethod(string input); } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Abstract tool")] public abstract partial string AbstractMethod(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithMcpServerPrompt_GeneratesCorrectly() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerPromptType] public partial class TestPrompts { /// /// Test prompt /// [McpServerPrompt] public static partial string TestPrompt(string input) { return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestPrompts { [Description("Test prompt")] public static partial string TestPrompt(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithMcpServerResource_GeneratesCorrectly() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerResourceType] public partial class TestResources { /// /// Test resource /// [McpServerResource("test://resource")] public static partial string TestResource(string input) { return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestResources { [Description("Test resource")] public static partial string TestResource(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithGlobalNamespace_GeneratesCorrectly() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; [McpServerToolType] public partial class GlobalTools { /// /// Tool in global namespace /// [McpServerTool] public static partial string GlobalMethod(string input) { return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; partial class GlobalTools { [Description("Tool in global namespace")] public static partial string GlobalMethod(string input); } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithDefaultParameterValues_PreservesDefaults() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Test tool with defaults /// /// The project name /// Enable flag /// Item count [McpServerTool] public static partial string TestMethod( string? project = null, bool flag = false, int count = 42) { return project ?? "default"; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Test tool with defaults")] public static partial string TestMethod([Description("The project name")] string? project = null, [Description("Enable flag")] bool flag = false, [Description("Item count")] int count = 42); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithAsyncMethod_ExcludesAsyncModifier() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; using System.Threading.Tasks; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Async tool /// [McpServerTool] public async partial Task DoWorkAsync(string input) { await Task.Delay(100); return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Async tool")] public partial global::System.Threading.Tasks.Task DoWorkAsync(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithAsyncStaticMethod_ExcludesAsyncModifier() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; using System.Threading.Tasks; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Static async tool /// [McpServerTool] public static async partial Task StaticAsyncMethod(string input) { await Task.Delay(100); return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Static async tool")] public static partial global::System.Threading.Tasks.Task StaticAsyncMethod(string input); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithDefaultParameterValuesAndAsync_HandlesBothCorrectly() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; using System.Threading.Tasks; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Async tool with defaults /// /// The input /// Timeout in ms [McpServerTool] public static async partial Task AsyncWithDefaults(string input, int timeout = 1000) { await Task.Delay(timeout); return input; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Async tool with defaults")] public static partial global::System.Threading.Tasks.Task AsyncWithDefaults([Description("The input")] string input, [Description("Timeout in ms")] int timeout = 1000); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithStringDefaultValue_PreservesQuotedDefault() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace Test; [McpServerToolType] public partial class TestTools { /// /// Test tool with string default /// [McpServerTool] public static partial string TestMethod(string name = "default value") { return name; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace Test { partial class TestTools { [Description("Test tool with string default")] public static partial string TestMethod(string name = "default value"); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Theory] [InlineData(true)] [InlineData(false)] public void Generator_WithTypeFromDifferentNamespace_GeneratesFullyQualifiedTypeName(bool useFullyQualifiedTypesInSource) { // This test validates that regardless of whether the source code uses fully qualified // or unqualified type names, the generator always emits fully qualified type names // with global:: prefix. This fixes the issue where parameter types from different // namespaces caused build failures. string usingDirective = useFullyQualifiedTypesInSource ? "" : "using MyApp.Actions;"; string returnType = useFullyQualifiedTypesInSource ? "System.Threading.Tasks.Task" : "Task"; string parameterType = useFullyQualifiedTypesInSource ? "MyApp.Actions.MyAction" : "MyAction"; var result = RunGenerator($$""" using ModelContextProtocol.Server; using System.ComponentModel; using System.Threading.Tasks; {{usingDirective}} namespace MyApp.Actions { public enum MyAction { One, Two } } namespace MyApp { [McpServerToolType] public sealed partial class Tools { /// Do a thing based on an action. /// The action to perform. [McpServerTool] public async partial {{returnType}} DoThing({{parameterType}} action) => await Task.FromResult("ok"); } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); // Regardless of source qualification, generated code should always use // fully qualified type names with global:: prefix var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace MyApp { partial class Tools { [Description("Do a thing based on an action.")] public partial global::System.Threading.Tasks.Task DoThing([Description("The action to perform.")] global::MyApp.Actions.MyAction action); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithGenericListParameter_GeneratesFullyQualifiedTypeName() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; using System.Collections.Generic; namespace MyApp.Models { public class Item { } } namespace MyApp { [McpServerToolType] public sealed partial class Tools { /// Process items. /// The items to process. [McpServerTool] public static partial string ProcessItems(List items) => "ok"; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace MyApp { partial class Tools { [Description("Process items.")] public static partial string ProcessItems([Description("The items to process.")] global::System.Collections.Generic.List items); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithGenericDictionaryParameter_GeneratesFullyQualifiedTypeName() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; using System.Collections.Generic; namespace MyApp.Models { public class Key { } public class Value { } } namespace MyApp { [McpServerToolType] public sealed partial class Tools { /// Process mapping. /// The mapping to process. [McpServerTool] public static partial string ProcessMapping(Dictionary mapping) => "ok"; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace MyApp { partial class Tools { [Description("Process mapping.")] public static partial string ProcessMapping([Description("The mapping to process.")] global::System.Collections.Generic.Dictionary mapping); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithArrayParameter_GeneratesFullyQualifiedTypeName() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace MyApp.Models { public class Item { } } namespace MyApp { [McpServerToolType] public sealed partial class Tools { /// Process items array. /// The items array to process. [McpServerTool] public static partial string ProcessItemsArray(MyApp.Models.Item[] items) => "ok"; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace MyApp { partial class Tools { [Description("Process items array.")] public static partial string ProcessItemsArray([Description("The items array to process.")] global::MyApp.Models.Item[] items); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithNullableReferenceTypeParameter_GeneratesFullyQualifiedTypeName() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace MyApp.Models { public class Item { } } namespace MyApp { [McpServerToolType] public sealed partial class Tools { /// Process optional item. /// The optional item to process. [McpServerTool] public static partial string ProcessOptionalItem(MyApp.Models.Item? item) => "ok"; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace MyApp { partial class Tools { [Description("Process optional item.")] public static partial string ProcessOptionalItem([Description("The optional item to process.")] global::MyApp.Models.Item? item); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithNestedTypeParameter_GeneratesFullyQualifiedTypeName() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace MyApp.Models { public class Container { public class NestedItem { } } } namespace MyApp { [McpServerToolType] public sealed partial class Tools { /// Process nested item. /// The nested item to process. [McpServerTool] public static partial string ProcessNestedItem(MyApp.Models.Container.NestedItem item) => "ok"; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace MyApp { partial class Tools { [Description("Process nested item.")] public static partial string ProcessNestedItem([Description("The nested item to process.")] global::MyApp.Models.Container.NestedItem item); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } [Fact] public void Generator_WithNullableValueTypeParameter_GeneratesFullyQualifiedTypeName() { var result = RunGenerator(""" using ModelContextProtocol.Server; using System.ComponentModel; namespace MyApp.Models { public struct MyStruct { } } namespace MyApp { [McpServerToolType] public sealed partial class Tools { /// Process optional struct. /// The optional struct to process. [McpServerTool] public static partial string ProcessOptionalStruct(MyApp.Models.MyStruct? value) => "ok"; } } """); Assert.True(result.Success); Assert.Single(result.GeneratedSources); var expected = $$""" // // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} #pragma warning disable using System.ComponentModel; using ModelContextProtocol.Server; namespace MyApp { partial class Tools { [Description("Process optional struct.")] public static partial string ProcessOptionalStruct([Description("The optional struct to process.")] global::MyApp.Models.MyStruct? value); } } """; AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); } private GeneratorRunResult RunGenerator([StringSyntax("C#-test")] string source, params string[] expectedDiagnosticIds) { var syntaxTree = CSharpSyntaxTree.ParseText(source); // Get reference assemblies - we need to include all the basic runtime types List referenceList = [ MetadataReference.CreateFromFile(typeof(object).Assembly.Location), MetadataReference.CreateFromFile(typeof(System.ComponentModel.DescriptionAttribute).Assembly.Location), ]; // Add all necessary runtime assemblies var runtimePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; referenceList.Add(MetadataReference.CreateFromFile(Path.Combine(runtimePath, "System.Runtime.dll"))); referenceList.Add(MetadataReference.CreateFromFile(Path.Combine(runtimePath, "netstandard.dll"))); // Try to find and add ModelContextProtocol.Core try { var coreAssemblyPath = Path.Combine(AppContext.BaseDirectory, "ModelContextProtocol.Core.dll"); if (File.Exists(coreAssemblyPath)) { referenceList.Add(MetadataReference.CreateFromFile(coreAssemblyPath)); } } catch { // If we can't find it, the compilation will fail with appropriate errors } var compilation = CSharpCompilation.Create( "TestAssembly", [syntaxTree], referenceList, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); var driver = (CSharpGeneratorDriver)CSharpGeneratorDriver .Create(new XmlToDescriptionGenerator()) .RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); var runResult = driver.GetRunResult(); // Run the suppressor to check that CS1066 warnings for MCP methods are suppressed var analyzers = ImmutableArray.Create(new CS1066Suppressor()); var compilationWithAnalyzers = outputCompilation.WithAnalyzers(analyzers); var allDiagnostics = compilationWithAnalyzers.GetAllDiagnosticsAsync().GetAwaiter().GetResult(); // Check for any unsuppressed CS1066 warnings - these should be suppressed by our suppressor var unsuppressedCs1066 = allDiagnostics .Where(d => d.Id == "CS1066" && !d.IsSuppressed) .ToList(); // Collect all diagnostics from the generator (any verbosity level) var allGeneratorDiagnostics = generatorDiagnostics.Concat(unsuppressedCs1066).ToList(); // Check for unexpected diagnostics - any diagnostic that isn't in the expected list var expectedSet = new HashSet(expectedDiagnosticIds); var unexpectedDiagnostics = allGeneratorDiagnostics .Where(d => !expectedSet.Contains(d.Id)) .ToList(); return new GeneratorRunResult { Success = unexpectedDiagnostics.Count == 0, GeneratedSources = runResult.GeneratedTrees.Select(t => (t.FilePath, t.GetText())).ToList(), Diagnostics = allGeneratorDiagnostics, Compilation = outputCompilation }; } private static void AssertGeneratedSourceEquals( [StringSyntax("C#-test")] string expected, [StringSyntax("C#-test")] string actual) { // Normalize line endings to \n, remove trailing whitespace from each line, and trim the end static string Normalize(string s) { var lines = s.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n'); for (int i = 0; i < lines.Length; i++) { lines[i] = lines[i].TrimEnd(); } return string.Join('\n', lines).TrimEnd(); } var normalizedExpected = Normalize(expected); var normalizedActual = Normalize(actual); Assert.Equal(normalizedExpected, normalizedActual); } private class GeneratorRunResult { public bool Success { get; set; } public List<(string FilePath, Microsoft.CodeAnalysis.Text.SourceText SourceText)> GeneratedSources { get; set; } = []; public List Diagnostics { get; set; } = []; public Compilation? Compilation { get; set; } } [Fact] public void Caching_WithIdenticalCompilation_AllOutputsCached() { // This tests that running the same compilation twice uses cached results const string Source = """ using ModelContextProtocol.Server; namespace Test; [McpServerToolType] public partial class TestTools { /// Test tool [McpServerTool] public static partial string TestMethod(string input) => input; } """; var compilation = CreateCompilation(Source); var driver = CreateTrackedDriver(); // Run #1 driver = driver.RunGenerators(compilation, TestContext.Current.CancellationToken); var result1 = driver.GetRunResult(); Assert.Single(result1.Results); Assert.Single(result1.Results[0].GeneratedSources); // Run #2 with same compilation - should be fully cached driver = driver.RunGenerators(compilation, TestContext.Current.CancellationToken); var result2 = driver.GetRunResult(); Assert.Single(result2.Results); var allOutputs = result2.Results[0].TrackedSteps.Values .SelectMany(steps => steps.SelectMany(step => step.Outputs)) .ToList(); Assert.NotEmpty(allOutputs); Assert.All(allOutputs, output => Assert.True(output.Reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged, $"Expected Cached or Unchanged but got {output.Reason}")); } [Fact] public void Caching_WithNewCompilationSameSource_OutputsCached() { const string Source = """ using ModelContextProtocol.Server; namespace Test; [McpServerToolType] public partial class TestTools { /// Test tool [McpServerTool] public static partial string TestMethod(string input) => input; } """; var driver = CreateTrackedDriver(); // Run #1 with first compilation var compilation1 = CreateCompilation(Source); driver = driver.RunGenerators(compilation1, TestContext.Current.CancellationToken); var result1 = driver.GetRunResult(); Assert.Single(result1.Results); // Run #2 with NEW compilation from same source // This creates new syntax trees and new symbol instances var compilation2 = CreateCompilation(Source); Assert.NotSame(compilation1, compilation2); // Verify these are different instances driver = driver.RunGenerators(compilation2, TestContext.Current.CancellationToken); var result2 = driver.GetRunResult(); Assert.Single(result2.Results); // The source generation output should be cached because the extracted data is semantically identical var sourceOutputSteps = result2.Results[0].TrackedSteps .Where(kvp => kvp.Key.Contains("SourceOutput") || kvp.Key.Contains("RegisterSourceOutput")) .SelectMany(kvp => kvp.Value.SelectMany(step => step.Outputs)) .ToList(); // At minimum, check that we're not regenerating everything from scratch var allOutputs = result2.Results[0].TrackedSteps.Values .SelectMany(steps => steps.SelectMany(step => step.Outputs)) .ToList(); Assert.NotEmpty(allOutputs); // With proper value equality, the final output should be unchanged // (the source text should be identical even if intermediate steps ran) Assert.Equal( result1.Results[0].GeneratedSources[0].SourceText.ToString(), result2.Results[0].GeneratedSources[0].SourceText.ToString()); } [Fact] public void Caching_WithUnrelatedFileChange_McpMethodCached() { // Adding an unrelated file should not cause MCP method extraction to re-run const string McpSource = """ using ModelContextProtocol.Server; namespace Test; [McpServerToolType] public partial class TestTools { /// Test tool [McpServerTool] public static partial string TestMethod(string input) => input; } """; const string UnrelatedSource1 = """ namespace Other; public class Unrelated { public int Value { get; set; } } """; const string UnrelatedSource2 = """ namespace Other; public class Unrelated { public int Value { get; set; } public string Name { get; set; } } """; var driver = CreateTrackedDriver(); // Run #1 with MCP file + unrelated file driver = driver.RunGenerators(CreateCompilation(McpSource, UnrelatedSource1), TestContext.Current.CancellationToken); var result1 = driver.GetRunResult(); Assert.Single(result1.Results); var output1 = result1.Results[0].GeneratedSources[0].SourceText.ToString(); // Run #2 with MCP file + MODIFIED unrelated file driver = driver.RunGenerators(CreateCompilation(McpSource, UnrelatedSource2), TestContext.Current.CancellationToken); var result2 = driver.GetRunResult(); Assert.Single(result2.Results); var output2 = result2.Results[0].GeneratedSources[0].SourceText.ToString(); // Output should be identical Assert.Equal(output1, output2); // Check that ForAttributeWithMetadataName steps for the MCP method are cached var forAttributeSteps = result2.Results[0].TrackedSteps .Where(kvp => kvp.Key.Contains("ForAttributeWithMetadataName")) .SelectMany(kvp => kvp.Value.SelectMany(step => step.Outputs)) .ToList(); // The MCP method should be cached since it didn't change Assert.Contains(forAttributeSteps, output => output.Reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged); } [Fact] public void Caching_WithXmlDocChange_OutputRegenerated() { // Changing XML docs should cause regeneration const string Source1 = """ using ModelContextProtocol.Server; namespace Test; [McpServerToolType] public partial class TestTools { /// Original description [McpServerTool] public static partial string TestMethod(string input) => input; } """; const string Source2 = """ using ModelContextProtocol.Server; namespace Test; [McpServerToolType] public partial class TestTools { /// Modified description [McpServerTool] public static partial string TestMethod(string input) => input; } """; var driver = CreateTrackedDriver(); // Run #1 driver = driver.RunGenerators(CreateCompilation(Source1), TestContext.Current.CancellationToken); var result1 = driver.GetRunResult(); var output1 = result1.Results[0].GeneratedSources[0].SourceText.ToString(); Assert.Contains("Original description", output1); // Run #2 with modified XML docs driver = driver.RunGenerators(CreateCompilation(Source2), TestContext.Current.CancellationToken); var result2 = driver.GetRunResult(); var output2 = result2.Results[0].GeneratedSources[0].SourceText.ToString(); Assert.Contains("Modified description", output2); Assert.DoesNotContain("Original description", output2); // Verify that there was actual regeneration (not just cached) var allOutputs = result2.Results[0].TrackedSteps.Values .SelectMany(steps => steps.SelectMany(step => step.Outputs)) .ToList(); Assert.Contains(allOutputs, output => output.Reason is IncrementalStepRunReason.Modified or IncrementalStepRunReason.New); } [Fact] public void Caching_WithAddedMethod_ExistingMethodCached() { const string Source1 = """ using ModelContextProtocol.Server; namespace Test; [McpServerToolType] public partial class TestTools { /// First tool [McpServerTool] public static partial string FirstMethod(string input) => input; } """; const string Source2 = """ using ModelContextProtocol.Server; namespace Test; [McpServerToolType] public partial class TestTools { /// First tool [McpServerTool] public static partial string FirstMethod(string input) => input; /// Second tool [McpServerTool] public static partial string SecondMethod(string input) => input; } """; var driver = CreateTrackedDriver(); // Run #1 driver = driver.RunGenerators(CreateCompilation(Source1), TestContext.Current.CancellationToken); var result1 = driver.GetRunResult(); Assert.Single(result1.Results[0].GeneratedSources); // Run #2 with added method driver = driver.RunGenerators(CreateCompilation(Source2), TestContext.Current.CancellationToken); var result2 = driver.GetRunResult(); Assert.Single(result2.Results[0].GeneratedSources); var output2 = result2.Results[0].GeneratedSources[0].SourceText.ToString(); Assert.Contains("First tool", output2); Assert.Contains("Second tool", output2); // The ForAttributeWithMetadataName step should have some cached outputs (the first method) var forAttributeSteps = result2.Results[0].TrackedSteps .Where(kvp => kvp.Key.Contains("ForAttributeWithMetadataName")) .SelectMany(kvp => kvp.Value.SelectMany(step => step.Outputs)) .ToList(); // Should have both cached (first method) and new (second method) outputs Assert.Contains(forAttributeSteps, output => output.Reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged); Assert.Contains(forAttributeSteps, output => output.Reason is IncrementalStepRunReason.New or IncrementalStepRunReason.Modified); } [Fact] public void Caching_MultipleMethodsAcrossFiles_IndependentCaching() { const string File1 = """ using ModelContextProtocol.Server; namespace Test; [McpServerToolType] public partial class Tools1 { /// Tool in file 1 [McpServerTool] public static partial string Method1(string input) => input; } """; const string File2Original = """ using ModelContextProtocol.Server; namespace Test; [McpServerToolType] public partial class Tools2 { /// Tool in file 2 [McpServerTool] public static partial string Method2(string input) => input; } """; const string File2Modified = """ using ModelContextProtocol.Server; namespace Test; [McpServerToolType] public partial class Tools2 { /// Modified tool in file 2 [McpServerTool] public static partial string Method2(string input) => input; } """; var driver = CreateTrackedDriver(); // Run #1 driver = driver.RunGenerators(CreateCompilation(File1, File2Original), TestContext.Current.CancellationToken); var result1 = driver.GetRunResult(); Assert.Single(result1.Results[0].GeneratedSources); // Run #2 - only File2 changed driver = driver.RunGenerators(CreateCompilation(File1, File2Modified), TestContext.Current.CancellationToken); var result2 = driver.GetRunResult(); Assert.Single(result2.Results[0].GeneratedSources); var output2 = result2.Results[0].GeneratedSources[0].SourceText.ToString(); Assert.Contains("Tool in file 1", output2); // Unchanged Assert.Contains("Modified tool in file 2", output2); // Changed // Method1 extraction should be cached, Method2 should be modified var forAttributeSteps = result2.Results[0].TrackedSteps .Where(kvp => kvp.Key.Contains("ForAttributeWithMetadataName")) .SelectMany(kvp => kvp.Value.SelectMany(step => step.Outputs)) .ToList(); Assert.Contains(forAttributeSteps, output => output.Reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged); Assert.Contains(forAttributeSteps, output => output.Reason is IncrementalStepRunReason.Modified or IncrementalStepRunReason.New); } /// /// Creates a compilation with the specified source and standard references. /// Each call creates a NEW compilation instance to ensure we're testing value equality, not reference equality. /// private static CSharpCompilation CreateCompilation(params string[] sources) { var syntaxTrees = sources.Select(s => CSharpSyntaxTree.ParseText(s)).ToArray(); var runtimePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; List referenceList = [ MetadataReference.CreateFromFile(typeof(object).Assembly.Location), MetadataReference.CreateFromFile(typeof(System.ComponentModel.DescriptionAttribute).Assembly.Location), MetadataReference.CreateFromFile(Path.Combine(runtimePath, "System.Runtime.dll")), MetadataReference.CreateFromFile(Path.Combine(runtimePath, "netstandard.dll")), ]; try { var coreAssemblyPath = Path.Combine(AppContext.BaseDirectory, "ModelContextProtocol.Core.dll"); if (File.Exists(coreAssemblyPath)) { referenceList.Add(MetadataReference.CreateFromFile(coreAssemblyPath)); } } catch { // Ignore } return CSharpCompilation.Create( "TestAssembly", syntaxTrees, referenceList, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); } /// /// Creates a generator driver with step tracking enabled. /// private static GeneratorDriver CreateTrackedDriver() => CSharpGeneratorDriver.Create( generators: [new XmlToDescriptionGenerator().AsSourceGenerator()], driverOptions: new GeneratorDriverOptions( disabledOutputs: default, trackIncrementalGeneratorSteps: true)); }