diff --git a/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs b/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs index 0110ac018..40109158d 100644 --- a/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs +++ b/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs @@ -321,10 +321,13 @@ private static void AppendMethodDeclaration( writer.WriteLine($"[return: Description(\"{EscapeString(xmlDocs.Returns)}\")]"); } - // Copy modifiers from original method syntax. + // Copy modifiers from original method syntax, excluding 'async' which is invalid on partial declarations (CS1994). // Add return type (without nullable annotations). // Add method name. - writer.Write(string.Join(" ", methodDeclaration.Modifiers.Select(m => m.Text))); + var modifiers = methodDeclaration.Modifiers + .Where(m => !m.IsKind(SyntaxKind.AsyncKeyword)) + .Select(m => m.Text); + writer.Write(string.Join(" ", modifiers)); writer.Write(' '); writer.Write(methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); writer.Write(' '); @@ -332,9 +335,11 @@ private static void AppendMethodDeclaration( // Add parameters with their Description attributes. writer.Write("("); + var parameterSyntaxList = methodDeclaration.ParameterList.Parameters; for (int i = 0; i < methodSymbol.Parameters.Length; i++) { IParameterSymbol param = methodSymbol.Parameters[i]; + ParameterSyntax? paramSyntax = i < parameterSyntaxList.Count ? parameterSyntaxList[i] : null; if (i > 0) { @@ -352,6 +357,13 @@ private static void AppendMethodDeclaration( writer.Write(param.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); writer.Write(' '); writer.Write(param.Name); + + // Preserve default parameter values from the original syntax. + if (paramSyntax?.Default is { } defaultValue) + { + writer.Write(' '); + writer.Write(defaultValue.ToFullString().Trim()); + } } writer.WriteLine(");"); } diff --git a/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs b/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs index f9b13bf65..bffb24647 100644 --- a/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs +++ b/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs @@ -1463,6 +1463,260 @@ partial class GlobalTools 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 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 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 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()); + } + private GeneratorRunResult RunGenerator([StringSyntax("C#-test")] string source) { var syntaxTree = CSharpSyntaxTree.ParseText(source);