66using System . Collections . Immutable ;
77using System . Linq ;
88using System . Text ;
9+ using System . Threading ;
910using System . Xml . Linq ;
1011
1112namespace ModelContextProtocol . SourceGenerators ;
@@ -19,34 +20,54 @@ public class XmlToDescriptionGenerator : IIncrementalGenerator
1920{
2021 public void Initialize ( IncrementalGeneratorInitializationContext context )
2122 {
22- // Filter method declarations with attributes
23- var methodDeclarations = context . SyntaxProvider
23+ // Filter method declarations with attributes and transform to model
24+ var methodModels = context . SyntaxProvider
2425 . CreateSyntaxProvider (
25- predicate : static ( s , _ ) => s is MethodDeclarationSyntax { AttributeLists . Count : > 0 } ,
26- transform : static ( ctx , _ ) => GetMethodForGeneration ( ctx ) )
26+ predicate : static ( s , _ ) => IsCandidateMethod ( s ) ,
27+ transform : static ( ctx , ct ) => GetMethodModel ( ctx , ct ) )
2728 . Where ( static m => m is not null ) ;
2829
29- // Combine with compilation to get symbols
30- var compilationAndMethods = context . CompilationProvider . Combine ( methodDeclarations . Collect ( ) ) ;
30+ // Combine with compilation to get well-known type symbols
31+ var compilationAndMethods = context . CompilationProvider . Combine ( methodModels . Collect ( ) ) ;
3132
3233 context . RegisterSourceOutput ( compilationAndMethods ,
3334 static ( spc , source ) => Execute ( source . Left , source . Right ! , spc ) ) ;
3435 }
3536
36- private static MethodDeclarationSyntax ? GetMethodForGeneration ( GeneratorSyntaxContext context )
37+ private static bool IsCandidateMethod ( SyntaxNode node )
38+ {
39+ // Quick syntax-only filter
40+ return node is MethodDeclarationSyntax
41+ {
42+ AttributeLists . Count : > 0
43+ } method && method . Modifiers . Any ( SyntaxKind . PartialKeyword ) ;
44+ }
45+
46+ private static MethodToGenerate ? GetMethodModel ( GeneratorSyntaxContext context , CancellationToken cancellationToken )
3747 {
3848 var methodDeclaration = ( MethodDeclarationSyntax ) context . Node ;
49+
50+ cancellationToken . ThrowIfCancellationRequested ( ) ;
51+
52+ var methodSymbol = context . SemanticModel . GetDeclaredSymbol ( methodDeclaration , cancellationToken ) ;
53+ if ( methodSymbol == null )
54+ {
55+ return null ;
56+ }
3957
40- // Quick check: must be partial
41- if ( ! methodDeclaration . Modifiers . Any ( m => m . IsKind ( SyntaxKind . PartialKeyword ) ) )
58+ // Check for McpServerTool attribute early (before expensive XML parsing)
59+ var hasMcpAttribute = methodSymbol . GetAttributes ( ) . Any ( attr =>
60+ attr . AttributeClass ? . Name is "McpServerToolAttribute" or "McpServerTool" ) ;
61+
62+ if ( ! hasMcpAttribute )
4263 {
4364 return null ;
4465 }
4566
46- return methodDeclaration ;
67+ return new MethodToGenerate ( methodDeclaration , methodSymbol ) ;
4768 }
4869
49- private static void Execute ( Compilation compilation , ImmutableArray < MethodDeclarationSyntax > methods , SourceProductionContext context )
70+ private static void Execute ( Compilation compilation , ImmutableArray < MethodToGenerate ? > methods , SourceProductionContext context )
5071 {
5172 if ( methods . IsDefaultOrEmpty )
5273 {
@@ -57,23 +78,26 @@ private static void Execute(Compilation compilation, ImmutableArray<MethodDeclar
5778 var mcpServerToolAttribute = compilation . GetTypeByMetadataName ( "ModelContextProtocol.Server.McpServerToolAttribute" ) ;
5879 var descriptionAttribute = compilation . GetTypeByMetadataName ( "System.ComponentModel.DescriptionAttribute" ) ;
5980
60- if ( mcpServerToolAttribute == null || descriptionAttribute == null )
81+ if ( descriptionAttribute == null )
6182 {
83+ // Description attribute is required - can't generate without it
6284 return ;
6385 }
6486
65- foreach ( var methodDeclaration in methods )
87+ foreach ( var methodModel in methods )
6688 {
67- var semanticModel = compilation . GetSemanticModel ( methodDeclaration . SyntaxTree ) ;
68- var methodSymbol = semanticModel . GetDeclaredSymbol ( methodDeclaration ) ;
69-
70- if ( methodSymbol == null )
89+ if ( methodModel == null )
7190 {
7291 continue ;
7392 }
7493
75- // Check if method has McpServerTool attribute
76- if ( ! HasAttribute ( methodSymbol , mcpServerToolAttribute ) )
94+ context . CancellationToken . ThrowIfCancellationRequested ( ) ;
95+
96+ var methodSymbol = methodModel . Value . MethodSymbol ;
97+ var methodDeclaration = methodModel . Value . MethodDeclaration ;
98+
99+ // Double-check McpServerTool attribute with symbol comparison if available
100+ if ( mcpServerToolAttribute != null && ! HasAttribute ( methodSymbol , mcpServerToolAttribute ) )
77101 {
78102 continue ;
79103 }
@@ -89,8 +113,8 @@ private static void Execute(Compilation compilation, ImmutableArray<MethodDeclar
89113 var source = GeneratePartialMethodDeclaration ( methodSymbol , methodDeclaration , xmlDocs , descriptionAttribute ) ;
90114 if ( source != null )
91115 {
92- var fileName = $ " { methodSymbol . ContainingType . Name } _ { methodSymbol . Name } _Description.g.cs" ;
93- context . AddSource ( fileName , SourceText . From ( source , Encoding . UTF8 ) ) ;
116+ var hintName = GetHintName ( methodSymbol ) ;
117+ context . AddSource ( hintName , SourceText . From ( source , Encoding . UTF8 ) ) ;
94118 }
95119 }
96120 }
@@ -134,7 +158,7 @@ private static bool HasAttribute(ISymbol symbol, INamedTypeSymbol attributeType)
134158 return null ;
135159 }
136160
137- var paramDocs = new Dictionary < string , string > ( ) ;
161+ var paramDocs = new Dictionary < string , string > ( StringComparer . Ordinal ) ;
138162 foreach ( var paramElement in memberElement . Elements ( "param" ) )
139163 {
140164 var name = paramElement . Attribute ( "name" ) ? . Value ;
@@ -152,8 +176,9 @@ private static bool HasAttribute(ISymbol symbol, INamedTypeSymbol attributeType)
152176 Parameters = paramDocs
153177 } ;
154178 }
155- catch
179+ catch ( System . Xml . XmlException )
156180 {
181+ // Invalid XML in documentation comments - skip this method
157182 return null ;
158183 }
159184 }
@@ -308,9 +333,53 @@ private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol typ
308333
309334 private static string EscapeString ( string text )
310335 {
311- return text . Replace ( "\\ " , "\\ \\ " ) . Replace ( "\" " , "\\ \" " ) ;
336+ if ( string . IsNullOrEmpty ( text ) )
337+ {
338+ return text ;
339+ }
340+
341+ // Escape special characters for C# string literals
342+ return text
343+ . Replace ( "\\ " , "\\ \\ " ) // Backslash must be first
344+ . Replace ( "\" " , "\\ \" " ) // Quote
345+ . Replace ( "\r " , "\\ r" ) // Carriage return
346+ . Replace ( "\n " , "\\ n" ) // Newline
347+ . Replace ( "\t " , "\\ t" ) ; // Tab
348+ }
349+
350+ private static string GetHintName ( IMethodSymbol methodSymbol )
351+ {
352+ var containingType = methodSymbol . ContainingType ;
353+ var typeName = containingType . Name ;
354+
355+ // Include generic arity if present to avoid collisions
356+ if ( containingType . IsGenericType )
357+ {
358+ typeName = $ "{ typeName } `{ containingType . Arity } ";
359+ }
360+
361+ return $ "{ typeName } .{ methodSymbol . Name } .g.cs";
362+ }
363+
364+ /// <summary>
365+ /// Represents a method that may need Description attributes generated.
366+ /// Using a struct for better incremental generator caching.
367+ /// </summary>
368+ private readonly struct MethodToGenerate
369+ {
370+ public MethodToGenerate ( MethodDeclarationSyntax methodDeclaration , IMethodSymbol methodSymbol )
371+ {
372+ MethodDeclaration = methodDeclaration ;
373+ MethodSymbol = methodSymbol ;
374+ }
375+
376+ public MethodDeclarationSyntax MethodDeclaration { get ; }
377+ public IMethodSymbol MethodSymbol { get ; }
312378 }
313379
380+ /// <summary>
381+ /// Holds extracted XML documentation for a method.
382+ /// </summary>
314383 private sealed class XmlDocumentation
315384 {
316385 public string MethodDescription { get ; set ; } = string . Empty ;
0 commit comments