diff --git a/Directory.Packages.props b/Directory.Packages.props
index 656f5fe9..f86a7c69 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -48,12 +48,19 @@
+
+
+
+
-
+
+
+
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx
index a70e3e31..1f6dce1e 100644
--- a/ModelContextProtocol.slnx
+++ b/ModelContextProtocol.slnx
@@ -62,11 +62,13 @@
+
+
diff --git a/README.md b/README.md
index 3099dfcd..19672bfe 100644
--- a/README.md
+++ b/README.md
@@ -225,6 +225,11 @@ await using McpServer server = McpServer.Create(new StdioServerTransport("MyServ
await server.RunAsync();
```
+Descriptions can be added to tools, prompts, and resources in a variety of ways, including via the `[Description]` attribute from `System.ComponentModel`.
+This attribute may be placed on a method to provide for the tool, prompt, or resource, or on individual parameters to describe each's purpose.
+XML comments may also be used; if an `[McpServerTool]`, `[McpServerPrompt]`, or `[McpServerResource]`-attributed method is marked as `partial`,
+XML comments placed on the method will be used automatically to generate `[Description]` attributes for the method and its parameters.
+
## Acknowledgements
The starting point for this library was a project called [mcpdotnet](https://github.com/PederHP/mcpdotnet), initiated by [Peder Holdgaard Pedersen](https://github.com/PederHP). We are grateful for the work done by Peder and other contributors to that repository, which created a solid foundation for this library.
diff --git a/src/ModelContextProtocol.Analyzers/Diagnostics.cs b/src/ModelContextProtocol.Analyzers/Diagnostics.cs
new file mode 100644
index 00000000..3dd486be
--- /dev/null
+++ b/src/ModelContextProtocol.Analyzers/Diagnostics.cs
@@ -0,0 +1,22 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using System.Collections.Immutable;
+using System.Text;
+using System.Xml.Linq;
+
+namespace ModelContextProtocol.Analyzers;
+
+/// Provides the diagnostic descriptors used by the assembly.
+internal static class Diagnostics
+{
+ public static DiagnosticDescriptor InvalidXmlDocumentation { get; } = new(
+ id: "MCP001",
+ title: "Invalid XML documentation for MCP method",
+ messageFormat: "XML comment for method '{0}' is invalid and cannot be processed to generate [Description] attributes.",
+ category: "mcp",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "The XML documentation comment contains invalid XML and cannot be processed to generate Description attributes.");
+}
diff --git a/src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj b/src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj
new file mode 100644
index 00000000..5338bbb8
--- /dev/null
+++ b/src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netstandard2.0
+ true
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs b/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs
new file mode 100644
index 00000000..ae0808bd
--- /dev/null
+++ b/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs
@@ -0,0 +1,386 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using System.Collections.Immutable;
+using System.Text;
+using System.Xml.Linq;
+
+namespace ModelContextProtocol.Analyzers;
+
+///
+/// Source generator that creates [Description] attributes from XML comments
+/// for partial methods tagged with MCP attributes.
+///
+[Generator]
+public sealed class XmlToDescriptionGenerator : IIncrementalGenerator
+{
+ private const string GeneratedFileName = "ModelContextProtocol.Descriptions.g.cs";
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Filter method declarations with attributes. We're looking for attributed partial methods.
+ var methodModels = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ static (s, _) => s is MethodDeclarationSyntax { AttributeLists.Count: > 0 } method && method.Modifiers.Any(SyntaxKind.PartialKeyword),
+ static (ctx, ct) =>
+ {
+ var methodDeclaration = (MethodDeclarationSyntax)ctx.Node;
+ return ctx.SemanticModel.GetDeclaredSymbol(methodDeclaration, ct) is { } methodSymbol ?
+ new MethodToGenerate(methodDeclaration, methodSymbol) :
+ null;
+ })
+ .Where(static m => m is not null);
+
+ // Combine with compilation to get well-known type symbols.
+ var compilationAndMethods = context.CompilationProvider.Combine(methodModels.Collect());
+
+ // Write out the source for all methods.
+ context.RegisterSourceOutput(compilationAndMethods, static (spc, source) => Execute(source.Left, source.Right!, spc));
+ }
+
+ private static void Execute(Compilation compilation, ImmutableArray methods, SourceProductionContext context)
+ {
+ if (methods.IsDefaultOrEmpty)
+ {
+ return;
+ }
+
+ // Get well-known type symbols upfront. If any of them are missing, give up.
+ var toolAttribute = compilation.GetTypeByMetadataName("ModelContextProtocol.Server.McpServerToolAttribute");
+ var promptAttribute = compilation.GetTypeByMetadataName("ModelContextProtocol.Server.McpServerPromptAttribute");
+ var resourceAttribute = compilation.GetTypeByMetadataName("ModelContextProtocol.Server.McpServerResourceAttribute");
+ var descriptionAttribute = compilation.GetTypeByMetadataName("System.ComponentModel.DescriptionAttribute");
+ if (descriptionAttribute is null || toolAttribute is null || promptAttribute is null || resourceAttribute is null)
+ {
+ return;
+ }
+
+ // Gather a list of all methods needing generation.
+ List<(IMethodSymbol MethodSymbol, MethodDeclarationSyntax MethodDeclaration, XmlDocumentation? XmlDocs)>? methodsToGenerate = null;
+ foreach (var methodModel in methods)
+ {
+ if (methodModel is not null)
+ {
+ // Check if method has any MCP attribute with symbol comparison
+ var methodSymbol = methodModel.Value.MethodSymbol;
+ bool hasMcpAttribute =
+ HasAttribute(methodSymbol, toolAttribute) ||
+ HasAttribute(methodSymbol, promptAttribute) ||
+ HasAttribute(methodSymbol, resourceAttribute);
+ if (hasMcpAttribute)
+ {
+ // Extract XML documentation. Even if there's no documentation or it's invalid,
+ // we still need to generate the partial implementation to avoid compilation errors.
+ var xmlDocs = ExtractXmlDocumentation(methodSymbol, context);
+
+ // Always add the method to generate its implementation, but emit diagnostics
+ // to guide the developer if documentation is missing or invalid.
+ (methodsToGenerate ??= []).Add((methodSymbol, methodModel.Value.MethodDeclaration, xmlDocs));
+ }
+ }
+ }
+
+ // Generate a single file with all partial declarations.
+ if (methodsToGenerate is not null)
+ {
+ string source = GenerateSourceFile(compilation, methodsToGenerate, descriptionAttribute);
+ context.AddSource(GeneratedFileName, SourceText.From(source, Encoding.UTF8));
+ }
+ }
+
+ private static XmlDocumentation? ExtractXmlDocumentation(IMethodSymbol methodSymbol, SourceProductionContext context)
+ {
+ string? xmlDoc = methodSymbol.GetDocumentationCommentXml();
+ if (string.IsNullOrWhiteSpace(xmlDoc))
+ {
+ return null;
+ }
+
+ try
+ {
+ if (XDocument.Parse(xmlDoc).Element("member") is not { } memberElement)
+ {
+ return null;
+ }
+
+ var summary = CleanXmlDocText(memberElement.Element("summary")?.Value);
+ var remarks = CleanXmlDocText(memberElement.Element("remarks")?.Value);
+ var returns = CleanXmlDocText(memberElement.Element("returns")?.Value);
+
+ // Combine summary and remarks for method description.
+ var methodDescription =
+ string.IsNullOrWhiteSpace(remarks) ? summary :
+ string.IsNullOrWhiteSpace(summary) ? remarks :
+ $"{summary}\n{remarks}";
+
+ Dictionary paramDocs = new(StringComparer.Ordinal);
+ foreach (var paramElement in memberElement.Elements("param"))
+ {
+ var name = paramElement.Attribute("name")?.Value;
+ var value = CleanXmlDocText(paramElement.Value);
+ if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(value))
+ {
+ paramDocs[name!] = value;
+ }
+ }
+
+ // Return documentation even if empty - we'll still generate the partial implementation
+ return new XmlDocumentation(methodDescription ?? string.Empty, returns ?? string.Empty, paramDocs);
+ }
+ catch (System.Xml.XmlException)
+ {
+ // Emit warning for invalid XML
+ context.ReportDiagnostic(Diagnostic.Create(
+ Diagnostics.InvalidXmlDocumentation,
+ methodSymbol.Locations.FirstOrDefault(),
+ methodSymbol.Name));
+ return null;
+ }
+ }
+
+ private static string CleanXmlDocText(string? text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ return string.Empty;
+ }
+
+ // Remove leading/trailing whitespace and normalize line breaks
+ var lines = text!.Split('\n')
+ .Select(line => line.Trim())
+ .Where(line => !string.IsNullOrEmpty(line));
+
+ return string.Join(" ", lines).Trim();
+ }
+
+ private static string GenerateSourceFile(
+ Compilation compilation,
+ List<(IMethodSymbol MethodSymbol, MethodDeclarationSyntax MethodDeclaration, XmlDocumentation? XmlDocs)> methods,
+ INamedTypeSymbol descriptionAttribute)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("// ")
+ .AppendLine($"// ModelContextProtocol.Analyzers {typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}")
+ .AppendLine()
+ .AppendLine("#pragma warning disable")
+ .AppendLine()
+ .AppendLine("using System.ComponentModel;")
+ .AppendLine("using ModelContextProtocol.Server;")
+ .AppendLine();
+
+ // Group methods by namespace and containing type
+ var groupedMethods = methods.GroupBy(m =>
+ m.MethodSymbol.ContainingNamespace.Name == compilation.GlobalNamespace.Name ? "" :
+ m.MethodSymbol.ContainingNamespace?.ToDisplayString() ??
+ "");
+
+ foreach (var namespaceGroup in groupedMethods)
+ {
+ // Check if this is the global namespace (methods with null ContainingNamespace)
+ bool isGlobalNamespace = string.IsNullOrEmpty(namespaceGroup.Key);
+ if (!isGlobalNamespace)
+ {
+ sb.Append("namespace ")
+ .AppendLine(namespaceGroup.Key)
+ .AppendLine("{");
+ }
+
+ // Group by containing type within namespace
+ var typeGroups = namespaceGroup.GroupBy(m => m.MethodSymbol.ContainingType, SymbolEqualityComparer.Default);
+
+ foreach (var typeGroup in typeGroups)
+ {
+ if (typeGroup.Key is not INamedTypeSymbol containingType)
+ {
+ continue;
+ }
+
+ // Calculate nesting depth for proper indentation
+ // For global namespace, start at 0; for namespaced types, start at 1
+ int nestingDepth = isGlobalNamespace ? 0 : 1;
+ var temp = containingType;
+ while (temp is not null)
+ {
+ nestingDepth++;
+ temp = temp.ContainingType;
+ }
+
+ // Handle nested types by building the full type hierarchy
+ int startIndent = isGlobalNamespace ? 0 : 1;
+ AppendNestedTypeDeclarations(sb, containingType, startIndent, typeGroup, descriptionAttribute, nestingDepth);
+
+ sb.AppendLine();
+ }
+
+ if (!isGlobalNamespace)
+ {
+ sb.AppendLine("}");
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ private static void AppendNestedTypeDeclarations(
+ StringBuilder sb,
+ INamedTypeSymbol typeSymbol,
+ int indentLevel,
+ IGrouping typeGroup,
+ INamedTypeSymbol descriptionAttribute,
+ int nestingDepth)
+ {
+ // Build stack of nested types from innermost to outermost
+ Stack types = new();
+ for (var current = typeSymbol; current is not null; current = current.ContainingType)
+ {
+ types.Push(current);
+ }
+
+ // Generate type declarations from outermost to innermost
+ int nestingCount = types.Count;
+ while (types.Count > 0)
+ {
+ // Get the type keyword and handle records
+ var type = types.Pop();
+ var typeDecl = type.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as TypeDeclarationSyntax;
+ string typeKeyword;
+ if (typeDecl is RecordDeclarationSyntax rds)
+ {
+ var classOrStruct = rds.ClassOrStructKeyword.ValueText;
+ typeKeyword = string.IsNullOrEmpty(classOrStruct) ?
+ $"{typeDecl.Keyword.ValueText} class" :
+ $"{typeDecl.Keyword.ValueText} {classOrStruct}";
+ }
+ else
+ {
+ typeKeyword = typeDecl?.Keyword.ValueText ?? "class";
+ }
+
+ sb.Append(' ', indentLevel * 4).Append("partial ").Append(typeKeyword).Append(' ').AppendLine(type.Name)
+ .Append(' ', indentLevel * 4).AppendLine("{");
+
+ indentLevel++;
+ }
+
+ // Generate methods for this type.
+ bool firstMethodInType = true;
+ foreach (var (methodSymbol, methodDeclaration, xmlDocs) in typeGroup)
+ {
+ AppendMethodDeclaration(sb, methodSymbol, methodDeclaration, xmlDocs, descriptionAttribute, firstMethodInType, nestingDepth);
+ firstMethodInType = false;
+ }
+
+ // Close all type declarations.
+ for (int i = 0; i < nestingCount; i++)
+ {
+ indentLevel--;
+ sb.Append(' ', indentLevel * 4).AppendLine("}");
+ }
+ }
+
+ private static void AppendMethodDeclaration(
+ StringBuilder sb,
+ IMethodSymbol methodSymbol,
+ MethodDeclarationSyntax methodDeclaration,
+ XmlDocumentation? xmlDocs,
+ INamedTypeSymbol descriptionAttribute,
+ bool firstMethodInType,
+ int indentLevel)
+ {
+ int indent = indentLevel * 4;
+
+ if (!firstMethodInType)
+ {
+ sb.AppendLine();
+ }
+
+ // Add the Description attribute for method if needed and documentation exists
+ if (xmlDocs is not null &&
+ !string.IsNullOrWhiteSpace(xmlDocs.MethodDescription) &&
+ !HasAttribute(methodSymbol, descriptionAttribute))
+ {
+ sb.Append(' ', indent)
+ .Append("[Description(\"")
+ .Append(EscapeString(xmlDocs.MethodDescription))
+ .AppendLine("\")]");
+ }
+
+ // Add return: Description attribute if needed and documentation exists
+ if (xmlDocs is not null &&
+ !string.IsNullOrWhiteSpace(xmlDocs.Returns) &&
+ methodSymbol.GetReturnTypeAttributes().All(attr => !SymbolEqualityComparer.Default.Equals(attr.AttributeClass, descriptionAttribute)))
+ {
+ sb.Append(' ', indent)
+ .Append("[return: Description(\"")
+ .Append(EscapeString(xmlDocs.Returns))
+ .AppendLine("\")]");
+ }
+
+ // Copy modifiers from original method syntax.
+ // Add return type (without nullable annotations).
+ // Add method name.
+ sb.Append(' ', indent)
+ .Append(string.Join(" ", methodDeclaration.Modifiers.Select(m => m.Text)))
+ .Append(' ')
+ .Append(methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))
+ .Append(' ')
+ .Append(methodSymbol.Name);
+
+ // Add parameters with their Description attributes.
+ sb.Append("(");
+ for (int i = 0; i < methodSymbol.Parameters.Length; i++)
+ {
+ IParameterSymbol param = methodSymbol.Parameters[i];
+
+ if (i > 0)
+ {
+ sb.Append(", ");
+ }
+
+ if (xmlDocs is not null &&
+ !HasAttribute(param, descriptionAttribute) &&
+ xmlDocs.Parameters.TryGetValue(param.Name, out var paramDoc) &&
+ !string.IsNullOrWhiteSpace(paramDoc))
+ {
+ sb.Append("[Description(\"")
+ .Append(EscapeString(paramDoc))
+ .Append("\")] ");
+ }
+
+ sb.Append(param.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))
+ .Append(' ')
+ .Append(param.Name);
+ }
+ sb.AppendLine(");");
+ }
+
+ /// Checks if a symbol has a specific attribute applied.
+ private static bool HasAttribute(ISymbol symbol, INamedTypeSymbol attributeType)
+ {
+ foreach (var attr in symbol.GetAttributes())
+ {
+ if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeType))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// Escape special characters for C# string literals.
+ private static string EscapeString(string text) =>
+ string.IsNullOrEmpty(text) ? text :
+ text.Replace("\\", "\\\\")
+ .Replace("\"", "\\\"")
+ .Replace("\r", "\\r")
+ .Replace("\n", "\\n")
+ .Replace("\t", "\\t");
+
+ /// Represents a method that may need Description attributes generated.
+ private readonly record struct MethodToGenerate(MethodDeclarationSyntax MethodDeclaration, IMethodSymbol MethodSymbol);
+
+ /// Holds extracted XML documentation for a method.
+ private sealed record XmlDocumentation(string MethodDescription, string Returns, Dictionary Parameters);
+}
diff --git a/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj b/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj
index d39c008e..cdbe25a2 100644
--- a/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj
+++ b/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj
@@ -43,6 +43,23 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/ModelContextProtocol.Analyzers.Tests/ModelContextProtocol.Analyzers.Tests.csproj b/tests/ModelContextProtocol.Analyzers.Tests/ModelContextProtocol.Analyzers.Tests.csproj
new file mode 100644
index 00000000..430db1b0
--- /dev/null
+++ b/tests/ModelContextProtocol.Analyzers.Tests/ModelContextProtocol.Analyzers.Tests.csproj
@@ -0,0 +1,36 @@
+
+
+
+ net9.0
+ enable
+ true
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
diff --git a/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs b/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs
new file mode 100644
index 00000000..8c1fb8bf
--- /dev/null
+++ b/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs
@@ -0,0 +1,752 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("[Description(\"Test tool description\")]", generatedSource);
+ Assert.Contains("public static partial string TestMethod", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("[Description(\"Test tool summary\\nAdditional remarks\")]", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("[Description(\"Input parameter description\")]", generatedSource);
+ Assert.Contains("[Description(\"Count parameter description\")]", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("[return: Description(\"The result of the operation\")]", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ // Should not contain method description, only return description
+ Assert.DoesNotContain("Test tool summary", generatedSource);
+ Assert.Contains("[return: Description(\"Result\")]", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ // Should generate description for input but not count
+ Assert.Contains("[Description(\"Input description\")] string input", generatedSource);
+ Assert.DoesNotContain("Count description", generatedSource);
+ }
+
+ [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;
+ }
+ }
+ """);
+
+ Assert.True(result.Success);
+ Assert.Empty(result.GeneratedSources);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ // Verify quotes are escaped
+ Assert.Contains("\\\"quotes\\\"", generatedSource);
+ // Verify backslashes are escaped
+ Assert.Contains("\\\\backslash", generatedSource);
+ }
+
+ [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;
+ }
+ }
+ """);
+
+ // Should not throw, generates partial implementation without Description attributes
+ Assert.True(result.Success);
+ Assert.Single(result.GeneratedSources);
+
+ var generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ // Should generate the partial implementation
+ Assert.Contains("public static partial string TestInvalidXml", generatedSource);
+ // Should NOT contain Description attribute since XML was invalid
+ Assert.DoesNotContain("[Description(", generatedSource);
+
+ // 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_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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("[Description(\"Test generic\")]", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ // Should generate the partial implementation
+ Assert.Contains("public static partial string TestEmpty", generatedSource);
+ // Should NOT contain Description attribute since documentation was empty
+ Assert.DoesNotContain("[Description(", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("[Description(\"First line Second line Third line\")]", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("[Description(\"Input parameter\")]", generatedSource);
+ Assert.Contains("[Description(\"Count parameter\")]", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("partial class OuterClass", generatedSource);
+ Assert.Contains("partial class InnerClass", generatedSource);
+ Assert.Contains("[Description(\"Nested tool\")]", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ // Records are generated with "record class" keyword
+ Assert.Contains("partial record class TestTools", generatedSource);
+ Assert.Contains("[Description(\"Record tool\")]", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("partial record struct TestTools", generatedSource);
+ Assert.Contains("[Description(\"Record struct tool\")]", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("public virtual partial string VirtualMethod", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("public abstract partial string AbstractMethod", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("[Description(\"Test prompt\")]", generatedSource);
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("[Description(\"Test resource\")]", generatedSource);
+ }
+
+ private GeneratorRunResult RunGenerator([StringSyntax("C#-test")] string source)
+ {
+ 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 diagnostics);
+
+ var runResult = driver.GetRunResult();
+
+ return new GeneratorRunResult
+ {
+ Success = !diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error),
+ GeneratedSources = runResult.GeneratedTrees.Select(t => (t.FilePath, t.GetText())).ToList(),
+ Diagnostics = diagnostics.ToList(),
+ Compilation = outputCompilation
+ };
+ }
+
+ [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 generatedSource = result.GeneratedSources[0].SourceText.ToString();
+
+ // Should not have a namespace declaration
+ Assert.DoesNotContain("namespace ", generatedSource);
+ // Should have the class at the root level
+ Assert.Contains("partial class GlobalTools", generatedSource);
+ Assert.Contains("[Description(\"Tool in global namespace\")]", generatedSource);
+ }
+
+ 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; }
+ }
+}