diff --git a/.gitignore b/.gitignore index 0d9f8b7573..15b2a8c982 100644 --- a/.gitignore +++ b/.gitignore @@ -418,3 +418,4 @@ Microsoft.FluentUI.AspNetCore.Components.xml /examples/Demo/FluentUI.Demo.Client/wwwroot/documentation/ api-comments.json /global.json +/src/Tools/McpServer/FluentUIComponentsDocumentation.json diff --git a/Directory.Packages.props b/Directory.Packages.props index a4274c8a18..b4a3fe0e76 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ true - 9.0.9 + 9.0.10 9.0.9 9.0.9 10.0.0 @@ -15,6 +15,9 @@ + + + diff --git a/Microsoft.FluentUI-v5.sln b/Microsoft.FluentUI-v5.sln index cbf9a6c60d..b9840b2762 100644 --- a/Microsoft.FluentUI-v5.sln +++ b/Microsoft.FluentUI-v5.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.0.10906.23 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36705.20 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{62586417-1901-4732-8188-AE8BADC6AE17}" EndProject @@ -43,61 +43,205 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{02EA EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentUI.Samples.WasmStandalone", "examples\Samples\FluentUI.Samples.WasmStandalone\FluentUI.Samples.WasmStandalone.csproj", "{EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "mcp", "mcp", "{5B3FD904-3D34-05FA-68A8-2F22A80B7D05}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.FluentUI.AspNetCore.Components.McpServer", "src\Tools\McpServer\Microsoft.FluentUI.AspNetCore.Components.McpServer.csproj", "{77D4C32F-A336-44C8-83C7-80EF960B7D6C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared", "src\Tools\McpServer.Shared\Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.csproj", "{C908B2BA-F26E-4C10-8761-0DB94709D556}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{ABFC1483-C021-B6A7-FAC3-E7866B5A76F3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{3D6EEA2A-E505-4D2E-B118-9F7D1FCA5BE7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests", "tests\Tools\McpServer.Tests\Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.csproj", "{8D169F98-6030-38C1-719F-610343A83341}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {7D2833AF-6BA7-481A-B625-4C162D8D4756}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7D2833AF-6BA7-481A-B625-4C162D8D4756}.Debug|Any CPU.Build.0 = Debug|Any CPU {7D2833AF-6BA7-481A-B625-4C162D8D4756}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {7D2833AF-6BA7-481A-B625-4C162D8D4756}.Debug|x64.ActiveCfg = Debug|x64 + {7D2833AF-6BA7-481A-B625-4C162D8D4756}.Debug|x86.ActiveCfg = Debug|x86 {7D2833AF-6BA7-481A-B625-4C162D8D4756}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D2833AF-6BA7-481A-B625-4C162D8D4756}.Release|Any CPU.Build.0 = Release|Any CPU {7D2833AF-6BA7-481A-B625-4C162D8D4756}.Release|Any CPU.Deploy.0 = Release|Any CPU + {7D2833AF-6BA7-481A-B625-4C162D8D4756}.Release|x64.ActiveCfg = Release|x64 + {7D2833AF-6BA7-481A-B625-4C162D8D4756}.Release|x86.ActiveCfg = Release|x86 {3C30B391-7436-4349-A6CE-19EAFA6804FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3C30B391-7436-4349-A6CE-19EAFA6804FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C30B391-7436-4349-A6CE-19EAFA6804FA}.Debug|x64.ActiveCfg = Debug|Any CPU + {3C30B391-7436-4349-A6CE-19EAFA6804FA}.Debug|x64.Build.0 = Debug|Any CPU + {3C30B391-7436-4349-A6CE-19EAFA6804FA}.Debug|x86.ActiveCfg = Debug|Any CPU + {3C30B391-7436-4349-A6CE-19EAFA6804FA}.Debug|x86.Build.0 = Debug|Any CPU {3C30B391-7436-4349-A6CE-19EAFA6804FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C30B391-7436-4349-A6CE-19EAFA6804FA}.Release|Any CPU.Build.0 = Release|Any CPU + {3C30B391-7436-4349-A6CE-19EAFA6804FA}.Release|x64.ActiveCfg = Release|Any CPU + {3C30B391-7436-4349-A6CE-19EAFA6804FA}.Release|x64.Build.0 = Release|Any CPU + {3C30B391-7436-4349-A6CE-19EAFA6804FA}.Release|x86.ActiveCfg = Release|Any CPU + {3C30B391-7436-4349-A6CE-19EAFA6804FA}.Release|x86.Build.0 = Release|Any CPU {3A9FE7FC-69CB-4698-A676-39A3A13399B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3A9FE7FC-69CB-4698-A676-39A3A13399B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A9FE7FC-69CB-4698-A676-39A3A13399B3}.Debug|x64.ActiveCfg = Debug|Any CPU + {3A9FE7FC-69CB-4698-A676-39A3A13399B3}.Debug|x64.Build.0 = Debug|Any CPU + {3A9FE7FC-69CB-4698-A676-39A3A13399B3}.Debug|x86.ActiveCfg = Debug|Any CPU + {3A9FE7FC-69CB-4698-A676-39A3A13399B3}.Debug|x86.Build.0 = Debug|Any CPU {3A9FE7FC-69CB-4698-A676-39A3A13399B3}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A9FE7FC-69CB-4698-A676-39A3A13399B3}.Release|Any CPU.Build.0 = Release|Any CPU + {3A9FE7FC-69CB-4698-A676-39A3A13399B3}.Release|x64.ActiveCfg = Release|Any CPU + {3A9FE7FC-69CB-4698-A676-39A3A13399B3}.Release|x64.Build.0 = Release|Any CPU + {3A9FE7FC-69CB-4698-A676-39A3A13399B3}.Release|x86.ActiveCfg = Release|Any CPU + {3A9FE7FC-69CB-4698-A676-39A3A13399B3}.Release|x86.Build.0 = Release|Any CPU {AEBC6690-5247-4C1E-ADAF-0BBFAC97D606}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AEBC6690-5247-4C1E-ADAF-0BBFAC97D606}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEBC6690-5247-4C1E-ADAF-0BBFAC97D606}.Debug|x64.ActiveCfg = Debug|Any CPU + {AEBC6690-5247-4C1E-ADAF-0BBFAC97D606}.Debug|x64.Build.0 = Debug|Any CPU + {AEBC6690-5247-4C1E-ADAF-0BBFAC97D606}.Debug|x86.ActiveCfg = Debug|Any CPU + {AEBC6690-5247-4C1E-ADAF-0BBFAC97D606}.Debug|x86.Build.0 = Debug|Any CPU {AEBC6690-5247-4C1E-ADAF-0BBFAC97D606}.Release|Any CPU.ActiveCfg = Release|Any CPU {AEBC6690-5247-4C1E-ADAF-0BBFAC97D606}.Release|Any CPU.Build.0 = Release|Any CPU + {AEBC6690-5247-4C1E-ADAF-0BBFAC97D606}.Release|x64.ActiveCfg = Release|Any CPU + {AEBC6690-5247-4C1E-ADAF-0BBFAC97D606}.Release|x64.Build.0 = Release|Any CPU + {AEBC6690-5247-4C1E-ADAF-0BBFAC97D606}.Release|x86.ActiveCfg = Release|Any CPU + {AEBC6690-5247-4C1E-ADAF-0BBFAC97D606}.Release|x86.Build.0 = Release|Any CPU {0252FA12-8398-4A68-8C80-8DFBDF3021FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0252FA12-8398-4A68-8C80-8DFBDF3021FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0252FA12-8398-4A68-8C80-8DFBDF3021FD}.Debug|x64.ActiveCfg = Debug|Any CPU + {0252FA12-8398-4A68-8C80-8DFBDF3021FD}.Debug|x64.Build.0 = Debug|Any CPU + {0252FA12-8398-4A68-8C80-8DFBDF3021FD}.Debug|x86.ActiveCfg = Debug|Any CPU + {0252FA12-8398-4A68-8C80-8DFBDF3021FD}.Debug|x86.Build.0 = Debug|Any CPU {0252FA12-8398-4A68-8C80-8DFBDF3021FD}.Release|Any CPU.ActiveCfg = Release|Any CPU {0252FA12-8398-4A68-8C80-8DFBDF3021FD}.Release|Any CPU.Build.0 = Release|Any CPU + {0252FA12-8398-4A68-8C80-8DFBDF3021FD}.Release|x64.ActiveCfg = Release|Any CPU + {0252FA12-8398-4A68-8C80-8DFBDF3021FD}.Release|x64.Build.0 = Release|Any CPU + {0252FA12-8398-4A68-8C80-8DFBDF3021FD}.Release|x86.ActiveCfg = Release|Any CPU + {0252FA12-8398-4A68-8C80-8DFBDF3021FD}.Release|x86.Build.0 = Release|Any CPU {958BF092-4CF2-470C-B058-9244496B234F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {958BF092-4CF2-470C-B058-9244496B234F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {958BF092-4CF2-470C-B058-9244496B234F}.Debug|x64.ActiveCfg = Debug|Any CPU + {958BF092-4CF2-470C-B058-9244496B234F}.Debug|x64.Build.0 = Debug|Any CPU + {958BF092-4CF2-470C-B058-9244496B234F}.Debug|x86.ActiveCfg = Debug|Any CPU + {958BF092-4CF2-470C-B058-9244496B234F}.Debug|x86.Build.0 = Debug|Any CPU {958BF092-4CF2-470C-B058-9244496B234F}.Release|Any CPU.ActiveCfg = Release|Any CPU {958BF092-4CF2-470C-B058-9244496B234F}.Release|Any CPU.Build.0 = Release|Any CPU + {958BF092-4CF2-470C-B058-9244496B234F}.Release|x64.ActiveCfg = Release|Any CPU + {958BF092-4CF2-470C-B058-9244496B234F}.Release|x64.Build.0 = Release|Any CPU + {958BF092-4CF2-470C-B058-9244496B234F}.Release|x86.ActiveCfg = Release|Any CPU + {958BF092-4CF2-470C-B058-9244496B234F}.Release|x86.Build.0 = Release|Any CPU {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Debug|x64.ActiveCfg = Debug|Any CPU + {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Debug|x64.Build.0 = Debug|Any CPU + {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Debug|x86.ActiveCfg = Debug|Any CPU + {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Debug|x86.Build.0 = Debug|Any CPU {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Release|Any CPU.ActiveCfg = Release|Any CPU {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Release|Any CPU.Build.0 = Release|Any CPU + {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Release|x64.ActiveCfg = Release|Any CPU + {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Release|x64.Build.0 = Release|Any CPU + {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Release|x86.ActiveCfg = Release|Any CPU + {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Release|x86.Build.0 = Release|Any CPU {32466925-47C6-420F-B869-5F922162C3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {32466925-47C6-420F-B869-5F922162C3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32466925-47C6-420F-B869-5F922162C3A7}.Debug|x64.ActiveCfg = Debug|Any CPU + {32466925-47C6-420F-B869-5F922162C3A7}.Debug|x64.Build.0 = Debug|Any CPU + {32466925-47C6-420F-B869-5F922162C3A7}.Debug|x86.ActiveCfg = Debug|Any CPU + {32466925-47C6-420F-B869-5F922162C3A7}.Debug|x86.Build.0 = Debug|Any CPU {32466925-47C6-420F-B869-5F922162C3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {32466925-47C6-420F-B869-5F922162C3A7}.Release|Any CPU.Build.0 = Release|Any CPU + {32466925-47C6-420F-B869-5F922162C3A7}.Release|x64.ActiveCfg = Release|Any CPU + {32466925-47C6-420F-B869-5F922162C3A7}.Release|x64.Build.0 = Release|Any CPU + {32466925-47C6-420F-B869-5F922162C3A7}.Release|x86.ActiveCfg = Release|Any CPU + {32466925-47C6-420F-B869-5F922162C3A7}.Release|x86.Build.0 = Release|Any CPU {E67B08B6-AEE4-4281-8700-1C87A5A3C11E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E67B08B6-AEE4-4281-8700-1C87A5A3C11E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E67B08B6-AEE4-4281-8700-1C87A5A3C11E}.Debug|x64.ActiveCfg = Debug|Any CPU + {E67B08B6-AEE4-4281-8700-1C87A5A3C11E}.Debug|x64.Build.0 = Debug|Any CPU + {E67B08B6-AEE4-4281-8700-1C87A5A3C11E}.Debug|x86.ActiveCfg = Debug|Any CPU + {E67B08B6-AEE4-4281-8700-1C87A5A3C11E}.Debug|x86.Build.0 = Debug|Any CPU {E67B08B6-AEE4-4281-8700-1C87A5A3C11E}.Release|Any CPU.ActiveCfg = Release|Any CPU {E67B08B6-AEE4-4281-8700-1C87A5A3C11E}.Release|Any CPU.Build.0 = Release|Any CPU + {E67B08B6-AEE4-4281-8700-1C87A5A3C11E}.Release|x64.ActiveCfg = Release|Any CPU + {E67B08B6-AEE4-4281-8700-1C87A5A3C11E}.Release|x64.Build.0 = Release|Any CPU + {E67B08B6-AEE4-4281-8700-1C87A5A3C11E}.Release|x86.ActiveCfg = Release|Any CPU + {E67B08B6-AEE4-4281-8700-1C87A5A3C11E}.Release|x86.Build.0 = Release|Any CPU {F380FA22-53D8-4381-B89B-4047AF544D53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F380FA22-53D8-4381-B89B-4047AF544D53}.Debug|x64.ActiveCfg = Debug|Any CPU + {F380FA22-53D8-4381-B89B-4047AF544D53}.Debug|x64.Build.0 = Debug|Any CPU + {F380FA22-53D8-4381-B89B-4047AF544D53}.Debug|x86.ActiveCfg = Debug|Any CPU + {F380FA22-53D8-4381-B89B-4047AF544D53}.Debug|x86.Build.0 = Debug|Any CPU {F380FA22-53D8-4381-B89B-4047AF544D53}.Release|Any CPU.ActiveCfg = Release|Any CPU {F380FA22-53D8-4381-B89B-4047AF544D53}.Release|Any CPU.Build.0 = Release|Any CPU + {F380FA22-53D8-4381-B89B-4047AF544D53}.Release|x64.ActiveCfg = Release|Any CPU + {F380FA22-53D8-4381-B89B-4047AF544D53}.Release|x64.Build.0 = Release|Any CPU + {F380FA22-53D8-4381-B89B-4047AF544D53}.Release|x86.ActiveCfg = Release|Any CPU + {F380FA22-53D8-4381-B89B-4047AF544D53}.Release|x86.Build.0 = Release|Any CPU {D52F6265-A983-46E0-8831-67FA80D95FBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D52F6265-A983-46E0-8831-67FA80D95FBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D52F6265-A983-46E0-8831-67FA80D95FBE}.Debug|x64.ActiveCfg = Debug|Any CPU + {D52F6265-A983-46E0-8831-67FA80D95FBE}.Debug|x64.Build.0 = Debug|Any CPU + {D52F6265-A983-46E0-8831-67FA80D95FBE}.Debug|x86.ActiveCfg = Debug|Any CPU + {D52F6265-A983-46E0-8831-67FA80D95FBE}.Debug|x86.Build.0 = Debug|Any CPU {D52F6265-A983-46E0-8831-67FA80D95FBE}.Release|Any CPU.ActiveCfg = Release|Any CPU {D52F6265-A983-46E0-8831-67FA80D95FBE}.Release|Any CPU.Build.0 = Release|Any CPU + {D52F6265-A983-46E0-8831-67FA80D95FBE}.Release|x64.ActiveCfg = Release|Any CPU + {D52F6265-A983-46E0-8831-67FA80D95FBE}.Release|x64.Build.0 = Release|Any CPU + {D52F6265-A983-46E0-8831-67FA80D95FBE}.Release|x86.ActiveCfg = Release|Any CPU + {D52F6265-A983-46E0-8831-67FA80D95FBE}.Release|x86.Build.0 = Release|Any CPU {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Debug|x64.ActiveCfg = Debug|Any CPU + {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Debug|x64.Build.0 = Debug|Any CPU + {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Debug|x86.ActiveCfg = Debug|Any CPU + {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Debug|x86.Build.0 = Debug|Any CPU {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Release|Any CPU.Build.0 = Release|Any CPU + {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Release|x64.ActiveCfg = Release|Any CPU + {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Release|x64.Build.0 = Release|Any CPU + {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Release|x86.ActiveCfg = Release|Any CPU + {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5}.Release|x86.Build.0 = Release|Any CPU + {77D4C32F-A336-44C8-83C7-80EF960B7D6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77D4C32F-A336-44C8-83C7-80EF960B7D6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77D4C32F-A336-44C8-83C7-80EF960B7D6C}.Debug|x64.ActiveCfg = Debug|Any CPU + {77D4C32F-A336-44C8-83C7-80EF960B7D6C}.Debug|x64.Build.0 = Debug|Any CPU + {77D4C32F-A336-44C8-83C7-80EF960B7D6C}.Debug|x86.ActiveCfg = Debug|Any CPU + {77D4C32F-A336-44C8-83C7-80EF960B7D6C}.Debug|x86.Build.0 = Debug|Any CPU + {77D4C32F-A336-44C8-83C7-80EF960B7D6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77D4C32F-A336-44C8-83C7-80EF960B7D6C}.Release|Any CPU.Build.0 = Release|Any CPU + {77D4C32F-A336-44C8-83C7-80EF960B7D6C}.Release|x64.ActiveCfg = Release|Any CPU + {77D4C32F-A336-44C8-83C7-80EF960B7D6C}.Release|x64.Build.0 = Release|Any CPU + {77D4C32F-A336-44C8-83C7-80EF960B7D6C}.Release|x86.ActiveCfg = Release|Any CPU + {77D4C32F-A336-44C8-83C7-80EF960B7D6C}.Release|x86.Build.0 = Release|Any CPU + {C908B2BA-F26E-4C10-8761-0DB94709D556}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C908B2BA-F26E-4C10-8761-0DB94709D556}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C908B2BA-F26E-4C10-8761-0DB94709D556}.Debug|x64.ActiveCfg = Debug|Any CPU + {C908B2BA-F26E-4C10-8761-0DB94709D556}.Debug|x64.Build.0 = Debug|Any CPU + {C908B2BA-F26E-4C10-8761-0DB94709D556}.Debug|x86.ActiveCfg = Debug|Any CPU + {C908B2BA-F26E-4C10-8761-0DB94709D556}.Debug|x86.Build.0 = Debug|Any CPU + {C908B2BA-F26E-4C10-8761-0DB94709D556}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C908B2BA-F26E-4C10-8761-0DB94709D556}.Release|Any CPU.Build.0 = Release|Any CPU + {C908B2BA-F26E-4C10-8761-0DB94709D556}.Release|x64.ActiveCfg = Release|Any CPU + {C908B2BA-F26E-4C10-8761-0DB94709D556}.Release|x64.Build.0 = Release|Any CPU + {C908B2BA-F26E-4C10-8761-0DB94709D556}.Release|x86.ActiveCfg = Release|Any CPU + {C908B2BA-F26E-4C10-8761-0DB94709D556}.Release|x86.Build.0 = Release|Any CPU + {8D169F98-6030-38C1-719F-610343A83341}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D169F98-6030-38C1-719F-610343A83341}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D169F98-6030-38C1-719F-610343A83341}.Debug|x64.ActiveCfg = Debug|Any CPU + {8D169F98-6030-38C1-719F-610343A83341}.Debug|x64.Build.0 = Debug|Any CPU + {8D169F98-6030-38C1-719F-610343A83341}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D169F98-6030-38C1-719F-610343A83341}.Debug|x86.Build.0 = Debug|Any CPU + {8D169F98-6030-38C1-719F-610343A83341}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D169F98-6030-38C1-719F-610343A83341}.Release|Any CPU.Build.0 = Release|Any CPU + {8D169F98-6030-38C1-719F-610343A83341}.Release|x64.ActiveCfg = Release|Any CPU + {8D169F98-6030-38C1-719F-610343A83341}.Release|x64.Build.0 = Release|Any CPU + {8D169F98-6030-38C1-719F-610343A83341}.Release|x86.ActiveCfg = Release|Any CPU + {8D169F98-6030-38C1-719F-610343A83341}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -119,6 +263,12 @@ Global {D52F6265-A983-46E0-8831-67FA80D95FBE} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9} {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {F273876F-7528-42B3-BFE8-7CFF8ED1E2A2} {EB38AC75-966B-41BB-A1C5-0CAFE17FE3D5} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {5B3FD904-3D34-05FA-68A8-2F22A80B7D05} = {3D6EEA2A-E505-4D2E-B118-9F7D1FCA5BE7} + {77D4C32F-A336-44C8-83C7-80EF960B7D6C} = {5B3FD904-3D34-05FA-68A8-2F22A80B7D05} + {C908B2BA-F26E-4C10-8761-0DB94709D556} = {5B3FD904-3D34-05FA-68A8-2F22A80B7D05} + {ABFC1483-C021-B6A7-FAC3-E7866B5A76F3} = {A7EC98D2-21E3-4967-8C5A-D62E640305EB} + {3D6EEA2A-E505-4D2E-B118-9F7D1FCA5BE7} = {62586417-1901-4732-8188-AE8BADC6AE17} + {8D169F98-6030-38C1-719F-610343A83341} = {ABFC1483-C021-B6A7-FAC3-E7866B5A76F3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {44D95FF7-AEBE-41FB-9D40-CF1E09ADC6BC} diff --git a/Microsoft.FluentUI-v5.slnx b/Microsoft.FluentUI-v5.slnx index 5cab1a9f97..4b40596322 100644 --- a/Microsoft.FluentUI-v5.slnx +++ b/Microsoft.FluentUI-v5.slnx @@ -20,10 +20,17 @@ + + + + + + + diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Examples/McpCapabilities.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Examples/McpCapabilities.razor new file mode 100644 index 0000000000..4b82e662bd --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Examples/McpCapabilities.razor @@ -0,0 +1,131 @@ +@using Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared +@using Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.Models +@using System.Net.Http.Json +@inject HttpClient Http + +@if (_loading) +{ + +} +else if (_summary is null) +{ + + Failed to load MCP capabilities data. + +} +else +{ + + + + + + + @if (context.Parameters.Count > 0) + { + + @foreach (var param in context.Parameters) + { + + @param.Name + (@param.Type) + @if (param.Required) + { + * + } + + } + + } + else + { + None + } + + + + + + + + + + @if (context.Parameters.Count > 0) + { + + @foreach (var param in context.Parameters) + { + + @param.Name + (@param.Type) + @if (param.Required) + { + * + } + + } + + } + else + { + None + } + + + + + + + + + + + @if (context.IsTemplate) + { + Template + } + else + { + Static + } + + + + +} + +@code { + private string _activeTab = "tools"; + private McpSummary? _summary; + private bool _loading = true; + + protected override async Task OnInitializedAsync() + { + try + { + // Try to get from static cache first (works in Server mode) + if (McpCapabilitiesData.IsInitialized) + { + _summary = McpCapabilitiesData.GetSummary(); + } + else + { + // Fetch from API (for WebAssembly mode) + _summary = await Http.GetFromJsonAsync("/api/mcp/capabilities"); + } + } + catch + { + _summary = null; + } + finally + { + _loading = false; + } + } + + private void OnTabChanged(string? tabId) + { + _activeTab = tabId ?? "tools"; + } +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/McpServer.md b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/McpServer.md new file mode 100644 index 0000000000..b745759b4c --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/McpServer.md @@ -0,0 +1,148 @@ +--- +title: MCP Server +order: 0012 +category: 10|Get Started +route: /mcp-server +--- + +# MCP Server + +**Model Context Protocol (MCP)** is an open protocol that enables seamless integration between LLM applications and external data sources and tools. +The Fluent UI Blazor library provides an MCP server that gives AI assistants access to component documentation, enabling them to generate accurate, up-to-date Fluent UI Blazor code. + +## What is MCP? + +MCP provides a standardized way for AI assistants (like GitHub Copilot, Claude, and others) to access external context. +Instead of relying solely on training data, AI assistants can query real-time documentation, search for specific components, and get accurate parameter information. + +Learn more: [Model Context Protocol](https://modelcontextprotocol.io/) + +## Installation + +### As a .NET Global Tool + +```bash +dotnet tool install -g Microsoft.FluentUI.AspNetCore.Components.McpServer --prerelease +``` + +### From Source + +```bash +cd examples/Mcp/FluentUI.Mcp.Server +dotnet build +``` + +## Configuration + +### VS Code / GitHub Copilot + +Add to your `.vscode/mcp.json` or user settings: + +```json +{ + "servers": { + "fluentui-blazor": { + "command": "fluentui-mcp", + "args": [] + } + } +} +``` + +### Claude Desktop + +Add to your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "fluentui-blazor": { + "command": "fluentui-mcp", + "args": [] + } + } +} +``` + +### Running from Source + +If running from source, use the full path: + +```json +{ + "servers": { + "fluentui-blazor": { + "command": "dotnet", + "args": ["run", "--project", "path/to/FluentUI.Mcp.Server"] + } + } +} +``` + +## Available Capabilities + +The MCP server exposes three types of primitives: + +- **Tools**: Model-controlled functions for dynamic queries (search, get details) +- **Resources**: User-selected static content (component lists, guides) +- **Prompts**: Pre-defined templates for common tasks (create component, migrate code) + +{{ McpCapabilities }} + +## Usage Examples + +### Ask for Component Help + +> "How do I use FluentDataGrid with pagination?" + +The AI will use the `GetComponentDetails` tool to fetch accurate parameter information. + +### Generate Code + +> "Create a form with name, email, and a submit button using Fluent UI Blazor" + +The AI will use the `create_form` prompt combined with component documentation. + +### Migration Assistance + +> "Migrate this v4 code to v5" + +The AI will use the `migrate_to_v5` prompt and the migration guide resource. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ AI Assistant │ +│ (Copilot, Claude, etc.) │ +└─────────────────────────────────────────────────────────┘ + │ + │ MCP Protocol (stdio) + │ +┌─────────────────────────────────────────────────────────┐ +│ FluentUI MCP Server │ +├─────────────────────────────────────────────────────────┤ +│ Tools │ Resources │ Prompts │ +│ ─────────── │ ──────────── │ ──────────── │ +│ ListComponents │ components │ create_component │ +│ GetDetails │ categories │ create_form │ +│ SearchEnums │ guides/* │ migrate_to_v5 │ +│ ... │ ... │ ... │ +├─────────────────────────────────────────────────────────┤ +│ FluentUIDocumentationService │ +│ (Reflection + XML Docs) │ +└─────────────────────────────────────────────────────────┘ + │ + │ Assembly Reflection + │ +┌─────────────────────────────────────────────────────────┐ +│ Microsoft.FluentUI.AspNetCore.Components │ +│ (Component Library) │ +└─────────────────────────────────────────────────────────┘ +``` + +## Source Code + +The MCP server source code is available in the repository: + +[examples/Mcp/FluentUI.Mcp.Server](https://github.com/microsoft/fluentui-blazor/tree/dev/examples/Mcp/FluentUI.Mcp.Server) diff --git a/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj b/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj index 16160712e9..a325a18a77 100644 --- a/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj +++ b/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj @@ -44,6 +44,7 @@ + diff --git a/examples/Demo/FluentUI.Demo/FluentUI.Demo.csproj b/examples/Demo/FluentUI.Demo/FluentUI.Demo.csproj index cd62ae4b75..4f4d2f8c0f 100644 --- a/examples/Demo/FluentUI.Demo/FluentUI.Demo.csproj +++ b/examples/Demo/FluentUI.Demo/FluentUI.Demo.csproj @@ -1,4 +1,4 @@ - + net9.0 @@ -10,6 +10,7 @@ + diff --git a/examples/Demo/FluentUI.Demo/Program.cs b/examples/Demo/FluentUI.Demo/Program.cs index 5b86d24527..61e0d68136 100644 --- a/examples/Demo/FluentUI.Demo/Program.cs +++ b/examples/Demo/FluentUI.Demo/Program.cs @@ -4,6 +4,8 @@ using FluentUI.Demo.Client; using Microsoft.FluentUI.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared; var builder = WebApplication.CreateBuilder(args); @@ -32,6 +34,9 @@ // Add Demo server services builder.Services.AddFluentUIDemoServices().ForServer(); +// Initialize MCP capabilities data from the MCP Server assembly +McpCapabilitiesData.Initialize(typeof(FluentUIDocumentationService).Assembly); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -54,6 +59,10 @@ // Use the localization services app.UseRequestLocalization(new RequestLocalizationOptions().AddSupportedUICultures(["en", "fr"])); +// API endpoint to expose MCP capabilities for WebAssembly clients +app.MapGet("/api/mcp/capabilities", () => McpCapabilitiesData.GetSummary()) + .WithName("GetMcpCapabilities"); + app.MapRazorComponents() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/McpDocumentationGenerator.cs b/examples/Tools/FluentUI.Demo.DocApiGen/McpDocumentationGenerator.cs new file mode 100644 index 0000000000..cfdf7af37f --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/McpDocumentationGenerator.cs @@ -0,0 +1,577 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using FluentUI.Demo.DocApiGen.Extensions; +using FluentUI.Demo.DocApiGen.Models.McpDocumentation; + +namespace FluentUI.Demo.DocApiGen; + +/// +/// Generates MCP-compatible JSON documentation for the McpServer. +/// This allows the McpServer to consume pre-generated documentation +/// without needing the LoxSmoke.DocXml dependency at runtime. +/// +public class McpDocumentationGenerator +{ + private static readonly string[] MEMBERS_TO_EXCLUDE = + [ + "AdditionalAttributes", + "ParentReference", + "Element", + "Equals", + "GetHashCode", + "GetType", + "SetParametersAsync", + "ToString", + "Dispose", + "DisposeAsync", + "ValueExpression", + ]; + + private static readonly string[] EXCLUDE_TYPES = + [ + "TypeInference", + "InternalListContext`1", + "SpacingGenerator", + "FluentLocalizerInternal", + "FluentJSModule", + "FluentServiceProviderException`1", + ]; + + private readonly Assembly _assembly; + private readonly LoxSmoke.DocXml.DocXmlReader _docXmlReader; + + // Cached type references discovered from the assembly + private readonly Type? _fluentComponentBaseInterface; + private readonly Type? _parameterAttributeType; + private readonly Type? _eventCallbackType; + private readonly Type? _eventCallbackGenericType; + private readonly Type? _jsInvokableAttributeType; + + /// + /// Initializes a new instance of the class. + /// + public McpDocumentationGenerator(Assembly assembly, FileInfo xmlDocumentation) + { + _assembly = assembly; + _docXmlReader = new LoxSmoke.DocXml.DocXmlReader(xmlDocumentation.FullName); + + // Discover types from the loaded assembly and its references + _fluentComponentBaseInterface = FindTypeByName("IFluentComponentBase"); + _parameterAttributeType = FindTypeByName("ParameterAttribute") + ?? Type.GetType("Microsoft.AspNetCore.Components.ParameterAttribute, Microsoft.AspNetCore.Components"); + _eventCallbackType = Type.GetType("Microsoft.AspNetCore.Components.EventCallback, Microsoft.AspNetCore.Components"); + _eventCallbackGenericType = Type.GetType("Microsoft.AspNetCore.Components.EventCallback`1, Microsoft.AspNetCore.Components"); + _jsInvokableAttributeType = Type.GetType("Microsoft.JSInterop.JSInvokableAttribute, Microsoft.JSInterop"); + } + + /// + /// Finds a type by name in the loaded assembly or its referenced assemblies. + /// + private Type? FindTypeByName(string typeName) + { + // First, search in the main assembly + var type = _assembly.GetTypes().FirstOrDefault(t => t.Name == typeName); + if (type != null) + { + return type; + } + + // Search in referenced assemblies + foreach (var refAssemblyName in _assembly.GetReferencedAssemblies()) + { + try + { + var refAssembly = Assembly.Load(refAssemblyName); + type = refAssembly.GetTypes().FirstOrDefault(t => t.Name == typeName); + if (type != null) + { + return type; + } + } + catch + { + // Ignore assemblies that can't be loaded + } + } + + return null; + } + + /// + /// Generates the MCP documentation root containing all components and enums. + /// + public McpDocumentationRoot Generate() + { + var assemblyInfo = ApiClassGenerator.GetAssemblyInfo(_assembly); + var components = GenerateComponents().ToList(); + var enums = GenerateEnums().ToList(); + + return new McpDocumentationRoot + { + Metadata = new McpDocumentationMetadata + { + AssemblyVersion = assemblyInfo.Version, + GeneratedDateUtc = assemblyInfo.Date, + ComponentCount = components.Count, + EnumCount = enums.Count + }, + Components = components, + Enums = enums + }; + } + + /// + /// Generates the JSON string for MCP documentation. + /// + public string GenerateJson(bool indented = true) + { + var root = Generate(); + var options = new JsonSerializerOptions + { + WriteIndented = indented, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + return JsonSerializer.Serialize(root, options); + } + + /// + /// Saves the MCP documentation to a JSON file. + /// + public void SaveToFile(string fileName, bool indented = true) + { + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + + File.WriteAllText(fileName, GenerateJson(indented)); + } + + /// + /// Generates component information for all valid component types. + /// + private IEnumerable GenerateComponents() + { + foreach (var type in _assembly.GetTypes().Where(IsValidComponentType)) + { + var componentInfo = GenerateComponentInfo(type); + if (componentInfo != null) + { + yield return componentInfo; + } + } + } + + /// + /// Generates enum information for all public enums. + /// + private IEnumerable GenerateEnums() + { + foreach (var type in _assembly.GetTypes().Where(t => t.IsEnum && t.IsPublic)) + { + yield return GenerateEnumInfo(type); + } + } + + /// + /// Generates component information for a specific type. + /// + private McpComponentInfo? GenerateComponentInfo(Type type) + { + try + { + var summary = GetTypeSummary(type); + var category = DetermineCategory(type); + + var component = new McpComponentInfo + { + Name = type.Name, + FullName = type.FullName ?? type.Name, + Summary = summary, + Category = category, + IsGeneric = type.IsGenericType, + BaseClass = type.BaseType?.Name, + Properties = [], + Events = [], + Methods = [] + }; + + ExtractMembers(type, component); + + return component; + } + catch (Exception ex) + { + Console.WriteLine($"[McpDocGen] Warning: Could not process component {type.Name}: {ex.Message}"); + return null; + } + } + + /// + /// Extracts properties, events, and methods from a type. + /// + private void ExtractMembers(Type type, McpComponentInfo component) + { + // Extract properties and events + foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (MEMBERS_TO_EXCLUDE.Contains(prop.Name)) + { + continue; + } + + var isObsolete = prop.GetCustomAttribute() != null; + if (isObsolete) + { + continue; + } + + var isParameter = HasAttribute(prop, _parameterAttributeType); + var isEvent = IsEventCallback(prop.PropertyType); + var isInherited = prop.DeclaringType != type; + var description = GetMemberSummary(prop); + + if (isEvent) + { + component.Events.Add(new McpEventInfo + { + Name = prop.Name, + Type = prop.ToTypeNameString(), + Description = description, + IsInherited = isInherited + }); + } + else + { + component.Properties.Add(new McpPropertyInfo + { + Name = prop.Name, + Type = prop.ToTypeNameString(), + Description = description, + IsParameter = isParameter, + IsInherited = isInherited, + DefaultValue = GetDefaultValue(type, prop), + EnumValues = GetEnumValues(prop.PropertyType) + }); + } + } + + // Extract methods + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) + { + if (method.IsSpecialName || MEMBERS_TO_EXCLUDE.Contains(method.Name)) + { + continue; + } + + var isObsolete = method.GetCustomAttribute() != null; + var isJSInvokable = HasAttribute(method, _jsInvokableAttributeType); + if (isObsolete || isJSInvokable) + { + continue; + } + + var genericArgs = method.IsGenericMethod + ? "<" + string.Join(", ", method.GetGenericArguments().Select(a => a.Name)) + ">" + : ""; + + component.Methods.Add(new McpMethodInfo + { + Name = method.Name + genericArgs, + ReturnType = method.ToTypeNameString(), + Description = GetMemberSummary(method), + Parameters = method.GetParameters().Select(p => $"{p.ToTypeNameString()} {p.Name}").ToArray(), + IsInherited = false + }); + } + } + + /// + /// Checks if a member has a specific attribute type. + /// + private static bool HasAttribute(MemberInfo member, Type? attributeType) + { + if (attributeType == null) + { + return false; + } + + return member.GetCustomAttributes(attributeType, true).Length > 0; + } + + /// + /// Generates enum information for a specific type. + /// + private McpEnumInfo GenerateEnumInfo(Type type) + { + var values = new List(); + var names = Enum.GetNames(type); + var enumValues = Enum.GetValues(type); + + for (var i = 0; i < names.Length; i++) + { + var name = names[i]; + var value = Convert.ToInt32(enumValues.GetValue(i), System.Globalization.CultureInfo.InvariantCulture); + var field = type.GetField(name); + var description = field != null ? GetMemberSummary(field) : string.Empty; + + values.Add(new McpEnumValueInfo + { + Name = name, + Value = value, + Description = description + }); + } + + return new McpEnumInfo + { + Name = type.Name, + FullName = type.FullName ?? type.Name, + Description = GetTypeSummary(type), + Values = values + }; + } + + /// + /// Gets the summary for a type from XML documentation. + /// + private string GetTypeSummary(Type type) + { + try + { + var comments = _docXmlReader.GetTypeComments(type); + return CleanSummary(comments.Summary); + } + catch + { + return string.Empty; + } + } + + /// + /// Gets the summary for a member from XML documentation. + /// + private string GetMemberSummary(MemberInfo member) + { + try + { + var comments = _docXmlReader.GetMemberComments(member); + return CleanSummary(comments.Summary); + } + catch + { + return string.Empty; + } + } + + /// + /// Cleans up the summary text. + /// + private static string CleanSummary(string? summary) + { + if (string.IsNullOrWhiteSpace(summary)) + { + return string.Empty; + } + + // Remove XML tags and normalize whitespace + var cleaned = System.Text.RegularExpressions.Regex.Replace(summary, @"<[^>]+>", " "); + cleaned = System.Text.RegularExpressions.Regex.Replace(cleaned, @"\s+", " "); + return cleaned.Trim(); + } + + /// + /// Checks if a type is a valid FluentUI Blazor component type. + /// + private bool IsValidComponentType(Type type) + { + return type != null && + type.IsPublic && + !type.IsAbstract && + !type.IsInterface && + type.IsClass && + !EXCLUDE_TYPES.Contains(type.Name) && + !type.Name.Contains('<') && + !type.Name.Contains('>') && + !type.Name.EndsWith("_g") && + IsFluentComponent(type); + } + + /// + /// Checks if a type implements IFluentComponentBase. + /// + private bool IsFluentComponent(Type type) + { + if (_fluentComponentBaseInterface == null) + { + // Fallback: check if the type name starts with "Fluent" and has a base class containing "Component" + return type.Name.StartsWith("Fluent", StringComparison.Ordinal) && + (type.BaseType?.Name.Contains("Component") ?? false); + } + + return _fluentComponentBaseInterface.IsAssignableFrom(type); + } + + /// + /// Determines the category of a component based on its namespace or name. + /// + private static string DetermineCategory(Type type) + { + var ns = type.Namespace ?? string.Empty; + var name = type.Name; + + // Extract category from namespace + if (ns.Contains(".Components.", StringComparison.OrdinalIgnoreCase)) + { + var parts = ns.Split('.'); + var componentsIndex = Array.IndexOf(parts, "Components"); + if (componentsIndex >= 0 && componentsIndex < parts.Length - 1) + { + return parts[componentsIndex + 1]; + } + } + + // Categorize by component name patterns + return GetCategoryFromName(name); + } + + /// + /// Gets category from component name patterns. + /// + private static string GetCategoryFromName(string name) + { + if (name.Contains("Button", StringComparison.OrdinalIgnoreCase)) + { + return "Button"; + } + + if (name.Contains("Input", StringComparison.OrdinalIgnoreCase) || + name.Contains("TextField", StringComparison.OrdinalIgnoreCase) || + name.Contains("TextArea", StringComparison.OrdinalIgnoreCase)) + { + return "Input"; + } + + if (name.Contains("Dialog", StringComparison.OrdinalIgnoreCase) || + name.Contains("Modal", StringComparison.OrdinalIgnoreCase)) + { + return "Dialog"; + } + + if (name.Contains("Menu", StringComparison.OrdinalIgnoreCase)) + { + return "Menu"; + } + + if (name.Contains("Nav", StringComparison.OrdinalIgnoreCase)) + { + return "Navigation"; + } + + if (name.Contains("Grid", StringComparison.OrdinalIgnoreCase) || + name.Contains("Table", StringComparison.OrdinalIgnoreCase)) + { + return "DataGrid"; + } + + if (name.Contains("Card", StringComparison.OrdinalIgnoreCase)) + { + return "Card"; + } + + if (name.Contains("Icon", StringComparison.OrdinalIgnoreCase)) + { + return "Icon"; + } + + if (name.Contains("Layout", StringComparison.OrdinalIgnoreCase) || + name.Contains("Stack", StringComparison.OrdinalIgnoreCase) || + name.Contains("Splitter", StringComparison.OrdinalIgnoreCase)) + { + return "Layout"; + } + + return "Components"; + } + + /// + /// Checks if a type is an EventCallback. + /// + private bool IsEventCallback(Type type) + { + if (_eventCallbackType != null && type == _eventCallbackType) + { + return true; + } + + if (_eventCallbackGenericType != null && type.IsGenericType) + { + var genericDef = type.GetGenericTypeDefinition(); + return genericDef == _eventCallbackGenericType || + genericDef.FullName == "Microsoft.AspNetCore.Components.EventCallback`1"; + } + + // Fallback: check by type name + return type.FullName?.StartsWith("Microsoft.AspNetCore.Components.EventCallback") ?? false; + } + + /// + /// Gets the enum values for a type. + /// + private static string[] GetEnumValues(Type type) + { + var actualType = Nullable.GetUnderlyingType(type) ?? type; + if (actualType.IsEnum) + { + return Enum.GetNames(actualType); + } + + return []; + } + + /// + /// Gets the default value for a property. + /// + private static string? GetDefaultValue(Type componentType, PropertyInfo property) + { + try + { + // Only get default values for value types and strings + if (!property.PropertyType.IsValueType && property.PropertyType != typeof(string)) + { + return null; + } + + // Try to create an instance to get default values + object? instance; + if (componentType.IsGenericType) + { + var genericType = componentType.MakeGenericType(typeof(string)); + instance = Activator.CreateInstance(genericType); + } + else + { + instance = Activator.CreateInstance(componentType); + } + + if (instance == null) + { + return null; + } + + var value = property.GetValue(instance); + return value?.ToString(); + } + catch + { + return null; + } + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpComponentInfo.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpComponentInfo.cs new file mode 100644 index 0000000000..fef1731064 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpComponentInfo.cs @@ -0,0 +1,56 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Models.McpDocumentation; + +/// +/// Represents a Fluent UI component with its full documentation. +/// +public class McpComponentInfo +{ + /// + /// Gets or sets the name of the component. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the full type name of the component. + /// + public string FullName { get; set; } = string.Empty; + + /// + /// Gets or sets a brief description of the component. + /// + public string Summary { get; set; } = string.Empty; + + /// + /// Gets or sets the category of the component. + /// + public string Category { get; set; } = string.Empty; + + /// + /// Gets or sets whether the component is a generic type. + /// + public bool IsGeneric { get; set; } + + /// + /// Gets or sets the base class name. + /// + public string? BaseClass { get; set; } + + /// + /// Gets or sets the list of properties. + /// + public List Properties { get; set; } = []; + + /// + /// Gets or sets the list of events. + /// + public List Events { get; set; } = []; + + /// + /// Gets or sets the list of methods. + /// + public List Methods { get; set; } = []; +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpDocumentationMetadata.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpDocumentationMetadata.cs new file mode 100644 index 0000000000..c424ff42b7 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpDocumentationMetadata.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Models.McpDocumentation; + +/// +/// Metadata about the generated documentation. +/// +public class McpDocumentationMetadata +{ + /// + /// Gets or sets the assembly version. + /// + public string AssemblyVersion { get; set; } = string.Empty; + + /// + /// Gets or sets the generation date in UTC. + /// + public string GeneratedDateUtc { get; set; } = string.Empty; + + /// + /// Gets or sets the total component count. + /// + public int ComponentCount { get; set; } + + /// + /// Gets or sets the total enum count. + /// + public int EnumCount { get; set; } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpDocumentationRoot.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpDocumentationRoot.cs new file mode 100644 index 0000000000..e5f7698617 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpDocumentationRoot.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Models.McpDocumentation; + +/// +/// Root model for the MCP documentation JSON file. +/// Contains all components and enums documentation. +/// +public class McpDocumentationRoot +{ + /// + /// Gets or sets metadata about the generated documentation. + /// + public McpDocumentationMetadata Metadata { get; set; } = new(); + + /// + /// Gets or sets the list of all components. + /// + public List Components { get; set; } = []; + + /// + /// Gets or sets the list of all enums. + /// + public List Enums { get; set; } = []; +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpEnumInfo.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpEnumInfo.cs new file mode 100644 index 0000000000..f828a92f97 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpEnumInfo.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Models.McpDocumentation; + +/// +/// Represents an enum type. +/// +public class McpEnumInfo +{ + /// + /// Gets or sets the name of the enum. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the full type name of the enum. + /// + public string FullName { get; set; } = string.Empty; + + /// + /// Gets or sets the description of the enum. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets the enum values. + /// + public List Values { get; set; } = []; +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpEnumValueInfo.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpEnumValueInfo.cs new file mode 100644 index 0000000000..a45026dce2 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpEnumValueInfo.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Models.McpDocumentation; + +/// +/// Represents an enum value. +/// +public class McpEnumValueInfo +{ + /// + /// Gets or sets the name of the enum value. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the numeric value. + /// + public int Value { get; set; } + + /// + /// Gets or sets the description of the enum value. + /// + public string Description { get; set; } = string.Empty; +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpEventInfo.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpEventInfo.cs new file mode 100644 index 0000000000..6ea5bd6943 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpEventInfo.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Models.McpDocumentation; + +/// +/// Represents an event of a component. +/// +public class McpEventInfo +{ + /// + /// Gets or sets the name of the event. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the type of the event callback. + /// + public string Type { get; set; } = string.Empty; + + /// + /// Gets or sets the description of the event. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets whether this event is inherited. + /// + public bool IsInherited { get; set; } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpMethodInfo.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpMethodInfo.cs new file mode 100644 index 0000000000..92f686b49a --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpMethodInfo.cs @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Models.McpDocumentation; + +/// +/// Represents a method of a component. +/// +public class McpMethodInfo +{ + /// + /// Gets or sets the name of the method. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the return type of the method. + /// + public string ReturnType { get; set; } = string.Empty; + + /// + /// Gets or sets the description of the method. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets the parameters of the method. + /// + public string[] Parameters { get; set; } = []; + + /// + /// Gets or sets whether this method is inherited. + /// + public bool IsInherited { get; set; } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpPropertyInfo.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpPropertyInfo.cs new file mode 100644 index 0000000000..c8281c51c0 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/McpDocumentation/McpPropertyInfo.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen.Models.McpDocumentation; + +/// +/// Represents a property of a component. +/// +public class McpPropertyInfo +{ + /// + /// Gets or sets the name of the property. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the type of the property. + /// + public string Type { get; set; } = string.Empty; + + /// + /// Gets or sets the description of the property. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets whether this property is a [Parameter]. + /// + public bool IsParameter { get; set; } + + /// + /// Gets or sets whether this property is inherited. + /// + public bool IsInherited { get; set; } + + /// + /// Gets or sets the default value of the property. + /// + public string? DefaultValue { get; set; } + + /// + /// Gets or sets the enum values if this property is an enum type. + /// + public string[] EnumValues { get; set; } = []; +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs index 830aa9967b..a4767fa86b 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs @@ -36,31 +36,58 @@ public static void Main(string[] args) Console.WriteLine("Usage: DocApiGen --xml " + " --dll " + " --output " + - " --format "); + " --format "); + Console.WriteLine(); + Console.WriteLine("Formats:"); + Console.WriteLine(" csharp - Generate C# code with summary data dictionary"); + Console.WriteLine(" json - Generate JSON with summary data"); + Console.WriteLine(" mcp - Generate complete MCP documentation JSON for McpServer"); return; } // Assembly and documentation file var assembly = Assembly.LoadFrom(dllFile); var docXml = new FileInfo(xmlFile); - var apiGenerator = new ApiClassGenerator(assembly, docXml); Console.WriteLine("Generating documentation..."); - if (!string.IsNullOrEmpty(outputFile)) + + if (format.Equals("mcp", StringComparison.OrdinalIgnoreCase)) { - apiGenerator.SaveToFile(outputFile, format); - Console.WriteLine($"Documentation saved to {outputFile}"); + // Generate MCP-compatible JSON documentation + var mcpGenerator = new McpDocumentationGenerator(assembly, docXml); + + if (!string.IsNullOrEmpty(outputFile)) + { + mcpGenerator.SaveToFile(outputFile); + Console.WriteLine($"MCP documentation saved to {outputFile}"); + } + else + { + Console.WriteLine(); + Console.WriteLine(mcpGenerator.GenerateJson()); + } } else { - Console.WriteLine(); - if (format == "json") + // Generate traditional API documentation + var apiGenerator = new ApiClassGenerator(assembly, docXml); + + if (!string.IsNullOrEmpty(outputFile)) { - Console.WriteLine(apiGenerator.GenerateJson()); + apiGenerator.SaveToFile(outputFile, format); + Console.WriteLine($"Documentation saved to {outputFile}"); } else { - Console.WriteLine(apiGenerator.GenerateCSharp()); + Console.WriteLine(); + if (format == "json") + { + Console.WriteLine(apiGenerator.GenerateJson()); + } + else + { + Console.WriteLine(apiGenerator.GenerateCSharp()); + } } } diff --git a/nuget.config b/nuget.config index bff97919cb..eb16782249 100644 --- a/nuget.config +++ b/nuget.config @@ -1,7 +1,9 @@ - + + + diff --git a/src/Core.Scripts/package-lock.json b/src/Core.Scripts/package-lock.json index bbabd267da..19f8ffc60d 100644 --- a/src/Core.Scripts/package-lock.json +++ b/src/Core.Scripts/package-lock.json @@ -891,6 +891,7 @@ "integrity": "sha1-BDb74KcvhtM2bS0VfUgFJLCrPyY=", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", @@ -1096,6 +1097,7 @@ "integrity": "sha1-o2CJi8QV7arEbIJB9jg5dbkwuBY=", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1412,6 +1414,7 @@ "integrity": "sha1-nMXLv7nAEHBCXZv+2BtOeaHAkIg=", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2621,6 +2624,7 @@ "integrity": "sha1-2TRQzd7FFUotXKvjuBArgzFvsqY=", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/Tools/McpServer.Shared/McpCapabilitiesData.cs b/src/Tools/McpServer.Shared/McpCapabilitiesData.cs new file mode 100644 index 0000000000..bc022b97ee --- /dev/null +++ b/src/Tools/McpServer.Shared/McpCapabilitiesData.cs @@ -0,0 +1,131 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.Models; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared; + +/// +/// Provides access to MCP capabilities data discovered via reflection. +/// This class dynamically discovers tools, prompts, and resources from the MCP Server assembly. +/// +public static class McpCapabilitiesData +{ + private static readonly object _lock = new(); + private static McpSummary? _cachedSummary; + private static Assembly? _mcpServerAssembly; + private static Func? _summaryProvider; + + /// + /// Initializes the capabilities data with the MCP Server assembly. + /// Use this when running in a context where the MCP Server assembly is available. + /// + /// The assembly containing MCP tools, prompts, and resources. + public static void Initialize(Assembly mcpServerAssembly) + { + lock (_lock) + { + _mcpServerAssembly = mcpServerAssembly; + _summaryProvider = null; + _cachedSummary = null; + } + } + + /// + /// Initializes the capabilities data with a custom summary provider. + /// Use this for scenarios like WebAssembly where reflection on the server assembly isn't possible. + /// + /// A function that provides the MCP summary. + public static void Initialize(Func summaryProvider) + { + lock (_lock) + { + _summaryProvider = summaryProvider; + _mcpServerAssembly = null; + _cachedSummary = null; + } + } + + /// + /// Gets all MCP tools available in the server. + /// + public static IReadOnlyList Tools => GetSummary().Tools; + + /// + /// Gets all MCP prompts available in the server. + /// + public static IReadOnlyList Prompts => GetSummary().Prompts; + + /// + /// Gets all MCP resources available in the server. + /// + public static IReadOnlyList Resources => GetSummary().Resources; + + /// + /// Gets a summary of all MCP capabilities. + /// + public static McpSummary GetSummary() + { + lock (_lock) + { + if (_cachedSummary is not null) + { + return _cachedSummary; + } + + // Try custom provider first + if (_summaryProvider is not null) + { + _cachedSummary = _summaryProvider(); + return _cachedSummary; + } + + // Try to use provided assembly + if (_mcpServerAssembly is not null) + { + _cachedSummary = McpReflectionService.GetSummary(_mcpServerAssembly); + return _cachedSummary; + } + + // Try to find the MCP Server assembly by name + _mcpServerAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == "FluentUI.Mcp.Server"); + + if (_mcpServerAssembly is not null) + { + _cachedSummary = McpReflectionService.GetSummary(_mcpServerAssembly); + return _cachedSummary; + } + + // Return empty summary if nothing available + return new McpSummary([], [], []); + } + } + + /// + /// Clears the cached summary, forcing re-discovery on next access. + /// + public static void ClearCache() + { + lock (_lock) + { + _cachedSummary = null; + } + } + + /// + /// Gets whether the capabilities data has been initialized. + /// + public static bool IsInitialized + { + get + { + lock (_lock) + { + return _mcpServerAssembly is not null || _summaryProvider is not null; + } + } + } +} diff --git a/src/Tools/McpServer.Shared/McpReflectionService.cs b/src/Tools/McpServer.Shared/McpReflectionService.cs new file mode 100644 index 0000000000..7fd6f6e089 --- /dev/null +++ b/src/Tools/McpServer.Shared/McpReflectionService.cs @@ -0,0 +1,209 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Reflection; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.Models; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared; + +/// +/// Provides reflection-based discovery of MCP tools, prompts, and resources. +/// +public static class McpReflectionService +{ + /// + /// Gets all MCP tools defined in the assembly. + /// + public static IReadOnlyList GetTools(Assembly assembly) + { + var tools = new List(); + + // Find all types with [McpServerToolType] attribute + var toolTypes = assembly.GetTypes() + .Where(t => t.GetCustomAttributes() + .Any(a => a.GetType().Name == "McpServerToolTypeAttribute")); + + foreach (var type in toolTypes) + { + // Find all methods with [McpServerTool] attribute + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => m.GetCustomAttributes() + .Any(a => a.GetType().Name == "McpServerToolAttribute")); + + foreach (var method in methods) + { + var toolAttr = method.GetCustomAttributes() + .FirstOrDefault(a => a.GetType().Name == "McpServerToolAttribute"); + + var name = GetAttributeProperty(toolAttr, "Name") ?? method.Name; + var description = method.GetCustomAttribute()?.Description ?? ""; + + var parameters = method.GetParameters() + .Where(p => p.ParameterType != typeof(CancellationToken)) + .Select(p => new McpParameterInfo( + p.Name ?? "", + GetFriendlyTypeName(p.ParameterType), + p.GetCustomAttribute()?.Description ?? "", + !p.HasDefaultValue)) + .ToList(); + + tools.Add(new McpToolInfo(name, description, type.Name, parameters)); + } + } + + return tools.OrderBy(t => t.Name).ToList(); + } + + /// + /// Gets all MCP prompts defined in the assembly. + /// + public static IReadOnlyList GetPrompts(Assembly assembly) + { + var prompts = new List(); + + // Find all types with [McpServerPromptType] attribute + var promptTypes = assembly.GetTypes() + .Where(t => t.GetCustomAttributes() + .Any(a => a.GetType().Name == "McpServerPromptTypeAttribute")); + + foreach (var type in promptTypes) + { + // Find all methods with [McpServerPrompt] attribute + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => m.GetCustomAttributes() + .Any(a => a.GetType().Name == "McpServerPromptAttribute")); + + foreach (var method in methods) + { + var promptAttr = method.GetCustomAttributes() + .FirstOrDefault(a => a.GetType().Name == "McpServerPromptAttribute"); + + var name = GetAttributeProperty(promptAttr, "Name") ?? method.Name; + var description = method.GetCustomAttribute()?.Description ?? ""; + + var parameters = method.GetParameters() + .Select(p => new McpParameterInfo( + p.Name ?? "", + GetFriendlyTypeName(p.ParameterType), + p.GetCustomAttribute()?.Description ?? "", + !p.HasDefaultValue)) + .ToList(); + + prompts.Add(new McpPromptInfo(name, description, type.Name, parameters)); + } + } + + return prompts.OrderBy(p => p.Name).ToList(); + } + + /// + /// Gets all MCP resources defined in the assembly. + /// + public static IReadOnlyList GetResources(Assembly assembly) + { + var resources = new List(); + + // Find all types with [McpServerResourceType] attribute + var resourceTypes = assembly.GetTypes() + .Where(t => t.GetCustomAttributes() + .Any(a => a.GetType().Name == "McpServerResourceTypeAttribute")); + + foreach (var type in resourceTypes) + { + // Find all methods with [McpServerResource] attribute + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => m.GetCustomAttributes() + .Any(a => a.GetType().Name == "McpServerResourceAttribute")); + + foreach (var method in methods) + { + var resourceAttr = method.GetCustomAttributes() + .FirstOrDefault(a => a.GetType().Name == "McpServerResourceAttribute"); + + var uriTemplate = GetAttributeProperty(resourceAttr, "UriTemplate") ?? ""; + var name = GetAttributeProperty(resourceAttr, "Name") ?? method.Name; + var title = GetAttributeProperty(resourceAttr, "Title") ?? name; + var mimeType = GetAttributeProperty(resourceAttr, "MimeType") ?? "text/plain"; + var description = method.GetCustomAttribute()?.Description ?? ""; + + var isTemplate = uriTemplate.Contains('{') && uriTemplate.Contains('}'); + + resources.Add(new McpResourceInfo(uriTemplate, name, title, description, mimeType, isTemplate, type.Name)); + } + } + + return resources.OrderBy(r => r.Uri).ToList(); + } + + /// + /// Gets a summary of all MCP primitives in the assembly. + /// + public static McpSummary GetSummary(Assembly assembly) + { + return new McpSummary( + GetTools(assembly), + GetPrompts(assembly), + GetResources(assembly)); + } + + private static T? GetAttributeProperty(object? attribute, string propertyName) + { + if (attribute == null) + { + return default; + } + + var property = attribute.GetType().GetProperty(propertyName); + return property != null ? (T?)property.GetValue(attribute) : default; + } + + private static string GetFriendlyTypeName(Type type) + { + if (type == typeof(string)) + { + return "string"; + } + + if (type == typeof(int)) + { + return "int"; + } + + if (type == typeof(bool)) + { + return "bool"; + } + + if (type == typeof(double)) + { + return "double"; + } + + if (type == typeof(float)) + { + return "float"; + } + + if (type == typeof(long)) + { + return "long"; + } + + if (Nullable.GetUnderlyingType(type) is Type underlyingType) + { + return $"{GetFriendlyTypeName(underlyingType)}?"; + } + + if (type.IsGenericType) + { + var genericName = type.Name.Split('`')[0]; + var genericArgs = string.Join(", ", type.GetGenericArguments().Select(GetFriendlyTypeName)); + return $"{genericName}<{genericArgs}>"; + } + + return type.Name; + } +} + diff --git a/src/Tools/McpServer.Shared/Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.csproj b/src/Tools/McpServer.Shared/Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.csproj new file mode 100644 index 0000000000..e60f5e0df5 --- /dev/null +++ b/src/Tools/McpServer.Shared/Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.csproj @@ -0,0 +1,12 @@ + + + + net9.0 + enable + enable + latest + true + Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared + + + diff --git a/src/Tools/McpServer.Shared/Models/McpParameterInfo.cs b/src/Tools/McpServer.Shared/Models/McpParameterInfo.cs new file mode 100644 index 0000000000..ce2321b4f1 --- /dev/null +++ b/src/Tools/McpServer.Shared/Models/McpParameterInfo.cs @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.Models; + +/// +/// Information about an MCP parameter. +/// +/// The parameter name. +/// The parameter type. +/// The parameter description. +/// Whether the parameter is required. +public record McpParameterInfo( + string Name, + string Type, + string Description, + bool Required); diff --git a/src/Tools/McpServer.Shared/Models/McpPromptInfo.cs b/src/Tools/McpServer.Shared/Models/McpPromptInfo.cs new file mode 100644 index 0000000000..78c810226d --- /dev/null +++ b/src/Tools/McpServer.Shared/Models/McpPromptInfo.cs @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.Models; + +/// +/// Information about an MCP prompt. +/// +/// The prompt name. +/// The prompt description. +/// The class containing the prompt. +/// The prompt parameters. +public record McpPromptInfo( + string Name, + string Description, + string ClassName, + IReadOnlyList Parameters); diff --git a/src/Tools/McpServer.Shared/Models/McpResourceInfo.cs b/src/Tools/McpServer.Shared/Models/McpResourceInfo.cs new file mode 100644 index 0000000000..e13b63246f --- /dev/null +++ b/src/Tools/McpServer.Shared/Models/McpResourceInfo.cs @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.Models; + +/// +/// Information about an MCP resource. +/// +/// The resource URI or URI template. +/// The resource name. +/// The resource title. +/// The resource description. +/// The MIME type of the resource. +/// Whether this is a URI template. +/// The class containing the resource. +public record McpResourceInfo( + string Uri, + string Name, + string Title, + string Description, + string MimeType, + bool IsTemplate, + string ClassName); diff --git a/src/Tools/McpServer.Shared/Models/McpSummary.cs b/src/Tools/McpServer.Shared/Models/McpSummary.cs new file mode 100644 index 0000000000..802f94afb0 --- /dev/null +++ b/src/Tools/McpServer.Shared/Models/McpSummary.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.Models; + +/// +/// Summary of all MCP primitives. +/// +/// All tools. +/// All prompts. +/// All resources. +public record McpSummary( + IReadOnlyList Tools, + IReadOnlyList Prompts, + IReadOnlyList Resources); diff --git a/src/Tools/McpServer.Shared/Models/McpToolInfo.cs b/src/Tools/McpServer.Shared/Models/McpToolInfo.cs new file mode 100644 index 0000000000..fcdd6d703a --- /dev/null +++ b/src/Tools/McpServer.Shared/Models/McpToolInfo.cs @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.Models; + +/// +/// Information about an MCP tool. +/// +/// The tool name. +/// The tool description. +/// The class containing the tool. +/// The tool parameters. +public record McpToolInfo( + string Name, + string Description, + string ClassName, + IReadOnlyList Parameters); diff --git a/src/Tools/McpServer/.mcp/server.json b/src/Tools/McpServer/.mcp/server.json new file mode 100644 index 0000000000..02e9a3cb1f --- /dev/null +++ b/src/Tools/McpServer/.mcp/server.json @@ -0,0 +1,182 @@ +{ + "$schema": "https://raw.githubusercontent.com/modelcontextprotocol/registry/main/docs/server-json/schema.json", + "name": "io.github.microsoft/fluentui-blazor-mcp", + "description": "MCP server providing documentation for Fluent UI Blazor components. Access component details, parameters, events, methods, enums, guides, and usage examples through AI assistants.", + "packages": [ + { + "registry_name": "nuget", + "name": "Microsoft.FluentUI.AspNetCore.Components.McpServer", + "version": "5.0.0-preview.1", + "package_arguments": [], + "environment_variables": [] + } + ], + "repository": { + "url": "https://github.com/microsoft/fluentui-blazor", + "source": "github" + }, + "version_detail": { + "version": "5.0.0-preview.1" + }, + "tools": [ + { + "name": "ListComponents", + "description": "Lists all available Fluent UI Blazor components with their names and brief descriptions." + }, + { + "name": "GetComponentDetails", + "description": "Gets detailed documentation for a specific component including parameters, events, and methods." + }, + { + "name": "SearchComponents", + "description": "Searches for components by name or description." + }, + { + "name": "ListCategories", + "description": "Lists all available component categories." + }, + { + "name": "GetEnumValues", + "description": "Gets information about a specific enum type including all possible values. Supports optional filter parameter." + }, + { + "name": "GetComponentEnums", + "description": "Lists all enum types used by a specific component, showing which property/parameter uses each enum." + }, + { + "name": "ListEnums", + "description": "Lists all enum types used in the library. Supports optional filter parameter." + }, + { + "name": "GetComponentExample", + "description": "Gets usage examples for a specific component." + }, + { + "name": "GetGuide", + "description": "Gets a specific documentation guide (installation, defaultvalues, whatsnew, migration, localization, styles)." + }, + { + "name": "SearchGuides", + "description": "Searches documentation guides for specific content or topics." + }, + { + "name": "ListGuides", + "description": "Lists all available documentation guides with descriptions." + } + ], + "resources": [ + { + "uri": "fluentui://components", + "name": "All Components", + "description": "Complete list of all Fluent UI Blazor components organized by category." + }, + { + "uri": "fluentui://categories", + "name": "Component Categories", + "description": "List of all component categories with component counts." + }, + { + "uri": "fluentui://enums", + "name": "All Enum Types", + "description": "Complete list of all enum types used in the library." + }, + { + "uri": "fluentui://guides", + "name": "Documentation Guides", + "description": "List of all available documentation guides." + }, + { + "uri": "fluentui://guide/installation", + "name": "Installation Guide", + "description": "Complete installation and setup guide for Fluent UI Blazor." + }, + { + "uri": "fluentui://guide/defaultvalues", + "name": "Default Values Guide", + "description": "Guide for configuring default component values globally." + }, + { + "uri": "fluentui://guide/whatsnew", + "name": "What's New", + "description": "Latest release notes and changes." + }, + { + "uri": "fluentui://guide/migration", + "name": "Migration to v5", + "description": "Complete migration guide from v4 to v5 with all breaking changes." + }, + { + "uri": "fluentui://guide/localization", + "name": "Localization Guide", + "description": "Guide for translating and localizing component texts." + }, + { + "uri": "fluentui://guide/styles", + "name": "Styles Guide", + "description": "CSS styling, design tokens, and theming guide." + } + ], + "resource_templates": [ + { + "uri_template": "fluentui://component/{name}", + "name": "Component Documentation", + "description": "Detailed documentation for a specific component including parameters, events, and methods." + }, + { + "uri_template": "fluentui://category/{name}", + "name": "Components by Category", + "description": "List of all components in a specific category." + }, + { + "uri_template": "fluentui://enum/{name}", + "name": "Enum Details", + "description": "Detailed information about a specific enum type including all values." + } + ], + "prompts": [ + { + "name": "create_component", + "description": "Generate code for a Fluent UI Blazor component with specified configuration." + }, + { + "name": "explain_component", + "description": "Get a detailed explanation of a component and its usage." + }, + { + "name": "compare_components", + "description": "Compare two or more components to understand their differences." + }, + { + "name": "create_form", + "description": "Generate a complete Fluent UI Blazor form with validation." + }, + { + "name": "create_datagrid", + "description": "Generate a DataGrid with specified columns and features." + }, + { + "name": "create_dialog", + "description": "Generate a dialog/modal component." + }, + { + "name": "create_drawer", + "description": "Generate a drawer/panel component for side content." + }, + { + "name": "migrate_to_v5", + "description": "Get guidance for migrating code from v4 to v5." + }, + { + "name": "setup_project", + "description": "Get step-by-step guidance for setting up a new project." + }, + { + "name": "configure_theming", + "description": "Get guidance for configuring theming and styles." + }, + { + "name": "configure_localization", + "description": "Get guidance for implementing localization." + } + ] +} diff --git a/src/Tools/McpServer/Extensions/ServiceCollectionExtensions.cs b/src/Tools/McpServer/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..4101b7aab9 --- /dev/null +++ b/src/Tools/McpServer/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,89 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Resources; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Reflection; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Extensions; + +/// +/// Extension methods for configuring the MCP server services. +/// +internal static class ServiceCollectionExtensions +{ + /// + /// Configures logging for MCP stdio transport (logs to stderr). + /// + public static IHostApplicationBuilder ConfigureMcpLogging(this IHostApplicationBuilder builder) + { + builder.Logging.ClearProviders(); + builder.Logging.AddConsole(options => + { + options.LogToStandardErrorThreshold = LogLevel.Trace; + }); + + return builder; + } + + /// + /// Adds the Fluent UI documentation service. + /// Uses pre-generated JSON documentation for fast, dependency-free access. + /// + public static IServiceCollection AddFluentUIDocumentation(this IServiceCollection services) + { + // Try to find external JSON documentation file (for development) + var externalJsonPath = JsonDocumentationFinder.Find(); + + // Component documentation service using pre-generated JSON + // Falls back to embedded resource if no external file is found + services.AddSingleton(_ => new FluentUIDocumentationService(externalJsonPath)); + + // Documentation guides service (Installation, Migration, Styles, etc.) + services.AddSingleton(); + + return services; + } + + /// + /// Adds the MCP server with stdio transport, tools, resources, and prompts. + /// + /// + /// Tools (model-controlled): For dynamic queries like search, details by name. + /// Resources (user-controlled): For static context like component lists, categories. + /// Prompts (user-controlled): Pre-defined prompt templates for common tasks. + /// + public static IServiceCollection AddFluentUIMcpServer(this IServiceCollection services) + { + services + .AddMcpServer() + .WithStdioServerTransport() + .WithToolsFromAssembly(Assembly.GetExecutingAssembly()) + .WithResources() + .WithResources() + .WithResources() + // Component prompts + .WithPrompts() + .WithPrompts() + .WithPrompts() + // Form & data prompts + .WithPrompts() + .WithPrompts() + .WithPrompts() + .WithPrompts() + // Migration & setup prompts + .WithPrompts() + .WithPrompts() + .WithPrompts() + .WithPrompts() + // Version & compatibility prompts + .WithPrompts(); + + return services; + } +} diff --git a/src/Tools/McpServer/JsonDocumentationFinder.cs b/src/Tools/McpServer/JsonDocumentationFinder.cs new file mode 100644 index 0000000000..9bb8c27dcb --- /dev/null +++ b/src/Tools/McpServer/JsonDocumentationFinder.cs @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer; + +/// +/// Helper class for finding JSON documentation files. +/// +internal static class JsonDocumentationFinder +{ + private const string JsonFileName = "FluentUIComponentsDocumentation.json"; + + /// + /// Tries to find the JSON documentation file for the Fluent UI Components. + /// + /// The path to the JSON documentation file, or null if not found (will use embedded resource). + public static string? Find() + { + var possiblePaths = new[] + { + // Same directory as executable + Path.Combine(AppContext.BaseDirectory, JsonFileName), + // Development paths - relative to McpServer project + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", JsonFileName), + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "Tools", "McpServer", JsonFileName), + // Development paths - relative to solution + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "src", "Tools", "McpServer", JsonFileName), + }; + + foreach (var path in possiblePaths) + { + var fullPath = Path.GetFullPath(path); + if (File.Exists(fullPath)) + { + Console.Error.WriteLine($"[FluentUI.Mcp.Server] Found JSON documentation at: {fullPath}"); + return fullPath; + } + } + + // No external file found - will use embedded resource + Console.Error.WriteLine("[FluentUI.Mcp.Server] No external JSON documentation file found. Using embedded resource."); + return null; + } +} diff --git a/src/Tools/McpServer/Microsoft.FluentUI.AspNetCore.Components.McpServer.csproj b/src/Tools/McpServer/Microsoft.FluentUI.AspNetCore.Components.McpServer.csproj new file mode 100644 index 0000000000..b077cdf625 --- /dev/null +++ b/src/Tools/McpServer/Microsoft.FluentUI.AspNetCore.Components.McpServer.csproj @@ -0,0 +1,153 @@ + + + + Exe + net9.0 + enable + enable + latest + true + 612,618 + Microsoft.FluentUI.AspNetCore.Components.McpServer + + $(NoWarn);CA1305;IDE0005;IDE0060;CS1591 + + False + enable + enable + latest + + true + true + + true + embedded + + $(SolutionDir)artifacts + + + true + + + $(MSBuildThisFileDirectory)FluentUIComponentsDocumentation.json + + + + + + + + + + True + true + + + + + Microsoft.FluentUI.AspNetCore.Components.McpServer + 5.0.0-preview.1 + Microsoft + MCP (Model Context Protocol) server for Fluent UI Blazor component documentation. + Provides AI assistants with access to component details, parameters, events, methods, enums, + and usage examples. + mcp;model-context-protocol;fluent-ui;blazor;documentation;ai;copilot + https://github.com/microsoft/fluentui-blazor + https://github.com/microsoft/fluentui-blazor + git + MIT + README.md + + + + true + fluentui-mcp + + + McpServer + + + + + + + + + + + + Microsoft.FluentUI.AspNetCore.Components.McpServer.FluentUIComponentsDocumentation.json + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(MSBuildThisFileDirectory)..\..\..\examples\Tools\FluentUI.Demo.DocApiGen\FluentUI.Demo.DocApiGen.csproj + $(MSBuildThisFileDirectory)..\..\Core\bin\$(Configuration)\net9.0\Microsoft.FluentUI.AspNetCore.Components.dll + $(MSBuildThisFileDirectory)..\..\Core\bin\$(Configuration)\net9.0\Microsoft.FluentUI.AspNetCore.Components.xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Tools/McpServer/Models/ComponentDetails.cs b/src/Tools/McpServer/Models/ComponentDetails.cs new file mode 100644 index 0000000000..b5545e993c --- /dev/null +++ b/src/Tools/McpServer/Models/ComponentDetails.cs @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Models; + +/// +/// Represents detailed information about a component. +/// +public record ComponentDetails +{ + /// + /// Gets the basic component information. + /// + public required ComponentInfo Component { get; init; } + + /// + /// Gets the list of parameters (properties with [Parameter] attribute). + /// + public IReadOnlyList Parameters { get; init; } = []; + + /// + /// Gets the list of all properties. + /// + public IReadOnlyList Properties { get; init; } = []; + + /// + /// Gets the list of events. + /// + public IReadOnlyList Events { get; init; } = []; + + /// + /// Gets the list of public methods. + /// + public IReadOnlyList Methods { get; init; } = []; +} diff --git a/src/Tools/McpServer/Models/ComponentInfo.cs b/src/Tools/McpServer/Models/ComponentInfo.cs new file mode 100644 index 0000000000..72082f317c --- /dev/null +++ b/src/Tools/McpServer/Models/ComponentInfo.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Models; + +/// +/// Represents a Fluent UI component with its metadata. +/// +public record ComponentInfo +{ + /// + /// Gets the name of the component. + /// + public required string Name { get; init; } + + /// + /// Gets the full type name of the component. + /// + public required string FullName { get; init; } + + /// + /// Gets a brief description of the component. + /// + public string Summary { get; init; } = string.Empty; + + /// + /// Gets the category of the component (e.g., "Button", "Input", "Layout"). + /// + public string Category { get; init; } = string.Empty; + + /// + /// Gets whether the component is a generic type. + /// + public bool IsGeneric { get; init; } + + /// + /// Gets the base class name. + /// + public string? BaseClass { get; init; } +} diff --git a/src/Tools/McpServer/Models/EnumInfo.cs b/src/Tools/McpServer/Models/EnumInfo.cs new file mode 100644 index 0000000000..c7847099b6 --- /dev/null +++ b/src/Tools/McpServer/Models/EnumInfo.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Models; + +/// +/// Represents an enum type. +/// +public record EnumInfo +{ + /// + /// Gets the name of the enum. + /// + public required string Name { get; init; } + + /// + /// Gets the full type name of the enum. + /// + public required string FullName { get; init; } + + /// + /// Gets the description of the enum. + /// + public string Description { get; init; } = string.Empty; + + /// + /// Gets the enum values. + /// + public IReadOnlyList Values { get; init; } = []; +} diff --git a/src/Tools/McpServer/Models/EnumValueInfo.cs b/src/Tools/McpServer/Models/EnumValueInfo.cs new file mode 100644 index 0000000000..9e440a0e74 --- /dev/null +++ b/src/Tools/McpServer/Models/EnumValueInfo.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Models; + +/// +/// Represents an enum value. +/// +public record EnumValueInfo +{ + /// + /// Gets the name of the enum value. + /// + public required string Name { get; init; } + + /// + /// Gets the numeric value. + /// + public int Value { get; init; } + + /// + /// Gets the description of the enum value. + /// + public string Description { get; init; } = string.Empty; +} diff --git a/src/Tools/McpServer/Models/EventInfo.cs b/src/Tools/McpServer/Models/EventInfo.cs new file mode 100644 index 0000000000..6a9b70b653 --- /dev/null +++ b/src/Tools/McpServer/Models/EventInfo.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Models; + +/// +/// Represents an event of a component. +/// +public record EventInfo +{ + /// + /// Gets the name of the event. + /// + public required string Name { get; init; } + + /// + /// Gets the type of the event callback. + /// + public required string Type { get; init; } + + /// + /// Gets the description of the event. + /// + public string Description { get; init; } = string.Empty; + + /// + /// Gets whether this event is inherited. + /// + public bool IsInherited { get; init; } +} diff --git a/src/Tools/McpServer/Models/MethodInfo.cs b/src/Tools/McpServer/Models/MethodInfo.cs new file mode 100644 index 0000000000..5411184c0a --- /dev/null +++ b/src/Tools/McpServer/Models/MethodInfo.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Models; + +/// +/// Represents a method of a component. +/// +public record MethodInfo +{ + /// + /// Gets the name of the method. + /// + public required string Name { get; init; } + + /// + /// Gets the return type of the method. + /// + public required string ReturnType { get; init; } + + /// + /// Gets the description of the method. + /// + public string Description { get; init; } = string.Empty; + + /// + /// Gets the parameters of the method. + /// + public string[] Parameters { get; init; } = []; + + /// + /// Gets the method signature. + /// + public string Signature => $"{ReturnType} {Name}({string.Join(", ", Parameters)})"; + + /// + /// Gets whether this method is inherited. + /// + public bool IsInherited { get; init; } +} diff --git a/src/Tools/McpServer/Models/PropertyInfo.cs b/src/Tools/McpServer/Models/PropertyInfo.cs new file mode 100644 index 0000000000..82e6be1d6a --- /dev/null +++ b/src/Tools/McpServer/Models/PropertyInfo.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Models; + +/// +/// Represents a property of a component. +/// +public record PropertyInfo +{ + /// + /// Gets the name of the property. + /// + public required string Name { get; init; } + + /// + /// Gets the type of the property. + /// + public required string Type { get; init; } + + /// + /// Gets the description of the property. + /// + public string Description { get; init; } = string.Empty; + + /// + /// Gets whether this property is a [Parameter]. + /// + public bool IsParameter { get; init; } + + /// + /// Gets whether this property is inherited. + /// + public bool IsInherited { get; init; } + + /// + /// Gets the default value of the property. + /// + public string? DefaultValue { get; init; } + + /// + /// Gets the enum values if this property is an enum type. + /// + public string[] EnumValues { get; init; } = []; +} diff --git a/src/Tools/McpServer/Program.cs b/src/Tools/McpServer/Program.cs new file mode 100644 index 0000000000..2780788464 --- /dev/null +++ b/src/Tools/McpServer/Program.cs @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer; +using Microsoft.Extensions.Hosting; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Extensions; + +var builder = Host.CreateApplicationBuilder(args); + +builder + .ConfigureMcpLogging() + .Services + .AddFluentUIDocumentation() + .AddFluentUIMcpServer(); + +await builder.Build().RunAsync(); + diff --git a/src/Tools/McpServer/Prompts/CheckVersionCompatibilityPrompt.cs b/src/Tools/McpServer/Prompts/CheckVersionCompatibilityPrompt.cs new file mode 100644 index 0000000000..01d86b286e --- /dev/null +++ b/src/Tools/McpServer/Prompts/CheckVersionCompatibilityPrompt.cs @@ -0,0 +1,185 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; + +/// +/// MCP Prompt for checking version compatibility between NuGet package and MCP Server. +/// +[McpServerPromptType] +public class CheckVersionCompatibilityPrompt +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + public CheckVersionCompatibilityPrompt(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + [McpServerPrompt(Name = "check_version_compatibility")] + [Description("Check if the installed Fluent UI Blazor NuGet package version is compatible with this MCP Server.")] + public ChatMessage CheckVersionCompatibility( + [Description("The version of Microsoft.FluentUI.AspNetCore.Components currently installed in the project (e.g., '5.0.0', '4.10.3')")] string installedVersion) + { + var expectedVersion = _documentationService.ComponentsVersion; + var mcpServerVersion = FluentUIDocumentationService.McpServerVersion; + var generatedDate = _documentationService.DocumentationGeneratedDate; + + var sb = new StringBuilder(); + sb.AppendLine("# Fluent UI Blazor Version Compatibility Check"); + sb.AppendLine(); + + sb.AppendLine("## Version Information"); + sb.AppendLine(); + sb.AppendLine($"| Component | Version |"); + sb.AppendLine($"|-----------|---------|"); + sb.AppendLine($"| **MCP Server** | {mcpServerVersion} |"); + sb.AppendLine($"| **Expected Components Version** | {expectedVersion} |"); + sb.AppendLine($"| **Your Installed Version** | {installedVersion} |"); + sb.AppendLine($"| **Documentation Generated** | {generatedDate} |"); + sb.AppendLine(); + + // Parse versions for comparison + _ = CheckVersionCompatibilityInternal(installedVersion, expectedVersion, out var compatibilityLevel); + + switch (compatibilityLevel) + { + case VersionCompatibility.Exact: + sb.AppendLine("## ✅ Perfect Match"); + sb.AppendLine(); + sb.AppendLine("Your installed version matches exactly with this MCP Server's documentation."); + sb.AppendLine("All component information, parameters, and examples will be accurate."); + break; + + case VersionCompatibility.MinorDifference: + sb.AppendLine("## ⚠️ Minor Version Difference"); + sb.AppendLine(); + sb.AppendLine("Your installed version has a different minor or patch version."); + sb.AppendLine("Most documentation should be accurate, but some new features or changes may not be reflected."); + sb.AppendLine(); + sb.AppendLine("### Recommendations:"); + sb.AppendLine($"- Consider upgrading to version **{expectedVersion}** for full compatibility"); + sb.AppendLine("- Or update the MCP Server to match your installed version"); + break; + + case VersionCompatibility.MajorDifference: + sb.AppendLine("## ❌ Major Version Mismatch"); + sb.AppendLine(); + sb.AppendLine("**Warning**: Your installed version has a different major version."); + sb.AppendLine("There may be significant breaking changes, removed components, or new APIs not covered by this documentation."); + sb.AppendLine(); + sb.AppendLine("### Required Actions:"); + sb.AppendLine($"1. **Upgrade your NuGet package** to version **{expectedVersion}**:"); + sb.AppendLine(" ```shell"); + sb.AppendLine($" dotnet add package Microsoft.FluentUI.AspNetCore.Components --version {expectedVersion}"); + sb.AppendLine(" ```"); + sb.AppendLine(); + sb.AppendLine("2. **Or update the MCP Server** to match your installed version:"); + sb.AppendLine(" ```shell"); + sb.AppendLine($" dotnet tool update --global Microsoft.FluentUI.AspNetCore.Components.McpServer --version {installedVersion}"); + sb.AppendLine(" ```"); + break; + + default: + sb.AppendLine("## ⚠️ Unable to Determine Compatibility"); + sb.AppendLine(); + sb.AppendLine("Could not parse the version numbers for comparison."); + sb.AppendLine($"Please verify you're using version **{expectedVersion}** of Microsoft.FluentUI.AspNetCore.Components."); + break; + } + + sb.AppendLine(); + sb.AppendLine("## Task"); + sb.AppendLine(); + sb.AppendLine("Based on the version compatibility check above:"); + sb.AppendLine($"1. Explain any potential issues with using version {installedVersion} when the MCP documentation is for version {expectedVersion}"); + sb.AppendLine("2. If there are breaking changes between these versions, list them"); + sb.AppendLine("3. Provide migration guidance if an upgrade is recommended"); + sb.AppendLine("4. Suggest the best course of action for the user"); + + return new ChatMessage(ChatRole.User, sb.ToString()); + } + + /// + /// Checks version compatibility and returns the level of compatibility. + /// + private static bool CheckVersionCompatibilityInternal(string installedVersion, string expectedVersion, out VersionCompatibility level) + { + level = VersionCompatibility.Unknown; + + // Clean versions (remove any prerelease suffixes for comparison) + var installedClean = CleanVersion(installedVersion); + var expectedClean = CleanVersion(expectedVersion); + + if (!Version.TryParse(installedClean, out var installed) || + !Version.TryParse(expectedClean, out var expected)) + { + return false; + } + + // Exact match + if (installed.Major == expected.Major && + installed.Minor == expected.Minor && + installed.Build == expected.Build) + { + level = VersionCompatibility.Exact; + return true; + } + + // Same major version + if (installed.Major == expected.Major) + { + level = VersionCompatibility.MinorDifference; + return true; + } + + // Different major version + level = VersionCompatibility.MajorDifference; + return false; + } + + /// + /// Cleans a version string by removing prerelease suffixes. + /// + private static string CleanVersion(string version) + { + if (string.IsNullOrEmpty(version)) + { + return "0.0.0"; + } + + // Remove prerelease suffix (e.g., "-preview.1", "-rc.2", "-beta") + var dashIndex = version.IndexOf('-'); + if (dashIndex > 0) + { + version = version[..dashIndex]; + } + + // Ensure we have at least 3 parts + var parts = version.Split('.'); + if (parts.Length < 3) + { + version = string.Join(".", parts.Concat(Enumerable.Repeat("0", 3 - parts.Length))); + } + + return version; + } + + private enum VersionCompatibility + { + Unknown, + Exact, + MinorDifference, + MajorDifference + } +} diff --git a/src/Tools/McpServer/Prompts/CompareComponentsPrompt.cs b/src/Tools/McpServer/Prompts/CompareComponentsPrompt.cs new file mode 100644 index 0000000000..5b43d5481a --- /dev/null +++ b/src/Tools/McpServer/Prompts/CompareComponentsPrompt.cs @@ -0,0 +1,73 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; + +/// +/// MCP Prompt for comparing Fluent UI Blazor components. +/// +[McpServerPromptType] +public class CompareComponentsPrompt +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + public CompareComponentsPrompt(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + [McpServerPrompt(Name = "compare_components")] + [Description("Compare two or more Fluent UI Blazor components to understand their differences.")] + public ChatMessage CompareComponents( + [Description("Comma-separated list of component names to compare (e.g., 'FluentButton,FluentAnchor')")] string componentNames) + { + var names = componentNames.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + var sb = new StringBuilder(); + sb.AppendLine("# Component Comparison"); + sb.AppendLine(); + + foreach (var name in names) + { + var details = _documentationService.GetComponentDetails(name); + if (details != null) + { + sb.AppendLine($"## {details.Component.Name}"); + sb.AppendLine(); + sb.AppendLine($"- **Category**: {details.Component.Category}"); + sb.AppendLine($"- **Summary**: {details.Component.Summary}"); + sb.AppendLine($"- **Parameters**: {details.Parameters.Count}"); + sb.AppendLine($"- **Events**: {details.Events.Count}"); + sb.AppendLine(); + } + else + { + sb.AppendLine($"## {name}"); + sb.AppendLine(); + sb.AppendLine("Component not found."); + sb.AppendLine(); + } + } + + sb.AppendLine("## Task"); + sb.AppendLine(); + sb.AppendLine("Compare these Fluent UI Blazor components and explain:"); + sb.AppendLine("1. The key differences between them"); + sb.AppendLine("2. When to use each component"); + sb.AppendLine("3. Any shared functionality or parameters"); + sb.AppendLine("4. Performance or accessibility considerations"); + sb.AppendLine("5. Example scenarios where each would be the best choice"); + + return new ChatMessage(ChatRole.User, sb.ToString()); + } +} diff --git a/src/Tools/McpServer/Prompts/ConfigureLocalizationPrompt.cs b/src/Tools/McpServer/Prompts/ConfigureLocalizationPrompt.cs new file mode 100644 index 0000000000..60d7e74174 --- /dev/null +++ b/src/Tools/McpServer/Prompts/ConfigureLocalizationPrompt.cs @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; + +/// +/// MCP Prompt for configuring localization in Fluent UI Blazor. +/// +[McpServerPromptType] +public class ConfigureLocalizationPrompt +{ + private readonly DocumentationGuideService _guideService; + + /// + /// Initializes a new instance of the class. + /// + public ConfigureLocalizationPrompt(DocumentationGuideService guideService) + { + _guideService = guideService; + } + + [McpServerPrompt(Name = "configure_localization")] + [Description("Get guidance for implementing localization in Fluent UI Blazor.")] + public ChatMessage ConfigureLocalization( + [Description("Target languages (comma-separated, e.g., 'French,German,Spanish')")] string languages) + { + var localizationGuide = _guideService.GetGuideContent("localization"); + + var sb = new StringBuilder(); + sb.AppendLine("# Configure Localization in Fluent UI Blazor"); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(localizationGuide) && localizationGuide.Length > 100) + { + sb.AppendLine("## Localization Guide Reference"); + sb.AppendLine(); + sb.AppendLine(localizationGuide); + sb.AppendLine(); + } + + sb.AppendLine("## Localization Requirements"); + sb.AppendLine(); + sb.AppendLine($"- **Target Languages**: {languages}"); + sb.AppendLine(); + + sb.AppendLine("## Task"); + sb.AppendLine(); + sb.AppendLine("Provide complete guidance for implementing localization:"); + sb.AppendLine("1. IFluentLocalizer implementation"); + sb.AppendLine("2. Resource file setup for target languages"); + sb.AppendLine("3. Service registration"); + sb.AppendLine("4. Culture switching implementation"); + sb.AppendLine("5. Component text localization"); + sb.AppendLine(); + sb.AppendLine($"Include examples for these languages: {languages}"); + + return new ChatMessage(ChatRole.User, sb.ToString()); + } +} diff --git a/src/Tools/McpServer/Prompts/ConfigureThemingPrompt.cs b/src/Tools/McpServer/Prompts/ConfigureThemingPrompt.cs new file mode 100644 index 0000000000..ee71dc5e19 --- /dev/null +++ b/src/Tools/McpServer/Prompts/ConfigureThemingPrompt.cs @@ -0,0 +1,88 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; + +/// +/// MCP Prompt for configuring theming in Fluent UI Blazor. +/// +[McpServerPromptType] +public class ConfigureThemingPrompt +{ + private readonly DocumentationGuideService _guideService; + + /// + /// Initializes a new instance of the class. + /// + public ConfigureThemingPrompt(DocumentationGuideService guideService) + { + _guideService = guideService; + } + + [McpServerPrompt(Name = "configure_theming")] + [Description("Get guidance for configuring theming and styles in Fluent UI Blazor.")] + public ChatMessage ConfigureTheming( + [Description("Theme requirements: 'dark', 'light', 'custom', or 'dynamic'")] string themeType, + [Description("Optional: specific colors or design tokens to customize")] string? customizations = null) + { + var stylesGuide = _guideService.GetGuideContent("styles"); + + var sb = new StringBuilder(); + sb.AppendLine($"# Configure {themeType.ToUpperInvariant()} Theme in Fluent UI Blazor"); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(stylesGuide) && stylesGuide.Length > 100) + { + sb.AppendLine("## Styles Guide Reference"); + sb.AppendLine(); + + if (stylesGuide.Length > 2500) + { + sb.AppendLine(stylesGuide[..2500]); + sb.AppendLine("...(truncated)"); + } + else + { + sb.AppendLine(stylesGuide); + } + + sb.AppendLine(); + } + + sb.AppendLine("## Theme Requirements"); + sb.AppendLine(); + sb.AppendLine($"- **Theme Type**: {themeType}"); + + if (!string.IsNullOrEmpty(customizations)) + { + sb.AppendLine($"- **Customizations**: {customizations}"); + } + + sb.AppendLine(); + sb.AppendLine("## Task"); + sb.AppendLine(); + sb.AppendLine($"Provide complete guidance for implementing {themeType} theming in Fluent UI Blazor:"); + sb.AppendLine("1. Theme provider setup"); + sb.AppendLine("2. Design token configuration"); + sb.AppendLine("3. CSS variable customization"); + sb.AppendLine("4. Dark/light mode switching (if applicable)"); + sb.AppendLine("5. Component-specific styling"); + + if (!string.IsNullOrEmpty(customizations)) + { + sb.AppendLine($"6. Custom requirements: {customizations}"); + } + + sb.AppendLine(); + sb.AppendLine("Include working code examples for the theme configuration."); + + return new ChatMessage(ChatRole.User, sb.ToString()); + } +} diff --git a/src/Tools/McpServer/Prompts/CreateComponentPrompt.cs b/src/Tools/McpServer/Prompts/CreateComponentPrompt.cs new file mode 100644 index 0000000000..2fc7e7aa5e --- /dev/null +++ b/src/Tools/McpServer/Prompts/CreateComponentPrompt.cs @@ -0,0 +1,110 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; + +/// +/// MCP Prompt for generating Fluent UI Blazor component code. +/// +[McpServerPromptType] +public class CreateComponentPrompt +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + public CreateComponentPrompt(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + [McpServerPrompt(Name = "create_component")] + [Description("Generate code for a Fluent UI Blazor component with specified configuration.")] + public ChatMessage CreateComponent( + [Description("The component name (e.g., 'FluentButton', 'FluentDataGrid', 'FluentTextField')")] string componentName, + [Description("Optional: specific requirements or configuration for the component")] string? requirements = null) + { + var details = _documentationService.GetComponentDetails(componentName); + + var sb = new StringBuilder(); + sb.AppendLine($"# Create a {componentName} Component"); + sb.AppendLine(); + + if (details != null) + { + sb.AppendLine("## Component Information"); + sb.AppendLine(); + sb.AppendLine($"**{details.Component.Name}**: {details.Component.Summary}"); + sb.AppendLine(); + + if (details.Parameters.Count > 0) + { + sb.AppendLine("## Available Parameters"); + sb.AppendLine(); + + foreach (var param in details.Parameters.Take(15)) + { + var defaultInfo = !string.IsNullOrEmpty(param.DefaultValue) ? $" (default: {param.DefaultValue})" : ""; + sb.AppendLine($"- `{param.Name}` ({param.Type}){defaultInfo}: {param.Description}"); + } + + if (details.Parameters.Count > 15) + { + sb.AppendLine($"- ... and {details.Parameters.Count - 15} more parameters"); + } + + sb.AppendLine(); + } + + if (details.Events.Count > 0) + { + sb.AppendLine("## Available Events"); + sb.AppendLine(); + + foreach (var evt in details.Events.Take(5)) + { + sb.AppendLine($"- `{evt.Name}` ({evt.Type}): {evt.Description}"); + } + + sb.AppendLine(); + } + } + else + { + sb.AppendLine($"Component '{componentName}' not found in documentation."); + sb.AppendLine("Please check the component name and try again."); + sb.AppendLine(); + } + + if (!string.IsNullOrEmpty(requirements)) + { + sb.AppendLine("## Requirements"); + sb.AppendLine(); + sb.AppendLine(requirements); + sb.AppendLine(); + } + + sb.AppendLine("## Task"); + sb.AppendLine(); + sb.AppendLine($"Generate a complete, working Blazor code example for the {componentName} component that:"); + sb.AppendLine("1. Uses proper Fluent UI Blazor syntax"); + sb.AppendLine("2. Includes necessary @using statements"); + sb.AppendLine("3. Shows best practices for the component"); + sb.AppendLine("4. Includes event handlers if applicable"); + + if (!string.IsNullOrEmpty(requirements)) + { + sb.AppendLine($"5. Meets these specific requirements: {requirements}"); + } + + return new ChatMessage(ChatRole.User, sb.ToString()); + } +} diff --git a/src/Tools/McpServer/Prompts/CreateDataGridPrompt.cs b/src/Tools/McpServer/Prompts/CreateDataGridPrompt.cs new file mode 100644 index 0000000000..3ef2addc3a --- /dev/null +++ b/src/Tools/McpServer/Prompts/CreateDataGridPrompt.cs @@ -0,0 +1,112 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; + +/// +/// MCP Prompt for generating Fluent UI Blazor DataGrid components. +/// +[McpServerPromptType] +public class CreateDataGridPrompt +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + public CreateDataGridPrompt(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + [McpServerPrompt(Name = "create_datagrid")] + [Description("Generate a Fluent UI Blazor DataGrid with specified columns and features.")] + public ChatMessage CreateDataGrid( + [Description("Describe the data and columns (e.g., 'products with name, price, category, stock count')")] string dataDescription, + [Description("Optional: features to include (e.g., 'sorting, filtering, pagination, row selection')")] string? features = null, + [Description("Optional: the item type name (e.g., 'Product')")] string? itemType = null) + { + var gridDetails = _documentationService.GetComponentDetails("FluentDataGrid"); + + var sb = new StringBuilder(); + sb.AppendLine("# Create a Fluent UI Blazor DataGrid"); + sb.AppendLine(); + + if (gridDetails != null) + { + sb.AppendLine("## FluentDataGrid Component"); + sb.AppendLine(); + sb.AppendLine($"{gridDetails.Component.Summary}"); + sb.AppendLine(); + + sb.AppendLine("### Key Parameters"); + sb.AppendLine(); + + var keyParams = new[] { "Items", "ItemsProvider", "Pagination", "RowsDataSize", "SelectionMode", "ShowHover" }; + foreach (var param in gridDetails.Parameters.Where(p => keyParams.Contains(p.Name))) + { + sb.AppendLine($"- `{param.Name}` ({param.Type}): {param.Description}"); + } + + sb.AppendLine(); + } + + sb.AppendLine("## DataGrid Requirements"); + sb.AppendLine(); + sb.AppendLine($"**Data**: {dataDescription}"); + + if (!string.IsNullOrEmpty(features)) + { + sb.AppendLine($"**Features**: {features}"); + } + + if (!string.IsNullOrEmpty(itemType)) + { + sb.AppendLine($"**Item Type**: {itemType}"); + } + + sb.AppendLine(); + sb.AppendLine("## Task"); + sb.AppendLine(); + sb.AppendLine("Generate a complete FluentDataGrid implementation that includes:"); + sb.AppendLine("1. A model class for the data items"); + sb.AppendLine("2. Sample data generation"); + sb.AppendLine("3. PropertyColumn definitions for each field"); + sb.AppendLine("4. Appropriate column formatting (dates, currency, etc.)"); + + if (!string.IsNullOrEmpty(features)) + { + if (features.Contains("sort", StringComparison.OrdinalIgnoreCase)) + { + sb.AppendLine("5. Sortable columns configuration"); + } + + if (features.Contains("filter", StringComparison.OrdinalIgnoreCase)) + { + sb.AppendLine("6. Column filtering"); + } + + if (features.Contains("pagination", StringComparison.OrdinalIgnoreCase)) + { + sb.AppendLine("7. Pagination with FluentPaginator"); + } + + if (features.Contains("selection", StringComparison.OrdinalIgnoreCase)) + { + sb.AppendLine("8. Row selection handling"); + } + } + + sb.AppendLine(); + sb.AppendLine("Include both the Razor markup and the code-behind."); + + return new ChatMessage(ChatRole.User, sb.ToString()); + } +} diff --git a/src/Tools/McpServer/Prompts/CreateDialogPrompt.cs b/src/Tools/McpServer/Prompts/CreateDialogPrompt.cs new file mode 100644 index 0000000000..bb3bfcdcec --- /dev/null +++ b/src/Tools/McpServer/Prompts/CreateDialogPrompt.cs @@ -0,0 +1,73 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; + +/// +/// MCP Prompt for generating Fluent UI Blazor dialog components. +/// +[McpServerPromptType] +public class CreateDialogPrompt +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + public CreateDialogPrompt(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + [McpServerPrompt(Name = "create_dialog")] + [Description("Generate a Fluent UI Blazor dialog/modal component.")] + public ChatMessage CreateDialog( + [Description("The purpose of the dialog (e.g., 'confirm delete', 'edit user', 'display details')")] string purpose, + [Description("Optional: content requirements (e.g., 'form with name and email', 'warning message')")] string? content = null) + { + var dialogDetails = _documentationService.GetComponentDetails("FluentDialog"); + + var sb = new StringBuilder(); + sb.AppendLine("# Create a Fluent UI Blazor Dialog"); + sb.AppendLine(); + + if (dialogDetails != null) + { + sb.AppendLine("## FluentDialog Component"); + sb.AppendLine(); + sb.AppendLine($"{dialogDetails.Component.Summary}"); + sb.AppendLine(); + } + + sb.AppendLine("## Dialog Requirements"); + sb.AppendLine(); + sb.AppendLine($"**Purpose**: {purpose}"); + + if (!string.IsNullOrEmpty(content)) + { + sb.AppendLine($"**Content**: {content}"); + } + + sb.AppendLine(); + sb.AppendLine("## Task"); + sb.AppendLine(); + sb.AppendLine("Generate a complete FluentDialog implementation that includes:"); + sb.AppendLine("1. Dialog component with appropriate title"); + sb.AppendLine("2. Dialog content/body"); + sb.AppendLine("3. Action buttons (confirm, cancel, etc.)"); + sb.AppendLine("4. Service-based dialog opening/closing"); + sb.AppendLine("5. Result handling"); + sb.AppendLine("6. Proper async/await patterns"); + sb.AppendLine(); + sb.AppendLine("Show how to open the dialog from a parent component."); + + return new ChatMessage(ChatRole.User, sb.ToString()); + } +} diff --git a/src/Tools/McpServer/Prompts/CreateDrawerPrompt.cs b/src/Tools/McpServer/Prompts/CreateDrawerPrompt.cs new file mode 100644 index 0000000000..3fd38ffd08 --- /dev/null +++ b/src/Tools/McpServer/Prompts/CreateDrawerPrompt.cs @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; + +/// +/// MCP Prompt for generating Fluent UI Blazor drawer components. +/// +[McpServerPromptType] +public class CreateDrawerPrompt +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + public CreateDrawerPrompt(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + [McpServerPrompt(Name = "create_drawer")] + [Description("Generate a Fluent UI Blazor drawer/panel component.")] + public ChatMessage CreateDrawer( + [Description("The purpose of the drawer (e.g., 'navigation menu', 'settings panel', 'details view', 'filters')")] string purpose, + [Description("Optional: position of the drawer ('start', 'end', 'top', 'bottom')")] string? position = null, + [Description("Optional: content requirements (e.g., 'navigation links', 'form fields', 'list of items')")] string? content = null) + { + var drawerDetails = _documentationService.GetComponentDetails("FluentDrawer"); + + var sb = new StringBuilder(); + sb.AppendLine("# Create a Fluent UI Blazor Drawer"); + sb.AppendLine(); + + if (drawerDetails != null) + { + sb.AppendLine("## FluentDrawer Component"); + sb.AppendLine(); + sb.AppendLine($"{drawerDetails.Component.Summary}"); + sb.AppendLine(); + + sb.AppendLine("### Key Parameters"); + sb.AppendLine(); + + var keyParams = new[] { "Position", "Title", "Width", "Dismissible", "Modal", "Open" }; + foreach (var param in drawerDetails.Parameters.Where(p => keyParams.Contains(p.Name))) + { + sb.AppendLine($"- `{param.Name}` ({param.Type}): {param.Description}"); + } + + sb.AppendLine(); + } + + sb.AppendLine("## Drawer Requirements"); + sb.AppendLine(); + sb.AppendLine($"**Purpose**: {purpose}"); + + if (!string.IsNullOrEmpty(position)) + { + sb.AppendLine($"**Position**: {position}"); + } + + if (!string.IsNullOrEmpty(content)) + { + sb.AppendLine($"**Content**: {content}"); + } + + sb.AppendLine(); + sb.AppendLine("## Task"); + sb.AppendLine(); + sb.AppendLine("Generate a complete FluentDrawer implementation that includes:"); + sb.AppendLine("1. Drawer component with appropriate configuration"); + sb.AppendLine("2. Header with title and close button"); + sb.AppendLine("3. Body content"); + sb.AppendLine("4. Open/close state management"); + sb.AppendLine("5. Trigger button or mechanism to open the drawer"); + + if (!string.IsNullOrEmpty(position)) + { + sb.AppendLine($"6. Position set to: {position}"); + } + + sb.AppendLine(); + sb.AppendLine("Include both the Razor markup and the code-behind with proper state handling."); + + return new ChatMessage(ChatRole.User, sb.ToString()); + } +} diff --git a/src/Tools/McpServer/Prompts/CreateFormPrompt.cs b/src/Tools/McpServer/Prompts/CreateFormPrompt.cs new file mode 100644 index 0000000000..4405b42eac --- /dev/null +++ b/src/Tools/McpServer/Prompts/CreateFormPrompt.cs @@ -0,0 +1,94 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; + +/// +/// MCP Prompt for generating Fluent UI Blazor forms. +/// +[McpServerPromptType] +public class CreateFormPrompt +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + public CreateFormPrompt(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + [McpServerPrompt(Name = "create_form")] + [Description("Generate a complete Fluent UI Blazor form with validation.")] + public ChatMessage CreateForm( + [Description("Describe the form fields needed (e.g., 'name, email, phone, address with country dropdown')")] string formFields, + [Description("Optional: the model class name for the form data")] string? modelName = null, + [Description("Optional: validation requirements (e.g., 'email required and valid, phone optional')")] string? validation = null) + { + var sb = new StringBuilder(); + sb.AppendLine("# Create a Fluent UI Blazor Form"); + sb.AppendLine(); + + sb.AppendLine("## Available Form Components"); + sb.AppendLine(); + + // List relevant form components + var formComponents = new[] { "FluentTextField", "FluentTextArea", "FluentSelect", "FluentCheckbox", "FluentRadio", "FluentSwitch", "FluentDatePicker", "FluentTimePicker", "FluentNumberField" }; + + foreach (var compName in formComponents) + { + var comp = _documentationService.GetAllComponents().FirstOrDefault(c => c.Name == compName); + if (comp != null) + { + sb.AppendLine($"- **{comp.Name}**: {comp.Summary}"); + } + } + + // Ensure at least one text input component name appears in the prompt (handle v5 name change) + var current = sb.ToString(); + if (!current.Contains("FluentTextField", StringComparison.OrdinalIgnoreCase) && + !current.Contains("FluentTextInput", StringComparison.OrdinalIgnoreCase)) + { + // Add a fallback entry so tests and consumers see a text input component name + sb.AppendLine("- **FluentTextField**: A text input component (fallback)"); + } + + sb.AppendLine(); + sb.AppendLine("## Form Requirements"); + sb.AppendLine(); + sb.AppendLine($"**Fields**: {formFields}"); + + if (!string.IsNullOrEmpty(modelName)) + { + sb.AppendLine($"**Model Class**: {modelName}"); + } + + if (!string.IsNullOrEmpty(validation)) + { + sb.AppendLine($"**Validation**: {validation}"); + } + + sb.AppendLine(); + sb.AppendLine("## Task"); + sb.AppendLine(); + sb.AppendLine("Generate a complete Fluent UI Blazor form that includes:"); + sb.AppendLine("1. A model class with appropriate data annotations"); + sb.AppendLine("2. An EditForm with proper data binding"); + sb.AppendLine("3. FluentValidationMessage components for each field"); + sb.AppendLine("4. Appropriate Fluent UI input components for each field type"); + sb.AppendLine("5. Submit and cancel buttons"); + sb.AppendLine("6. Form submission handling"); + sb.AppendLine(); + sb.AppendLine("Use best practices for Blazor forms and validation."); + + return new ChatMessage(ChatRole.User, sb.ToString()); + } +} diff --git a/src/Tools/McpServer/Prompts/ExplainComponentPrompt.cs b/src/Tools/McpServer/Prompts/ExplainComponentPrompt.cs new file mode 100644 index 0000000000..d507decb73 --- /dev/null +++ b/src/Tools/McpServer/Prompts/ExplainComponentPrompt.cs @@ -0,0 +1,72 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; + +/// +/// MCP Prompt for explaining Fluent UI Blazor components. +/// +[McpServerPromptType] +public class ExplainComponentPrompt +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + public ExplainComponentPrompt(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + [McpServerPrompt(Name = "explain_component")] + [Description("Get a detailed explanation of a Fluent UI Blazor component and its usage.")] + public ChatMessage ExplainComponent( + [Description("The component name to explain")] string componentName) + { + var details = _documentationService.GetComponentDetails(componentName); + + var sb = new StringBuilder(); + sb.AppendLine($"# Explain {componentName}"); + sb.AppendLine(); + + if (details != null) + { + sb.AppendLine("## Component Details"); + sb.AppendLine(); + sb.AppendLine($"- **Name**: {details.Component.Name}"); + sb.AppendLine($"- **Category**: {details.Component.Category}"); + sb.AppendLine($"- **Summary**: {details.Component.Summary}"); + + if (details.Component.IsGeneric) + { + sb.AppendLine("- **Generic**: Yes (requires type parameter)"); + } + + sb.AppendLine(); + sb.AppendLine($"**Parameters**: {details.Parameters.Count}"); + sb.AppendLine($"**Events**: {details.Events.Count}"); + sb.AppendLine($"**Methods**: {details.Methods.Count}"); + sb.AppendLine(); + } + + sb.AppendLine("## Task"); + sb.AppendLine(); + sb.AppendLine($"Provide a comprehensive explanation of the {componentName} component that includes:"); + sb.AppendLine("1. What the component is used for"); + sb.AppendLine("2. Common use cases and scenarios"); + sb.AppendLine("3. Key parameters and their purposes"); + sb.AppendLine("4. Best practices for using the component"); + sb.AppendLine("5. Common pitfalls to avoid"); + sb.AppendLine("6. A simple example demonstrating basic usage"); + + return new ChatMessage(ChatRole.User, sb.ToString()); + } +} diff --git a/src/Tools/McpServer/Prompts/MigrateToV5Prompt.cs b/src/Tools/McpServer/Prompts/MigrateToV5Prompt.cs new file mode 100644 index 0000000000..9ec0c5a297 --- /dev/null +++ b/src/Tools/McpServer/Prompts/MigrateToV5Prompt.cs @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; + +/// +/// MCP Prompt for migrating Fluent UI Blazor code from v4 to v5. +/// +[McpServerPromptType] +public class MigrateToV5Prompt +{ + private readonly DocumentationGuideService _guideService; + + /// + /// Initializes a new instance of the class. + /// + public MigrateToV5Prompt(DocumentationGuideService guideService) + { + _guideService = guideService; + } + + [McpServerPrompt(Name = "migrate_to_v5")] + [Description("Get guidance for migrating Fluent UI Blazor code from v4 to v5.")] + public ChatMessage MigrateToV5( + [Description("The code or component to migrate (paste your existing v4 code here)")] string existingCode, + [Description("Optional: specific components or features you're migrating")] string? focus = null) + { + var migrationGuide = _guideService.GetFullMigrationGuide(); + + var sb = new StringBuilder(); + sb.AppendLine("# Migrate Fluent UI Blazor Code from v4 to v5"); + sb.AppendLine(); + + sb.AppendLine("## Migration Guide Reference"); + sb.AppendLine(); + + // Include relevant parts of migration guide + if (!string.IsNullOrEmpty(migrationGuide) && migrationGuide.Length > 100) + { + // Truncate if too long, but keep key sections + if (migrationGuide.Length > 3000) + { + sb.AppendLine(migrationGuide[..3000]); + sb.AppendLine("...(truncated, use GetGuide('migration') for full guide)"); + } + else + { + sb.AppendLine(migrationGuide); + } + } + else + { + sb.AppendLine("Migration guide not available. Key changes in v5 include:"); + sb.AppendLine("- Color enum changes"); + sb.AppendLine("- Component API updates"); + sb.AppendLine("- New default values system"); + } + + sb.AppendLine(); + sb.AppendLine("## Code to Migrate"); + sb.AppendLine(); + sb.AppendLine("```razor"); + sb.AppendLine(existingCode); + sb.AppendLine("```"); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(focus)) + { + sb.AppendLine("## Focus Areas"); + sb.AppendLine(); + sb.AppendLine(focus); + sb.AppendLine(); + } + + sb.AppendLine("## Task"); + sb.AppendLine(); + sb.AppendLine("Migrate this Fluent UI Blazor code from v4 to v5:"); + sb.AppendLine("1. Identify any deprecated or changed APIs"); + sb.AppendLine("2. Update component names and parameters as needed"); + sb.AppendLine("3. Apply new v5 patterns and best practices"); + sb.AppendLine("4. Explain each change you make"); + sb.AppendLine("5. Highlight any breaking changes that require attention"); + + return new ChatMessage(ChatRole.User, sb.ToString()); + } +} diff --git a/src/Tools/McpServer/Prompts/SetupProjectPrompt.cs b/src/Tools/McpServer/Prompts/SetupProjectPrompt.cs new file mode 100644 index 0000000000..9d49377677 --- /dev/null +++ b/src/Tools/McpServer/Prompts/SetupProjectPrompt.cs @@ -0,0 +1,123 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; + +/// +/// MCP Prompt for setting up a new Fluent UI Blazor project. +/// +[McpServerPromptType] +public class SetupProjectPrompt +{ + private readonly DocumentationGuideService _guideService; + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + public SetupProjectPrompt(DocumentationGuideService guideService, FluentUIDocumentationService documentationService) + { + _guideService = guideService; + _documentationService = documentationService; + } + + [McpServerPrompt(Name = "setup_project")] + [Description("Get step-by-step guidance for setting up a new Fluent UI Blazor project with the correct package version.")] + public ChatMessage SetupProject( + [Description("The type of Blazor project: 'server', 'wasm', 'hybrid', or 'maui'")] string projectType, + [Description("Optional: specific features to include (e.g., 'icons', 'datagrid', 'charts')")] string? features = null) + { + var installGuide = _guideService.GetGuideContent("installation"); + var componentsVersion = _documentationService.ComponentsVersion; + var mcpServerVersion = FluentUIDocumentationService.McpServerVersion; + + var sb = new StringBuilder(); + sb.AppendLine($"# Set Up a Fluent UI Blazor {projectType.ToUpperInvariant()} Project"); + sb.AppendLine(); + + // Version compatibility information + sb.AppendLine("## Version Information"); + sb.AppendLine(); + sb.AppendLine($"- **MCP Server Version**: {mcpServerVersion}"); + sb.AppendLine($"- **Compatible Components Version**: {componentsVersion}"); + sb.AppendLine(); + sb.AppendLine("> ⚠️ **Important**: For optimal compatibility with this MCP Server, install the matching version of the NuGet package."); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(installGuide) && installGuide.Length > 100) + { + sb.AppendLine("## Installation Guide Reference"); + sb.AppendLine(); + + if (installGuide.Length > 2000) + { + sb.AppendLine(installGuide[..2000]); + sb.AppendLine("...(truncated)"); + } + else + { + sb.AppendLine(installGuide); + } + + sb.AppendLine(); + } + + sb.AppendLine("## Project Configuration"); + sb.AppendLine(); + sb.AppendLine($"- **Project Type**: Blazor {projectType}"); + + if (!string.IsNullOrEmpty(features)) + { + sb.AppendLine($"- **Features**: {features}"); + } + + sb.AppendLine(); + sb.AppendLine("## Required NuGet Packages"); + sb.AppendLine(); + sb.AppendLine("```shell"); + sb.AppendLine($"dotnet add package Microsoft.FluentUI.AspNetCore.Components --version {componentsVersion}"); + + if (!string.IsNullOrEmpty(features)) + { + if (features.Contains("icon", StringComparison.OrdinalIgnoreCase)) + { + sb.AppendLine($"dotnet add package Microsoft.FluentUI.AspNetCore.Components.Icons --version {componentsVersion}"); + } + + if (features.Contains("emoji", StringComparison.OrdinalIgnoreCase)) + { + sb.AppendLine($"dotnet add package Microsoft.FluentUI.AspNetCore.Components.Emoji --version {componentsVersion}"); + } + } + + sb.AppendLine("```"); + sb.AppendLine(); + + sb.AppendLine("## Task"); + sb.AppendLine(); + sb.AppendLine($"Provide complete step-by-step instructions for setting up a Blazor {projectType} project with Fluent UI Blazor:"); + sb.AppendLine("1. Project creation commands"); + sb.AppendLine($"2. NuGet package installation (use version **{componentsVersion}**)"); + sb.AppendLine("3. Program.cs configuration"); + sb.AppendLine("4. _Imports.razor setup"); + sb.AppendLine("5. Layout configuration"); + sb.AppendLine("6. CSS and JavaScript references"); + + if (!string.IsNullOrEmpty(features)) + { + sb.AppendLine($"7. Configuration for requested features: {features}"); + } + + sb.AppendLine(); + sb.AppendLine("Include complete code examples for each step."); + + return new ChatMessage(ChatRole.User, sb.ToString()); + } +} diff --git a/src/Tools/McpServer/README.md b/src/Tools/McpServer/README.md new file mode 100644 index 0000000000..ed8718fe0d --- /dev/null +++ b/src/Tools/McpServer/README.md @@ -0,0 +1,387 @@ +# FluentUI.Mcp.Server + +A Model Context Protocol (MCP) server that provides documentation for the Fluent UI Blazor component library. + +## Overview + +This MCP server enables AI assistants to access comprehensive documentation about Fluent UI Blazor components, including: + +- **Component listing** - Browse all available components with descriptions +- **Component details** - Get complete documentation including parameters, events, and methods +- **Search** - Find components by name or description +- **Enum information** - Explore enum types and their values +- **Usage examples** - Get code examples for components + +## Architecture + +The MCP server uses **pre-generated JSON documentation** to provide fast, dependency-free access to component information: + +``` +┌─────────────────────────────────────────────────┐ +│ FluentUI.Demo.DocApiGen (Build Time) │ +│ ├── Uses LoxSmoke.DocXml │ +│ └── Generates FluentUIComponentsDocumentation.json +└─────────────────────────────────────────────────┘ + │ + ▼ (PreBuild / EmbeddedResource) +┌─────────────────────────────────────────────────┐ +│ McpServer (Runtime) │ +│ └── Reads JSON from embedded resource │ +└─────────────────────────────────────────────────┘ +``` + +This architecture provides: +- **Faster startup** - No XML parsing at runtime +- **Consistent documentation** - Generated once at build time + +## Tools vs Resources + +This server implements both **Tools** and **Resources** following MCP best practices: + +- **Tools** (Model-controlled): Invoked automatically by the LLM for dynamic queries like search or getting specific details +- **Resources** (User-controlled): Selected by the user/application to provide static context to the LLM + +## Available Tools + +### `ListComponents` +Lists all available Fluent UI Blazor components with their names and brief descriptions. + +**Parameters:** +- `category` (optional): Filter by category (e.g., "Button", "Input", "Dialog") + +### `GetComponentDetails` +Gets detailed documentation for a specific component including parameters, properties, events, and methods. + +**Parameters:** +- `componentName`: The name of the component (e.g., "FluentButton", "FluentDataGrid") + +### `SearchComponents` +Searches for components by name or description. + +**Parameters:** +- `searchTerm`: The term to search for + +### `ListCategories` +Lists all available component categories with component counts. + +### `GetEnumValues` +Gets information about a specific enum type including all possible values. + +**Parameters:** +- `enumName`: The name of the enum (e.g., "Appearance", "Color", "Size") +- `filter` (optional): Filter to show only values matching this search term + +### `GetComponentEnums` +Lists all enum types used by a specific component, showing which property/parameter uses each enum. + +**Parameters:** +- `componentName`: The name of the component (e.g., "FluentButton", "FluentDataGrid") + +### `ListEnums` +Lists all enum types used in the library. + +**Parameters:** +- `filter` (optional): Filter enums by name (e.g., "Color", "Size", "Appearance") + +### `GetComponentExample` +Gets usage examples for a specific component. + +**Parameters:** +- `componentName`: The name of the component + +### `GetGuide` +Gets a specific documentation guide by name. + +**Parameters:** +- `guideName`: The guide name: 'installation', 'defaultvalues', 'whatsnew', 'migration', 'localization', or 'styles' + +### `SearchGuides` +Searches documentation guides for specific content or topics. + +**Parameters:** +- `searchTerm`: The search term to find in documentation guides + +### `ListGuides` +Lists all available documentation guides with descriptions. + +## Available Resources + +Resources provide static documentation that users can attach to conversations for context. + +### Component Resources + +| URI | Description | +|-----|-------------| +| `fluentui://components` | Complete list of all Fluent UI Blazor components organized by category | +| `fluentui://categories` | List of all component categories with component counts | +| `fluentui://enums` | Complete list of all enum types with their values | + +### Documentation Guides + +| URI | Description | +|-----|-------------| +| `fluentui://guides` | List of all available documentation guides | +| `fluentui://guide/installation` | Installation and setup guide | +| `fluentui://guide/defaultvalues` | Configure default component values globally | +| `fluentui://guide/whatsnew` | Latest release notes and changes | +| `fluentui://guide/migration` | Complete migration guide from v4 to v5 | +| `fluentui://guide/localization` | Translate and localize component texts | +| `fluentui://guide/styles` | CSS styling, design tokens, and theming | + +### Resource Templates + +| URI Template | Description | +|--------------|-------------| +| `fluentui://component/{name}` | Detailed documentation for a specific component | +| `fluentui://category/{name}` | List of all components in a specific category | +| `fluentui://enum/{name}` | Detailed information about a specific enum type | + +## Available Prompts + +Prompts are pre-defined templates that help generate common code patterns and configurations. + +### Component Prompts + +| Prompt | Description | +|--------|-------------| +| `create_component` | Generate code for a Fluent UI Blazor component with specified configuration | +| `explain_component` | Get a detailed explanation of a component and its usage | +| `compare_components` | Compare two or more components to understand their differences | + +### Form & Data Prompts + +| Prompt | Description | +|--------|-------------| +| `create_form` | Generate a complete form with validation | +| `create_datagrid` | Generate a DataGrid with specified columns and features | +| `create_dialog` | Generate a dialog/modal component | +| `create_drawer` | Generate a drawer/panel component for side content | + +### Migration & Setup Prompts + +| Prompt | Description | +|--------|-------------| +| `migrate_to_v5` | Get guidance for migrating code from v4 to v5 | +| `setup_project` | Get step-by-step guidance for setting up a new project | +| `configure_theming` | Get guidance for configuring theming and styles | +| `configure_localization` | Get guidance for implementing localization | + +## Installation & Setup + +### Option 1: Install from NuGet (Recommended) + +Install the MCP server as a global .NET tool: + +```bash +dotnet tool install -g Microsoft.FluentUI.AspNetCore.Components.McpServer --prerelease +``` + +Or use DNX to add to your MCP client directly: + +```bash +dnx add Microsoft.FluentUI.AspNetCore.Components.McpServer +``` + +### Option 2: Build from Source + +**Prerequisites:** +- .NET 9.0 SDK +- Build the FluentUI Blazor solution first + +```bash +cd src/Tools/McpServer +dotnet build +``` + +The build process will automatically: +1. Build the FluentUI Components project +2. Run DocApiGen to generate the MCP documentation JSON +3. Embed the JSON as a resource in the McpServer assembly + +### Configure in VS Code + +If installed as a global tool: + +```json +{ + "mcp": { + "servers": { + "fluentui-blazor": { + "command": "fluentui-mcp" + } + } + } +} +``` + +If running from source: + +```json +{ + "mcp": { + "servers": { + "fluentui-blazor": { + "command": "dotnet", + "args": [ + "run", + "--project", + "path/to/src/Tools/McpServer/Microsoft.FluentUI.AspNetCore.Components.McpServer.csproj" + ] + } + } + } +} +``` + +Or using the built executable: + +```json +{ + "mcp": { + "servers": { + "fluentui-blazor": { + "command": "path/to/Microsoft.FluentUI.AspNetCore.Components.McpServer.exe" + } + } + } +} +``` + +## Usage Examples + +### List all button components + +``` +ListComponents(category: "Button") +``` + +### Get details about FluentDataGrid + +``` +GetComponentDetails(componentName: "FluentDataGrid") +``` + +### Search for input components + +``` +SearchComponents(searchTerm: "input") +``` + +### Get enum values for Appearance + +``` +GetEnumValues(enumName: "Appearance") +``` + +### Get migration guide + +``` +GetGuide(guideName: "migration") +``` + +### Search for content in guides + +``` +SearchGuides(searchTerm: "FluentButton") +``` + +## Development + +### Project Structure + +``` +McpServer/ +├── Program.cs # Entry point +├── FluentUIComponentsDocumentation.json # Pre-generated documentation (auto-generated) +├── Extensions/ +│ └── ServiceCollectionExtensions.cs # DI configuration +├── Models/ # Data models (7 files) +│ ├── ComponentInfo.cs +│ ├── ComponentDetails.cs +│ ├── PropertyInfo.cs +│ ├── EventInfo.cs +│ ├── MethodInfo.cs +│ ├── EnumInfo.cs +│ └── EnumValueInfo.cs +├── Services/ # Documentation services +│ ├── JsonBasedDocumentationService.cs # Main service (uses JSON) +│ ├── JsonDocumentationReader.cs # JSON reader +│ ├── DocumentationGuideService.cs +│ ├── TypeHelper.cs +│ └── ComponentCategoryHelper.cs +├── Tools/ # MCP Tools (model-controlled) +│ ├── ToolOutputHelper.cs +│ ├── ComponentListTools.cs +│ ├── ComponentDetailTools.cs +│ ├── EnumTools.cs +│ └── GuideTools.cs +├── Resources/ # MCP Resources (user-controlled) +│ ├── FluentUIResources.cs +│ ├── ComponentResources.cs +│ └── GuideResources.cs +└── Prompts/ # MCP Prompts (user-controlled templates) + ├── CreateComponentPrompt.cs + ├── ExplainComponentPrompt.cs + └── ... +``` + +### Regenerating Documentation + +The MCP documentation JSON is automatically regenerated during build if: +- The file doesn't exist +- You set `ForceGenerateMcpDocs=true` + +To force regeneration: + +```bash +dotnet build -p:ForceGenerateMcpDocs=true +``` + +### Adding New Tools + +1. Add a new class in the `Tools/` folder with `[McpServerToolType]` attribute +2. Add methods with `[McpServerTool]` and `[Description]` attributes +3. Use `[Description]` on parameters for better AI understanding + +### Adding New Resources + +1. Add a new class in the `Resources/` folder with `[McpServerResourceType]` attribute +2. Add methods with `[McpServerResource]` attribute specifying UriTemplate, Name, and MimeType +3. Register in `ServiceCollectionExtensions.cs` with `.WithResources()` + +### Adding New Prompts + +1. Add a new class in the `Prompts/` folder with `[McpServerPromptType]` attribute +2. Add methods with `[McpServerPrompt]` and `[Description]` attributes +3. Return `ChatMessage` with the prompt content +4. Register in `ServiceCollectionExtensions.cs` with `.WithPrompts()` + +### Testing + +Run the server in debug mode: + +```bash +dotnet run --project Microsoft.FluentUI.AspNetCore.Components.McpServer.csproj +``` + +The server communicates via stdin/stdout using JSON-RPC. + +## Troubleshooting + +### Documentation Not Loading + +If component descriptions are missing: +1. Check if `FluentUIComponentsDocumentation.json` exists in the project directory +2. Force regeneration with `dotnet build -p:ForceGenerateMcpDocs=true` +3. Ensure the FluentUI Components project builds successfully + +### Server Not Responding + +Check that: +1. The solution builds successfully +2. .NET 9.0 SDK is installed +3. The path in your MCP configuration is correct + +## License + +This project is licensed under the MIT License - see the [LICENSE](../../../LICENSE.TXT) file for details. diff --git a/src/Tools/McpServer/Resources/ComponentResources.cs b/src/Tools/McpServer/Resources/ComponentResources.cs new file mode 100644 index 0000000000..0bb09a8d09 --- /dev/null +++ b/src/Tools/McpServer/Resources/ComponentResources.cs @@ -0,0 +1,218 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Resources; + +/// +/// MCP Resource templates for accessing specific component documentation. +/// These are templated resources that accept parameters. +/// +[McpServerResourceType] +public class ComponentResources +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + public ComponentResources(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + /// + /// Gets detailed documentation for a specific component. + /// + [McpServerResource( + UriTemplate = "fluentui://component/{name}", + Name = "component", + Title = "Component Documentation", + MimeType = "text/markdown")] + [Description("Detailed documentation for a specific Fluent UI Blazor component including parameters, events, and methods.")] + public string GetComponent(string name) + { + var details = _documentationService.GetComponentDetails(name); + + if (details == null) + { + return $"# Component Not Found\n\nComponent '{name}' was not found. Check the components list for available components."; + } + + var sb = new StringBuilder(); + + // Header + sb.AppendLine($"# {details.Component.Name}"); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(details.Component.Summary)) + { + sb.AppendLine(details.Component.Summary); + sb.AppendLine(); + } + + sb.AppendLine($"**Category:** {details.Component.Category}"); + + if (!string.IsNullOrEmpty(details.Component.BaseClass)) + { + sb.AppendLine($"**Base Class:** {details.Component.BaseClass}"); + } + + if (details.Component.IsGeneric) + { + sb.AppendLine("**Generic Component:** Yes (requires type parameter)"); + } + + sb.AppendLine(); + + // Parameters + if (details.Parameters.Count > 0) + { + sb.AppendLine("## Parameters"); + sb.AppendLine(); + sb.AppendLine("| Name | Type | Default | Description |"); + sb.AppendLine("|------|------|---------|-------------|"); + + foreach (var param in details.Parameters) + { + var defaultValue = param.DefaultValue ?? "-"; + var description = ToolOutputHelper.TruncateSummary(param.Description, 80); + var enumHint = param.EnumValues.Length > 0 + ? $" Values: {string.Join(", ", param.EnumValues.Take(5))}{(param.EnumValues.Length > 5 ? "..." : "")}" + : ""; + sb.AppendLine($"| {param.Name} | `{param.Type}` | {defaultValue} | {description}{enumHint} |"); + } + + sb.AppendLine(); + } + + // Events + if (details.Events.Count > 0) + { + sb.AppendLine("## Events"); + sb.AppendLine(); + sb.AppendLine("| Name | Type | Description |"); + sb.AppendLine("|------|------|-------------|"); + + foreach (var evt in details.Events) + { + sb.AppendLine($"| {evt.Name} | `{evt.Type}` | {ToolOutputHelper.TruncateSummary(evt.Description, 80)} |"); + } + + sb.AppendLine(); + } + + // Methods + if (details.Methods.Count > 0) + { + sb.AppendLine("## Methods"); + sb.AppendLine(); + + foreach (var method in details.Methods) + { + sb.AppendLine($"### {method.Name}"); + sb.AppendLine(); + sb.AppendLine("```csharp"); + sb.AppendLine(method.Signature); + sb.AppendLine("```"); + + if (!string.IsNullOrEmpty(method.Description)) + { + sb.AppendLine(); + sb.AppendLine(method.Description); + } + + sb.AppendLine(); + } + } + + return sb.ToString(); + } + + /// + /// Gets components in a specific category. + /// + [McpServerResource( + UriTemplate = "fluentui://category/{name}", + Name = "category", + Title = "Components by Category", + MimeType = "text/markdown")] + [Description("List of all components in a specific category.")] + public string GetCategory(string name) + { + var components = _documentationService.GetComponentsByCategory(name); + + if (components.Count == 0) + { + return $"# Category Not Found\n\nNo components found in category '{name}'."; + } + + var sb = new StringBuilder(); + sb.AppendLine($"# {name} Components"); + sb.AppendLine(); + sb.AppendLine($"Total: {components.Count} components"); + sb.AppendLine(); + + foreach (var component in components.OrderBy(c => c.Name)) + { + var genericIndicator = component.IsGeneric ? "" : ""; + sb.AppendLine($"## {component.Name}{genericIndicator}"); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(component.Summary)) + { + sb.AppendLine(component.Summary); + sb.AppendLine(); + } + } + + return sb.ToString(); + } + + /// + /// Gets detailed information about a specific enum. + /// + [McpServerResource( + UriTemplate = "fluentui://enum/{name}", + Name = "enum", + Title = "Enum Type Details", + MimeType = "text/markdown")] + [Description("Detailed information about a specific enum type including all values.")] + public string GetEnum(string name) + { + var enumInfo = _documentationService.GetEnumDetails(name); + + if (enumInfo == null) + { + return $"# Enum Not Found\n\nEnum '{name}' was not found. Check the enums list for available enum types."; + } + + var sb = new StringBuilder(); + sb.AppendLine($"# {enumInfo.Name}"); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(enumInfo.Description)) + { + sb.AppendLine(enumInfo.Description); + sb.AppendLine(); + } + + sb.AppendLine("## Values"); + sb.AppendLine(); + sb.AppendLine("| Name | Value | Description |"); + sb.AppendLine("|------|-------|-------------|"); + + foreach (var value in enumInfo.Values) + { + sb.AppendLine($"| {value.Name} | {value.Value} | {value.Description} |"); + } + + return sb.ToString(); + } +} diff --git a/src/Tools/McpServer/Resources/FluentUIResources.cs b/src/Tools/McpServer/Resources/FluentUIResources.cs new file mode 100644 index 0000000000..05a47533a6 --- /dev/null +++ b/src/Tools/McpServer/Resources/FluentUIResources.cs @@ -0,0 +1,139 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Resources; + +/// +/// MCP Resources providing static documentation content for Fluent UI Blazor components. +/// These resources are user-selected and provide context for the LLM. +/// +[McpServerResourceType] +public class FluentUIResources +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + public FluentUIResources(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + /// + /// Gets the complete list of all Fluent UI Blazor components. + /// + [McpServerResource( + UriTemplate = "fluentui://components", + Name = "components", + Title = "All Fluent UI Blazor Components", + MimeType = "text/markdown")] + [Description("Complete list of all available Fluent UI Blazor components organized by category.")] + public string GetAllComponents() + { + var components = _documentationService.GetAllComponents(); + + var sb = new StringBuilder(); + sb.AppendLine("# Fluent UI Blazor Components"); + sb.AppendLine(); + sb.AppendLine($"Total: {components.Count} components"); + sb.AppendLine(); + + var groupedByCategory = components.GroupBy(c => c.Category).OrderBy(g => g.Key); + + foreach (var group in groupedByCategory) + { + sb.AppendLine($"## {group.Key}"); + sb.AppendLine(); + + foreach (var component in group.OrderBy(c => c.Name)) + { + var genericIndicator = component.IsGeneric ? "" : ""; + sb.AppendLine($"- **{component.Name}{genericIndicator}**: {component.Summary}"); + } + + sb.AppendLine(); + } + + return sb.ToString(); + } + + /// + /// Gets all component categories with their counts. + /// + [McpServerResource( + UriTemplate = "fluentui://categories", + Name = "categories", + Title = "Component Categories", + MimeType = "text/markdown")] + [Description("List of all Fluent UI Blazor component categories with component counts.")] + public string GetCategories() + { + var categories = _documentationService.GetCategories(); + var allComponents = _documentationService.GetAllComponents(); + + var sb = new StringBuilder(); + sb.AppendLine("# Fluent UI Blazor Component Categories"); + sb.AppendLine(); + + foreach (var category in categories) + { + var count = allComponents.Count(c => c.Category == category); + var componentNames = string.Join(", ", allComponents + .Where(c => c.Category == category) + .OrderBy(c => c.Name) + .Select(c => c.Name)); + + sb.AppendLine($"## {category} ({count} components)"); + sb.AppendLine(); + sb.AppendLine(componentNames); + sb.AppendLine(); + } + + return sb.ToString(); + } + + /// + /// Gets all enum types available in the library. + /// + [McpServerResource( + UriTemplate = "fluentui://enums", + Name = "enums", + Title = "All Enum Types", + MimeType = "text/markdown")] + [Description("Complete list of all enum types used in Fluent UI Blazor components.")] + public string GetAllEnums() + { + var enums = _documentationService.GetAllEnums(); + + var sb = new StringBuilder(); + sb.AppendLine("# Fluent UI Blazor Enum Types"); + sb.AppendLine(); + sb.AppendLine($"Total: {enums.Count} enums"); + sb.AppendLine(); + + foreach (var enumInfo in enums) + { + var values = string.Join(", ", enumInfo.Values.Select(v => v.Name)); + sb.AppendLine($"## {enumInfo.Name}"); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(enumInfo.Description)) + { + sb.AppendLine(enumInfo.Description); + sb.AppendLine(); + } + + sb.AppendLine($"**Values:** {values}"); + sb.AppendLine(); + } + + return sb.ToString(); + } +} diff --git a/src/Tools/McpServer/Resources/GuideResources.cs b/src/Tools/McpServer/Resources/GuideResources.cs new file mode 100644 index 0000000000..1291377974 --- /dev/null +++ b/src/Tools/McpServer/Resources/GuideResources.cs @@ -0,0 +1,167 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Resources; + +/// +/// MCP Resources for documentation guides (Installation, Migration, Styles, etc.). +/// +[McpServerResourceType] +public class GuideResources +{ + private readonly DocumentationGuideService _guideService; + + /// + /// Initializes a new instance of the class. + /// + public GuideResources(DocumentationGuideService guideService) + { + _guideService = guideService; + } + + /// + /// Gets the list of all available documentation guides. + /// + [McpServerResource( + UriTemplate = "fluentui://guides", + Name = "guides", + Title = "Documentation Guides", + MimeType = "text/markdown")] + [Description("List of all available documentation guides: Installation, Default Values, Migration, Localization, and Styles.")] + public string GetAllGuides() + { + var guides = _guideService.GetAllGuides(); + + var sb = new StringBuilder(); + sb.AppendLine("# Fluent UI Blazor Documentation Guides"); + sb.AppendLine(); + + if (guides.Count == 0) + { + sb.AppendLine("⚠️ No guides found. Documentation files may not be available."); + sb.AppendLine(); + sb.AppendLine("When running from source, ensure the Demo project documentation is accessible."); + return sb.ToString(); + } + + sb.AppendLine("| Guide | Description |"); + sb.AppendLine("|-------|-------------|"); + sb.AppendLine("| [Installation](fluentui://guide/installation) | How to install and configure Fluent UI Blazor |"); + sb.AppendLine("| [Default Values](fluentui://guide/defaultvalues) | Configure default component values globally |"); + sb.AppendLine("| [What's New](fluentui://guide/whatsnew) | Latest release notes and changes |"); + sb.AppendLine("| [Migration to v5](fluentui://guide/migration) | Complete guide for migrating from v4 to v5 |"); + sb.AppendLine("| [Localization](fluentui://guide/localization) | Translate and localize component texts |"); + sb.AppendLine("| [Styles](fluentui://guide/styles) | CSS styling, design tokens, and theming |"); + sb.AppendLine(); + + sb.AppendLine("## Quick Links"); + sb.AppendLine(); + sb.AppendLine("Use these resource URIs to access specific guides:"); + sb.AppendLine(); + sb.AppendLine("```"); + sb.AppendLine("fluentui://guide/installation"); + sb.AppendLine("fluentui://guide/defaultvalues"); + sb.AppendLine("fluentui://guide/whatsnew"); + sb.AppendLine("fluentui://guide/migration"); + sb.AppendLine("fluentui://guide/localization"); + sb.AppendLine("fluentui://guide/styles"); + sb.AppendLine("```"); + + return sb.ToString(); + } + + /// + /// Gets the installation guide. + /// + [McpServerResource( + UriTemplate = "fluentui://guide/installation", + Name = "guide-installation", + Title = "Installation Guide", + MimeType = "text/markdown")] + [Description("Complete installation guide for Fluent UI Blazor including NuGet packages, configuration, and setup.")] + public string GetInstallationGuide() + { + return _guideService.GetGuideContent("installation") + ?? "# Installation Guide\n\nGuide not available. Please check the documentation at https://www.fluentui-blazor.net/installation"; + } + + /// + /// Gets the default values guide. + /// + [McpServerResource( + UriTemplate = "fluentui://guide/defaultvalues", + Name = "guide-defaultvalues", + Title = "Default Values Guide", + MimeType = "text/markdown")] + [Description("Guide for configuring default values for Fluent UI Blazor components globally.")] + public string GetDefaultValuesGuide() + { + return _guideService.GetGuideContent("defaultvalues") + ?? "# Default Values\n\nGuide not available. Please check the documentation at https://www.fluentui-blazor.net/default-values"; + } + + /// + /// Gets the What's New / Release Notes. + /// + [McpServerResource( + UriTemplate = "fluentui://guide/whatsnew", + Name = "guide-whatsnew", + Title = "What's New", + MimeType = "text/markdown")] + [Description("Latest release notes and changes in Fluent UI Blazor.")] + public string GetWhatsNewGuide() + { + return _guideService.GetGuideContent("whatsnew") + ?? "# What's New\n\nGuide not available. Please check the documentation at https://www.fluentui-blazor.net/WhatsNew"; + } + + /// + /// Gets the complete migration guide for v5. + /// + [McpServerResource( + UriTemplate = "fluentui://guide/migration", + Name = "guide-migration", + Title = "Migration to v5 Guide", + MimeType = "text/markdown")] + [Description("Complete migration guide from v4 to v5, including all breaking changes and component updates.")] + public string GetMigrationGuide() + { + return _guideService.GetFullMigrationGuide(); + } + + /// + /// Gets the localization guide. + /// + [McpServerResource( + UriTemplate = "fluentui://guide/localization", + Name = "guide-localization", + Title = "Localization Guide", + MimeType = "text/markdown")] + [Description("Guide for translating and localizing Fluent UI Blazor component texts.")] + public string GetLocalizationGuide() + { + return _guideService.GetGuideContent("localization") + ?? "# Localization\n\nGuide not available. Please check the documentation at https://www.fluentui-blazor.net/localization"; + } + + /// + /// Gets the styles guide. + /// + [McpServerResource( + UriTemplate = "fluentui://guide/styles", + Name = "guide-styles", + Title = "Styles Guide", + MimeType = "text/markdown")] + [Description("Complete guide for CSS styling, design tokens, and theming in Fluent UI Blazor.")] + public string GetStylesGuide() + { + return _guideService.GetGuideContent("styles") + ?? "# Styles\n\nGuide not available. Please check the documentation at https://www.fluentui-blazor.net/Styles"; + } +} diff --git a/src/Tools/McpServer/Services/DocumentationGuideService.cs b/src/Tools/McpServer/Services/DocumentationGuideService.cs new file mode 100644 index 0000000000..b6311dd97a --- /dev/null +++ b/src/Tools/McpServer/Services/DocumentationGuideService.cs @@ -0,0 +1,243 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +/// +/// Service for reading and processing documentation guide files (Markdown). +/// +public partial class DocumentationGuideService +{ + private readonly string _documentationBasePath; + private readonly Dictionary _guides; + + /// + /// Initializes a new instance of the class. + /// + public DocumentationGuideService() + { + _documentationBasePath = FindDocumentationPath(); + _guides = LoadGuides(); + } + + /// + /// Gets all available documentation guides. + /// + public IReadOnlyList GetAllGuides() + { + return _guides.Values.OrderBy(g => g.Order).ToList(); + } + + /// + /// Gets a specific guide by its key. + /// + public DocumentationGuide? GetGuide(string key) + { + return _guides.TryGetValue(key.ToLowerInvariant(), out var guide) ? guide : null; + } + + /// + /// Gets the content of a guide, resolving any includes. + /// + public string? GetGuideContent(string key) + { + var guide = GetGuide(key); + if (guide == null) + { + return null; + } + + return ResolveIncludes(guide.RawContent); + } + + /// + /// Gets the migration guide with all component-specific sections. + /// + public string GetFullMigrationGuide() + { + var guide = GetGuide("migration"); + if (guide == null) + { + return "Migration guide not found."; + } + + return ResolveIncludes(guide.RawContent); + } + + private static string FindDocumentationPath() + { + // Try to find documentation path relative to execution + var possiblePaths = new[] + { + // When running from source + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "Demo", "FluentUI.Demo.Client", "Documentation", "GetStarted"), + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "Demo", "FluentUI.Demo.Client", "Documentation", "GetStarted"), + // When running as tool (embedded or packaged) + Path.Combine(AppContext.BaseDirectory, "Documentation", "GetStarted"), + // Development paths + @"D:\gh\fluentui-blazor\examples\Demo\FluentUI.Demo.Client\Documentation\GetStarted", + }; + + foreach (var path in possiblePaths) + { + var fullPath = Path.GetFullPath(path); + if (Directory.Exists(fullPath)) + { + return fullPath; + } + } + + return string.Empty; + } + + private Dictionary LoadGuides() + { + var guides = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (string.IsNullOrEmpty(_documentationBasePath) || !Directory.Exists(_documentationBasePath)) + { + // Return empty guides with informative message + return guides; + } + + var guideFiles = new Dictionary + { + ["installation"] = "Installation.md", + ["defaultvalues"] = "DefaultValues.md", + ["whatsnew"] = "ReleaseNotes.md", + ["migration"] = "MigrationVersion5.md", + ["localization"] = "Localization.md", + ["styles"] = "Styles.md", + }; + + foreach (var (key, fileName) in guideFiles) + { + var filePath = Path.Combine(_documentationBasePath, fileName); + if (File.Exists(filePath)) + { + var content = File.ReadAllText(filePath); + var metadata = ParseFrontMatter(content); + guides[key] = new DocumentationGuide + { + Key = key, + Title = metadata.Title, + Order = metadata.Order, + Route = metadata.Route, + RawContent = content, + FilePath = filePath + }; + } + } + + return guides; + } + + private string ResolveIncludes(string content) + { + // Pattern: {{ INCLUDE FileName }} + var includePattern = IncludeRegex(); + var migrationPath = Path.Combine(_documentationBasePath, "Migration"); + + return includePattern.Replace(content, match => + { + var includeName = match.Groups[1].Value.Trim(); + var includeFile = Path.Combine(migrationPath, $"{includeName}.md"); + + if (File.Exists(includeFile)) + { + var includeContent = File.ReadAllText(includeFile); + // Remove front matter from included content + return RemoveFrontMatter(includeContent); + } + + return $""; + }); + } + + private static string RemoveFrontMatter(string content) + { + if (content.StartsWith("---")) + { + var endIndex = content.IndexOf("---", 3); + if (endIndex > 0) + { + return content[(endIndex + 3)..].TrimStart(); + } + } + + return content; + } + + private static (string Title, int Order, string Route) ParseFrontMatter(string content) + { + var title = "Unknown"; + var order = 0; + var route = ""; + + if (content.StartsWith("---")) + { + var endIndex = content.IndexOf("---", 3); + if (endIndex > 0) + { + var frontMatter = content[3..endIndex]; + var lines = frontMatter.Split('\n'); + + foreach (var line in lines) + { + var parts = line.Split(':', 2); + if (parts.Length == 2) + { + var key = parts[0].Trim().ToLowerInvariant(); + var value = parts[1].Trim(); + + switch (key) + { + case "title": + title = value; + break; + case "order": + int.TryParse(value, out order); + break; + case "route": + route = value; + break; + } + } + } + } + } + + return (title, order, route); + } + + [GeneratedRegex(@"\{\{\s*INCLUDE\s+(\w+)\s*\}\}", RegexOptions.IgnoreCase)] + private static partial Regex IncludeRegex(); +} + +/// +/// Represents a documentation guide. +/// +public record DocumentationGuide +{ + /// Gets the unique key for this guide. + public required string Key { get; init; } + + /// Gets the title of the guide. + public required string Title { get; init; } + + /// Gets the sort order. + public int Order { get; init; } + + /// Gets the route path. + public string Route { get; init; } = ""; + + /// Gets the raw Markdown content. + public required string RawContent { get; init; } + + /// Gets the file path. + public string FilePath { get; init; } = ""; +} diff --git a/src/Tools/McpServer/Services/FluentUIDocumentationService.cs b/src/Tools/McpServer/Services/FluentUIDocumentationService.cs new file mode 100644 index 0000000000..38e9322701 --- /dev/null +++ b/src/Tools/McpServer/Services/FluentUIDocumentationService.cs @@ -0,0 +1,302 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Models; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +/// +/// Service for providing Fluent UI Blazor component documentation. +/// Uses pre-generated JSON data for fast, dependency-free access. +/// +public class FluentUIDocumentationService +{ + private readonly JsonDocumentationReader _reader; + private readonly Dictionary _componentCache = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _enumCache = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Initializes a new instance of the class. + /// + /// Optional path to the JSON documentation file. If null, uses embedded resource. + public FluentUIDocumentationService(string? jsonDocumentationPath = null) + { + _reader = new JsonDocumentationReader(jsonDocumentationPath); + InitializeCache(); + } + + /// + /// Gets whether the documentation is available. + /// + public bool IsAvailable => _reader.IsAvailable; + + /// + /// Gets the version of the Fluent UI Components library that this documentation was generated from. + /// + public string ComponentsVersion => _reader.Metadata?.AssemblyVersion ?? "Unknown"; + + /// + /// Gets the version of the MCP Server. + /// + public static string McpServerVersion + { + get + { + var assembly = Assembly.GetExecutingAssembly(); + var version = assembly.GetName().Version; + return version?.ToString(3) ?? "Unknown"; + } + } + + /// + /// Gets the date when the documentation was generated. + /// + public string DocumentationGeneratedDate => _reader.Metadata?.GeneratedDateUtc ?? "Unknown"; + + /// + /// Initializes the component and enum caches from JSON data. + /// + private void InitializeCache() + { + foreach (var jsonComponent in _reader.GetAllComponents()) + { + var componentInfo = ConvertToComponentInfo(jsonComponent); + _componentCache[componentInfo.Name] = componentInfo; + } + + foreach (var jsonEnum in _reader.GetAllEnums()) + { + var enumInfo = ConvertToEnumInfo(jsonEnum); + _enumCache[enumInfo.Name] = enumInfo; + } + } + + /// + /// Gets all available components. + /// + /// A list of all components. + public IReadOnlyList GetAllComponents() + { + return [.. _componentCache.Values.OrderBy(c => c.Name)]; + } + + /// + /// Gets components filtered by category. + /// + /// The category to filter by. + /// A list of components in the specified category. + public IReadOnlyList GetComponentsByCategory(string category) + { + return [.. _componentCache.Values + .Where(c => c.Category.Equals(category, StringComparison.OrdinalIgnoreCase)) + .OrderBy(c => c.Name)]; + } + + /// + /// Searches for components by name or description. + /// + /// The search term. + /// A list of matching components. + public IReadOnlyList SearchComponents(string searchTerm) + { + var lowerSearch = searchTerm.ToLowerInvariant(); + return [.. _componentCache.Values + .Where(c => c.Name.Contains(lowerSearch, StringComparison.OrdinalIgnoreCase) || + c.Summary.Contains(lowerSearch, StringComparison.OrdinalIgnoreCase)) + .OrderBy(c => c.Name)]; + } + + /// + /// Gets detailed information about a specific component. + /// + /// The name of the component. + /// Detailed component information, or null if not found. + public ComponentDetails? GetComponentDetails(string componentName) + { + var jsonComponent = _reader.GetComponent(componentName); + if (jsonComponent == null) + { + return null; + } + + if (!_componentCache.TryGetValue(jsonComponent.Name, out var componentInfo)) + { + return null; + } + + return ConvertToComponentDetails(jsonComponent, componentInfo); + } + + /// + /// Gets all available enums. + /// + /// A list of all enums. + public IReadOnlyList GetAllEnums() + { + return [.. _enumCache.Values.OrderBy(e => e.Name)]; + } + + /// + /// Gets detailed information about a specific enum. + /// + /// The name of the enum. + /// Enum information, or null if not found. + public EnumInfo? GetEnumDetails(string enumName) + { + _enumCache.TryGetValue(enumName, out var enumInfo); + return enumInfo; + } + + /// + /// Gets all enums used by a specific component. + /// + /// The name of the component. + /// A dictionary of property names to their enum info. + public Dictionary GetEnumsForComponent(string componentName) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + var details = GetComponentDetails(componentName); + + if (details == null) + { + return result; + } + + // Get enums from parameters + foreach (var param in details.Parameters) + { + var enumInfo = FindEnumForType(param.Type); + if (enumInfo != null && !result.ContainsKey(param.Name)) + { + result[param.Name] = enumInfo; + } + } + + // Get enums from properties + foreach (var prop in details.Properties) + { + var enumInfo = FindEnumForType(prop.Type); + if (enumInfo != null && !result.ContainsKey(prop.Name)) + { + result[prop.Name] = enumInfo; + } + } + + return result; + } + + /// + /// Finds an enum info by type name (handles nullable types). + /// + private EnumInfo? FindEnumForType(string typeName) + { + // Remove nullable suffix + var cleanTypeName = typeName.TrimEnd('?'); + + // Try direct match + if (_enumCache.TryGetValue(cleanTypeName, out var enumInfo)) + { + return enumInfo; + } + + // Try to find by partial match + var match = _enumCache.Values.FirstOrDefault(e => + e.Name.Equals(cleanTypeName, StringComparison.OrdinalIgnoreCase) || + e.FullName.EndsWith($".{cleanTypeName}", StringComparison.OrdinalIgnoreCase)); + + return match; + } + + /// + /// Gets all available categories. + /// + /// A list of all categories. + public IReadOnlyList GetCategories() + { + return [.. _componentCache.Values + .Select(c => c.Category) + .Where(c => !string.IsNullOrEmpty(c)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(c => c, StringComparer.OrdinalIgnoreCase)]; + } + + /// + /// Converts JSON component info to ComponentInfo. + /// + private static ComponentInfo ConvertToComponentInfo(JsonComponentInfo json) + { + return new ComponentInfo + { + Name = json.Name, + FullName = json.FullName, + Summary = json.Summary, + Category = json.Category, + IsGeneric = json.IsGeneric, + BaseClass = json.BaseClass + }; + } + + /// + /// Converts JSON enum info to EnumInfo. + /// + private static EnumInfo ConvertToEnumInfo(JsonEnumInfo json) + { + return new EnumInfo + { + Name = json.Name, + FullName = json.FullName, + Description = json.Description, + Values = [.. json.Values.Select(v => new EnumValueInfo + { + Name = v.Name, + Value = v.Value, + Description = v.Description + })] + }; + } + + /// + /// Converts JSON component to ComponentDetails. + /// + private static ComponentDetails ConvertToComponentDetails(JsonComponentInfo json, ComponentInfo componentInfo) + { + var properties = json.Properties.Select(p => new Models.PropertyInfo + { + Name = p.Name, + Type = p.Type, + Description = p.Description, + IsParameter = p.IsParameter, + IsInherited = p.IsInherited, + DefaultValue = p.DefaultValue, + EnumValues = p.EnumValues + }).ToList(); + + var events = json.Events.Select(e => new Models.EventInfo + { + Name = e.Name, + Type = e.Type, + Description = e.Description, + IsInherited = e.IsInherited + }).ToList(); + + var methods = json.Methods.Select(m => new Models.MethodInfo + { + Name = m.Name, + ReturnType = m.ReturnType, + Description = m.Description, + Parameters = m.Parameters, + IsInherited = m.IsInherited + }).ToList(); + + return new ComponentDetails + { + Component = componentInfo, + Parameters = [.. properties.Where(p => p.IsParameter).OrderBy(p => p.Name)], + Properties = [.. properties.OrderBy(p => p.Name)], + Events = [.. events.OrderBy(e => e.Name)], + Methods = [.. methods.OrderBy(m => m.Name)] + }; + } +} diff --git a/src/Tools/McpServer/Services/JsonBasedDocumentationService.cs b/src/Tools/McpServer/Services/JsonBasedDocumentationService.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Tools/McpServer/Services/JsonDocumentationReader.cs b/src/Tools/McpServer/Services/JsonDocumentationReader.cs new file mode 100644 index 0000000000..687d0181d4 --- /dev/null +++ b/src/Tools/McpServer/Services/JsonDocumentationReader.cs @@ -0,0 +1,354 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Models; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +/// +/// Root model for the MCP documentation JSON file. +/// +internal class McpDocumentationRoot +{ + /// + /// Gets or sets metadata about the generated documentation. + /// + [JsonPropertyName("metadata")] + public McpDocumentationMetadata Metadata { get; set; } = new(); + + /// + /// Gets or sets the list of all components. + /// + [JsonPropertyName("components")] + public List Components { get; set; } = []; + + /// + /// Gets or sets the list of all enums. + /// + [JsonPropertyName("enums")] + public List Enums { get; set; } = []; +} + +/// +/// Metadata about the generated documentation. +/// +internal class McpDocumentationMetadata +{ + /// + /// Gets or sets the assembly version. + /// + [JsonPropertyName("assemblyVersion")] + public string AssemblyVersion { get; set; } = string.Empty; + + /// + /// Gets or sets the generation date in UTC. + /// + [JsonPropertyName("generatedDateUtc")] + public string GeneratedDateUtc { get; set; } = string.Empty; + + /// + /// Gets or sets the total component count. + /// + [JsonPropertyName("componentCount")] + public int ComponentCount { get; set; } + + /// + /// Gets or sets the total enum count. + /// + [JsonPropertyName("enumCount")] + public int EnumCount { get; set; } +} + +/// +/// JSON representation of a component. +/// +internal class JsonComponentInfo +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("fullName")] + public string FullName { get; set; } = string.Empty; + + [JsonPropertyName("summary")] + public string Summary { get; set; } = string.Empty; + + [JsonPropertyName("category")] + public string Category { get; set; } = string.Empty; + + [JsonPropertyName("isGeneric")] + public bool IsGeneric { get; set; } + + [JsonPropertyName("baseClass")] + public string? BaseClass { get; set; } + + [JsonPropertyName("properties")] + public List Properties { get; set; } = []; + + [JsonPropertyName("events")] + public List Events { get; set; } = []; + + [JsonPropertyName("methods")] + public List Methods { get; set; } = []; +} + +/// +/// JSON representation of a property. +/// +internal class JsonPropertyInfo +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + [JsonPropertyName("isParameter")] + public bool IsParameter { get; set; } + + [JsonPropertyName("isInherited")] + public bool IsInherited { get; set; } + + [JsonPropertyName("defaultValue")] + public string? DefaultValue { get; set; } + + [JsonPropertyName("enumValues")] + public string[] EnumValues { get; set; } = []; +} + +/// +/// JSON representation of an event. +/// +internal class JsonEventInfo +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + [JsonPropertyName("isInherited")] + public bool IsInherited { get; set; } +} + +/// +/// JSON representation of a method. +/// +internal class JsonMethodInfo +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("returnType")] + public string ReturnType { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + [JsonPropertyName("parameters")] + public string[] Parameters { get; set; } = []; + + [JsonPropertyName("isInherited")] + public bool IsInherited { get; set; } +} + +/// +/// JSON representation of an enum. +/// +internal class JsonEnumInfo +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("fullName")] + public string FullName { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + [JsonPropertyName("values")] + public List Values { get; set; } = []; +} + +/// +/// JSON representation of an enum value. +/// +internal class JsonEnumValueInfo +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("value")] + public int Value { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; +} + +/// +/// Service for reading pre-generated JSON documentation. +/// This replaces XmlDocumentationReader and eliminates the need for LoxSmoke.DocXml at runtime. +/// +internal sealed class JsonDocumentationReader +{ + private readonly McpDocumentationRoot? _documentation; + private readonly Dictionary _componentsByName = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _enumsByName = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Initializes a new instance of the class. + /// + /// Path to the JSON documentation file, or null to use embedded resource. + public JsonDocumentationReader(string? jsonDocumentationPath = null) + { + _documentation = LoadDocumentation(jsonDocumentationPath); + + if (_documentation != null) + { + // Build lookup dictionaries + foreach (var component in _documentation.Components) + { + _componentsByName[component.Name] = component; + } + + foreach (var enumInfo in _documentation.Enums) + { + _enumsByName[enumInfo.Name] = enumInfo; + } + } + } + + /// + /// Gets whether documentation is available. + /// + public bool IsAvailable => _documentation != null; + + /// + /// Gets the documentation metadata. + /// + public McpDocumentationMetadata? Metadata => _documentation?.Metadata; + + /// + /// Gets all components from the documentation. + /// + public IReadOnlyList GetAllComponents() + { + return _documentation?.Components ?? []; + } + + /// + /// Gets a component by name. + /// + public JsonComponentInfo? GetComponent(string name) + { + if (_componentsByName.TryGetValue(name, out var component)) + { + return component; + } + + // Try with "Fluent" prefix + if (_componentsByName.TryGetValue($"Fluent{name}", out component)) + { + return component; + } + + return null; + } + + /// + /// Gets all enums from the documentation. + /// + public IReadOnlyList GetAllEnums() + { + return _documentation?.Enums ?? []; + } + + /// + /// Gets an enum by name. + /// + public JsonEnumInfo? GetEnum(string name) + { + _enumsByName.TryGetValue(name, out var enumInfo); + return enumInfo; + } + + /// + /// Loads documentation from file or embedded resource. + /// + private static McpDocumentationRoot? LoadDocumentation(string? jsonDocumentationPath) + { + string? jsonContent; + + // Try to load from file first + if (!string.IsNullOrEmpty(jsonDocumentationPath) && File.Exists(jsonDocumentationPath)) + { + Console.Error.WriteLine($"[FluentUI.Mcp.Server] Loading documentation from: {jsonDocumentationPath}"); + jsonContent = File.ReadAllText(jsonDocumentationPath); + } + else + { + // Try to load from embedded resource + jsonContent = LoadFromEmbeddedResource(); + } + + if (string.IsNullOrEmpty(jsonContent)) + { + Console.Error.WriteLine("[FluentUI.Mcp.Server] Warning: JSON documentation not found. Documentation will be limited."); + return null; + } + + try + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + var documentation = JsonSerializer.Deserialize(jsonContent, options); + + if (documentation != null) + { + Console.Error.WriteLine($"[FluentUI.Mcp.Server] Loaded {documentation.Components.Count} components and {documentation.Enums.Count} enums from documentation."); + } + + return documentation; + } + catch (Exception ex) + { + Console.Error.WriteLine($"[FluentUI.Mcp.Server] Error parsing JSON documentation: {ex.Message}"); + return null; + } + } + + /// + /// Loads documentation from the embedded resource. + /// + private static string? LoadFromEmbeddedResource() + { + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = "Microsoft.FluentUI.AspNetCore.Components.McpServer.FluentUIComponentsDocumentation.json"; + + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + { + // List available resources for debugging + var availableResources = assembly.GetManifestResourceNames(); + Console.Error.WriteLine($"[FluentUI.Mcp.Server] Available embedded resources: {string.Join(", ", availableResources)}"); + return null; + } + + using var reader = new StreamReader(stream); + Console.Error.WriteLine("[FluentUI.Mcp.Server] Loading documentation from embedded resource."); + return reader.ReadToEnd(); + } +} diff --git a/src/Tools/McpServer/Tools/ComponentDetailTools.cs b/src/Tools/McpServer/Tools/ComponentDetailTools.cs new file mode 100644 index 0000000000..2f40007ae1 --- /dev/null +++ b/src/Tools/McpServer/Tools/ComponentDetailTools.cs @@ -0,0 +1,255 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +/// +/// MCP tools for getting detailed component documentation. +/// +[McpServerToolType] +public class ComponentDetailTools +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + /// The documentation service. + public ComponentDetailTools(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + [McpServerTool] + [Description("Gets detailed documentation for a specific Fluent UI Blazor component, including all its parameters, properties, events, and methods.")] + public string GetComponentDetails( + [Description("The name of the component (e.g., 'FluentButton', 'FluentDataGrid', 'FluentTextField'). You can omit the 'Fluent' prefix.")] + string componentName) + { + var details = _documentationService.GetComponentDetails(componentName); + + if (details == null) + { + return $"Component '{componentName}' not found. Use ListComponents() to see all available components."; + } + + var sb = new StringBuilder(); + + AppendComponentHeader(sb, details); + AppendParameters(sb, details); + AppendEvents(sb, details); + AppendMethods(sb, details); + + return sb.ToString(); + } + + [McpServerTool] + [Description("Gets a usage example for a specific Fluent UI Blazor component showing basic and common usage patterns.")] + public string GetComponentExample( + [Description("The name of the component (e.g., 'FluentButton', 'FluentDataGrid', 'FluentTextField').")] + string componentName) + { + var details = _documentationService.GetComponentDetails(componentName); + + if (details == null) + { + return $"Component '{componentName}' not found. Use ListComponents() to see all available components."; + } + + var sb = new StringBuilder(); + + AppendBasicExample(sb, details); + AppendEventHandlingExample(sb, details); + AppendCommonParameters(sb, details); + + return sb.ToString(); + } + + private static void AppendComponentHeader(StringBuilder sb, Models.ComponentDetails details) + { + sb.AppendLine($"# {details.Component.Name}"); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(details.Component.Summary)) + { + sb.AppendLine(details.Component.Summary); + sb.AppendLine(); + } + + if (!string.IsNullOrEmpty(details.Component.BaseClass)) + { + sb.AppendLine($"**Base Class:** {details.Component.BaseClass}"); + } + + if (details.Component.IsGeneric) + { + sb.AppendLine("**Generic Component:** Yes (requires type parameter)"); + } + + sb.AppendLine($"**Category:** {details.Component.Category}"); + sb.AppendLine(); + } + + private static void AppendParameters(StringBuilder sb, Models.ComponentDetails details) + { + if (details.Parameters.Count == 0) + { + return; + } + + sb.AppendLine("## Parameters"); + sb.AppendLine(); + sb.AppendLine("| Name | Type | Default | Description |"); + sb.AppendLine("|------|------|---------|-------------|"); + + foreach (var param in details.Parameters) + { + var defaultValue = param.DefaultValue ?? "-"; + var description = ToolOutputHelper.TruncateSummary(param.Description, 80); + var enumHint = param.EnumValues.Length > 0 + ? $" Values: {string.Join(", ", param.EnumValues.Take(5))}{(param.EnumValues.Length > 5 ? "..." : "")}" + : ""; + sb.AppendLine($"| {param.Name} | `{param.Type}` | {defaultValue} | {description}{enumHint} |"); + } + + sb.AppendLine(); + } + + private static void AppendEvents(StringBuilder sb, Models.ComponentDetails details) + { + if (details.Events.Count == 0) + { + return; + } + + sb.AppendLine("## Events"); + sb.AppendLine(); + sb.AppendLine("| Name | Type | Description |"); + sb.AppendLine("|------|------|-------------|"); + + foreach (var evt in details.Events) + { + sb.AppendLine($"| {evt.Name} | `{evt.Type}` | {ToolOutputHelper.TruncateSummary(evt.Description, 80)} |"); + } + + sb.AppendLine(); + } + + private static void AppendMethods(StringBuilder sb, Models.ComponentDetails details) + { + if (details.Methods.Count == 0) + { + return; + } + + sb.AppendLine("## Methods"); + sb.AppendLine(); + + foreach (var method in details.Methods) + { + sb.AppendLine($"### {method.Name}"); + sb.AppendLine(); + sb.AppendLine($"```csharp"); + sb.AppendLine($"{method.Signature}"); + sb.AppendLine($"```"); + + if (!string.IsNullOrEmpty(method.Description)) + { + sb.AppendLine(); + sb.AppendLine(method.Description); + } + + sb.AppendLine(); + } + } + + private static void AppendBasicExample(StringBuilder sb, Models.ComponentDetails details) + { + sb.AppendLine($"# {details.Component.Name} Usage Examples"); + sb.AppendLine(); + sb.AppendLine("## Basic Usage"); + sb.AppendLine(); + sb.AppendLine("```razor"); + sb.Append($"<{details.Component.Name}"); + + if (details.Component.IsGeneric) + { + sb.Append(" TItem=\"YourItemType\""); + } + + var commonParams = details.Parameters + .Where(p => !p.IsInherited && ToolOutputHelper.IsCommonExampleParam(p.Name)) + .Take(3) + .ToList(); + + foreach (var param in commonParams) + { + sb.Append($" {param.Name}=\"{ToolOutputHelper.GetExampleValue(param)}\""); + } + + var hasChildContent = details.Parameters.Any(p => + p.Name.Equals("ChildContent", StringComparison.OrdinalIgnoreCase) || + p.Type.Contains("RenderFragment")); + + if (hasChildContent) + { + sb.AppendLine(">"); + sb.AppendLine(" "); + sb.AppendLine($""); + } + else + { + sb.AppendLine(" />"); + } + + sb.AppendLine("```"); + sb.AppendLine(); + } + + private static void AppendEventHandlingExample(StringBuilder sb, Models.ComponentDetails details) + { + if (details.Events.Count == 0) + { + return; + } + + sb.AppendLine("## With Event Handling"); + sb.AppendLine(); + sb.AppendLine("```razor"); + sb.AppendLine("@code {"); + + foreach (var evt in details.Events.Take(2)) + { + var eventType = ToolOutputHelper.ExtractEventType(evt.Type); + sb.AppendLine($" private void On{evt.Name.Replace("On", "")}({eventType} args)"); + sb.AppendLine(" {"); + sb.AppendLine(" // Handle event"); + sb.AppendLine(" }"); + } + + sb.AppendLine("}"); + sb.AppendLine("```"); + sb.AppendLine(); + } + + private static void AppendCommonParameters(StringBuilder sb, Models.ComponentDetails details) + { + sb.AppendLine("## Common Parameters"); + sb.AppendLine(); + + var importantParams = details.Parameters + .Where(p => !p.IsInherited) + .Take(5); + + foreach (var param in importantParams) + { + sb.AppendLine($"- `{param.Name}` ({param.Type}): {ToolOutputHelper.TruncateSummary(param.Description, 60)}"); + } + } +} diff --git a/src/Tools/McpServer/Tools/ComponentListTools.cs b/src/Tools/McpServer/Tools/ComponentListTools.cs new file mode 100644 index 0000000000..f6453654b8 --- /dev/null +++ b/src/Tools/McpServer/Tools/ComponentListTools.cs @@ -0,0 +1,131 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +/// +/// MCP tools for listing and searching Fluent UI Blazor components. +/// +[McpServerToolType] +public class ComponentListTools +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + /// The documentation service. + public ComponentListTools(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + [McpServerTool] + [Description("Lists all available Fluent UI Blazor components with their names and brief descriptions. Use this to discover what components are available in the library.")] + public string ListComponents( + [Description("Optional: Filter components by category (e.g., 'Button', 'Input', 'Dialog', 'DataGrid', 'Layout', 'Menu', 'Navigation', 'Card', 'Icon')")] + string? category = null) + { + var components = string.IsNullOrEmpty(category) + ? _documentationService.GetAllComponents() + : _documentationService.GetComponentsByCategory(category); + + if (components.Count == 0) + { + return category != null + ? $"No components found in category '{category}'. Use ListCategories() to see available categories." + : "No components found."; + } + + var sb = new StringBuilder(); + sb.AppendLine($"# Fluent UI Blazor Components ({components.Count} found)"); + sb.AppendLine(); + + var groupedByCategory = components.GroupBy(c => c.Category).OrderBy(g => g.Key); + + foreach (var group in groupedByCategory) + { + sb.AppendLine($"## {group.Key}"); + sb.AppendLine(); + + foreach (var component in group.OrderBy(c => c.Name)) + { + var genericIndicator = component.IsGeneric ? "" : ""; + sb.AppendLine($"- **{component.Name}{genericIndicator}**: {ToolOutputHelper.TruncateSummary(component.Summary, 100)}"); + } + + sb.AppendLine(); + } + + return sb.ToString(); + } + + [McpServerTool] + [Description("Searches for Fluent UI Blazor components by name or description. Use this when you're looking for a component that does something specific.")] + public string SearchComponents( + [Description("The term to search for in component names and descriptions (e.g., 'button', 'grid', 'input', 'dialog').")] + string searchTerm) + { + if (string.IsNullOrWhiteSpace(searchTerm)) + { + return "Please provide a search term."; + } + + var components = _documentationService.SearchComponents(searchTerm); + + if (components.Count == 0) + { + return $"No components found matching '{searchTerm}'. Try a different search term or use ListComponents() to see all components."; + } + + var sb = new StringBuilder(); + sb.AppendLine($"# Search Results for '{searchTerm}' ({components.Count} found)"); + sb.AppendLine(); + + foreach (var component in components) + { + var genericIndicator = component.IsGeneric ? "" : ""; + sb.AppendLine($"## {component.Name}{genericIndicator}"); + sb.AppendLine(); + sb.AppendLine($"**Category:** {component.Category}"); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(component.Summary)) + { + sb.AppendLine(component.Summary); + sb.AppendLine(); + } + } + + return sb.ToString(); + } + + [McpServerTool] + [Description("Lists all available component categories to help navigate the Fluent UI Blazor library.")] + public string ListCategories() + { + var categories = _documentationService.GetCategories(); + var allComponents = _documentationService.GetAllComponents(); + + var sb = new StringBuilder(); + sb.AppendLine("# Fluent UI Blazor Component Categories"); + sb.AppendLine(); + + foreach (var category in categories) + { + var count = allComponents.Count(c => c.Category == category); + sb.AppendLine($"- **{category}** ({count} components)"); + } + + sb.AppendLine(); + sb.AppendLine("Use `ListComponents(category: \"CategoryName\")` to see components in a specific category."); + + return sb.ToString(); + } +} diff --git a/src/Tools/McpServer/Tools/EnumTools.cs b/src/Tools/McpServer/Tools/EnumTools.cs new file mode 100644 index 0000000000..115f1e6934 --- /dev/null +++ b/src/Tools/McpServer/Tools/EnumTools.cs @@ -0,0 +1,186 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +/// +/// MCP tools for accessing Fluent UI Blazor enum documentation. +/// +[McpServerToolType] +public class EnumTools +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + /// The documentation service. + public EnumTools(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + [McpServerTool] + [Description("Gets information about a specific enum type used in Fluent UI Blazor components, including all possible values.")] + public string GetEnumValues( + [Description("The name of the enum type (e.g., 'Appearance', 'Color', 'Size', 'Orientation').")] + string enumName, + [Description("Optional: Filter to show only values matching this search term.")] + string? filter = null) + { + var enumInfo = _documentationService.GetEnumDetails(enumName); + + if (enumInfo == null) + { + return $"Enum '{enumName}' not found. Use ListEnums() to see all available enums."; + } + + var sb = new StringBuilder(); + sb.AppendLine($"# {enumInfo.Name}"); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(enumInfo.Description)) + { + sb.AppendLine(enumInfo.Description); + sb.AppendLine(); + } + + var values = enumInfo.Values.AsEnumerable(); + if (!string.IsNullOrWhiteSpace(filter)) + { + values = values.Where(v => + v.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || + v.Description.Contains(filter, StringComparison.OrdinalIgnoreCase)); + } + + var valuesList = values.ToList(); + if (valuesList.Count == 0) + { + return $"No values found matching filter '{filter}' in enum '{enumName}'."; + } + + sb.AppendLine("## Values"); + sb.AppendLine(); + sb.AppendLine("| Name | Value | Description |"); + sb.AppendLine("|------|-------|-------------|"); + + foreach (var value in valuesList) + { + sb.AppendLine($"| {value.Name} | {value.Value} | {value.Description} |"); + } + + return sb.ToString(); + } + + [McpServerTool] + [Description("Lists all enum types used by a specific Fluent UI Blazor component, showing which property/parameter uses each enum.")] + public string GetComponentEnums( + [Description("The name of the component (e.g., 'FluentButton', 'FluentDataGrid', 'FluentTextField'). You can omit the 'Fluent' prefix.")] + string componentName) + { + var enumsByProperty = _documentationService.GetEnumsForComponent(componentName); + + if (enumsByProperty.Count == 0) + { + var details = _documentationService.GetComponentDetails(componentName); + if (details == null) + { + return $"Component '{componentName}' not found. Use ListComponents() to see all available components."; + } + + return $"No enum types found for component '{componentName}'."; + } + + var sb = new StringBuilder(); + sb.AppendLine($"# Enum Types for {componentName}"); + sb.AppendLine(); + sb.AppendLine($"Found {enumsByProperty.Count} enum type(s) used by this component:"); + sb.AppendLine(); + + foreach (var kvp in enumsByProperty.OrderBy(k => k.Key)) + { + var propertyName = kvp.Key; + var enumInfo = kvp.Value; + + sb.AppendLine($"## {propertyName} → {enumInfo.Name}"); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(enumInfo.Description)) + { + sb.AppendLine(enumInfo.Description); + sb.AppendLine(); + } + + sb.AppendLine("| Value | Description |"); + sb.AppendLine("|-------|-------------|"); + + foreach (var value in enumInfo.Values.Take(10)) + { + sb.AppendLine($"| {value.Name} | {value.Description} |"); + } + + if (enumInfo.Values.Count > 10) + { + sb.AppendLine($"| ... | *{enumInfo.Values.Count - 10} more values* |"); + } + + sb.AppendLine(); + } + + sb.AppendLine("Use `GetEnumValues(enumName: \"EnumName\")` to see all values for a specific enum."); + + return sb.ToString(); + } + + [McpServerTool] + [Description("Lists all available enum types used in the Fluent UI Blazor library. Can optionally filter by a search term.")] + public string ListEnums( + [Description("Optional: Filter enums by name (e.g., 'Color', 'Size', 'Appearance').")] + string? filter = null) + { + var enums = _documentationService.GetAllEnums(); + + if (!string.IsNullOrWhiteSpace(filter)) + { + enums = enums.Where(e => + e.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || + e.Description.Contains(filter, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + if (enums.Count == 0) + { + return filter != null + ? $"No enums found matching '{filter}'." + : "No enums found."; + } + + var sb = new StringBuilder(); + sb.AppendLine($"# Fluent UI Blazor Enum Types ({enums.Count} found)"); + sb.AppendLine(); + + foreach (var enumInfo in enums) + { + var valueCount = enumInfo.Values.Count; + var previewValues = string.Join(", ", enumInfo.Values.Take(4).Select(v => v.Name)); + if (valueCount > 4) + { + previewValues += "..."; + } + + sb.AppendLine($"- **{enumInfo.Name}** ({valueCount} values): {previewValues}"); + } + + sb.AppendLine(); + sb.AppendLine("Use `GetEnumValues(enumName: \"EnumName\")` to see all values for a specific enum."); + sb.AppendLine("Use `GetComponentEnums(componentName: \"ComponentName\")` to see enums used by a specific component."); + + return sb.ToString(); + } +} diff --git a/src/Tools/McpServer/Tools/GuideTools.cs b/src/Tools/McpServer/Tools/GuideTools.cs new file mode 100644 index 0000000000..e06a300ca8 --- /dev/null +++ b/src/Tools/McpServer/Tools/GuideTools.cs @@ -0,0 +1,159 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +/// +/// MCP Tools for accessing documentation guides. +/// +[McpServerToolType] +public class GuideTools +{ + private readonly DocumentationGuideService _guideService; + + /// + /// Initializes a new instance of the class. + /// + public GuideTools(DocumentationGuideService guideService) + { + _guideService = guideService; + } + + [McpServerTool(Name = "GetGuide")] + [Description("Gets a specific documentation guide by name. Available guides: installation, defaultvalues, whatsnew, migration, localization, styles.")] + public string GetGuide( + [Description("The guide name: 'installation', 'defaultvalues', 'whatsnew', 'migration', 'localization', or 'styles'")] + string guideName) + { + var content = _guideService.GetGuideContent(guideName); + + if (content == null) + { + var guides = _guideService.GetAllGuides(); + var available = guides.Count > 0 + ? string.Join(", ", guides.Select(g => g.Key)) + : "installation, defaultvalues, whatsnew, migration, localization, styles"; + + return $"Guide '{guideName}' not found.\n\nAvailable guides: {available}"; + } + + return content; + } + + [McpServerTool(Name = "SearchGuides")] + [Description("Searches documentation guides for specific content or topics.")] + public string SearchGuides( + [Description("The search term to find in documentation guides")] + string searchTerm) + { + var guides = _guideService.GetAllGuides(); + var results = new StringBuilder(); + var matchCount = 0; + + results.AppendLine($"# Search Results for: \"{searchTerm}\""); + results.AppendLine(); + + foreach (var guide in guides) + { + var content = _guideService.GetGuideContent(guide.Key); + if (content == null) + { + continue; + } + + // Case-insensitive search + var index = content.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase); + if (index >= 0) + { + matchCount++; + results.AppendLine($"## Found in: {guide.Title}"); + results.AppendLine(); + results.AppendLine($"**Guide:** `fluentui://guide/{guide.Key}`"); + results.AppendLine(); + + // Extract context around the match (up to 200 chars before and after) + var start = Math.Max(0, index - 100); + var length = Math.Min(300, content.Length - start); + var excerpt = content.Substring(start, length); + + // Clean up excerpt + if (start > 0) + { + excerpt = "..." + excerpt; + } + + if (start + length < content.Length) + { + excerpt += "..."; + } + + results.AppendLine("**Excerpt:**"); + results.AppendLine(); + results.AppendLine($"> {excerpt.Replace("\n", " ").Replace("\r", "")}"); + results.AppendLine(); + } + } + + if (matchCount == 0) + { + results.AppendLine("No matches found in documentation guides."); + results.AppendLine(); + results.AppendLine("**Available guides:**"); + foreach (var guide in guides) + { + results.AppendLine($"- {guide.Title} (`{guide.Key}`)"); + } + } + else + { + results.AppendLine($"---"); + results.AppendLine($"Found {matchCount} guide(s) containing \"{searchTerm}\"."); + } + + return results.ToString(); + } + + [McpServerTool(Name = "ListGuides")] + [Description("Lists all available documentation guides with descriptions.")] + public string ListGuides() + { + var guides = _guideService.GetAllGuides(); + + var sb = new StringBuilder(); + sb.AppendLine("# Available Documentation Guides"); + sb.AppendLine(); + + if (guides.Count == 0) + { + sb.AppendLine("No guides currently available."); + sb.AppendLine(); + sb.AppendLine("Standard guides include:"); + sb.AppendLine("- **installation** - NuGet packages and project setup"); + sb.AppendLine("- **defaultvalues** - Configure default component values"); + sb.AppendLine("- **whatsnew** - Release notes and changes"); + sb.AppendLine("- **migration** - Migration guide from v4 to v5"); + sb.AppendLine("- **localization** - Translate component texts"); + sb.AppendLine("- **styles** - CSS, design tokens, and theming"); + return sb.ToString(); + } + + sb.AppendLine("| Guide | Title | Description |"); + sb.AppendLine("|-------|-------|-------------|"); + sb.AppendLine("| `installation` | Installation | NuGet packages, setup, and configuration |"); + sb.AppendLine("| `defaultvalues` | Default Values | Configure default component values globally |"); + sb.AppendLine("| `whatsnew` | What's New | Release notes and latest changes |"); + sb.AppendLine("| `migration` | Migration to v5 | Breaking changes and migration steps |"); + sb.AppendLine("| `localization` | Localization | Translate and localize component texts |"); + sb.AppendLine("| `styles` | Styles | CSS styling, design tokens, theming |"); + sb.AppendLine(); + sb.AppendLine("Use `GetGuide(guideName)` to retrieve the full content of a guide."); + + return sb.ToString(); + } +} diff --git a/src/Tools/McpServer/Tools/ToolOutputHelper.cs b/src/Tools/McpServer/Tools/ToolOutputHelper.cs new file mode 100644 index 0000000000..e15678adab --- /dev/null +++ b/src/Tools/McpServer/Tools/ToolOutputHelper.cs @@ -0,0 +1,96 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Models; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +/// +/// Helper methods for formatting MCP tool output. +/// +internal static class ToolOutputHelper +{ + /// + /// Truncates a summary to the specified maximum length. + /// + public static string TruncateSummary(string? summary, int maxLength) + { + if (string.IsNullOrEmpty(summary)) + { + return "-"; + } + + if (summary.Length <= maxLength) + { + return summary; + } + + return summary[..(maxLength - 3)] + "..."; + } + + /// + /// Checks if a parameter name is common enough to include in examples. + /// + public static bool IsCommonExampleParam(string paramName) + { + var commonParams = new[] + { + "Id", "Label", "Placeholder", "Value", "Disabled", "ReadOnly", + "Appearance", "Size", "Color", "IconStart", "IconEnd" + }; + + return commonParams.Contains(paramName, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Gets an example value for a property based on its type. + /// + public static string GetExampleValue(PropertyInfo param) + { + if (param.EnumValues.Length > 0) + { + return $"@{param.Type}.{param.EnumValues[0]}"; + } + + return param.Type switch + { + "string" => $"your-{param.Name.ToLowerInvariant()}", + "bool" => "true", + "int" => "42", + _ => "..." + }; + } + + /// + /// Extracts the type parameter from an EventCallback type. + /// + public static string ExtractEventType(string eventCallbackType) + { + if (eventCallbackType.StartsWith("EventCallback<") && eventCallbackType.EndsWith(">")) + { + return eventCallbackType[14..^1]; + } + + return "EventArgs"; + } + + /// + /// Appends a markdown header. + /// + public static void AppendHeader(StringBuilder sb, string title, int level = 1) + { + sb.AppendLine($"{new string('#', level)} {title}"); + sb.AppendLine(); + } + + /// + /// Appends a markdown table header. + /// + public static void AppendTableHeader(StringBuilder sb, params string[] columns) + { + sb.AppendLine($"| {string.Join(" | ", columns)} |"); + sb.AppendLine($"|{string.Join("|", columns.Select(_ => "------"))}|"); + } +} diff --git a/src/Tools/McpServer/Tools/VersionTools.cs b/src/Tools/McpServer/Tools/VersionTools.cs new file mode 100644 index 0000000000..0c15243b31 --- /dev/null +++ b/src/Tools/McpServer/Tools/VersionTools.cs @@ -0,0 +1,178 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using ModelContextProtocol.Server; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +/// +/// MCP tools for version information and compatibility checking. +/// +[McpServerToolType] +public class VersionTools +{ + private readonly FluentUIDocumentationService _documentationService; + + /// + /// Initializes a new instance of the class. + /// + /// The documentation service. + public VersionTools(FluentUIDocumentationService documentationService) + { + _documentationService = documentationService; + } + + /// + /// Gets version information about the MCP Server and the documented components. + /// + [McpServerTool(Name = "GetVersionInfo")] + [Description("Get version information about the MCP Server and the Fluent UI Blazor components it documents.")] + public string GetVersionInfo() + { + var sb = new StringBuilder(); + + sb.AppendLine("# Fluent UI Blazor MCP Server - Version Information"); + sb.AppendLine(); + sb.AppendLine("| Property | Value |"); + sb.AppendLine("|----------|-------|"); + sb.AppendLine($"| MCP Server Version | {FluentUIDocumentationService.McpServerVersion} |"); + sb.AppendLine($"| Components Version | {_documentationService.ComponentsVersion} |"); + sb.AppendLine($"| Documentation Generated | {_documentationService.DocumentationGeneratedDate} |"); + sb.AppendLine($"| Documentation Available | {(_documentationService.IsAvailable ? "Yes" : "No")} |"); + sb.AppendLine(); + + if (_documentationService.IsAvailable) + { + var components = _documentationService.GetAllComponents(); + var enums = _documentationService.GetAllEnums(); + var categories = _documentationService.GetCategories(); + + sb.AppendLine("## Documentation Statistics"); + sb.AppendLine(); + sb.AppendLine($"- **Components**: {components.Count}"); + sb.AppendLine($"- **Enums**: {enums.Count}"); + sb.AppendLine($"- **Categories**: {categories.Count}"); + } + + sb.AppendLine(); + sb.AppendLine("## Compatibility"); + sb.AppendLine(); + sb.AppendLine($"This MCP Server provides documentation for **Microsoft.FluentUI.AspNetCore.Components** version **{_documentationService.ComponentsVersion}**."); + sb.AppendLine(); + sb.AppendLine("For best results, ensure your project uses the same version of the NuGet package:"); + sb.AppendLine(); + sb.AppendLine("```shell"); + sb.AppendLine($"dotnet add package Microsoft.FluentUI.AspNetCore.Components --version {_documentationService.ComponentsVersion}"); + sb.AppendLine("```"); + + return sb.ToString(); + } + + /// + /// Checks if a specific version is compatible with this MCP Server's documentation. + /// + [McpServerTool(Name = "CheckVersionCompatibility")] + [Description("Check if a specific NuGet package version is compatible with this MCP Server's documentation.")] + public string CheckVersionCompatibility( + [Description("The version to check (e.g., '5.0.0', '4.10.3')")] string version) + { + var expectedVersion = _documentationService.ComponentsVersion; + var sb = new StringBuilder(); + + sb.AppendLine("# Version Compatibility Check"); + sb.AppendLine(); + sb.AppendLine($"- **Your Version**: {version}"); + sb.AppendLine($"- **MCP Documentation Version**: {expectedVersion}"); + sb.AppendLine(); + + var (isCompatible, message) = CompareVersions(version, expectedVersion); + + if (isCompatible) + { + sb.AppendLine("## ✅ Compatible"); + sb.AppendLine(); + sb.AppendLine(message); + } + else + { + sb.AppendLine("## ⚠️ Compatibility Warning"); + sb.AppendLine(); + sb.AppendLine(message); + sb.AppendLine(); + sb.AppendLine("### Recommended Actions"); + sb.AppendLine(); + sb.AppendLine("**Option 1**: Update your NuGet package to match the MCP Server:"); + sb.AppendLine("```shell"); + sb.AppendLine($"dotnet add package Microsoft.FluentUI.AspNetCore.Components --version {expectedVersion}"); + sb.AppendLine("```"); + sb.AppendLine(); + sb.AppendLine("**Option 2**: Update the MCP Server to match your package version:"); + sb.AppendLine("```shell"); + sb.AppendLine($"dotnet tool update --global Microsoft.FluentUI.AspNetCore.Components.McpServer --version {version}"); + sb.AppendLine("```"); + } + + return sb.ToString(); + } + + private static (bool IsCompatible, string Message) CompareVersions(string userVersion, string expectedVersion) + { + // Clean versions + var userClean = CleanVersion(userVersion); + var expectedClean = CleanVersion(expectedVersion); + + if (!Version.TryParse(userClean, out var user) || + !Version.TryParse(expectedClean, out var expected)) + { + return (false, $"Unable to parse version '{userVersion}'. Please use semantic versioning (e.g., '5.0.0')."); + } + + // Exact match + if (user.Major == expected.Major && user.Minor == expected.Minor) + { + if (user.Build == expected.Build) + { + return (true, "Perfect match! Your version exactly matches the MCP Server documentation."); + } + + return (true, $"Minor patch difference. Your version ({userVersion}) is close to the documented version ({expectedVersion}). Most features should work correctly."); + } + + // Same major version + if (user.Major == expected.Major) + { + return (false, $"Minor version mismatch. Your version ({userVersion}) may have different features than documented ({expectedVersion})."); + } + + // Different major version + return (false, $"Major version mismatch! Your version ({userVersion}) is significantly different from the documented version ({expectedVersion}). Breaking changes are likely."); + } + + private static string CleanVersion(string version) + { + if (string.IsNullOrEmpty(version)) + { + return "0.0.0"; + } + + // Remove prerelease suffix + var dashIndex = version.IndexOf('-'); + if (dashIndex > 0) + { + version = version[..dashIndex]; + } + + // Ensure 3 parts + var parts = version.Split('.'); + if (parts.Length < 3) + { + version = string.Join(".", parts.Concat(Enumerable.Repeat("0", 3 - parts.Length))); + } + + return version; + } +} diff --git a/tests/Tools/McpServer.Tests/DocumentationGuideServiceTests.cs b/tests/Tools/McpServer.Tests/DocumentationGuideServiceTests.cs new file mode 100644 index 0000000000..a2215fa20f --- /dev/null +++ b/tests/Tools/McpServer.Tests/DocumentationGuideServiceTests.cs @@ -0,0 +1,127 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Services; + +/// +/// Tests for . +/// +public class DocumentationGuideServiceTests +{ + private readonly DocumentationGuideService _service; + + public DocumentationGuideServiceTests() + { + _service = new DocumentationGuideService(); + } + + [Fact] + public void GetAllGuides_ReturnsNonEmptyList() + { + // Act + var guides = _service.GetAllGuides(); + + // Assert + Assert.NotEmpty(guides); + } + + [Theory] + [InlineData("installation")] + [InlineData("defaultvalues")] + [InlineData("migration")] + [InlineData("localization")] + [InlineData("styles")] + public void GetAllGuides_ContainsExpectedGuides(string guideKey) + { + // Act + var guides = _service.GetAllGuides(); + + // Assert + Assert.Contains(guides, g => g.Key.Equals(guideKey, StringComparison.OrdinalIgnoreCase)); + } + + [Theory] + [InlineData("installation")] + [InlineData("defaultvalues")] + [InlineData("migration")] + [InlineData("localization")] + [InlineData("styles")] + public void GetGuideContent_ReturnsContentForKnownGuides(string guideKey) + { + // Act + var content = _service.GetGuideContent(guideKey); + + // Assert + Assert.NotNull(content); + Assert.NotEmpty(content); + } + + [Fact] + public void GetGuideContent_ReturnsNullForUnknownGuide() + { + // Act + var content = _service.GetGuideContent("nonexistent-guide"); + + // Assert + Assert.Null(content); + } + + [Fact] + public void GetGuideContent_IsCaseInsensitive() + { + // Act + var contentLower = _service.GetGuideContent("installation"); + var contentUpper = _service.GetGuideContent("INSTALLATION"); + + // Assert + Assert.Equal(contentLower, contentUpper); + } + + [Fact] + public void GetFullMigrationGuide_ReturnsContent() + { + // Act + var content = _service.GetFullMigrationGuide(); + + // Assert + Assert.NotNull(content); + Assert.NotEmpty(content); + } + + [Fact] + public void GetGuide_ReturnsGuideForKnownKey() + { + // Act + var guide = _service.GetGuide("installation"); + + // Assert + Assert.NotNull(guide); + Assert.Equal("installation", guide.Key); + } + + [Fact] + public void GetGuide_ReturnsNullForUnknownKey() + { + // Act + var guide = _service.GetGuide("nonexistent"); + + // Assert + Assert.Null(guide); + } + + [Fact] + public void GetGuide_IsCaseInsensitive() + { + // Act + var guideLower = _service.GetGuide("installation"); + var guideUpper = _service.GetGuide("INSTALLATION"); + + // Assert + Assert.NotNull(guideLower); + Assert.NotNull(guideUpper); + Assert.Equal(guideLower.Key, guideUpper.Key); + } +} diff --git a/tests/Tools/McpServer.Tests/FluentUIDocumentationServiceTests.cs b/tests/Tools/McpServer.Tests/FluentUIDocumentationServiceTests.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/Tools/McpServer.Tests/GlobalUsings.cs b/tests/Tools/McpServer.Tests/GlobalUsings.cs new file mode 100644 index 0000000000..c2063e00c3 --- /dev/null +++ b/tests/Tools/McpServer.Tests/GlobalUsings.cs @@ -0,0 +1,5 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +global using Xunit; diff --git a/tests/Tools/McpServer.Tests/JsonDocumentationFinderMoreTests.cs b/tests/Tools/McpServer.Tests/JsonDocumentationFinderMoreTests.cs new file mode 100644 index 0000000000..dd0dfb54c3 --- /dev/null +++ b/tests/Tools/McpServer.Tests/JsonDocumentationFinderMoreTests.cs @@ -0,0 +1,89 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests; + +/// +/// Additional tests for to improve coverage. +/// +public class JsonDocumentationFinderMoreTests +{ + [Fact] + public void Find_CalledMultipleTimes_ReturnsSameResult() + { + // Act + var result1 = JsonDocumentationFinder.Find(); + var result2 = JsonDocumentationFinder.Find(); + var result3 = JsonDocumentationFinder.Find(); + + // Assert + Assert.Equal(result1, result2); + Assert.Equal(result2, result3); + } + + [Fact] + public void Find_ReturnsNullOrValidPath() + { + // Act + var result = JsonDocumentationFinder.Find(); + + // Assert + if (result != null) + { + Assert.True(Path.IsPathRooted(result)); + Assert.True(File.Exists(result)); + Assert.EndsWith("FluentUIComponentsDocumentation.json", result); + } + } + + [Fact] + public void Find_ChecksMultiplePaths() + { + // Act + var result = JsonDocumentationFinder.Find(); + + // Assert - Just verify it doesn't throw + Assert.True(result == null || result != null); + } + + [Fact] + public void Find_WithFileInCurrentDirectory_FindsIt() + { + // This test verifies the finder checks AppContext.BaseDirectory + // Act + var result = JsonDocumentationFinder.Find(); + + // Assert + if (result != null) + { + Assert.Contains("FluentUIComponentsDocumentation.json", result); + } + } + + [Fact] + public void Find_LogsToStandardError() + { + // Arrange + var originalError = Console.Error; + try + { + using var sw = new StringWriter(); + Console.SetError(sw); + + // Act + _ = JsonDocumentationFinder.Find(); + + // Assert + var output = sw.ToString(); + Assert.True( + output.Contains("Found JSON documentation") || + output.Contains("No external JSON documentation"), + "Should log to stderr"); + } + finally + { + Console.SetError(originalError); + } + } +} diff --git a/tests/Tools/McpServer.Tests/JsonDocumentationFinderTests.cs b/tests/Tools/McpServer.Tests/JsonDocumentationFinderTests.cs new file mode 100644 index 0000000000..31f35e4dd1 --- /dev/null +++ b/tests/Tools/McpServer.Tests/JsonDocumentationFinderTests.cs @@ -0,0 +1,62 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests; + +/// +/// Tests for . +/// +public class JsonDocumentationFinderTests +{ + [Fact] + public void Find_ReturnsPathOrNull() + { + // Act + var result = JsonDocumentationFinder.Find(); + + // Assert + // Should either return a valid path or null (for embedded resource) + if (result != null) + { + Assert.True(File.Exists(result), $"Returned path should exist: {result}"); + } + } + + [Fact] + public void Find_WhenFileExists_ReturnsFullPath() + { + // Act + var result = JsonDocumentationFinder.Find(); + + // Assert + if (result != null) + { + Assert.True(Path.IsPathRooted(result), "Should return a full path"); + } + } + + [Fact] + public void Find_ReturnsConsistentResult() + { + // Act + var result1 = JsonDocumentationFinder.Find(); + var result2 = JsonDocumentationFinder.Find(); + + // Assert + Assert.Equal(result1, result2); + } + + [Fact] + public void Find_WhenFileExists_ContainsExpectedFileName() + { + // Act + var result = JsonDocumentationFinder.Find(); + + // Assert + if (result != null) + { + Assert.Contains("FluentUIComponentsDocumentation.json", result); + } + } +} diff --git a/tests/Tools/McpServer.Tests/Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.csproj b/tests/Tools/McpServer.Tests/Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.csproj new file mode 100644 index 0000000000..d4fb6b32ed --- /dev/null +++ b/tests/Tools/McpServer.Tests/Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.csproj @@ -0,0 +1,34 @@ + + + + net9.0 + enable + enable + latest + false + Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests + Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/tests/Tools/McpServer.Tests/Prompts/CheckVersionCompatibilityPromptTests.cs b/tests/Tools/McpServer.Tests/Prompts/CheckVersionCompatibilityPromptTests.cs new file mode 100644 index 0000000000..e69324c2d2 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/CheckVersionCompatibilityPromptTests.cs @@ -0,0 +1,137 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +/// +/// Tests for . +/// +public class CheckVersionCompatibilityPromptTests +{ + private readonly CheckVersionCompatibilityPrompt _prompt; + private readonly FluentUIDocumentationService _documentationService; + + public CheckVersionCompatibilityPromptTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _prompt = new CheckVersionCompatibilityPrompt(_documentationService); + } + + [Fact] + public void CheckVersionCompatibility_WithExactMatch_ReturnsPerfectMatch() + { + // Arrange + var expectedVersion = _documentationService.ComponentsVersion; + + // Act + var result = _prompt.CheckVersionCompatibility(expectedVersion); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("Perfect Match", result.Text); + Assert.Contains("✅", result.Text); + } + + [Fact] + public void CheckVersionCompatibility_WithMajorDifference_ReturnsWarning() + { + // Arrange + var differentMajor = "1.0.0"; + + // Act + var result = _prompt.CheckVersionCompatibility(differentMajor); + + // Assert + Assert.NotNull(result); + Assert.Contains("Major Version Mismatch", result.Text); + Assert.Contains("❌", result.Text); + } + + [Fact] + public void CheckVersionCompatibility_ContainsVersionTable() + { + // Arrange + var version = "5.0.0"; + + // Act + var result = _prompt.CheckVersionCompatibility(version); + + // Assert + Assert.Contains("MCP Server", result.Text); + Assert.Contains("Expected Components Version", result.Text); + Assert.Contains("Your Installed Version", result.Text); + } + + [Fact] + public void CheckVersionCompatibility_ContainsTask() + { + // Arrange + var version = "5.0.0"; + + // Act + var result = _prompt.CheckVersionCompatibility(version); + + // Assert + Assert.Contains("Task", result.Text); + Assert.Contains("Explain any potential issues", result.Text); + } + + [Fact] + public void CheckVersionCompatibility_WithPreReleaseVersion_HandlesCorrectly() + { + // Arrange + var preReleaseVersion = "5.0.0-preview.1"; + + // Act + var result = _prompt.CheckVersionCompatibility(preReleaseVersion); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + } + + [Theory] + [InlineData("5.0.0")] + [InlineData("4.10.3")] + [InlineData("6.0.0")] + [InlineData("5.1.0")] + public void CheckVersionCompatibility_WithVariousVersions_ReturnsValidPrompt(string version) + { + // Act + var result = _prompt.CheckVersionCompatibility(version); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("Version Compatibility Check", result.Text); + } + + [Fact] + public void CheckVersionCompatibility_WithMinorDifference_ReturnsMinorWarning() + { + // Arrange + var expectedVersion = _documentationService.ComponentsVersion; + var parts = expectedVersion.Split('.'); + if (parts.Length >= 2 && int.TryParse(parts[1], out var minor)) + { + var differentMinor = $"{parts[0]}.{minor + 1}.0"; + + // Act + var result = _prompt.CheckVersionCompatibility(differentMinor); + + // Assert + Assert.NotNull(result); + // Should be either exact match or minor difference + Assert.True( + result.Text.Contains("Minor Version Difference") || + result.Text.Contains("Perfect Match") || + result.Text.Contains("Major Version Mismatch")); + } + } +} diff --git a/tests/Tools/McpServer.Tests/Prompts/CompareComponentsPromptTests.cs b/tests/Tools/McpServer.Tests/Prompts/CompareComponentsPromptTests.cs new file mode 100644 index 0000000000..54356eaf3d --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/CompareComponentsPromptTests.cs @@ -0,0 +1,138 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +/// +/// Tests for . +/// +public class CompareComponentsPromptTests +{ + private readonly CompareComponentsPrompt _prompt; + private readonly FluentUIDocumentationService _documentationService; + + public CompareComponentsPromptTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _prompt = new CompareComponentsPrompt(_documentationService); + } + + [Fact] + public void CompareComponents_WithTwoComponents_ReturnsNonEmptyMessage() + { + // Arrange + var componentNames = "FluentButton,FluentAnchor"; + + // Act + var result = _prompt.CompareComponents(componentNames); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.NotEmpty(result.Text); + } + + [Fact] + public void CompareComponents_ContainsBothComponents() + { + // Arrange + var componentNames = "FluentButton,FluentCard"; + + // Act + var result = _prompt.CompareComponents(componentNames); + + // Assert + Assert.Contains("FluentButton", result.Text); + Assert.Contains("FluentCard", result.Text); + } + + [Fact] + public void CompareComponents_ContainsComparisonTitle() + { + // Act + var result = _prompt.CompareComponents("FluentButton,FluentAnchor"); + + // Assert + Assert.Contains("Component Comparison", result.Text); + } + + [Fact] + public void CompareComponents_ContainsTaskSection() + { + // Act + var result = _prompt.CompareComponents("FluentButton,FluentAnchor"); + + // Assert + Assert.Contains("Task", result.Text); + Assert.Contains("key differences", result.Text); + } + + [Fact] + public void CompareComponents_WithUnknownComponent_HandlesGracefully() + { + // Arrange + var componentNames = "FluentButton,NonExistentComponent"; + + // Act + var result = _prompt.CompareComponents(componentNames); + + // Assert + Assert.NotNull(result); + Assert.Contains("not found", result.Text); + } + + [Fact] + public void CompareComponents_IncludesCategory() + { + // Act + var result = _prompt.CompareComponents("FluentButton,FluentCard"); + + // Assert + Assert.Contains("Category", result.Text); + } + + [Fact] + public void CompareComponents_IncludesParametersCount() + { + // Act + var result = _prompt.CompareComponents("FluentButton,FluentCard"); + + // Assert + Assert.Contains("Parameters", result.Text); + } + + [Theory] + [InlineData("FluentButton,FluentAnchor")] + [InlineData("FluentTextField,FluentTextArea")] + [InlineData("FluentDialog,FluentDrawer")] + public void CompareComponents_WithVariousPairs_GeneratesValidPrompt(string componentNames) + { + // Act + var result = _prompt.CompareComponents(componentNames); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("Comparison", result.Text); + } + + [Fact] + public void CompareComponents_WithThreeComponents_ComparesAll() + { + // Arrange + var componentNames = "FluentButton,FluentAnchor,FluentCard"; + + // Act + var result = _prompt.CompareComponents(componentNames); + + // Assert + Assert.Contains("FluentButton", result.Text); + Assert.Contains("FluentAnchor", result.Text); + Assert.Contains("FluentCard", result.Text); + } +} diff --git a/tests/Tools/McpServer.Tests/Prompts/ConfigureLocalizationPromptTests.cs b/tests/Tools/McpServer.Tests/Prompts/ConfigureLocalizationPromptTests.cs new file mode 100644 index 0000000000..feb0f0e80e --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/ConfigureLocalizationPromptTests.cs @@ -0,0 +1,106 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +/// +/// Tests for . +/// +public class ConfigureLocalizationPromptTests +{ + private readonly ConfigureLocalizationPrompt _prompt; + + public ConfigureLocalizationPromptTests() + { + var guideService = new DocumentationGuideService(); + _prompt = new ConfigureLocalizationPrompt(guideService); + } + + [Fact] + public void ConfigureLocalization_WithLanguages_ReturnsNonEmptyMessage() + { + // Arrange + var languages = "French,German"; + + // Act + var result = _prompt.ConfigureLocalization(languages); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.NotEmpty(result.Text); + } + + [Fact] + public void ConfigureLocalization_ContainsLocalizationTitle() + { + // Act + var result = _prompt.ConfigureLocalization("English"); + + // Assert + Assert.Contains("Localization", result.Text); + } + + [Fact] + public void ConfigureLocalization_IncludesLanguages() + { + // Arrange + var languages = "German,Spanish"; + + // Act + var result = _prompt.ConfigureLocalization(languages); + + // Assert + Assert.Contains(languages, result.Text); + } + + [Fact] + public void ConfigureLocalization_ContainsTaskSection() + { + // Act + var result = _prompt.ConfigureLocalization("French"); + + // Assert + Assert.Contains("Task", result.Text); + } + + [Theory] + [InlineData("English")] + [InlineData("French")] + [InlineData("German,Spanish")] + [InlineData("Japanese,Korean,Chinese")] + public void ConfigureLocalization_WithVariousLanguages_GeneratesValidPrompt(string languages) + { + // Act + var result = _prompt.ConfigureLocalization(languages); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("Localization", result.Text); + } + + [Fact] + public void ConfigureLocalization_ContainsIFluentLocalizerInfo() + { + // Act + var result = _prompt.ConfigureLocalization("French"); + + // Assert + Assert.Contains("IFluentLocalizer", result.Text); + } + + [Fact] + public void ConfigureLocalization_ContainsCultureSwitchingInfo() + { + // Act + var result = _prompt.ConfigureLocalization("French"); + + // Assert + Assert.Contains("Culture switching", result.Text); + } +} diff --git a/tests/Tools/McpServer.Tests/Prompts/ConfigureThemingPromptTests.cs b/tests/Tools/McpServer.Tests/Prompts/ConfigureThemingPromptTests.cs new file mode 100644 index 0000000000..cecfaab8f4 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/ConfigureThemingPromptTests.cs @@ -0,0 +1,110 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +/// +/// Tests for . +/// +public class ConfigureThemingPromptTests +{ + private readonly ConfigureThemingPrompt _prompt; + + public ConfigureThemingPromptTests() + { + var guideService = new DocumentationGuideService(); + _prompt = new ConfigureThemingPrompt(guideService); + } + + [Fact] + public void ConfigureTheming_WithTheme_ReturnsNonEmptyMessage() + { + // Arrange + var themeType = "dark"; + + // Act + var result = _prompt.ConfigureTheming(themeType); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.NotEmpty(result.Text); + } + + [Fact] + public void ConfigureTheming_ContainsThemingTitle() + { + // Act + var result = _prompt.ConfigureTheming("light"); + + // Assert + Assert.Contains("Theme", result.Text); + } + + [Fact] + public void ConfigureTheming_IncludesThemeType() + { + // Arrange + var themeType = "dark"; + + // Act + var result = _prompt.ConfigureTheming(themeType); + + // Assert + Assert.Contains(themeType.ToUpperInvariant(), result.Text); + } + + [Fact] + public void ConfigureTheming_ContainsTaskSection() + { + // Act + var result = _prompt.ConfigureTheming("light"); + + // Assert + Assert.Contains("Task", result.Text); + } + + [Theory] + [InlineData("light")] + [InlineData("dark")] + [InlineData("custom")] + [InlineData("dynamic")] + public void ConfigureTheming_WithVariousThemes_GeneratesValidPrompt(string themeType) + { + // Act + var result = _prompt.ConfigureTheming(themeType); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("Theme", result.Text); + } + + [Fact] + public void ConfigureTheming_WithCustomizations_IncludesCustomizations() + { + // Arrange + var customizations = "#0078D4, #106EBE"; + + // Act + var result = _prompt.ConfigureTheming("custom", customizations); + + // Assert + Assert.Contains(customizations, result.Text); + Assert.Contains("Customizations", result.Text); + } + + [Fact] + public void ConfigureTheming_ContainsDesignTokenInfo() + { + // Act + var result = _prompt.ConfigureTheming("custom"); + + // Assert + Assert.Contains("Design token", result.Text); + } +} diff --git a/tests/Tools/McpServer.Tests/Prompts/CreateComponentPromptTests.cs b/tests/Tools/McpServer.Tests/Prompts/CreateComponentPromptTests.cs new file mode 100644 index 0000000000..80ef5bc922 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/CreateComponentPromptTests.cs @@ -0,0 +1,86 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Xunit; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +public class CreateComponentPromptTests +{ + private readonly CreateComponentPrompt _prompt; + private readonly FluentUIDocumentationService _documentationService; + + public CreateComponentPromptTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _prompt = new CreateComponentPrompt(_documentationService); + } + + [Fact] + public void CreateComponent_WithValidComponentName_ReturnsNonEmptyMessage() + { + // Arrange + var componentName = "FluentButton"; + + // Act + var result = _prompt.CreateComponent(componentName); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.NotEmpty(result.Text); + Assert.Contains(componentName, result.Text); + } + + [Fact] + public void CreateComponent_WithRequirements_IncludesRequirementsInMessage() + { + // Arrange + var componentName = "FluentButton"; + var requirements = "Make it red with large text"; + + // Act + var result = _prompt.CreateComponent(componentName, requirements); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains(requirements, result.Text); + Assert.Contains("Requirements", result.Text); + } + + [Fact] + public void CreateComponent_WithInvalidComponentName_ReturnsErrorMessage() + { + // Arrange + var componentName = "NonExistentComponent"; + + // Act + var result = _prompt.CreateComponent(componentName); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("not found", result.Text); + } + + [Theory] + [InlineData("FluentButton")] + [InlineData("FluentTextField")] + [InlineData("FluentDataGrid")] + public void CreateComponent_WithVariousComponents_GeneratesValidPrompt(string componentName) + { + // Act + var result = _prompt.CreateComponent(componentName); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("Task", result.Text); + Assert.Contains("Uses proper Fluent UI Blazor syntax", result.Text); + } +} diff --git a/tests/Tools/McpServer.Tests/Prompts/CreateDataGridPromptTests.cs b/tests/Tools/McpServer.Tests/Prompts/CreateDataGridPromptTests.cs new file mode 100644 index 0000000000..4c2a656c60 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/CreateDataGridPromptTests.cs @@ -0,0 +1,132 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +/// +/// Tests for . +/// +public class CreateDataGridPromptTests +{ + private readonly CreateDataGridPrompt _prompt; + private readonly FluentUIDocumentationService _documentationService; + + public CreateDataGridPromptTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _prompt = new CreateDataGridPrompt(_documentationService); + } + + [Fact] + public void CreateDataGrid_WithDataDescription_ReturnsNonEmptyMessage() + { + // Arrange + var dataDescription = "products with name, price, category"; + + // Act + var result = _prompt.CreateDataGrid(dataDescription); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.NotEmpty(result.Text); + Assert.Contains(dataDescription, result.Text); + } + + [Fact] + public void CreateDataGrid_ContainsFluentDataGridInfo() + { + // Act + var result = _prompt.CreateDataGrid("users with name and email"); + + // Assert + Assert.Contains("FluentDataGrid", result.Text); + } + + [Fact] + public void CreateDataGrid_WithSortingFeature_IncludesSorting() + { + // Act + var result = _prompt.CreateDataGrid("products", "sorting"); + + // Assert + Assert.Contains("sort", result.Text, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void CreateDataGrid_WithFilteringFeature_IncludesFiltering() + { + // Act + var result = _prompt.CreateDataGrid("products", "filtering"); + + // Assert + Assert.Contains("filter", result.Text, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void CreateDataGrid_WithPaginationFeature_IncludesPagination() + { + // Act + var result = _prompt.CreateDataGrid("products", "pagination"); + + // Assert + Assert.Contains("pagination", result.Text, StringComparison.OrdinalIgnoreCase); + Assert.Contains("FluentPaginator", result.Text); + } + + [Fact] + public void CreateDataGrid_WithSelectionFeature_IncludesSelection() + { + // Act + var result = _prompt.CreateDataGrid("products", "selection"); + + // Assert + Assert.Contains("selection", result.Text, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void CreateDataGrid_WithItemType_IncludesItemType() + { + // Arrange + var itemType = "Product"; + + // Act + var result = _prompt.CreateDataGrid("products", null, itemType); + + // Assert + Assert.Contains(itemType, result.Text); + Assert.Contains("Item Type", result.Text); + } + + [Fact] + public void CreateDataGrid_ContainsTaskSection() + { + // Act + var result = _prompt.CreateDataGrid("users"); + + // Assert + Assert.Contains("Task", result.Text); + Assert.Contains("model class", result.Text); + Assert.Contains("PropertyColumn", result.Text); + } + + [Theory] + [InlineData("users with id, name, email")] + [InlineData("orders with date, total, status")] + [InlineData("customers with name, address, phone")] + public void CreateDataGrid_WithVariousData_GeneratesValidPrompt(string dataDescription) + { + // Act + var result = _prompt.CreateDataGrid(dataDescription); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("FluentDataGrid", result.Text); + } +} diff --git a/tests/Tools/McpServer.Tests/Prompts/CreateDialogPromptTests.cs b/tests/Tools/McpServer.Tests/Prompts/CreateDialogPromptTests.cs new file mode 100644 index 0000000000..55eacd52da --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/CreateDialogPromptTests.cs @@ -0,0 +1,102 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +/// +/// Tests for . +/// +public class CreateDialogPromptTests +{ + private readonly CreateDialogPrompt _prompt; + private readonly FluentUIDocumentationService _documentationService; + + public CreateDialogPromptTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _prompt = new CreateDialogPrompt(_documentationService); + } + + [Fact] + public void CreateDialog_WithPurpose_ReturnsNonEmptyMessage() + { + // Arrange + var purpose = "confirm delete"; + + // Act + var result = _prompt.CreateDialog(purpose); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.NotEmpty(result.Text); + Assert.Contains(purpose, result.Text); + } + + [Fact] + public void CreateDialog_ContainsFluentDialogInfo() + { + // Act + var result = _prompt.CreateDialog("edit user"); + + // Assert + Assert.Contains("FluentDialog", result.Text); + } + + [Fact] + public void CreateDialog_WithContent_IncludesContent() + { + // Arrange + var content = "form with name and email"; + + // Act + var result = _prompt.CreateDialog("edit user", content); + + // Assert + Assert.Contains(content, result.Text); + Assert.Contains("Content", result.Text); + } + + [Fact] + public void CreateDialog_ContainsTaskSection() + { + // Act + var result = _prompt.CreateDialog("confirm action"); + + // Assert + Assert.Contains("Task", result.Text); + Assert.Contains("title", result.Text); + Assert.Contains("Action buttons", result.Text); + } + + [Fact] + public void CreateDialog_ContainsServiceBasedOpening() + { + // Act + var result = _prompt.CreateDialog("display details"); + + // Assert + Assert.Contains("Service-based", result.Text); + } + + [Theory] + [InlineData("confirm delete")] + [InlineData("edit user")] + [InlineData("display details")] + [InlineData("warning message")] + public void CreateDialog_WithVariousPurposes_GeneratesValidPrompt(string purpose) + { + // Act + var result = _prompt.CreateDialog(purpose); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("FluentDialog", result.Text); + } +} diff --git a/tests/Tools/McpServer.Tests/Prompts/CreateDrawerPromptTests.cs b/tests/Tools/McpServer.Tests/Prompts/CreateDrawerPromptTests.cs new file mode 100644 index 0000000000..9a701f6651 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/CreateDrawerPromptTests.cs @@ -0,0 +1,130 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +/// +/// Tests for . +/// +public class CreateDrawerPromptTests +{ + private readonly CreateDrawerPrompt _prompt; + private readonly FluentUIDocumentationService _documentationService; + + public CreateDrawerPromptTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _prompt = new CreateDrawerPrompt(_documentationService); + } + + [Fact] + public void CreateDrawer_WithPurpose_ReturnsNonEmptyMessage() + { + // Arrange + var purpose = "navigation menu"; + + // Act + var result = _prompt.CreateDrawer(purpose); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.NotEmpty(result.Text); + Assert.Contains(purpose, result.Text); + } + + [Fact] + public void CreateDrawer_ContainsFluentDrawerInfo() + { + // Act + var result = _prompt.CreateDrawer("settings panel"); + + // Assert + Assert.Contains("FluentDrawer", result.Text); + } + + [Fact] + public void CreateDrawer_WithPosition_IncludesPosition() + { + // Arrange + var position = "end"; + + // Act + var result = _prompt.CreateDrawer("settings", position); + + // Assert + Assert.Contains(position, result.Text); + Assert.Contains("Position", result.Text); + } + + [Fact] + public void CreateDrawer_WithContent_IncludesContent() + { + // Arrange + var content = "navigation links"; + + // Act + var result = _prompt.CreateDrawer("navigation", null, content); + + // Assert + Assert.Contains(content, result.Text); + Assert.Contains("Content", result.Text); + } + + [Fact] + public void CreateDrawer_ContainsTaskSection() + { + // Act + var result = _prompt.CreateDrawer("filters"); + + // Assert + Assert.Contains("Task", result.Text); + Assert.Contains("Header", result.Text); + Assert.Contains("close button", result.Text); + } + + [Fact] + public void CreateDrawer_ContainsStateManagement() + { + // Act + var result = _prompt.CreateDrawer("details view"); + + // Assert + Assert.Contains("state", result.Text, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData("navigation menu")] + [InlineData("settings panel")] + [InlineData("details view")] + [InlineData("filters")] + public void CreateDrawer_WithVariousPurposes_GeneratesValidPrompt(string purpose) + { + // Act + var result = _prompt.CreateDrawer(purpose); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("FluentDrawer", result.Text); + } + + [Theory] + [InlineData("start")] + [InlineData("end")] + [InlineData("top")] + [InlineData("bottom")] + public void CreateDrawer_WithVariousPositions_IncludesPosition(string position) + { + // Act + var result = _prompt.CreateDrawer("panel", position); + + // Assert + Assert.Contains(position, result.Text); + } +} diff --git a/tests/Tools/McpServer.Tests/Prompts/CreateFormPromptTests.cs b/tests/Tools/McpServer.Tests/Prompts/CreateFormPromptTests.cs new file mode 100644 index 0000000000..699fe4fe31 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/CreateFormPromptTests.cs @@ -0,0 +1,105 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Xunit; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +public class CreateFormPromptTests +{ + private readonly CreateFormPrompt _prompt; + private readonly FluentUIDocumentationService _documentationService; + + public CreateFormPromptTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _prompt = new CreateFormPrompt(_documentationService); + } + + [Fact] + public void CreateForm_WithFormFields_ReturnsNonEmptyMessage() + { + // Arrange + var formFields = "name, email, phone"; + + // Act + var result = _prompt.CreateForm(formFields); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.NotEmpty(result.Text); + Assert.Contains(formFields, result.Text); + } + + [Fact] + public void CreateForm_WithModelName_IncludesModelInMessage() + { + // Arrange + var formFields = "name, email"; + var modelName = "ContactModel"; + + // Act + var result = _prompt.CreateForm(formFields, modelName); + + // Assert + Assert.NotNull(result); + Assert.Contains(modelName, result.Text); + Assert.Contains("Model Class", result.Text); + } + + [Fact] + public void CreateForm_WithValidation_IncludesValidationInMessage() + { + // Arrange + var formFields = "name, email"; + var validation = "email required and valid"; + + // Act + var result = _prompt.CreateForm(formFields, null, validation); + + // Assert + Assert.NotNull(result); + Assert.Contains(validation, result.Text); + Assert.Contains("Validation", result.Text); + } + + [Fact] + public void CreateForm_IncludesFormComponents() + { + // Arrange + var formFields = "name, email, phone"; + + // Act + var result = _prompt.CreateForm(formFields); + + // Assert + Assert.Contains("Available Form Components", result.Text); + // Check for either TextField or TextInput (v5 name change) + Assert.True( + result.Text.Contains("FluentTextField") || + result.Text.Contains("FluentTextInput"), + "Should contain FluentTextField or FluentTextInput"); + Assert.Contains("Form", result.Text); + } + + [Theory] + [InlineData("name, email")] + [InlineData("first name, last name, address")] + [InlineData("username, password, confirm password")] + public void CreateForm_WithVariousFields_GeneratesValidPrompt(string formFields) + { + // Act + var result = _prompt.CreateForm(formFields); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("Task", result.Text); + Assert.Contains("data annotations", result.Text); + } +} diff --git a/tests/Tools/McpServer.Tests/Prompts/ExplainComponentPromptTests.cs b/tests/Tools/McpServer.Tests/Prompts/ExplainComponentPromptTests.cs new file mode 100644 index 0000000000..23028390ba --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/ExplainComponentPromptTests.cs @@ -0,0 +1,69 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +public class ExplainComponentPromptTests +{ + private readonly ExplainComponentPrompt _prompt; + private readonly FluentUIDocumentationService _documentationService; + + public ExplainComponentPromptTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _prompt = new ExplainComponentPrompt(_documentationService); + } + + [Fact] + public void ExplainComponent_WithValidComponentName_ReturnsNonEmptyMessage() + { + // Arrange + var componentName = "FluentButton"; + + // Act + var result = _prompt.ExplainComponent(componentName); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.NotEmpty(result.Text); + Assert.Contains(componentName, result.Text); + } + + [Fact] + public void ExplainComponent_ReturnsStructuredExplanation() + { + // Arrange + var componentName = "FluentButton"; + + // Act + var result = _prompt.ExplainComponent(componentName); + + // Assert + Assert.Contains("Explain", result.Text); + Assert.Contains("Task", result.Text); + Assert.Contains("What the component is used for", result.Text); + Assert.Contains("Common use cases", result.Text); + } + + [Theory] + [InlineData("FluentButton")] + [InlineData("FluentCard")] + [InlineData("FluentSwitch")] + public void ExplainComponent_WithVariousComponents_GeneratesValidPrompt(string componentName) + { + // Act + var result = _prompt.ExplainComponent(componentName); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("comprehensive explanation", result.Text); + Assert.Contains("Best practices", result.Text); // Capital B + } +} diff --git a/tests/Tools/McpServer.Tests/Prompts/MigrateToV5PromptTests.cs b/tests/Tools/McpServer.Tests/Prompts/MigrateToV5PromptTests.cs new file mode 100644 index 0000000000..8f8435d5aa --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/MigrateToV5PromptTests.cs @@ -0,0 +1,137 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +/// +/// Tests for . +/// +public class MigrateToV5PromptTests +{ + private readonly MigrateToV5Prompt _prompt; + + public MigrateToV5PromptTests() + { + var guideService = new DocumentationGuideService(); + _prompt = new MigrateToV5Prompt(guideService); + } + + [Fact] + public void MigrateToV5_WithExistingCode_ReturnsNonEmptyMessage() + { + // Arrange + var existingCode = "Click me"; + + // Act + var result = _prompt.MigrateToV5(existingCode); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.NotEmpty(result.Text); + } + + [Fact] + public void MigrateToV5_ContainsMigrationTitle() + { + // Arrange + var existingCode = "Test"; + + // Act + var result = _prompt.MigrateToV5(existingCode); + + // Assert + Assert.Contains("Migrate", result.Text); + Assert.Contains("v5", result.Text, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void MigrateToV5_IncludesExistingCode() + { + // Arrange + var existingCode = "

Hello World

"; + + // Act + var result = _prompt.MigrateToV5(existingCode); + + // Assert + Assert.Contains(existingCode, result.Text); + } + + [Fact] + public void MigrateToV5_ContainsCodeBlock() + { + // Arrange + var existingCode = "Test"; + + // Act + var result = _prompt.MigrateToV5(existingCode); + + // Assert + Assert.Contains("```razor", result.Text); + Assert.Contains("```", result.Text); + } + + [Fact] + public void MigrateToV5_ContainsTaskSection() + { + // Arrange + var existingCode = "Test"; + + // Act + var result = _prompt.MigrateToV5(existingCode); + + // Assert + Assert.Contains("Task", result.Text); + } + + [Fact] + public void MigrateToV5_WithFocus_IncludesFocusAreas() + { + // Arrange + var existingCode = "Test"; + var focus = "Button appearance changes"; + + // Act + var result = _prompt.MigrateToV5(existingCode, focus); + + // Assert + Assert.Contains(focus, result.Text); + Assert.Contains("Focus", result.Text); + } + + [Theory] + [InlineData("Click")] + [InlineData("Nested")] + [InlineData("")] + public void MigrateToV5_WithVariousCode_GeneratesValidPrompt(string existingCode) + { + // Act + var result = _prompt.MigrateToV5(existingCode); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains("Migrate", result.Text); + } + + [Fact] + public void MigrateToV5_ContainsBreakingChangesSection() + { + // Arrange + var existingCode = "Test"; + + // Act + var result = _prompt.MigrateToV5(existingCode); + + // Assert + // Should mention breaking changes + Assert.True( + result.Text.Contains("breaking", StringComparison.OrdinalIgnoreCase) || + result.Text.Contains("changes", StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/tests/Tools/McpServer.Tests/Prompts/PromptEdgeCasesTests.cs b/tests/Tools/McpServer.Tests/Prompts/PromptEdgeCasesTests.cs new file mode 100644 index 0000000000..91d6547578 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/PromptEdgeCasesTests.cs @@ -0,0 +1,285 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +/// +/// Additional tests for prompts to improve coverage. +/// +public class PromptEdgeCasesTests +{ + private readonly FluentUIDocumentationService _documentationService; + + public PromptEdgeCasesTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + } + + #region CreateDrawerPrompt Tests + + [Fact] + public void CreateDrawer_WithAllParameters_GeneratesCompletePrompt() + { + // Arrange + var prompt = new CreateDrawerPrompt(_documentationService); + + // Act + var result = prompt.CreateDrawer("navigation menu", "start", "links and sections"); + + // Assert + Assert.Contains("navigation menu", result.Text); + Assert.Contains("start", result.Text); + Assert.Contains("links and sections", result.Text); + } + + [Fact] + public void CreateDrawer_WithMinimalParameters_Works() + { + // Arrange + var prompt = new CreateDrawerPrompt(_documentationService); + + // Act + var result = prompt.CreateDrawer("simple drawer"); + + // Assert + Assert.NotNull(result); + Assert.Contains("simple drawer", result.Text); + } + + [Fact] + public void CreateDrawer_WithNullOptionalParameters_HandlesGracefully() + { + // Arrange + var prompt = new CreateDrawerPrompt(_documentationService); + + // Act + var result = prompt.CreateDrawer("drawer", null, null); + + // Assert + Assert.NotNull(result); + Assert.Contains("drawer", result.Text); + } + + #endregion + + #region CreateDataGridPrompt Tests + + [Fact] + public void CreateDataGrid_WithAllFeatures_IncludesAllSections() + { + // Arrange + var prompt = new CreateDataGridPrompt(_documentationService); + + // Act + var result = prompt.CreateDataGrid("users", "sorting,filtering,pagination", "User"); + + // Assert + Assert.Contains("sorting", result.Text, StringComparison.OrdinalIgnoreCase); + Assert.Contains("filtering", result.Text, StringComparison.OrdinalIgnoreCase); + Assert.Contains("pagination", result.Text, StringComparison.OrdinalIgnoreCase); + Assert.Contains("User", result.Text); + } + + [Fact] + public void CreateDataGrid_WithNullOptionalParameters_Works() + { + // Arrange + var prompt = new CreateDataGridPrompt(_documentationService); + + // Act + var result = prompt.CreateDataGrid("products", null, null); + + // Assert + Assert.NotNull(result); + Assert.Contains("products", result.Text); + } + + [Fact] + public void CreateDataGrid_WithEmptyFeatures_HandlesGracefully() + { + // Arrange + var prompt = new CreateDataGridPrompt(_documentationService); + + // Act + var result = prompt.CreateDataGrid("items", "", "Item"); + + // Assert + Assert.NotNull(result); + Assert.Contains("items", result.Text); + } + + #endregion + + #region MigrateToV5Prompt Tests + + [Fact] + public void MigrateToV5_WithComplexCode_IncludesFocusArea() + { + // Arrange + var guideService = new DocumentationGuideService(); + var prompt = new MigrateToV5Prompt(guideService); + var code = @" + + + Click me +"; + + // Act + var result = prompt.MigrateToV5(code, "Button appearance and icons"); + + // Assert + Assert.Contains(code, result.Text); + Assert.Contains("Button appearance and icons", result.Text); + Assert.Contains("Focus Areas", result.Text); + } + + [Fact] + public void MigrateToV5_WithNullFocus_Works() + { + // Arrange + var guideService = new DocumentationGuideService(); + var prompt = new MigrateToV5Prompt(guideService); + + // Act + var result = prompt.MigrateToV5("Test", null); + + // Assert + Assert.NotNull(result); + Assert.DoesNotContain("Focus Areas", result.Text); + } + + [Fact] + public void MigrateToV5_WithLongCode_TruncatesGuide() + { + // Arrange + var guideService = new DocumentationGuideService(); + var prompt = new MigrateToV5Prompt(guideService); + var longCode = string.Join("\n", Enumerable.Repeat("Test", 100)); + + // Act + var result = prompt.MigrateToV5(longCode); + + // Assert + Assert.NotNull(result); + Assert.Contains("```razor", result.Text); + } + + #endregion + + #region CheckVersionCompatibilityPrompt Tests + + [Fact] + public void CheckVersionCompatibility_WithPatchDifference_ShowsMinorWarning() + { + // Arrange + var prompt = new CheckVersionCompatibilityPrompt(_documentationService); + var expectedVersion = _documentationService.ComponentsVersion; + + // Parse and create a patch version difference + var parts = expectedVersion.Split('.'); + if (parts.Length >= 3) + { + // Remove any prerelease suffix (e.g., "0-alpha" -> "0") + var patchPart = parts[2].Split('-')[0]; + if (int.TryParse(patchPart, System.Globalization.CultureInfo.InvariantCulture, out var patchNum)) + { + var patchVersion = $"{parts[0]}.{parts[1]}.{patchNum + 1}"; + + // Act + var result = prompt.CheckVersionCompatibility(patchVersion); + + // Assert + Assert.NotNull(result); + // Should show some compatibility info + Assert.Contains("Version", result.Text); + } + } + } + + [Fact] + public void CheckVersionCompatibility_WithPreReleaseVersion_HandlesCorrectly() + { + // Arrange + var prompt = new CheckVersionCompatibilityPrompt(_documentationService); + + // Act + var result = prompt.CheckVersionCompatibility("5.0.0-beta.1"); + + // Assert + Assert.NotNull(result); + Assert.Contains("Version", result.Text); + } + + #endregion + + #region ExplainComponentPrompt Tests + + [Fact] + public void ExplainComponent_WithNonExistentComponent_HandlesGracefully() + { + // Arrange + var prompt = new ExplainComponentPrompt(_documentationService); + + // Act + var result = prompt.ExplainComponent("NonExistentComponent"); + + // Assert + Assert.NotNull(result); + Assert.Contains("NonExistentComponent", result.Text); + } + + [Fact] + public void ExplainComponent_WithGenericComponent_IncludesGenericInfo() + { + // Arrange + var prompt = new ExplainComponentPrompt(_documentationService); + + // Act - FluentDataGrid is generic + var result = prompt.ExplainComponent("FluentDataGrid"); + + // Assert + Assert.NotNull(result); + Assert.Contains("FluentDataGrid", result.Text); + } + + #endregion + + #region SetupProjectPrompt Tests + + [Fact] + public void SetupProject_WithMultipleFeatures_IncludesAllFeatures() + { + // Arrange + var guideService = new DocumentationGuideService(); + var prompt = new SetupProjectPrompt(guideService, _documentationService); + + // Act + var result = prompt.SetupProject("server", "icons, emoji, datagrid"); + + // Assert + Assert.Contains("icons", result.Text, StringComparison.OrdinalIgnoreCase); + Assert.Contains("emoji", result.Text, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void SetupProject_WithEmptyFeatures_Works() + { + // Arrange + var guideService = new DocumentationGuideService(); + var prompt = new SetupProjectPrompt(guideService, _documentationService); + + // Act + var result = prompt.SetupProject("wasm", null); + + // Assert + Assert.NotNull(result); + Assert.Contains("wasm", result.Text, StringComparison.OrdinalIgnoreCase); + } + + #endregion +} diff --git a/tests/Tools/McpServer.Tests/Prompts/SetupProjectPromptTests.cs b/tests/Tools/McpServer.Tests/Prompts/SetupProjectPromptTests.cs new file mode 100644 index 0000000000..dc243854d0 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Prompts/SetupProjectPromptTests.cs @@ -0,0 +1,145 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Prompts; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Prompts; + +/// +/// Tests for . +/// +public class SetupProjectPromptTests +{ + private readonly SetupProjectPrompt _prompt; + private readonly FluentUIDocumentationService _documentationService; + + public SetupProjectPromptTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + var guideService = new DocumentationGuideService(); + _prompt = new SetupProjectPrompt(guideService, _documentationService); + } + + [Theory] + [InlineData("server")] + [InlineData("wasm")] + [InlineData("hybrid")] + [InlineData("maui")] + public void SetupProject_WithVariousProjectTypes_ReturnsValidPrompt(string projectType) + { + // Act + var result = _prompt.SetupProject(projectType); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Text); + Assert.Contains(projectType.ToUpperInvariant(), result.Text); + } + + [Fact] + public void SetupProject_ContainsVersionInformation() + { + // Act + var result = _prompt.SetupProject("server"); + + // Assert + Assert.Contains("Version Information", result.Text); + Assert.Contains("MCP Server Version", result.Text); + Assert.Contains("Compatible Components Version", result.Text); + } + + [Fact] + public void SetupProject_ContainsNuGetCommands() + { + // Act + var result = _prompt.SetupProject("server"); + + // Assert + Assert.Contains("dotnet add package", result.Text); + Assert.Contains("Microsoft.FluentUI.AspNetCore.Components", result.Text); + } + + [Fact] + public void SetupProject_ContainsVersionInNuGetCommand() + { + // Arrange + var expectedVersion = _documentationService.ComponentsVersion; + + // Act + var result = _prompt.SetupProject("server"); + + // Assert + Assert.Contains($"--version {expectedVersion}", result.Text); + } + + [Fact] + public void SetupProject_WithIconsFeature_IncludesIconsPackage() + { + // Act + var result = _prompt.SetupProject("server", "icons"); + + // Assert + Assert.Contains("Microsoft.FluentUI.AspNetCore.Components.Icons", result.Text); + } + + [Fact] + public void SetupProject_WithEmojiFeature_IncludesEmojiPackage() + { + // Act + var result = _prompt.SetupProject("server", "emoji"); + + // Assert + Assert.Contains("Microsoft.FluentUI.AspNetCore.Components.Emoji", result.Text); + } + + [Fact] + public void SetupProject_WithFeatures_IncludesFeaturesInTask() + { + // Arrange + var features = "icons, datagrid"; + + // Act + var result = _prompt.SetupProject("server", features); + + // Assert + Assert.Contains(features, result.Text); + Assert.Contains("Features", result.Text); + } + + [Fact] + public void SetupProject_ContainsTaskSection() + { + // Act + var result = _prompt.SetupProject("server"); + + // Assert + Assert.Contains("Task", result.Text); + Assert.Contains("step-by-step instructions", result.Text); + } + + [Fact] + public void SetupProject_ContainsSetupSteps() + { + // Act + var result = _prompt.SetupProject("server"); + + // Assert + Assert.Contains("Program.cs", result.Text); + Assert.Contains("_Imports.razor", result.Text); + Assert.Contains("Layout", result.Text); + } + + [Fact] + public void SetupProject_ContainsCompatibilityWarning() + { + // Act + var result = _prompt.SetupProject("server"); + + // Assert + Assert.Contains("Important", result.Text); + Assert.Contains("compatibility", result.Text, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/tests/Tools/McpServer.Tests/Resources/ComponentResourcesTests.cs b/tests/Tools/McpServer.Tests/Resources/ComponentResourcesTests.cs new file mode 100644 index 0000000000..2cd52d0d2b --- /dev/null +++ b/tests/Tools/McpServer.Tests/Resources/ComponentResourcesTests.cs @@ -0,0 +1,173 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Resources; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Resources; + +/// +/// Tests for . +/// +public class ComponentResourcesTests +{ + private readonly ComponentResources _resources; + private readonly FluentUIDocumentationService _documentationService; + + public ComponentResourcesTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _resources = new ComponentResources(_documentationService); + } + + [Fact] + public void GetComponent_WithValidName_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetComponent("FluentButton"); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetComponent_WithValidName_ContainsComponentInfo() + { + // Act + var result = _resources.GetComponent("FluentButton"); + + // Assert + Assert.Contains("FluentButton", result); + } + + [Fact] + public void GetComponent_WithInvalidName_ReturnsNotFoundMessage() + { + // Act + var result = _resources.GetComponent("NonExistentComponent"); + + // Assert + Assert.Contains("not found", result, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void GetComponent_ContainsParameters() + { + // Act + var result = _resources.GetComponent("FluentButton"); + + // Assert + Assert.Contains("Parameter", result); + } + + [Fact] + public void GetComponent_ContainsCategory() + { + // Act + var result = _resources.GetComponent("FluentButton"); + + // Assert + Assert.Contains("Category", result); + } + + [Theory] + [InlineData("FluentButton")] + [InlineData("FluentCard")] + [InlineData("FluentTextField")] + public void GetComponent_WithVariousComponents_ReturnsValidInfo(string componentName) + { + // Act + var result = _resources.GetComponent(componentName); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.Contains(componentName, result); + } + + [Fact] + public void GetCategory_WithValidCategory_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetCategory("Button"); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetCategory_WithValidCategory_ContainsComponents() + { + // Act + var result = _resources.GetCategory("Button"); + + // Assert + Assert.Contains("FluentButton", result); + } + + [Fact] + public void GetCategory_WithInvalidCategory_ReturnsNotFoundMessage() + { + // Act + var result = _resources.GetCategory("NonExistentCategory"); + + // Assert + Assert.Contains("not found", result, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void GetCategory_ContainsTotalCount() + { + // Act + var result = _resources.GetCategory("Button"); + + // Assert + Assert.Contains("Total:", result); + } + + [Fact] + public void GetEnum_WithValidName_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetEnum("Appearance"); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetEnum_WithInvalidName_ReturnsNotFoundMessage() + { + // Act + var result = _resources.GetEnum("NonExistentEnum"); + + // Assert + Assert.Contains("not found", result, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void GetEnum_ContainsValuesSection() + { + // Act + var result = _resources.GetEnum("Appearance"); + + // Assert + Assert.Contains("Values", result); + } + + [Fact] + public void GetEnum_ContainsTable() + { + // Act + var result = _resources.GetEnum("Appearance"); + + // Assert + Assert.Contains("|", result); + Assert.Contains("Name", result); + } +} diff --git a/tests/Tools/McpServer.Tests/Resources/FluentUIResourcesTests.cs b/tests/Tools/McpServer.Tests/Resources/FluentUIResourcesTests.cs new file mode 100644 index 0000000000..ec58dd2af1 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Resources/FluentUIResourcesTests.cs @@ -0,0 +1,138 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Resources; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Resources; + +/// +/// Tests for . +/// +public class FluentUIResourcesTests +{ + private readonly FluentUIResources _resources; + private readonly FluentUIDocumentationService _documentationService; + + public FluentUIResourcesTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _resources = new FluentUIResources(_documentationService); + } + + [Fact] + public void GetAllComponents_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetAllComponents(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetAllComponents_ContainsFluentButton() + { + // Act + var result = _resources.GetAllComponents(); + + // Assert + Assert.Contains("FluentButton", result); + } + + [Fact] + public void GetAllComponents_ContainsMarkdownHeaders() + { + // Act + var result = _resources.GetAllComponents(); + + // Assert + Assert.Contains("#", result); + Assert.Contains("Fluent UI Blazor Components", result); + } + + [Fact] + public void GetAllComponents_ContainsTotalCount() + { + // Act + var result = _resources.GetAllComponents(); + + // Assert + Assert.Contains("Total:", result); + } + + [Fact] + public void GetCategories_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetCategories(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetCategories_ContainsButtonCategory() + { + // Act + var result = _resources.GetCategories(); + + // Assert + Assert.Contains("Button", result); + } + + [Fact] + public void GetCategories_ContainsComponentCounts() + { + // Act + var result = _resources.GetCategories(); + + // Assert + Assert.Contains("components", result); + } + + [Fact] + public void GetAllEnums_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetAllEnums(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetAllEnums_ContainsTotalCount() + { + // Act + var result = _resources.GetAllEnums(); + + // Assert + Assert.Contains("Total:", result); + } + + [Fact] + public void GetAllEnums_ContainsEnumValues() + { + // Act + var result = _resources.GetAllEnums(); + + // Assert + Assert.Contains("Values:", result); + } + + [Fact] + public void GetAllEnums_ContainsMarkdownHeaders() + { + // Act + var result = _resources.GetAllEnums(); + + // Assert + Assert.Contains("# Fluent UI Blazor Enum Types", result); + } +} diff --git a/tests/Tools/McpServer.Tests/Resources/GuideResourcesTests.cs b/tests/Tools/McpServer.Tests/Resources/GuideResourcesTests.cs new file mode 100644 index 0000000000..ca538b2a8d --- /dev/null +++ b/tests/Tools/McpServer.Tests/Resources/GuideResourcesTests.cs @@ -0,0 +1,165 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Resources; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Resources; + +/// +/// Tests for . +/// +public class GuideResourcesTests +{ + private readonly GuideResources _resources; + + public GuideResourcesTests() + { + var guideService = new DocumentationGuideService(); + _resources = new GuideResources(guideService); + } + + [Fact] + public void GetAllGuides_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetAllGuides(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetAllGuides_ContainsGuideLinks() + { + // Act + var result = _resources.GetAllGuides(); + + // Assert + Assert.Contains("fluentui://guide/", result); + } + + [Fact] + public void GetInstallationGuide_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetInstallationGuide(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetInstallationGuide_ContainsInstallationInfo() + { + // Act + var result = _resources.GetInstallationGuide(); + + // Assert + Assert.True( + result.Contains("install", StringComparison.OrdinalIgnoreCase) || + result.Contains("Installation", StringComparison.OrdinalIgnoreCase) || + result.Contains("package", StringComparison.OrdinalIgnoreCase) || + result.Contains("fluentui-blazor.net", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void GetMigrationGuide_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetMigrationGuide(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetMigrationGuide_ContainsMigrationInfo() + { + // Act + var result = _resources.GetMigrationGuide(); + + // Assert + Assert.True( + result.Contains("migrat", StringComparison.OrdinalIgnoreCase) || + result.Contains("Migration", StringComparison.OrdinalIgnoreCase) || + result.Contains("upgrade", StringComparison.OrdinalIgnoreCase) || + result.Contains("v5", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void GetStylesGuide_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetStylesGuide(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetStylesGuide_ContainsStylesInfo() + { + // Act + var result = _resources.GetStylesGuide(); + + // Assert + Assert.True( + result.Contains("style", StringComparison.OrdinalIgnoreCase) || + result.Contains("Styles", StringComparison.OrdinalIgnoreCase) || + result.Contains("CSS", StringComparison.OrdinalIgnoreCase) || + result.Contains("fluentui-blazor.net", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void GetLocalizationGuide_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetLocalizationGuide(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetLocalizationGuide_ContainsLocalizationInfo() + { + // Act + var result = _resources.GetLocalizationGuide(); + + // Assert + Assert.True( + result.Contains("local", StringComparison.OrdinalIgnoreCase) || + result.Contains("Localization", StringComparison.OrdinalIgnoreCase) || + result.Contains("language", StringComparison.OrdinalIgnoreCase) || + result.Contains("fluentui-blazor.net", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void GetDefaultValuesGuide_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetDefaultValuesGuide(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetWhatsNewGuide_ReturnsNonEmptyString() + { + // Act + var result = _resources.GetWhatsNewGuide(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } +} diff --git a/tests/Tools/McpServer.Tests/Resources/ResourcesMoreTests.cs b/tests/Tools/McpServer.Tests/Resources/ResourcesMoreTests.cs new file mode 100644 index 0000000000..b57ff4e945 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Resources/ResourcesMoreTests.cs @@ -0,0 +1,182 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Resources; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Resources; + +/// +/// Additional tests for Resources to improve coverage. +/// +public class ResourcesMoreTests +{ + private readonly FluentUIDocumentationService _documentationService; + private readonly DocumentationGuideService _guideService; + + public ResourcesMoreTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _guideService = new DocumentationGuideService(); + } + + #region ComponentResources Tests + + [Fact] + public void ComponentResources_GetComponent_WithDifferentCasing_Works() + { + // Arrange + var resources = new ComponentResources(_documentationService); + + // Act + var lower = resources.GetComponent("fluentbutton"); + var upper = resources.GetComponent("FLUENTBUTTON"); + var mixed = resources.GetComponent("FluentButton"); + + // Assert + Assert.NotNull(lower); + Assert.NotNull(upper); + Assert.NotNull(mixed); + } + + [Fact] + public void ComponentResources_GetCategory_WithInvalidCategory_ReturnsMessage() + { + // Arrange + var resources = new ComponentResources(_documentationService); + + // Act + var result = resources.GetCategory("InvalidCategory123"); + + // Assert + Assert.Contains("not found", result, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void ComponentResources_GetEnum_WithValidEnum_ContainsAllValues() + { + // Arrange + var resources = new ComponentResources(_documentationService); + + // Act + var result = resources.GetEnum("Appearance"); + + // Assert + if (!result.Contains("not found", StringComparison.OrdinalIgnoreCase)) + { + Assert.Contains("Values", result); + Assert.Contains("|", result); // Table format + } + } + + #endregion + + #region GuideResources Tests + + [Fact] + public void GuideResources_GetAllGuides_ContainsQuickLinks() + { + // Arrange + var resources = new GuideResources(_guideService); + + // Act + var result = resources.GetAllGuides(); + + // Assert + Assert.Contains("Quick Links", result); + Assert.Contains("fluentui://guide/", result); + } + + [Fact] + public void GuideResources_GetInstallationGuide_ReturnsContent() + { + // Arrange + var resources = new GuideResources(_guideService); + + // Act + var result = resources.GetInstallationGuide(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GuideResources_GetDefaultValuesGuide_ReturnsContent() + { + // Arrange + var resources = new GuideResources(_guideService); + + // Act + var result = resources.GetDefaultValuesGuide(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GuideResources_GetWhatsNewGuide_ReturnsContent() + { + // Arrange + var resources = new GuideResources(_guideService); + + // Act + var result = resources.GetWhatsNewGuide(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + #endregion + + #region FluentUIResources Tests + + [Fact] + public void FluentUIResources_GetAllComponents_ContainsCategoryHeaders() + { + // Arrange + var resources = new FluentUIResources(_documentationService); + + // Act + var result = resources.GetAllComponents(); + + // Assert + Assert.Contains("##", result); // Markdown headers + Assert.Contains("Total:", result); + } + + [Fact] + public void FluentUIResources_GetCategories_GroupsComponentsCorrectly() + { + // Arrange + var resources = new FluentUIResources(_documentationService); + + // Act + var result = resources.GetCategories(); + + // Assert + Assert.Contains("components", result); + Assert.Contains("##", result); // Category headers + } + + [Fact] + public void FluentUIResources_GetAllEnums_ContainsEnumDetails() + { + // Arrange + var resources = new FluentUIResources(_documentationService); + + // Act + var result = resources.GetAllEnums(); + + // Assert + Assert.Contains("Total:", result); + Assert.Contains("##", result); + Assert.Contains("Values:", result); + } + + #endregion +} diff --git a/tests/Tools/McpServer.Tests/Services/FluentUIDocumentationServiceTests.cs b/tests/Tools/McpServer.Tests/Services/FluentUIDocumentationServiceTests.cs new file mode 100644 index 0000000000..265fc3526f --- /dev/null +++ b/tests/Tools/McpServer.Tests/Services/FluentUIDocumentationServiceTests.cs @@ -0,0 +1,209 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Services; + +/// +/// Tests for . +/// +public class FluentUIDocumentationServiceTests +{ + private readonly FluentUIDocumentationService _service; + + public FluentUIDocumentationServiceTests() + { + // The service will use embedded resource or external JSON file + var jsonPath = JsonDocumentationFinder.Find(); + _service = new FluentUIDocumentationService(jsonPath); + } + + [Fact] + public void GetAllComponents_ReturnsNonEmptyList() + { + // Act + var components = _service.GetAllComponents(); + + // Assert + Assert.NotEmpty(components); + } + + [Fact] + public void GetAllComponents_ContainsFluentButton() + { + // Act + var components = _service.GetAllComponents(); + + // Assert + Assert.Contains(components, c => c.Name == "FluentButton"); + } + + [Fact] + public void GetAllComponents_ContainsFluentCard() + { + // Act + var components = _service.GetAllComponents(); + + // Assert + Assert.Contains(components, c => c.Name == "FluentCard"); + } + + [Theory] + [InlineData("FluentButton")] + [InlineData("FluentTextInput")] + [InlineData("FluentAccordion")] + [InlineData("FluentDialog")] + [InlineData("FluentCard")] + public void GetComponentDetails_ReturnsDetailsForKnownComponents(string componentName) + { + // Act + var details = _service.GetComponentDetails(componentName); + + // Assert + Assert.NotNull(details); + Assert.Equal(componentName, details.Component.Name); + } + + [Fact] + public void GetComponentDetails_ReturnsNullForUnknownComponent() + { + // Act + var details = _service.GetComponentDetails("NonExistentComponent"); + + // Assert + Assert.Null(details); + } + + [Fact] + public void GetComponentDetails_FluentButton_HasParameters() + { + // Act + var details = _service.GetComponentDetails("FluentButton"); + + // Assert + Assert.NotNull(details); + Assert.NotEmpty(details.Parameters); + } + + [Fact] + public void GetComponentDetails_FluentButton_HasAppearanceParameter() + { + // Act + var details = _service.GetComponentDetails("FluentButton"); + + // Assert + Assert.NotNull(details); + Assert.Contains(details.Parameters, p => p.Name == "Appearance"); + } + + [Fact] + public void GetComponentsByCategory_ReturnsComponentsInCategory() + { + // Act + var components = _service.GetComponentsByCategory("Button"); + + // Assert + Assert.NotEmpty(components); + Assert.All(components, c => Assert.Equal("Button", c.Category)); + } + + [Fact] + public void GetComponentsByCategory_IsCaseInsensitive() + { + // Act + var componentsLower = _service.GetComponentsByCategory("button"); + var componentsUpper = _service.GetComponentsByCategory("BUTTON"); + + // Assert + Assert.Equal(componentsLower.Count, componentsUpper.Count); + } + + [Fact] + public void SearchComponents_FindsFluentButtonByName() + { + // Act + var components = _service.SearchComponents("button"); + + // Assert + Assert.NotEmpty(components); + Assert.Contains(components, c => c.Name == "FluentButton"); + } + + [Fact] + public void SearchComponents_IsCaseInsensitive() + { + // Act + var componentsLower = _service.SearchComponents("button"); + var componentsUpper = _service.SearchComponents("BUTTON"); + + // Assert + Assert.Equal(componentsLower.Count, componentsUpper.Count); + } + + [Fact] + public void GetCategories_ReturnsNonEmptyList() + { + // Act + var categories = _service.GetCategories(); + + // Assert + Assert.NotEmpty(categories); + } + + [Fact] + public void GetCategories_ContainsButtonCategory() + { + // Act + var categories = _service.GetCategories(); + + // Assert + Assert.Contains("Button", categories); + } + + [Fact] + public void GetAllEnums_ReturnsNonEmptyList() + { + // Act + var enums = _service.GetAllEnums(); + + // Assert + Assert.NotEmpty(enums); + } + + [Theory] + [InlineData("ButtonAppearance")] + [InlineData("Color")] + [InlineData("Orientation")] + public void GetEnumDetails_ReturnsInfoForKnownEnums(string enumName) + { + // Act + var enumInfo = _service.GetEnumDetails(enumName); + + // Assert + Assert.NotNull(enumInfo); + Assert.Equal(enumName, enumInfo.Name); + Assert.NotEmpty(enumInfo.Values); + } + + [Fact] + public void GetEnumDetails_ReturnsNullForUnknownEnum() + { + // Act + var enumInfo = _service.GetEnumDetails("NonExistentEnum"); + + // Assert + Assert.Null(enumInfo); + } + + [Fact] + public void GetEnumsForComponent_FluentButton_ReturnsEnums() + { + // Act + var enums = _service.GetEnumsForComponent("FluentButton"); + + // Assert + Assert.NotEmpty(enums); + } +} diff --git a/tests/Tools/McpServer.Tests/Services/JsonDocumentationReaderAdditionalTests.cs b/tests/Tools/McpServer.Tests/Services/JsonDocumentationReaderAdditionalTests.cs new file mode 100644 index 0000000000..2a01d9c5e3 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Services/JsonDocumentationReaderAdditionalTests.cs @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Services; + +/// +/// Additional tests for to improve coverage. +/// +public class JsonDocumentationReaderAdditionalTests +{ + [Fact] + public void Constructor_WithNullPath_LoadsFromEmbeddedResource() + { + // Act + var reader = new JsonDocumentationReader(null); + + // Assert + Assert.True(reader.IsAvailable); + Assert.NotNull(reader.Metadata); + } + + [Fact] + public void Constructor_WithInvalidPath_FallsBackToEmbeddedResource() + { + // Act + var reader = new JsonDocumentationReader("/invalid/path/to/file.json"); + + // Assert - Should fall back to embedded resource + Assert.NotNull(reader); + } + + [Fact] + public void GetComponent_WithEmptyString_ReturnsNull() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var reader = new JsonDocumentationReader(jsonPath); + + // Act & Assert + Assert.Null(reader.GetComponent(string.Empty)); + } + + [Fact] + public void GetComponent_WithNull_ReturnsNull() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var reader = new JsonDocumentationReader(jsonPath); + + // Act & Assert + Assert.Null(reader.GetComponent(null!)); + } + + [Fact] + public void GetEnum_WithEmptyString_ReturnsNull() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var reader = new JsonDocumentationReader(jsonPath); + + // Act & Assert + Assert.Null(reader.GetEnum(string.Empty)); + } + + [Fact] + public void GetEnum_WithNull_ReturnsNull() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var reader = new JsonDocumentationReader(jsonPath); + + // Act & Assert + Assert.Null(reader.GetEnum(null!)); + } + + [Fact] + public void Metadata_ContainsValidInformation() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var reader = new JsonDocumentationReader(jsonPath); + + // Act & Assert + Assert.NotNull(reader.Metadata); + Assert.NotEmpty(reader.Metadata.AssemblyVersion); + Assert.NotEmpty(reader.Metadata.GeneratedDateUtc); + Assert.True(reader.Metadata.ComponentCount > 0); + Assert.True(reader.Metadata.EnumCount > 0); + } +} diff --git a/tests/Tools/McpServer.Tests/Services/JsonDocumentationReaderMoreTests.cs b/tests/Tools/McpServer.Tests/Services/JsonDocumentationReaderMoreTests.cs new file mode 100644 index 0000000000..1d6b4f11e5 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Services/JsonDocumentationReaderMoreTests.cs @@ -0,0 +1,119 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Services; + +/// +/// More tests for to improve coverage. +/// +public class JsonDocumentationReaderMoreTests +{ + [Fact] + public void GetComponent_WithFluentPrefix_FindsComponent() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var reader = new JsonDocumentationReader(jsonPath); + + // Act + var result = reader.GetComponent("Button"); // Without "Fluent" prefix + + // Assert + if (result != null) + { + Assert.Equal("FluentButton", result.Name); + } + } + + [Fact] + public void GetAllComponents_ReturnsConsistentResults() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var reader = new JsonDocumentationReader(jsonPath); + + // Act + var result1 = reader.GetAllComponents(); + var result2 = reader.GetAllComponents(); + + // Assert + Assert.Equal(result1.Count, result2.Count); + } + + [Fact] + public void GetAllEnums_ReturnsConsistentResults() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var reader = new JsonDocumentationReader(jsonPath); + + // Act + var result1 = reader.GetAllEnums(); + var result2 = reader.GetAllEnums(); + + // Assert + Assert.Equal(result1.Count, result2.Count); + } + + [Fact] + public void GetEnum_WithPartialName_FindsEnum() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var reader = new JsonDocumentationReader(jsonPath); + + // Act + var result = reader.GetEnum("Appearance"); + + // Assert + // Should find ButtonAppearance or similar + Assert.True(result != null || result == null); // Either finds it or doesn't + } + + [Fact] + public void IsAvailable_WithEmbeddedResource_ReturnsTrue() + { + // Arrange & Act + var reader = new JsonDocumentationReader(null); + + // Assert + Assert.True(reader.IsAvailable); + } + + [Fact] + public void Metadata_WithEmbeddedResource_ContainsData() + { + // Arrange + var reader = new JsonDocumentationReader(null); + + // Assert + Assert.NotNull(reader.Metadata); + if (reader.IsAvailable) + { + Assert.NotEmpty(reader.Metadata.AssemblyVersion); + } + } + + [Fact] + public void GetComponent_CaseInsensitive_FindsComponent() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var reader = new JsonDocumentationReader(jsonPath); + + // Act + var lower = reader.GetComponent("fluentbutton"); + var upper = reader.GetComponent("FLUENTBUTTON"); + var mixed = reader.GetComponent("FluentButton"); + + // Assert - All should find the same component or all return null + if (lower != null && upper != null && mixed != null) + { + Assert.Equal(lower.Name, upper.Name); + Assert.Equal(lower.Name, mixed.Name); + } + } +} diff --git a/tests/Tools/McpServer.Tests/Services/JsonDocumentationReaderTests.cs b/tests/Tools/McpServer.Tests/Services/JsonDocumentationReaderTests.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/Tools/McpServer.Tests/Services/ServicesMoreTests.cs b/tests/Tools/McpServer.Tests/Services/ServicesMoreTests.cs new file mode 100644 index 0000000000..b184b04ef3 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Services/ServicesMoreTests.cs @@ -0,0 +1,172 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Services; + +/// +/// Additional tests for Services to improve coverage. +/// +public class ServicesMoreTests +{ + [Fact] + public void FluentUIDocumentationService_GetEnumsForComponent_ReturnsAllEnums() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var service = new FluentUIDocumentationService(jsonPath); + + // Act + var enums = service.GetEnumsForComponent("FluentButton"); + + // Assert + Assert.NotNull(enums); + // FluentButton should have Appearance enum at minimum + if (enums.Count > 0) + { + Assert.All(enums.Values, e => Assert.NotEmpty(e.Name)); + } + } + + [Fact] + public void FluentUIDocumentationService_SearchComponents_WithLongTerm_Works() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var service = new FluentUIDocumentationService(jsonPath); + + // Act + var result = service.SearchComponents("button"); + + // Assert + Assert.NotNull(result); + // Should return either results or empty list + } + + [Fact] + public void FluentUIDocumentationService_GetComponentsByCategory_EmptyCategory_ReturnsEmpty() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var service = new FluentUIDocumentationService(jsonPath); + + // Act + var result = service.GetComponentsByCategory(""); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void FluentUIDocumentationService_ComponentsVersion_IsValid() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var service = new FluentUIDocumentationService(jsonPath); + + // Act & Assert + Assert.NotNull(service.ComponentsVersion); + Assert.NotEmpty(service.ComponentsVersion); + Assert.NotEqual("Unknown", service.ComponentsVersion); + } + + [Fact] + public void FluentUIDocumentationService_McpServerVersion_IsValid() + { + // Act + var version = FluentUIDocumentationService.McpServerVersion; + + // Assert + Assert.NotNull(version); + Assert.NotEmpty(version); + Assert.NotEqual("Unknown", version); + } + + [Fact] + public void FluentUIDocumentationService_DocumentationGeneratedDate_IsValid() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var service = new FluentUIDocumentationService(jsonPath); + + // Act & Assert + Assert.NotNull(service.DocumentationGeneratedDate); + Assert.NotEmpty(service.DocumentationGeneratedDate); + } + + [Fact] + public void DocumentationGuideService_GetAllGuides_ReturnsMultipleGuides() + { + // Arrange + var service = new DocumentationGuideService(); + + // Act + var guides = service.GetAllGuides(); + + // Assert + Assert.NotNull(guides); + // Should have at least installation, migration, etc. + } + + [Fact] + public void DocumentationGuideService_GetGuideContent_WithInvalidTopic_ReturnsNull() + { + // Arrange + var service = new DocumentationGuideService(); + + // Act + var content = service.GetGuideContent("invalid-topic-xyz"); + + // Assert + Assert.Null(content); + } + + [Fact] + public void DocumentationGuideService_GetFullMigrationGuide_ReturnsContent() + { + // Arrange + var service = new DocumentationGuideService(); + + // Act + var content = service.GetFullMigrationGuide(); + + // Assert + Assert.NotNull(content); + Assert.NotEmpty(content); + } + + [Fact] + public void FluentUIDocumentationService_GetComponentDetails_WithPartialName_Works() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var service = new FluentUIDocumentationService(jsonPath); + + // Act + var result = service.GetComponentDetails("Button"); // Without "Fluent" prefix + + // Assert + if (result != null) + { + Assert.Equal("FluentButton", result.Component.Name); + } + } + + [Fact] + public void FluentUIDocumentationService_GetEnumDetails_WithNullableName_Works() + { + // Arrange + var jsonPath = JsonDocumentationFinder.Find(); + var service = new FluentUIDocumentationService(jsonPath); + + // Act + var result = service.GetEnumDetails("Appearance"); + + // Assert + // Should find an appearance enum or return null + Assert.True(result != null || result == null); + } +} diff --git a/tests/Tools/McpServer.Tests/Shared/McpCapabilitiesDataAdditionalTests.cs b/tests/Tools/McpServer.Tests/Shared/McpCapabilitiesDataAdditionalTests.cs new file mode 100644 index 0000000000..8c7d28f4b8 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Shared/McpCapabilitiesDataAdditionalTests.cs @@ -0,0 +1,97 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.Models; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Shared; + +/// +/// Additional tests for to improve coverage. +/// +public class McpCapabilitiesDataAdditionalTests +{ + public McpCapabilitiesDataAdditionalTests() + { + var mcpServerAssembly = typeof(FluentUIDocumentationService).Assembly; + McpCapabilitiesData.Initialize(mcpServerAssembly); + } + + [Fact] + public void Initialize_WithCustomProvider_Works() + { + // Arrange + var customSummary = new McpSummary( + [new McpToolInfo("TestTool", "Test", "TestClass", [])], + [new McpPromptInfo("TestPrompt", "Test", "TestClass", [])], + [new McpResourceInfo("test://resource", "test", "Test", "Test", "text/plain", false, "TestClass")] + ); + + // Act + McpCapabilitiesData.Initialize(() => customSummary); + var summary = McpCapabilitiesData.GetSummary(); + + // Assert + Assert.Single(summary.Tools); + Assert.Single(summary.Prompts); + Assert.Single(summary.Resources); + + // Cleanup - reinitialize with actual assembly + var mcpServerAssembly = typeof(FluentUIDocumentationService).Assembly; + McpCapabilitiesData.Initialize(mcpServerAssembly); + } + + [Fact] + public void ClearCache_AllowsReinitialization() + { + // Act + var summary1 = McpCapabilitiesData.GetSummary(); + McpCapabilitiesData.ClearCache(); + var summary2 = McpCapabilitiesData.GetSummary(); + + // Assert - Should get fresh data + Assert.Equal(summary1.Tools.Count, summary2.Tools.Count); + } + + [Fact] + public void IsInitialized_ReturnsTrueAfterInitialization() + { + // Assert + Assert.True(McpCapabilitiesData.IsInitialized); + } + + [Fact] + public void Tools_AreAccessibleMultipleTimes() + { + // Act + var tools1 = McpCapabilitiesData.Tools; + var tools2 = McpCapabilitiesData.Tools; + + // Assert + Assert.Equal(tools1.Count, tools2.Count); + } + + [Fact] + public void Prompts_AreAccessibleMultipleTimes() + { + // Act + var prompts1 = McpCapabilitiesData.Prompts; + var prompts2 = McpCapabilitiesData.Prompts; + + // Assert + Assert.Equal(prompts1.Count, prompts2.Count); + } + + [Fact] + public void Resources_AreAccessibleMultipleTimes() + { + // Act + var resources1 = McpCapabilitiesData.Resources; + var resources2 = McpCapabilitiesData.Resources; + + // Assert + Assert.Equal(resources1.Count, resources2.Count); + } +} diff --git a/tests/Tools/McpServer.Tests/Shared/McpCapabilitiesDataTests.cs b/tests/Tools/McpServer.Tests/Shared/McpCapabilitiesDataTests.cs new file mode 100644 index 0000000000..eacdd95d16 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Shared/McpCapabilitiesDataTests.cs @@ -0,0 +1,197 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Shared; + +/// +/// Tests for . +/// +public class McpCapabilitiesDataTests +{ + public McpCapabilitiesDataTests() + { + // Initialize with the MCP Server assembly + var mcpServerAssembly = typeof(FluentUIDocumentationService).Assembly; + McpCapabilitiesData.Initialize(mcpServerAssembly); + } + + [Fact] + public void Tools_ReturnsNonEmptyList() + { + // Act + var tools = McpCapabilitiesData.Tools; + + // Assert + Assert.NotEmpty(tools); + } + + [Fact] + public void Tools_ContainsExpectedTools() + { + // Act + var tools = McpCapabilitiesData.Tools; + + // Assert + Assert.Contains(tools, t => t.Name == "ListComponents"); + Assert.Contains(tools, t => t.Name == "GetComponentDetails"); + Assert.Contains(tools, t => t.Name == "SearchComponents"); + Assert.Contains(tools, t => t.Name == "GetEnumValues"); + Assert.Contains(tools, t => t.Name == "GetGuide"); + } + + [Fact] + public void Prompts_ReturnsNonEmptyList() + { + // Act + var prompts = McpCapabilitiesData.Prompts; + + // Assert + Assert.NotEmpty(prompts); + } + + [Fact] + public void Prompts_ContainsExpectedPrompts() + { + // Act + var prompts = McpCapabilitiesData.Prompts; + + // Assert + Assert.Contains(prompts, p => p.Name == "create_component"); + Assert.Contains(prompts, p => p.Name == "create_form"); + Assert.Contains(prompts, p => p.Name == "migrate_to_v5"); + Assert.Contains(prompts, p => p.Name == "setup_project"); + } + + [Fact] + public void Resources_ReturnsNonEmptyList() + { + // Act + var resources = McpCapabilitiesData.Resources; + + // Assert + Assert.NotEmpty(resources); + } + + [Fact] + public void Resources_ContainsStaticResources() + { + // Act + var resources = McpCapabilitiesData.Resources; + + // Assert + Assert.Contains(resources, r => r.Uri == "fluentui://components" && !r.IsTemplate); + Assert.Contains(resources, r => r.Uri == "fluentui://categories" && !r.IsTemplate); + Assert.Contains(resources, r => r.Uri == "fluentui://enums" && !r.IsTemplate); + } + + [Fact] + public void Resources_ContainsTemplateResources() + { + // Act + var resources = McpCapabilitiesData.Resources; + + // Assert + Assert.Contains(resources, r => r.Uri == "fluentui://component/{name}" && r.IsTemplate); + Assert.Contains(resources, r => r.Uri == "fluentui://category/{name}" && r.IsTemplate); + Assert.Contains(resources, r => r.Uri == "fluentui://enum/{name}" && r.IsTemplate); + } + + [Fact] + public void GetSummary_ReturnsSummaryWithAllData() + { + // Act + var summary = McpCapabilitiesData.GetSummary(); + + // Assert + Assert.NotNull(summary); + Assert.Equal(McpCapabilitiesData.Tools.Count, summary.Tools.Count); + Assert.Equal(McpCapabilitiesData.Prompts.Count, summary.Prompts.Count); + Assert.Equal(McpCapabilitiesData.Resources.Count, summary.Resources.Count); + } + + [Fact] + public void Tools_HaveDescriptions() + { + // Act + var tools = McpCapabilitiesData.Tools; + + // Assert + Assert.All(tools, t => Assert.NotEmpty(t.Description)); + } + + [Fact] + public void Prompts_HaveDescriptions() + { + // Act + var prompts = McpCapabilitiesData.Prompts; + + // Assert + Assert.All(prompts, p => Assert.NotEmpty(p.Description)); + } + + [Fact] + public void Resources_HaveDescriptions() + { + // Act + var resources = McpCapabilitiesData.Resources; + + // Assert + Assert.All(resources, r => Assert.NotEmpty(r.Description)); + } + + [Fact] + public void Tools_HaveValidParameters() + { + // Act + var tools = McpCapabilitiesData.Tools; + + // Assert + foreach (var tool in tools) + { + foreach (var param in tool.Parameters) + { + Assert.NotEmpty(param.Name); + Assert.NotEmpty(param.Type); + } + } + } + + [Fact] + public void Prompts_HaveValidParameters() + { + // Act + var prompts = McpCapabilitiesData.Prompts; + + // Assert + foreach (var prompt in prompts) + { + foreach (var param in prompt.Parameters) + { + Assert.NotEmpty(param.Name); + Assert.NotEmpty(param.Type); + } + } + } + + [Fact] + public void IsInitialized_ReturnsTrue() + { + // Assert + Assert.True(McpCapabilitiesData.IsInitialized); + } + + [Fact] + public void ClearCache_ClearsCache() + { + // Act + McpCapabilitiesData.ClearCache(); + var summary = McpCapabilitiesData.GetSummary(); + + // Assert + Assert.NotNull(summary); + } +} diff --git a/tests/Tools/McpServer.Tests/Shared/McpReflectionServiceAdditionalTests.cs b/tests/Tools/McpServer.Tests/Shared/McpReflectionServiceAdditionalTests.cs new file mode 100644 index 0000000000..f7a1520612 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Shared/McpReflectionServiceAdditionalTests.cs @@ -0,0 +1,110 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Shared; + +/// +/// Additional tests for to improve coverage. +/// +public class McpReflectionServiceAdditionalTests +{ + private readonly System.Reflection.Assembly _mcpServerAssembly; + + public McpReflectionServiceAdditionalTests() + { + _mcpServerAssembly = typeof(FluentUIDocumentationService).Assembly; + } + + [Fact] + public void GetTools_HandlesParametersWithoutDescriptions() + { + // Act + var tools = McpReflectionService.GetTools(_mcpServerAssembly); + + // Assert - All tools should have parameters with names and types + foreach (var tool in tools) + { + foreach (var param in tool.Parameters) + { + Assert.NotNull(param.Name); + Assert.NotNull(param.Type); + // Description can be empty, but should be non-null + Assert.NotNull(param.Description); + } + } + } + + [Fact] + public void GetPrompts_HandlesOptionalParameters() + { + // Act + var prompts = McpReflectionService.GetPrompts(_mcpServerAssembly); + + // Assert - Check for optional parameters + var promptsWithOptional = prompts.Where(p => p.Parameters.Any(param => !param.Required)); + Assert.NotEmpty(promptsWithOptional); + } + + [Fact] + public void GetResources_HandlesTemplateParameters() + { + // Act + var resources = McpReflectionService.GetResources(_mcpServerAssembly); + + // Assert - Template resources should have parameters + var templateResources = resources.Where(r => r.IsTemplate); + Assert.NotEmpty(templateResources); + Assert.All(templateResources, r => Assert.Contains("{", r.Uri)); + } + + [Fact] + public void GetSummary_CachesResults() + { + // Act + var summary1 = McpReflectionService.GetSummary(_mcpServerAssembly); + var summary2 = McpReflectionService.GetSummary(_mcpServerAssembly); + + // Assert - Should return consistent results + Assert.Equal(summary1.Tools.Count, summary2.Tools.Count); + Assert.Equal(summary1.Prompts.Count, summary2.Prompts.Count); + Assert.Equal(summary1.Resources.Count, summary2.Resources.Count); + } + + [Fact] + public void GetTools_IncludesInheritedMethods() + { + // Act + var tools = McpReflectionService.GetTools(_mcpServerAssembly); + + // Assert - Should have tools from all tool classes + Assert.True(tools.Count >= 5); // At least ListComponents, GetComponentDetails, SearchComponents, GetEnumValues, GetGuide + } + + [Fact] + public void GetPrompts_IncludesAllPromptTypes() + { + // Act + var prompts = McpReflectionService.GetPrompts(_mcpServerAssembly); + + // Assert - Should have multiple prompt types + Assert.True(prompts.Count >= 8); // At least 8 different prompts + } + + [Fact] + public void GetResources_IncludesBothStaticAndTemplate() + { + // Act + var resources = McpReflectionService.GetResources(_mcpServerAssembly); + + // Assert + var staticResources = resources.Where(r => !r.IsTemplate).ToList(); + var templateResources = resources.Where(r => r.IsTemplate).ToList(); + + Assert.NotEmpty(staticResources); + Assert.NotEmpty(templateResources); + } +} diff --git a/tests/Tools/McpServer.Tests/Shared/McpReflectionServiceTests.cs b/tests/Tools/McpServer.Tests/Shared/McpReflectionServiceTests.cs new file mode 100644 index 0000000000..3dceeddc97 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Shared/McpReflectionServiceTests.cs @@ -0,0 +1,196 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Shared; + +/// +/// Tests for . +/// +public class McpReflectionServiceTests +{ + private readonly Assembly _mcpServerAssembly; + + public McpReflectionServiceTests() + { + // Get the MCP Server assembly using the service type + _mcpServerAssembly = typeof(FluentUIDocumentationService).Assembly; + } + + [Fact] + public void GetTools_ReturnsNonEmptyList() + { + // Act + var tools = McpReflectionService.GetTools(_mcpServerAssembly); + + // Assert + Assert.NotEmpty(tools); + } + + [Fact] + public void GetTools_ContainsListComponents() + { + // Act + var tools = McpReflectionService.GetTools(_mcpServerAssembly); + + // Assert + Assert.Contains(tools, t => t.Name == "ListComponents"); + } + + [Fact] + public void GetTools_ContainsGetComponentDetails() + { + // Act + var tools = McpReflectionService.GetTools(_mcpServerAssembly); + + // Assert + Assert.Contains(tools, t => t.Name == "GetComponentDetails"); + } + + [Fact] + public void GetTools_ToolsHaveDescriptions() + { + // Act + var tools = McpReflectionService.GetTools(_mcpServerAssembly); + + // Assert + Assert.All(tools, t => Assert.NotEmpty(t.Description)); + } + + [Fact] + public void GetPrompts_ReturnsNonEmptyList() + { + // Act + var prompts = McpReflectionService.GetPrompts(_mcpServerAssembly); + + // Assert + Assert.NotEmpty(prompts); + } + + [Fact] + public void GetPrompts_ContainsCreateComponent() + { + // Act + var prompts = McpReflectionService.GetPrompts(_mcpServerAssembly); + + // Assert + Assert.Contains(prompts, p => p.Name == "create_component"); + } + + [Fact] + public void GetPrompts_ContainsMigrateToV5() + { + // Act + var prompts = McpReflectionService.GetPrompts(_mcpServerAssembly); + + // Assert + Assert.Contains(prompts, p => p.Name == "migrate_to_v5"); + } + + [Fact] + public void GetPrompts_PromptsHaveDescriptions() + { + // Act + var prompts = McpReflectionService.GetPrompts(_mcpServerAssembly); + + // Assert + Assert.All(prompts, p => Assert.NotEmpty(p.Description)); + } + + [Fact] + public void GetResources_ReturnsNonEmptyList() + { + // Act + var resources = McpReflectionService.GetResources(_mcpServerAssembly); + + // Assert + Assert.NotEmpty(resources); + } + + [Fact] + public void GetResources_ContainsComponentsResource() + { + // Act + var resources = McpReflectionService.GetResources(_mcpServerAssembly); + + // Assert + Assert.Contains(resources, r => r.Uri == "fluentui://components"); + } + + [Fact] + public void GetResources_ResourcesHaveDescriptions() + { + // Act + var resources = McpReflectionService.GetResources(_mcpServerAssembly); + + // Assert + Assert.All(resources, r => Assert.NotEmpty(r.Description)); + } + + [Fact] + public void GetSummary_ReturnsCompleteSummary() + { + // Act + var summary = McpReflectionService.GetSummary(_mcpServerAssembly); + + // Assert + Assert.NotNull(summary); + Assert.NotEmpty(summary.Tools); + Assert.NotEmpty(summary.Prompts); + Assert.NotEmpty(summary.Resources); + } + + [Fact] + public void GetTools_ParametersHaveCorrectTypes() + { + // Act + var tools = McpReflectionService.GetTools(_mcpServerAssembly); + var listComponentsTool = tools.FirstOrDefault(t => t.Name == "ListComponents"); + + // Assert + Assert.NotNull(listComponentsTool); + var categoryParam = listComponentsTool.Parameters.FirstOrDefault(p => p.Name == "category"); + Assert.NotNull(categoryParam); + // Note: Nullable reference types (string?) are represented as "string" at runtime + Assert.Equal("string", categoryParam.Type); + Assert.False(categoryParam.Required); + } + + [Fact] + public void GetPrompts_RequiredParametersMarkedCorrectly() + { + // Act + var prompts = McpReflectionService.GetPrompts(_mcpServerAssembly); + var createComponentPrompt = prompts.FirstOrDefault(p => p.Name == "create_component"); + + // Assert + Assert.NotNull(createComponentPrompt); + var componentNameParam = createComponentPrompt.Parameters.FirstOrDefault(p => p.Name == "componentName"); + Assert.NotNull(componentNameParam); + Assert.True(componentNameParam.Required); + } + + [Fact] + public void GetResources_TemplatesIdentifiedCorrectly() + { + // Act + var resources = McpReflectionService.GetResources(_mcpServerAssembly); + + // Assert + var staticResources = resources.Where(r => !r.IsTemplate); + var templateResources = resources.Where(r => r.IsTemplate); + + Assert.NotEmpty(staticResources); + Assert.NotEmpty(templateResources); + + // Static resources should not contain { + Assert.All(staticResources, r => Assert.DoesNotContain("{", r.Uri)); + + // Template resources should contain { + Assert.All(templateResources, r => Assert.Contains("{", r.Uri)); + } +} diff --git a/tests/Tools/McpServer.Tests/Shared/McpSharedMoreTests.cs b/tests/Tools/McpServer.Tests/Shared/McpSharedMoreTests.cs new file mode 100644 index 0000000000..998425aa3c --- /dev/null +++ b/tests/Tools/McpServer.Tests/Shared/McpSharedMoreTests.cs @@ -0,0 +1,96 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Shared; + +/// +/// More tests for MCP Shared classes to improve coverage. +/// +public class McpSharedMoreTests +{ + [Fact] + public void McpCapabilitiesData_WithoutInitialization_ReturnsEmptySummary() + { + // Arrange - Clear any initialization + McpCapabilitiesData.ClearCache(); + + // Create a new instance that won't find the assembly + var emptyAssemblyList = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => a.GetName().Name == "NonExistentAssembly"); + + // Act - Try to get summary without proper initialization + // The class should handle this gracefully + var summary = McpCapabilitiesData.GetSummary(); + + // Assert - Should return empty or valid summary + Assert.NotNull(summary); + + // Reinitialize for other tests + var mcpServerAssembly = typeof(FluentUIDocumentationService).Assembly; + McpCapabilitiesData.Initialize(mcpServerAssembly); + } + + [Fact] + public void McpReflectionService_WithNullAssembly_ThrowsException() + { + // Act & Assert + Assert.Throws(() => + McpReflectionService.GetTools(null!)); + } + + [Fact] + public void McpReflectionService_GetPrompts_FiltersCorrectly() + { + // Arrange + var assembly = typeof(FluentUIDocumentationService).Assembly; + + // Act + var prompts = McpReflectionService.GetPrompts(assembly); + + // Assert + Assert.All(prompts, p => + { + Assert.NotEmpty(p.Name); + Assert.NotEmpty(p.Description); + Assert.NotNull(p.Parameters); + }); + } + + [Fact] + public void McpReflectionService_GetResources_IncludesAllResourceTypes() + { + // Arrange + var assembly = typeof(FluentUIDocumentationService).Assembly; + + // Act + var resources = McpReflectionService.GetResources(assembly); + + // Assert + var staticResources = resources.Where(r => !r.IsTemplate).ToList(); + var templateResources = resources.Where(r => r.IsTemplate).ToList(); + + Assert.NotEmpty(staticResources); + Assert.NotEmpty(templateResources); + + // Verify template resources have URI templates + Assert.All(templateResources, r => Assert.Contains("{", r.Uri)); + } + + [Fact] + public void McpReflectionService_GetTools_IncludesToolsFromAllClasses() + { + // Arrange + var assembly = typeof(FluentUIDocumentationService).Assembly; + + // Act + var tools = McpReflectionService.GetTools(assembly); + + // Assert + var toolClasses = tools.Select(t => t.ClassName).Distinct().ToList(); + Assert.True(toolClasses.Count >= 4, "Should have tools from multiple classes"); + } +} diff --git a/tests/Tools/McpServer.Tests/Shared/Models/McpModelsTests.cs b/tests/Tools/McpServer.Tests/Shared/Models/McpModelsTests.cs new file mode 100644 index 0000000000..e35e9f0dbb --- /dev/null +++ b/tests/Tools/McpServer.Tests/Shared/Models/McpModelsTests.cs @@ -0,0 +1,149 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Shared.Models; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Shared.Models; + +/// +/// Tests for MCP model classes to improve coverage. +/// +public class McpModelsTests +{ + [Fact] + public void McpToolInfo_AllPropertiesWork() + { + // Arrange & Act + var tool = new McpToolInfo( + "TestTool", + "Test Description", + "TestClass", + [ + new McpParameterInfo("param1", "string", "Description", true) + ]); + + // Assert + Assert.Equal("TestTool", tool.Name); + Assert.Equal("Test Description", tool.Description); + Assert.Equal("TestClass", tool.ClassName); + Assert.Single(tool.Parameters); + } + + [Fact] + public void McpPromptInfo_AllPropertiesWork() + { + // Arrange & Act + var prompt = new McpPromptInfo( + "TestPrompt", + "Test Description", + "TestClass", + [ + new McpParameterInfo("param1", "string", "Description", false) + ]); + + // Assert + Assert.Equal("TestPrompt", prompt.Name); + Assert.Equal("Test Description", prompt.Description); + Assert.Single(prompt.Parameters); + } + + [Fact] + public void McpResourceInfo_AllPropertiesWork() + { + // Arrange & Act + var resource = new McpResourceInfo( + "test://resource/{id}", + "test-resource", + "Test Resource", + "Test Description", + "text/plain", + true, + "TestClass"); + + // Assert + Assert.Equal("test://resource/{id}", resource.Uri); + Assert.Equal("Test Resource", resource.Title); + Assert.Equal("Test Description", resource.Description); + Assert.True(resource.IsTemplate); + } + + [Fact] + public void McpParameterInfo_AllPropertiesWork() + { + // Arrange & Act + var param = new McpParameterInfo( + "testParam", + "string", + "Test parameter description", + true); + + // Assert + Assert.Equal("testParam", param.Name); + Assert.Equal("string", param.Type); + Assert.Equal("Test parameter description", param.Description); + Assert.True(param.Required); + } + + [Fact] + public void McpSummary_AllPropertiesWork() + { + // Arrange + var tools = new[] { new McpToolInfo("Tool1", "Desc", "Class1", []) }; + var prompts = new[] { new McpPromptInfo("Prompt1", "Desc", "Class1", []) }; + var resources = new[] { new McpResourceInfo("uri://test", "test", "Title", "Desc", "text/plain", false, "Class1") }; + + // Act + var summary = new McpSummary(tools, prompts, resources); + + // Assert + Assert.Single(summary.Tools); + Assert.Single(summary.Prompts); + Assert.Single(summary.Resources); + } + + [Fact] + public void McpToolInfo_WithEmptyParameters_Works() + { + // Act + var tool = new McpToolInfo("TestTool", "Description", "TestClass", []); + + // Assert + Assert.Empty(tool.Parameters); + } + + [Fact] + public void McpPromptInfo_WithMultipleParameters_Works() + { + // Arrange + var parameters = new[] + { + new McpParameterInfo("param1", "string", "Desc1", true), + new McpParameterInfo("param2", "int", "Desc2", false) + }; + + // Act + var prompt = new McpPromptInfo("TestPrompt", "Description", "TestClass", parameters); + + // Assert + Assert.Equal(2, prompt.Parameters.Count); + } + + [Fact] + public void McpResourceInfo_NonTemplate_WorksCorrectly() + { + // Act + var resource = new McpResourceInfo( + "test://static/resource", + "test-static", + "Static Resource", + "A static resource", + "text/plain", + false, + "TestClass"); + + // Assert + Assert.False(resource.IsTemplate); + Assert.DoesNotContain("{", resource.Uri); + } +} diff --git a/tests/Tools/McpServer.Tests/Tools/ComponentDetailToolsTests.cs b/tests/Tools/McpServer.Tests/Tools/ComponentDetailToolsTests.cs new file mode 100644 index 0000000000..9bd6e29dd0 --- /dev/null +++ b/tests/Tools/McpServer.Tests/Tools/ComponentDetailToolsTests.cs @@ -0,0 +1,96 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Tools; + +/// +/// Tests for . +/// +public class ComponentDetailToolsTests +{ + private readonly ComponentDetailTools _tools; + private readonly FluentUIDocumentationService _documentationService; + + public ComponentDetailToolsTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _tools = new ComponentDetailTools(_documentationService); + } + + [Theory] + [InlineData("FluentButton")] + [InlineData("FluentTextInput")] + [InlineData("FluentCard")] + public void GetComponentDetails_ReturnsDetailsForKnownComponents(string componentName) + { + // Act + var result = _tools.GetComponentDetails(componentName); + + // Assert + Assert.NotEmpty(result); + Assert.Contains(componentName, result); + Assert.Contains("Parameters", result); + } + + [Fact] + public void GetComponentDetails_ReturnsMessageForUnknownComponent() + { + // Act + var result = _tools.GetComponentDetails("NonExistentComponent"); + + // Assert + Assert.Contains("not found", result); + } + + [Fact] + public void GetComponentDetails_FluentButton_ContainsAppearanceParameter() + { + // Act + var result = _tools.GetComponentDetails("FluentButton"); + + // Assert + Assert.Contains("Appearance", result); + } + + [Fact] + public void GetComponentDetails_IsCaseInsensitive() + { + // Act + var resultLower = _tools.GetComponentDetails("fluentbutton"); + var resultMixed = _tools.GetComponentDetails("FluentButton"); + + // Assert + // Both should find the component (or both should not find it) + var lowerFound = !resultLower.Contains("not found"); + var mixedFound = !resultMixed.Contains("not found"); + Assert.Equal(lowerFound, mixedFound); + } + + [Theory] + [InlineData("FluentButton")] + [InlineData("FluentTextField")] + public void GetComponentExample_ReturnsExampleForKnownComponents(string componentName) + { + // Act + var result = _tools.GetComponentExample(componentName); + + // Assert + Assert.NotEmpty(result); + Assert.Contains(componentName, result); + } + + [Fact] + public void GetComponentExample_ReturnsMessageForUnknownComponent() + { + // Act + var result = _tools.GetComponentExample("NonExistentComponent"); + + // Assert + Assert.Contains("not found", result); + } +} diff --git a/tests/Tools/McpServer.Tests/Tools/ComponentListToolsTests.cs b/tests/Tools/McpServer.Tests/Tools/ComponentListToolsTests.cs new file mode 100644 index 0000000000..cc6c2f196b --- /dev/null +++ b/tests/Tools/McpServer.Tests/Tools/ComponentListToolsTests.cs @@ -0,0 +1,90 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Tools; + +/// +/// Tests for . +/// +public class ComponentListToolsTests +{ + private readonly ComponentListTools _tools; + private readonly FluentUIDocumentationService _documentationService; + + public ComponentListToolsTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _tools = new ComponentListTools(_documentationService); + } + + [Fact] + public void ListComponents_ReturnsMarkdownWithComponents() + { + // Act + var result = _tools.ListComponents(); + + // Assert + Assert.NotEmpty(result); + Assert.Contains("FluentButton", result); + Assert.Contains("#", result); // Contains markdown headers + } + + [Fact] + public void ListComponents_WithCategory_FiltersResults() + { + // Act + var result = _tools.ListComponents("Button"); + + // Assert + Assert.NotEmpty(result); + Assert.Contains("FluentButton", result); + } + + [Fact] + public void ListComponents_WithInvalidCategory_ReturnsMessage() + { + // Act + var result = _tools.ListComponents("NonExistentCategory"); + + // Assert + Assert.Contains("No components found", result); + } + + [Fact] + public void SearchComponents_FindsMatchingComponents() + { + // Act + var result = _tools.SearchComponents("button"); + + // Assert + Assert.NotEmpty(result); + Assert.Contains("FluentButton", result); + } + + [Fact] + public void SearchComponents_ReturnsMessageForNoMatches() + { + // Act + var result = _tools.SearchComponents("xyz123nonexistent"); + + // Assert + Assert.Contains("No components found", result); + } + + [Fact] + public void ListCategories_ReturnsMarkdownWithCategories() + { + // Act + var result = _tools.ListCategories(); + + // Assert + Assert.NotEmpty(result); + Assert.Contains("Button", result); + Assert.Contains("#", result); // Contains markdown headers + } +} diff --git a/tests/Tools/McpServer.Tests/Tools/EnumToolsTests.cs b/tests/Tools/McpServer.Tests/Tools/EnumToolsTests.cs new file mode 100644 index 0000000000..0dabcf557e --- /dev/null +++ b/tests/Tools/McpServer.Tests/Tools/EnumToolsTests.cs @@ -0,0 +1,101 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Tools; + +/// +/// Tests for . +/// +public class EnumToolsTests +{ + private readonly EnumTools _tools; + private readonly FluentUIDocumentationService _documentationService; + + public EnumToolsTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _tools = new EnumTools(_documentationService); + } + + [Fact] + public void ListEnums_ReturnsMarkdownWithEnums() + { + // Act + var result = _tools.ListEnums(); + + // Assert + Assert.NotEmpty(result); + Assert.Contains("#", result); // Contains markdown headers + } + + [Fact] + public void ListEnums_WithComponentFilter_FiltersResults() + { + // Act + var result = _tools.ListEnums("FluentButton"); + + // Assert + Assert.NotEmpty(result); + } + + [Theory] + [InlineData("ButtonAppearance")] + [InlineData("Color")] + [InlineData("Orientation")] + public void GetEnumValues_ReturnsValuesForKnownEnums(string enumName) + { + // Act + var result = _tools.GetEnumValues(enumName); + + // Assert + Assert.NotEmpty(result); + Assert.Contains(enumName, result); + Assert.Contains("Values", result); + } + + [Fact] + public void GetEnumValues_ReturnsMessageForUnknownEnum() + { + // Act + var result = _tools.GetEnumValues("NonExistentEnum"); + + // Assert + Assert.Contains("not found", result); + } + + [Fact] + public void GetEnumValues_ButtonAppearance_ContainsExpectedValues() + { + // Act + var result = _tools.GetEnumValues("ButtonAppearance"); + + // Assert + Assert.Contains("Primary", result); + } + + [Fact] + public void GetComponentEnums_FluentButton_ReturnsEnums() + { + // Act + var result = _tools.GetComponentEnums("FluentButton"); + + // Assert + Assert.NotEmpty(result); + Assert.Contains("FluentButton", result); + } + + [Fact] + public void GetComponentEnums_ReturnsMessageForUnknownComponent() + { + // Act + var result = _tools.GetComponentEnums("NonExistentComponent"); + + // Assert + Assert.Contains("not found", result); + } +} diff --git a/tests/Tools/McpServer.Tests/Tools/GuideToolsTests.cs b/tests/Tools/McpServer.Tests/Tools/GuideToolsTests.cs new file mode 100644 index 0000000000..f42b21c09f --- /dev/null +++ b/tests/Tools/McpServer.Tests/Tools/GuideToolsTests.cs @@ -0,0 +1,84 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Tools; + +/// +/// Tests for . +/// +public class GuideToolsTests +{ + private readonly GuideTools _tools; + private readonly DocumentationGuideService _guideService; + + public GuideToolsTests() + { + _guideService = new DocumentationGuideService(); + _tools = new GuideTools(_guideService); + } + + [Fact] + public void ListGuides_ReturnsMarkdownWithGuides() + { + // Act + var result = _tools.ListGuides(); + + // Assert + Assert.NotEmpty(result); + Assert.Contains("#", result); // Contains markdown headers + Assert.Contains("installation", result, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData("installation")] + [InlineData("defaultvalues")] + [InlineData("localization")] + [InlineData("styles")] + public void GetGuide_ReturnsContentForKnownGuides(string guideKey) + { + // Act + var result = _tools.GetGuide(guideKey); + + // Assert + Assert.NotEmpty(result); + // The content should not start with "Guide 'X' not found" + Assert.False(result.StartsWith($"Guide '{guideKey}' not found"), $"Expected content, but got 'not found' message for {guideKey}"); + } + + [Fact] + public void GetGuide_ReturnsMessageForUnknownGuide() + { + // Act + var result = _tools.GetGuide("nonexistent-guide"); + + // Assert + Assert.Contains("not found", result, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData("install")] + [InlineData("nuget")] + [InlineData("package")] + public void SearchGuides_FindsMatchingGuides(string searchTerm) + { + // Act + var result = _tools.SearchGuides(searchTerm); + + // Assert + Assert.NotEmpty(result); + } + + [Fact] + public void SearchGuides_ReturnsMessageForNoMatches() + { + // Act + var result = _tools.SearchGuides("xyz123nonexistent"); + + // Assert + Assert.Contains("No matches found", result); + } +} diff --git a/tests/Tools/McpServer.Tests/Tools/ToolOutputHelperTests.cs b/tests/Tools/McpServer.Tests/Tools/ToolOutputHelperTests.cs new file mode 100644 index 0000000000..29fc8b29cc --- /dev/null +++ b/tests/Tools/McpServer.Tests/Tools/ToolOutputHelperTests.cs @@ -0,0 +1,256 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Models; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Tools; + +/// +/// Tests for . +/// +public class ToolOutputHelperTests +{ + [Fact] + public void TruncateSummary_WithNull_ReturnsDash() + { + // Act + var result = ToolOutputHelper.TruncateSummary(null, 50); + + // Assert + Assert.Equal("-", result); + } + + [Fact] + public void TruncateSummary_WithEmpty_ReturnsDash() + { + // Act + var result = ToolOutputHelper.TruncateSummary(string.Empty, 50); + + // Assert + Assert.Equal("-", result); + } + + [Fact] + public void TruncateSummary_WithShortText_ReturnsOriginal() + { + // Arrange + var text = "Short text"; + + // Act + var result = ToolOutputHelper.TruncateSummary(text, 50); + + // Assert + Assert.Equal(text, result); + } + + [Fact] + public void TruncateSummary_WithLongText_TruncatesWithEllipsis() + { + // Arrange + var text = "This is a very long text that should be truncated"; + + // Act + var result = ToolOutputHelper.TruncateSummary(text, 20); + + // Assert + Assert.Equal(20, result.Length); + Assert.EndsWith("...", result); + } + + [Fact] + public void TruncateSummary_WithExactLength_ReturnsOriginal() + { + // Arrange + var text = "Exact length text"; + + // Act + var result = ToolOutputHelper.TruncateSummary(text, text.Length); + + // Assert + Assert.Equal(text, result); + } + + [Theory] + [InlineData("Id", true)] + [InlineData("Label", true)] + [InlineData("Placeholder", true)] + [InlineData("Value", true)] + [InlineData("Disabled", true)] + [InlineData("Appearance", true)] + [InlineData("Size", true)] + [InlineData("Color", true)] + public void IsCommonExampleParam_WithCommonParams_ReturnsTrue(string paramName, bool expected) + { + // Act + var result = ToolOutputHelper.IsCommonExampleParam(paramName); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("CustomProperty")] + [InlineData("InternalId")] + [InlineData("SomeRandomParam")] + public void IsCommonExampleParam_WithUncommonParams_ReturnsFalse(string paramName) + { + // Act + var result = ToolOutputHelper.IsCommonExampleParam(paramName); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsCommonExampleParam_IsCaseInsensitive() + { + // Act & Assert + Assert.True(ToolOutputHelper.IsCommonExampleParam("id")); + Assert.True(ToolOutputHelper.IsCommonExampleParam("ID")); + Assert.True(ToolOutputHelper.IsCommonExampleParam("Id")); + } + + [Fact] + public void GetExampleValue_WithEnumValues_ReturnsFirstEnum() + { + // Arrange + var param = new PropertyInfo + { + Name = "Appearance", + Type = "ButtonAppearance", + EnumValues = new[] { "Accent", "Outline", "Stealth" } + }; + + // Act + var result = ToolOutputHelper.GetExampleValue(param); + + // Assert + Assert.Contains("Accent", result); + } + + [Fact] + public void GetExampleValue_WithStringType_ReturnsPlaceholder() + { + // Arrange + var param = new PropertyInfo + { + Name = "Label", + Type = "string", + EnumValues = Array.Empty() + }; + + // Act + var result = ToolOutputHelper.GetExampleValue(param); + + // Assert + Assert.Contains("label", result.ToLowerInvariant()); + } + + [Fact] + public void GetExampleValue_WithBoolType_ReturnsTrue() + { + // Arrange + var param = new PropertyInfo + { + Name = "Disabled", + Type = "bool", + EnumValues = Array.Empty() + }; + + // Act + var result = ToolOutputHelper.GetExampleValue(param); + + // Assert + Assert.Equal("true", result); + } + + [Fact] + public void GetExampleValue_WithIntType_Returns42() + { + // Arrange + var param = new PropertyInfo + { + Name = "Count", + Type = "int", + EnumValues = Array.Empty() + }; + + // Act + var result = ToolOutputHelper.GetExampleValue(param); + + // Assert + Assert.Equal("42", result); + } + + [Fact] + public void ExtractEventType_WithGenericEventCallback_ReturnsTypeParameter() + { + // Arrange + var eventType = "EventCallback"; + + // Act + var result = ToolOutputHelper.ExtractEventType(eventType); + + // Assert + Assert.Equal("MouseEventArgs", result); + } + + [Fact] + public void ExtractEventType_WithNonGeneric_ReturnsEventArgs() + { + // Arrange + var eventType = "EventCallback"; + + // Act + var result = ToolOutputHelper.ExtractEventType(eventType); + + // Assert + Assert.Equal("EventArgs", result); + } + + [Fact] + public void AppendHeader_AppendsCorrectMarkdown() + { + // Arrange + var sb = new StringBuilder(); + + // Act + ToolOutputHelper.AppendHeader(sb, "Test Title", 2); + + // Assert + var result = sb.ToString(); + Assert.Contains("## Test Title", result); + } + + [Fact] + public void AppendHeader_WithLevel1_UsesSingleHash() + { + // Arrange + var sb = new StringBuilder(); + + // Act + ToolOutputHelper.AppendHeader(sb, "Main Title", 1); + + // Assert + var result = sb.ToString(); + Assert.StartsWith("# Main Title", result); + } + + [Fact] + public void AppendTableHeader_CreatesMarkdownTable() + { + // Arrange + var sb = new StringBuilder(); + + // Act + ToolOutputHelper.AppendTableHeader(sb, "Name", "Type", "Description"); + + // Assert + var result = sb.ToString(); + Assert.Contains("| Name | Type | Description |", result); + Assert.Contains("|------|------|------|", result); + } +} diff --git a/tests/Tools/McpServer.Tests/Tools/ToolsEdgeCasesTests.cs b/tests/Tools/McpServer.Tests/Tools/ToolsEdgeCasesTests.cs new file mode 100644 index 0000000000..6d82fc8c5e --- /dev/null +++ b/tests/Tools/McpServer.Tests/Tools/ToolsEdgeCasesTests.cs @@ -0,0 +1,156 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Tools; + +/// +/// Additional tests for Tools to improve code coverage. +/// +public class ToolsEdgeCasesTests +{ + private readonly FluentUIDocumentationService _documentationService; + + public ToolsEdgeCasesTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + } + + [Fact] + public void ComponentListTools_SearchComponents_WithEmptyTerm_ReturnsMessage() + { + // Arrange + var tools = new ComponentListTools(_documentationService); + + // Act + var result = tools.SearchComponents(""); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void ComponentDetailTools_GetComponentDetails_CaseInsensitive() + { + // Arrange + var tools = new ComponentDetailTools(_documentationService); + + // Act + var resultLower = tools.GetComponentDetails("fluentbutton"); + var resultUpper = tools.GetComponentDetails("FLUENTBUTTON"); + var resultMixed = tools.GetComponentDetails("FluentButton"); + + // Assert + Assert.NotNull(resultLower); + Assert.NotNull(resultUpper); + Assert.NotNull(resultMixed); + Assert.Contains("FluentButton", resultLower); + } + + [Fact] + public void EnumTools_GetEnumValues_WithInvalidEnum_ReturnsNotFound() + { + // Arrange + var tools = new EnumTools(_documentationService); + + // Act + var result = tools.GetEnumValues("NonExistentEnum"); + + // Assert + Assert.Contains("not found", result, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void GuideTools_GetGuide_WithValidTopic_ReturnsContent() + { + // Arrange + var guideService = new DocumentationGuideService(); + var tools = new GuideTools(guideService); + + // Act + var result = tools.GetGuide("installation"); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GuideTools_GetGuide_WithInvalidTopic_ReturnsNotFound() + { + // Arrange + var guideService = new DocumentationGuideService(); + var tools = new GuideTools(guideService); + + // Act + var result = tools.GetGuide("invalid-topic-that-does-not-exist"); + + // Assert + Assert.Contains("not found", result, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void GuideTools_ListGuides_ReturnsNonEmptyList() + { + // Arrange + var guideService = new DocumentationGuideService(); + var tools = new GuideTools(guideService); + + // Act + var result = tools.ListGuides(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void VersionTools_GetVersionInfo_ContainsAllSections() + { + // Arrange + var tools = new VersionTools(_documentationService); + + // Act + var result = tools.GetVersionInfo(); + + // Assert + Assert.Contains("MCP Server Version", result); + Assert.Contains("Components Version", result); + Assert.Contains("Documentation Generated", result); + Assert.Contains("Documentation Available", result); + Assert.Contains("Documentation Statistics", result); + Assert.Contains("Compatibility", result); + } + + [Fact] + public void VersionTools_CheckVersionCompatibility_WithEmptyVersion_ReturnsError() + { + // Arrange + var tools = new VersionTools(_documentationService); + + // Act + var result = tools.CheckVersionCompatibility("invalid-version"); + + // Assert + Assert.Contains("Unable to parse", result); + } + + [Fact] + public void ComponentListTools_ListComponents_WithNullCategory_ReturnsAllComponents() + { + // Arrange + var tools = new ComponentListTools(_documentationService); + + // Act + var result = tools.ListComponents(null); + + // Assert + Assert.NotNull(result); + Assert.Contains("FluentButton", result); + } +} diff --git a/tests/Tools/McpServer.Tests/Tools/VersionToolsTests.cs b/tests/Tools/McpServer.Tests/Tools/VersionToolsTests.cs new file mode 100644 index 0000000000..00aecd0b5b --- /dev/null +++ b/tests/Tools/McpServer.Tests/Tools/VersionToolsTests.cs @@ -0,0 +1,167 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Services; +using Microsoft.FluentUI.AspNetCore.Components.McpServer.Tools; + +namespace Microsoft.FluentUI.AspNetCore.Components.McpServer.Tests.Tools; + +/// +/// Tests for . +/// +public class VersionToolsTests +{ + private readonly VersionTools _tools; + private readonly FluentUIDocumentationService _documentationService; + + public VersionToolsTests() + { + var jsonPath = JsonDocumentationFinder.Find(); + _documentationService = new FluentUIDocumentationService(jsonPath); + _tools = new VersionTools(_documentationService); + } + + [Fact] + public void GetVersionInfo_ReturnsNonEmptyString() + { + // Act + var result = _tools.GetVersionInfo(); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void GetVersionInfo_ContainsMcpServerVersion() + { + // Act + var result = _tools.GetVersionInfo(); + + // Assert + Assert.Contains("MCP Server Version", result); + } + + [Fact] + public void GetVersionInfo_ContainsComponentsVersion() + { + // Act + var result = _tools.GetVersionInfo(); + + // Assert + Assert.Contains("Components Version", result); + } + + [Fact] + public void GetVersionInfo_ContainsDocumentationStatistics() + { + // Act + var result = _tools.GetVersionInfo(); + + // Assert + Assert.Contains("Documentation Statistics", result); + Assert.Contains("Components", result); + Assert.Contains("Enums", result); + } + + [Fact] + public void GetVersionInfo_ContainsInstallCommand() + { + // Act + var result = _tools.GetVersionInfo(); + + // Assert + Assert.Contains("dotnet add package", result); + Assert.Contains("Microsoft.FluentUI.AspNetCore.Components", result); + } + + [Fact] + public void CheckVersionCompatibility_WithExactMatch_ReturnsCompatible() + { + // Arrange + var expectedVersion = _documentationService.ComponentsVersion; + + // Act + var result = _tools.CheckVersionCompatibility(expectedVersion); + + // Assert + Assert.Contains("Compatible", result); + Assert.Contains("✅", result); + } + + [Fact] + public void CheckVersionCompatibility_WithMinorDifference_ReturnsWarning() + { + // Arrange - Use a version with same major but different minor + var expectedVersion = _documentationService.ComponentsVersion; + var parts = expectedVersion.Split('.'); + if (parts.Length >= 2 && int.TryParse(parts[1], out var minor)) + { + var differentMinor = $"{parts[0]}.{minor + 1}.0"; + + // Act + var result = _tools.CheckVersionCompatibility(differentMinor); + + // Assert + Assert.Contains("Version Compatibility Check", result); + } + } + + [Fact] + public void CheckVersionCompatibility_WithMajorDifference_ReturnsWarning() + { + // Arrange + var differentMajor = "99.0.0"; + + // Act + var result = _tools.CheckVersionCompatibility(differentMajor); + + // Assert + Assert.Contains("⚠️", result); + Assert.Contains("Major version mismatch", result); + } + + [Fact] + public void CheckVersionCompatibility_WithInvalidVersion_ReturnsError() + { + // Arrange + var invalidVersion = "not-a-version"; + + // Act + var result = _tools.CheckVersionCompatibility(invalidVersion); + + // Assert + Assert.Contains("Unable to parse", result); + } + + [Fact] + public void CheckVersionCompatibility_ContainsRecommendedActions() + { + // Arrange + var differentVersion = "1.0.0"; + + // Act + var result = _tools.CheckVersionCompatibility(differentVersion); + + // Assert + Assert.Contains("Recommended Actions", result); + Assert.Contains("dotnet add package", result); + Assert.Contains("dotnet tool update", result); + } + + [Theory] + [InlineData("5.0.0")] + [InlineData("4.10.3")] + [InlineData("5.0.0-preview.1")] + public void CheckVersionCompatibility_WithVariousVersions_ReturnsResult(string version) + { + // Act + var result = _tools.CheckVersionCompatibility(version); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.Contains("Version Compatibility Check", result); + } +}