diff --git a/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs b/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs index 71982a3f1..c0cad01fe 100644 --- a/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs +++ b/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs @@ -19,6 +19,14 @@ public sealed class XmlToDescriptionGenerator : IIncrementalGenerator { private const string GeneratedFileName = "ModelContextProtocol.Descriptions.g.cs"; + /// + /// A display format that produces fully-qualified type names with "global::" prefix + /// and includes nullability annotations. + /// + private static readonly SymbolDisplayFormat s_fullyQualifiedFormatWithNullability = + SymbolDisplayFormat.FullyQualifiedFormat.AddMiscellaneousOptions( + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + public void Initialize(IncrementalGeneratorInitializationContext context) { // Extract method information for all MCP tools, prompts, and resources. @@ -125,7 +133,7 @@ private static MethodToGenerate ExtractMethodInfo( .Where(m => !m.IsKind(SyntaxKind.AsyncKeyword)) .Select(m => m.Text); string modifiersStr = string.Join(" ", modifiers); - string returnType = methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + string returnType = methodSymbol.ReturnType.ToDisplayString(s_fullyQualifiedFormatWithNullability); string methodName = methodSymbol.Name; // Extract parameters @@ -137,7 +145,7 @@ private static MethodToGenerate ExtractMethodInfo( var paramSyntax = i < parameterSyntaxList.Count ? parameterSyntaxList[i] : null; parameters[i] = new ParameterInfo( - ParameterType: param.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + ParameterType: param.Type.ToDisplayString(s_fullyQualifiedFormatWithNullability), Name: param.Name, HasDescriptionAttribute: descriptionAttribute is not null && HasAttribute(param, descriptionAttribute), XmlDescription: xmlDocs?.Parameters.TryGetValue(param.Name, out var pd) == true && !string.IsNullOrWhiteSpace(pd) ? pd : null, diff --git a/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs b/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs index b2cf83652..53fabfec3 100644 --- a/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs +++ b/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs @@ -1561,7 +1561,7 @@ namespace Test partial class TestTools { [Description("Async tool")] - public partial Task DoWorkAsync(string input); + public partial global::System.Threading.Tasks.Task DoWorkAsync(string input); } } """; @@ -1611,7 +1611,7 @@ namespace Test partial class TestTools { [Description("Static async tool")] - public static partial Task StaticAsyncMethod(string input); + public static partial global::System.Threading.Tasks.Task StaticAsyncMethod(string input); } } """; @@ -1663,7 +1663,7 @@ 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); + public static partial global::System.Threading.Tasks.Task AsyncWithDefaults([Description("The input")] string input, [Description("Timeout in ms")] int timeout = 1000); } } """; @@ -1719,6 +1719,387 @@ partial class TestTools 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);