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);