Skip to content

Commit 8316aa0

Browse files
authored
Add CS1066 suppressor for MCP server methods with optional parameters (#1110)
1 parent 8b73a42 commit 8316aa0

File tree

5 files changed

+429
-20
lines changed

5 files changed

+429
-20
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
using Microsoft.CodeAnalysis.CSharp.Syntax;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using System.Collections.Generic;
6+
using System.Collections.Immutable;
7+
using System.Threading;
8+
9+
namespace ModelContextProtocol.Analyzers;
10+
11+
/// <summary>
12+
/// Suppresses CS1066 warnings for MCP server methods that have optional parameters.
13+
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// CS1066 is issued when a partial method's implementing declaration has default parameter values.
17+
/// For partial methods, only the defining declaration's defaults are used by callers,
18+
/// making the implementing declaration's defaults redundant.
19+
/// </para>
20+
/// <para>
21+
/// However, for MCP tool, prompt, and resource methods, users often want to specify default values
22+
/// in their implementing declaration for documentation purposes. The XmlToDescriptionGenerator
23+
/// automatically copies these defaults to the generated defining declaration, making them functional.
24+
/// </para>
25+
/// <para>
26+
/// This suppressor suppresses CS1066 for methods marked with [McpServerTool], [McpServerPrompt],
27+
/// or [McpServerResource] attributes, allowing users to specify defaults in their code without warnings.
28+
/// </para>
29+
/// </remarks>
30+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
31+
public sealed class CS1066Suppressor : DiagnosticSuppressor
32+
{
33+
private static readonly SuppressionDescriptor McpToolSuppression = new(
34+
id: "MCP_CS1066_TOOL",
35+
suppressedDiagnosticId: "CS1066",
36+
justification: "Default values on MCP tool method implementing declarations are copied to the generated defining declaration by the source generator.");
37+
38+
private static readonly SuppressionDescriptor McpPromptSuppression = new(
39+
id: "MCP_CS1066_PROMPT",
40+
suppressedDiagnosticId: "CS1066",
41+
justification: "Default values on MCP prompt method implementing declarations are copied to the generated defining declaration by the source generator.");
42+
43+
private static readonly SuppressionDescriptor McpResourceSuppression = new(
44+
id: "MCP_CS1066_RESOURCE",
45+
suppressedDiagnosticId: "CS1066",
46+
justification: "Default values on MCP resource method implementing declarations are copied to the generated defining declaration by the source generator.");
47+
48+
/// <inheritdoc/>
49+
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions =>
50+
ImmutableArray.Create(McpToolSuppression, McpPromptSuppression, McpResourceSuppression);
51+
52+
/// <inheritdoc/>
53+
public override void ReportSuppressions(SuppressionAnalysisContext context)
54+
{
55+
// Cache semantic models and attribute symbols per syntax tree/compilation to avoid redundant calls
56+
Dictionary<SyntaxTree, SemanticModel>? semanticModelCache = null;
57+
INamedTypeSymbol? mcpToolAttribute = null;
58+
INamedTypeSymbol? mcpPromptAttribute = null;
59+
INamedTypeSymbol? mcpResourceAttribute = null;
60+
bool attributesResolved = false;
61+
62+
foreach (Diagnostic diagnostic in context.ReportedDiagnostics)
63+
{
64+
Location? location = diagnostic.Location;
65+
SyntaxTree? tree = location.SourceTree;
66+
if (tree is null)
67+
{
68+
continue;
69+
}
70+
71+
SyntaxNode root = tree.GetRoot(context.CancellationToken);
72+
SyntaxNode? node = root.FindNode(location.SourceSpan);
73+
74+
// Find the containing method declaration
75+
MethodDeclarationSyntax? method = node.FirstAncestorOrSelf<MethodDeclarationSyntax>();
76+
if (method is null)
77+
{
78+
continue;
79+
}
80+
81+
// Get or cache the semantic model for this tree
82+
semanticModelCache ??= new Dictionary<SyntaxTree, SemanticModel>();
83+
if (!semanticModelCache.TryGetValue(tree, out SemanticModel? semanticModel))
84+
{
85+
semanticModel = context.GetSemanticModel(tree);
86+
semanticModelCache[tree] = semanticModel;
87+
}
88+
89+
// Resolve attribute symbols once per compilation
90+
if (!attributesResolved)
91+
{
92+
mcpToolAttribute = semanticModel.Compilation.GetTypeByMetadataName(McpAttributeNames.McpServerToolAttribute);
93+
mcpPromptAttribute = semanticModel.Compilation.GetTypeByMetadataName(McpAttributeNames.McpServerPromptAttribute);
94+
mcpResourceAttribute = semanticModel.Compilation.GetTypeByMetadataName(McpAttributeNames.McpServerResourceAttribute);
95+
attributesResolved = true;
96+
}
97+
98+
// Check for MCP attributes
99+
SuppressionDescriptor? suppression = GetSuppressionForMethod(method, semanticModel, mcpToolAttribute, mcpPromptAttribute, mcpResourceAttribute, context.CancellationToken);
100+
if (suppression is not null)
101+
{
102+
context.ReportSuppression(Suppression.Create(suppression, diagnostic));
103+
}
104+
}
105+
}
106+
107+
private static SuppressionDescriptor? GetSuppressionForMethod(
108+
MethodDeclarationSyntax method,
109+
SemanticModel semanticModel,
110+
INamedTypeSymbol? mcpToolAttribute,
111+
INamedTypeSymbol? mcpPromptAttribute,
112+
INamedTypeSymbol? mcpResourceAttribute,
113+
CancellationToken cancellationToken)
114+
{
115+
IMethodSymbol? methodSymbol = semanticModel.GetDeclaredSymbol(method, cancellationToken);
116+
117+
if (methodSymbol is null)
118+
{
119+
return null;
120+
}
121+
122+
foreach (AttributeData attribute in methodSymbol.GetAttributes())
123+
{
124+
INamedTypeSymbol? attributeClass = attribute.AttributeClass;
125+
if (attributeClass is null)
126+
{
127+
continue;
128+
}
129+
130+
if (mcpToolAttribute is not null && SymbolEqualityComparer.Default.Equals(attributeClass, mcpToolAttribute))
131+
{
132+
return McpToolSuppression;
133+
}
134+
135+
if (mcpPromptAttribute is not null && SymbolEqualityComparer.Default.Equals(attributeClass, mcpPromptAttribute))
136+
{
137+
return McpPromptSuppression;
138+
}
139+
140+
if (mcpResourceAttribute is not null && SymbolEqualityComparer.Default.Equals(attributeClass, mcpResourceAttribute))
141+
{
142+
return McpResourceSuppression;
143+
}
144+
}
145+
146+
return null;
147+
}
148+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace ModelContextProtocol.Analyzers;
2+
3+
/// <summary>
4+
/// Contains the fully qualified metadata names for MCP server attributes.
5+
/// </summary>
6+
internal static class McpAttributeNames
7+
{
8+
public const string McpServerToolAttribute = "ModelContextProtocol.Server.McpServerToolAttribute";
9+
public const string McpServerPromptAttribute = "ModelContextProtocol.Server.McpServerPromptAttribute";
10+
public const string McpServerResourceAttribute = "ModelContextProtocol.Server.McpServerResourceAttribute";
11+
public const string DescriptionAttribute = "System.ComponentModel.DescriptionAttribute";
12+
}

src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,14 @@ namespace ModelContextProtocol.Analyzers;
1818
public sealed class XmlToDescriptionGenerator : IIncrementalGenerator
1919
{
2020
private const string GeneratedFileName = "ModelContextProtocol.Descriptions.g.cs";
21-
private const string McpServerToolAttributeName = "ModelContextProtocol.Server.McpServerToolAttribute";
22-
private const string McpServerPromptAttributeName = "ModelContextProtocol.Server.McpServerPromptAttribute";
23-
private const string McpServerResourceAttributeName = "ModelContextProtocol.Server.McpServerResourceAttribute";
24-
private const string DescriptionAttributeName = "System.ComponentModel.DescriptionAttribute";
2521

2622
public void Initialize(IncrementalGeneratorInitializationContext context)
2723
{
2824
// Extract method information for all MCP tools, prompts, and resources.
2925
// The transform extracts all necessary data upfront so the output doesn't depend on the compilation.
30-
var allMethods = CreateProviderForAttribute(context, McpServerToolAttributeName).Collect()
31-
.Combine(CreateProviderForAttribute(context, McpServerPromptAttributeName).Collect())
32-
.Combine(CreateProviderForAttribute(context, McpServerResourceAttributeName).Collect())
26+
var allMethods = CreateProviderForAttribute(context, McpAttributeNames.McpServerToolAttribute).Collect()
27+
.Combine(CreateProviderForAttribute(context, McpAttributeNames.McpServerPromptAttribute).Collect())
28+
.Combine(CreateProviderForAttribute(context, McpAttributeNames.McpServerResourceAttribute).Collect())
3329
.Select(static (tuple, _) =>
3430
{
3531
var ((tools, prompts), resources) = tuple;
@@ -84,7 +80,7 @@ private static MethodToGenerate ExtractMethodInfo(
8480
Compilation compilation)
8581
{
8682
bool isPartial = methodDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword);
87-
var descriptionAttribute = compilation.GetTypeByMetadataName(DescriptionAttributeName);
83+
var descriptionAttribute = compilation.GetTypeByMetadataName(McpAttributeNames.DescriptionAttribute);
8884

8985
// Try to extract XML documentation
9086
var (xmlDocs, hasInvalidXml) = TryExtractXmlDocumentation(methodSymbol);

0 commit comments

Comments
 (0)