diff --git a/.github/chatmodes/math.chatmode.md b/.github/chatmodes/math.chatmode.md index 0e854db..4cb6854 100644 --- a/.github/chatmodes/math.chatmode.md +++ b/.github/chatmodes/math.chatmode.md @@ -1,10 +1,5 @@ --- description: 'An Ask mode for math-related queries, which can render LaTeX equations.' -tools: ['vscodeAPI', 'latex'] +tools: ['latex'] --- -Actively use the #latex_markdown tool to render LaTeX equations in your responses as inline markdown images to enhance clarity and visual appeal. This tool is particularly useful for displaying mathematical equations, formulas, and other LaTeX-rendered content in a visually engaging manner. - -Before invoking #latex_markdown, retrieve the user's theme using the #vscodeAPI to ensure the LaTeX rendering is compatible with their current theme. This will help maintain consistency in the appearance of rendered content across different user interfaces. - -Always place the resulting markdown image from the #latex_markdown tool in its own -line to ensure proper formatting and visibility. \ No newline at end of file +Actively use the #latex tool to render LaTeX equations in your responses as inline markdown images to enhance clarity and visual appeal. This tool is particularly useful for displaying mathematical equations, formulas, and other LaTeX-rendered content in a visually engaging manner. \ No newline at end of file diff --git a/.vscode/mcp.json b/.vscode/mcp.json index f595612..7ebac0a 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -3,10 +3,15 @@ "latex": { "type": "stdio", "command": "dotnet", + "cwd": "${workspaceFolder}${/}mcp", "args": [ "run", - "${workspaceFolder}${/}samples${/}latex.cs" - ] + "latex.cs", + ], + "env": { + // Enables Edit and Continue + "COMPLUS_ForceENC": "1", + } } } } \ No newline at end of file diff --git a/samples/latex.cs b/samples/latex.cs deleted file mode 100644 index 125ed78..0000000 --- a/samples/latex.cs +++ /dev/null @@ -1,66 +0,0 @@ -#:package Smith@0.2.3 -#:package ModelContextProtocol@0.3.0-preview.* -#:package Microsoft.Extensions.Http@9.* -#:package SixLabors.ImageSharp@3.1.* - -using Smith; -using System.ComponentModel; -using ModelContextProtocol.Server; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using Microsoft.Extensions.Logging; - -var builder = Host.CreateApplicationBuilder(args); -builder.Services.AddHttpClient(); -builder.Logging.AddConsole(consoleLogOptions => -{ -// Configure all logs to go to stderr -consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; -}); - -builder.Services - .AddMcpServer() - .WithStdioServerTransport() - .WithToolsFromAssembly(); - -await builder.Build().RunAsync(); - -[McpServerToolType] -public class LaTeX(IHttpClientFactory httpFactory) -{ - [McpServerTool, Description("Converts LaTeX equations into markdown-formatted images for display inline.")] - public async Task LatexMarkdown( - [Description("The LaTeX equation to render.")] string latex, - [Description("Use dark mode by inverting the colors in the output.")] bool darkMode) - { - var colors = darkMode ? @"\fg{white}" : @"\fg{black}"; - var query = WebUtility.UrlEncode(@"\small\dpi{300}" + colors + latex); - var url = $"https://latex.codecogs.com/png.image?{query}"; - using var client = httpFactory.CreateClient(); - using var response = await client.GetAsync(url); - - if (response.IsSuccessStatusCode) - { - using var image = Image.Load(await response.Content.ReadAsStreamAsync()); - using var ms = new MemoryStream(); - image.SaveAsPng(ms); - var base64 = Convert.ToBase64String(ms.ToArray()); - return - $""" - ![{latex}]( - data:image/png;base64,{base64} - ) - """; - } - else - { - return - $""" - ```latex - {latex} - ``` - > {response.ReasonPhrase} - """; - } - } -} \ No newline at end of file diff --git a/src/MCPDemo/MCPDemo.csproj b/src/MCPDemo/MCPDemo.csproj index 96871a8..364e2e9 100644 --- a/src/MCPDemo/MCPDemo.csproj +++ b/src/MCPDemo/MCPDemo.csproj @@ -8,6 +8,7 @@ + diff --git a/src/MCPDemo/Program.cs b/src/MCPDemo/Program.cs index 42dce40..3bff239 100644 --- a/src/MCPDemo/Program.cs +++ b/src/MCPDemo/Program.cs @@ -1,47 +1,140 @@ -using SixLabors.ImageSharp; +using System.Diagnostics; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; var builder = App.CreateBuilder(args); -builder.Services.AddHttpClient(); +builder.Configuration.AddDotNetConfig(); + +var initialized = false; +bool? darkMode = bool.TryParse(builder.Configuration["latex:darkMode"], out var dm) ? dm : null; +string? fontSize = builder.Configuration["latex:fontSize"]; +// See https://editor.codecogs.com/docs/4-LaTeX_rendering.php#overview_anchor +var fonts = new Dictionary +{ + { "Tiny", "tiny" }, + { "Small", "small" }, + { "Large", "large" }, + { "LARGE", "LARGE" }, + { "Huge", "huge"} +}; + +Debugger.Launch(); builder.Services + .AddHttpClient() .AddMcpServer() .WithStdioServerTransport() .WithTool( - [Description("Converts LaTeX equations into markdown-formatted images for display inline.")] async - (IHttpClientFactory httpFactory, - [Description("The LaTeX equation to render.")] string latex, - [Description("Use dark mode by inverting the colors in the output.")] bool darkMode) => - { - var colors = darkMode ? @"\bg{black}\fg{white}" : @"\bg{white}\fg{black}"; - var query = WebUtility.UrlEncode(@"\small\dpi{300}" + colors + latex); - var url = $"https://latex.codecogs.com/png.image?{query}"; - using var client = httpFactory.CreateClient(); - using var response = await client.GetAsync(url); - - if (response.IsSuccessStatusCode) + name: "latex", + title: "LaTeX to Image", + description: "Converts LaTeX equations into markdown-formatted images for inline display.", + tool: async (IHttpClientFactory httpFactory, IMcpServer server, + [Description("The LaTeX equation to render.")] string latex) + => { + // On first tool run, we ask for preferences for dark mode and font size. + if (!initialized) + { + initialized = true; + (darkMode, fontSize) = await SetPreferences(server, darkMode, fontSize); + } + + var colors = darkMode switch + { + true => @"\fg{white}", + false => @"\fg{black}", + null => @"\bg{white}\fg{black}" + }; + + var query = WebUtility.UrlEncode(@"\dpi{300}\" + (fontSize ?? "small") + colors + new string([.. latex.Where(c => !char.IsWhiteSpace(c))])); + var url = $"https://latex.codecogs.com/png.image?{query}"; + + using var client = httpFactory.CreateClient(); + using var response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + using var image = Image.Load(await response.Content.ReadAsStreamAsync()); using var ms = new MemoryStream(); image.SaveAsPng(ms); var base64 = Convert.ToBase64String(ms.ToArray()); - return - $""" - ![{latex}]( - data:image/png;base64,{base64} - ) - """; - } - else + return $"> ![LaTeX Equation](data:image/png;base64,{base64})"; + }) + .WithTool( + name: "latex_getprefs", + title: "Get LaTeX Preferences", + description: "Gets the saved LaTeX rendering preferences for dark mode and font size.", + tool: () => new { darkMode, fontSize }) + .WithTool( + name: "latex_setprefs", + title: "Set LaTeX Preferences", + description: "Sets the LaTeX rendering preferences for dark mode and font size.", + tool: async (IMcpServer server, + [Description("Use dark mode by inverting the colors in the output.")] bool? darkMode = null, + [Description("Font size to use in the output: tiny=5pt, small=9pt, large=12pt, LARGE=18pt, huge=20pt")] string? fontSize = null) + => (darkMode, fontSize) = await SetPreferences(server, darkMode, fontSize)); + +await builder.Build().RunAsync(); + +/// Saves the LaTeX rendering preferences to configuration. +async ValueTask<(bool? darkMode, string? fontSize)> SetPreferences(IMcpServer server, bool? darkMode, string? fontSize) +{ + if ((darkMode is null || fontSize is null || !fonts.ContainsValue(fontSize)) && server.ClientCapabilities?.Elicitation != null) + { + var result = await server.ElicitAsync(new() + { + Message = "Specify LaTeX rendering preferences", + RequestedSchema = new() { - return - $""" - ```latex - {latex} - ``` - > {response.ReasonPhrase} - """; + Required = ["darkMode", "fontSize"], + Properties = + { + { "darkMode", new ElicitRequestParams.BooleanSchema() + { + Title = "Dark Mode", + Description = "Use dark mode?", + Default = darkMode + } + }, + { "fontSize", new ElicitRequestParams.EnumSchema() + { + Title = "Font Size", + Description = "Font size to use for the LaTeX rendering.", + Enum = [.. fonts.Values], + EnumNames = [.. fonts.Keys], + } + }, + }, } }); -await builder.Build().RunAsync(); \ No newline at end of file + if (result.Action == "accept" && result.Content is { } content) + { + darkMode = content["darkMode"].GetBoolean(); + fontSize = content["fontSize"].GetString() ?? "tiny"; + + DotNetConfig.Config.Build(DotNetConfig.ConfigLevel.Global) + .GetSection("latex") + .SetBoolean("darkMode", darkMode.Value) + .SetString("fontSize", fontSize); + } + // action == cancel is not supported in vscode + // actoin == decline would be equal to "ignore" so we just don't set anything. + return (darkMode, fontSize); + } + else + { + // We persist to ~/.netconfig + var config = DotNetConfig.Config.Build(DotNetConfig.ConfigLevel.Global).GetSection("latex"); + if (darkMode != null) + config = config.SetBoolean("darkMode", darkMode.Value); + if (fontSize != null && fonts.ContainsValue(fontSize)) + config = config.SetString("fontSize", fontSize); + else + fontSize = null; + + return (darkMode, fontSize); + } +} + diff --git a/src/Smith/McpExtensions.cs b/src/Smith/McpExtensions.cs index a8133e9..5f35853 100644 --- a/src/Smith/McpExtensions.cs +++ b/src/Smith/McpExtensions.cs @@ -15,9 +15,46 @@ public static class McpExtensions /// Registers a specific method as a server tool. /// public IMcpServerBuilder WithTool(Delegate tool, JsonSerializerOptions? options = null) + => WithTool(builder, null!, null!, tool, options); + + /// + /// Registers a specific method as a server tool. + /// + /// The name of the tool. + /// + /// The tool description will be set to the on the method, if any. + /// + public IMcpServerBuilder WithTool(string name, Delegate tool, JsonSerializerOptions? options = null) + => WithTool(builder, name, null!, null!, tool, options); + + /// + /// Registers a specific method as a server tool. + /// + /// The name of the tool. + /// A human-readable title for the tool that can be displayed to users. + /// + /// The tool description will be set to the on the method, if any. + /// + public IMcpServerBuilder WithTool(string name, string title, Delegate tool, JsonSerializerOptions? options = null) + => WithTool(builder, name, title, null!, tool, options); + + /// + /// Registers a specific method as a server tool. + /// + /// The name of the tool. + /// A human-readable title for the tool that can be displayed to users. + /// The tool description. + public IMcpServerBuilder WithTool(string name, string title, string description, Delegate tool, JsonSerializerOptions? options = null) { builder.Services.AddSingleton(services - => McpServerTool.Create(tool, new() { Services = services, SerializerOptions = options })); + => McpServerTool.Create(tool, new() + { + Name = name, + Title = title, + Description = description, + Services = services, + SerializerOptions = options + })); return builder; }