Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ public sealed class XmlToDescriptionGenerator : IIncrementalGenerator
{
private const string GeneratedFileName = "ModelContextProtocol.Descriptions.g.cs";

/// <summary>
/// A display format that produces fully-qualified type names with "global::" prefix
/// and includes nullability annotations.
/// </summary>
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.
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1561,7 +1561,7 @@ namespace Test
partial class TestTools
{
[Description("Async tool")]
public partial Task<string> DoWorkAsync(string input);
public partial global::System.Threading.Tasks.Task<string> DoWorkAsync(string input);
}
}
""";
Expand Down Expand Up @@ -1611,7 +1611,7 @@ namespace Test
partial class TestTools
{
[Description("Static async tool")]
public static partial Task<string> StaticAsyncMethod(string input);
public static partial global::System.Threading.Tasks.Task<string> StaticAsyncMethod(string input);
}
}
""";
Expand Down Expand Up @@ -1663,7 +1663,7 @@ namespace Test
partial class TestTools
{
[Description("Async tool with defaults")]
public static partial Task<string> AsyncWithDefaults([Description("The input")] string input, [Description("Timeout in ms")] int timeout = 1000);
public static partial global::System.Threading.Tasks.Task<string> AsyncWithDefaults([Description("The input")] string input, [Description("Timeout in ms")] int timeout = 1000);
}
}
""";
Expand Down Expand Up @@ -1719,6 +1719,75 @@ 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<string>" : "Task<string>";
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
{
/// <summary>Do a thing based on an action.</summary>
/// <param name="action">The action to perform.</param>
[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 = $$"""
// <auto-generated/>
// 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<string> DoThing([Description("The action to perform.")] global::MyApp.Actions.MyAction action);
}
}
""";

AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}

private GeneratorRunResult RunGenerator([StringSyntax("C#-test")] string source, params string[] expectedDiagnosticIds)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
Expand Down