Skip to content

Commit a988a84

Browse files
committed
Add overloads to WithTool to provide name/title/description
This looks nicer in code than custom attributes, and is a useful alternative to using `[McpServerTool]` attribute directly.
1 parent 96c6b4c commit a988a84

File tree

6 files changed

+171
-106
lines changed

6 files changed

+171
-106
lines changed

.github/chatmodes/math.chatmode.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
---
22
description: 'An Ask mode for math-related queries, which can render LaTeX equations.'
3-
tools: ['vscodeAPI', 'latex']
3+
tools: ['latex']
44
---
5-
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.
6-
7-
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.
8-
9-
Always place the resulting markdown image from the #latex_markdown tool in its own
10-
line to ensure proper formatting and visibility.
5+
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.

.vscode/mcp.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
"latex": {
44
"type": "stdio",
55
"command": "dotnet",
6+
"cwd": "${workspaceFolder}${/}mcp",
67
"args": [
78
"run",
8-
"${workspaceFolder}${/}samples${/}latex.cs"
9-
]
9+
"latex.cs",
10+
],
11+
"env": {
12+
// Enables Edit and Continue
13+
"COMPLUS_ForceENC": "1",
14+
}
1015
}
1116
}
1217
}

samples/latex.cs

Lines changed: 0 additions & 66 deletions
This file was deleted.

src/MCPDemo/MCPDemo.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11+
<PackageReference Include="DotNetConfig.Configuration" Version="1.2.0" />
1112
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.7" />
1213
<PackageReference Include="ModelContextProtocol" Version="0.3.0-preview.2" />
1314
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />

src/MCPDemo/Program.cs

Lines changed: 123 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,140 @@
1-
using SixLabors.ImageSharp;
1+
using System.Diagnostics;
2+
using ModelContextProtocol.Protocol;
3+
using ModelContextProtocol.Server;
4+
using SixLabors.ImageSharp;
25
using SixLabors.ImageSharp.PixelFormats;
36

47
var builder = App.CreateBuilder(args);
5-
builder.Services.AddHttpClient();
8+
builder.Configuration.AddDotNetConfig();
9+
10+
var initialized = false;
11+
bool? darkMode = bool.TryParse(builder.Configuration["latex:darkMode"], out var dm) ? dm : null;
12+
string? fontSize = builder.Configuration["latex:fontSize"];
13+
// See https://editor.codecogs.com/docs/4-LaTeX_rendering.php#overview_anchor
14+
var fonts = new Dictionary<string, string>
15+
{
16+
{ "Tiny", "tiny" },
17+
{ "Small", "small" },
18+
{ "Large", "large" },
19+
{ "LARGE", "LARGE" },
20+
{ "Huge", "huge"}
21+
};
22+
23+
Debugger.Launch();
624

725
builder.Services
26+
.AddHttpClient()
827
.AddMcpServer()
928
.WithStdioServerTransport()
1029
.WithTool(
11-
[Description("Converts LaTeX equations into markdown-formatted images for display inline.")] async
12-
(IHttpClientFactory httpFactory,
13-
[Description("The LaTeX equation to render.")] string latex,
14-
[Description("Use dark mode by inverting the colors in the output.")] bool darkMode) =>
15-
{
16-
var colors = darkMode ? @"\bg{black}\fg{white}" : @"\bg{white}\fg{black}";
17-
var query = WebUtility.UrlEncode(@"\small\dpi{300}" + colors + latex);
18-
var url = $"https://latex.codecogs.com/png.image?{query}";
19-
using var client = httpFactory.CreateClient();
20-
using var response = await client.GetAsync(url);
21-
22-
if (response.IsSuccessStatusCode)
30+
name: "latex",
31+
title: "LaTeX to Image",
32+
description: "Converts LaTeX equations into markdown-formatted images for inline display.",
33+
tool: async (IHttpClientFactory httpFactory, IMcpServer server,
34+
[Description("The LaTeX equation to render.")] string latex)
35+
=>
2336
{
37+
// On first tool run, we ask for preferences for dark mode and font size.
38+
if (!initialized)
39+
{
40+
initialized = true;
41+
(darkMode, fontSize) = await SetPreferences(server, darkMode, fontSize);
42+
}
43+
44+
var colors = darkMode switch
45+
{
46+
true => @"\fg{white}",
47+
false => @"\fg{black}",
48+
null => @"\bg{white}\fg{black}"
49+
};
50+
51+
var query = WebUtility.UrlEncode(@"\dpi{300}\" + (fontSize ?? "small") + colors + new string([.. latex.Where(c => !char.IsWhiteSpace(c))]));
52+
var url = $"https://latex.codecogs.com/png.image?{query}";
53+
54+
using var client = httpFactory.CreateClient();
55+
using var response = await client.GetAsync(url);
56+
response.EnsureSuccessStatusCode();
57+
2458
using var image = Image.Load<Rgba32>(await response.Content.ReadAsStreamAsync());
2559
using var ms = new MemoryStream();
2660
image.SaveAsPng(ms);
2761
var base64 = Convert.ToBase64String(ms.ToArray());
28-
return
29-
$"""
30-
![{latex}](
31-
data:image/png;base64,{base64}
32-
)
33-
""";
34-
}
35-
else
62+
return $"> ![LaTeX Equation](data:image/png;base64,{base64})";
63+
})
64+
.WithTool(
65+
name: "latex_getprefs",
66+
title: "Get LaTeX Preferences",
67+
description: "Gets the saved LaTeX rendering preferences for dark mode and font size.",
68+
tool: () => new { darkMode, fontSize })
69+
.WithTool(
70+
name: "latex_setprefs",
71+
title: "Set LaTeX Preferences",
72+
description: "Sets the LaTeX rendering preferences for dark mode and font size.",
73+
tool: async (IMcpServer server,
74+
[Description("Use dark mode by inverting the colors in the output.")] bool? darkMode = null,
75+
[Description("Font size to use in the output: tiny=5pt, small=9pt, large=12pt, LARGE=18pt, huge=20pt")] string? fontSize = null)
76+
=> (darkMode, fontSize) = await SetPreferences(server, darkMode, fontSize));
77+
78+
await builder.Build().RunAsync();
79+
80+
/// <summary>Saves the LaTeX rendering preferences to configuration.</summary>
81+
async ValueTask<(bool? darkMode, string? fontSize)> SetPreferences(IMcpServer server, bool? darkMode, string? fontSize)
82+
{
83+
if ((darkMode is null || fontSize is null || !fonts.ContainsValue(fontSize)) && server.ClientCapabilities?.Elicitation != null)
84+
{
85+
var result = await server.ElicitAsync(new()
86+
{
87+
Message = "Specify LaTeX rendering preferences",
88+
RequestedSchema = new()
3689
{
37-
return
38-
$"""
39-
```latex
40-
{latex}
41-
```
42-
> {response.ReasonPhrase}
43-
""";
90+
Required = ["darkMode", "fontSize"],
91+
Properties =
92+
{
93+
{ "darkMode", new ElicitRequestParams.BooleanSchema()
94+
{
95+
Title = "Dark Mode",
96+
Description = "Use dark mode?",
97+
Default = darkMode
98+
}
99+
},
100+
{ "fontSize", new ElicitRequestParams.EnumSchema()
101+
{
102+
Title = "Font Size",
103+
Description = "Font size to use for the LaTeX rendering.",
104+
Enum = [.. fonts.Values],
105+
EnumNames = [.. fonts.Keys],
106+
}
107+
},
108+
},
44109
}
45110
});
46111

47-
await builder.Build().RunAsync();
112+
if (result.Action == "accept" && result.Content is { } content)
113+
{
114+
darkMode = content["darkMode"].GetBoolean();
115+
fontSize = content["fontSize"].GetString() ?? "tiny";
116+
117+
DotNetConfig.Config.Build(DotNetConfig.ConfigLevel.Global)
118+
.GetSection("latex")
119+
.SetBoolean("darkMode", darkMode.Value)
120+
.SetString("fontSize", fontSize);
121+
}
122+
// action == cancel is not supported in vscode
123+
// actoin == decline would be equal to "ignore" so we just don't set anything.
124+
return (darkMode, fontSize);
125+
}
126+
else
127+
{
128+
// We persist to ~/.netconfig
129+
var config = DotNetConfig.Config.Build(DotNetConfig.ConfigLevel.Global).GetSection("latex");
130+
if (darkMode != null)
131+
config = config.SetBoolean("darkMode", darkMode.Value);
132+
if (fontSize != null && fonts.ContainsValue(fontSize))
133+
config = config.SetString("fontSize", fontSize);
134+
else
135+
fontSize = null;
136+
137+
return (darkMode, fontSize);
138+
}
139+
}
140+

src/Smith/McpExtensions.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,46 @@ public static class McpExtensions
1515
/// Registers a specific method as a server tool.
1616
/// </summary>
1717
public IMcpServerBuilder WithTool(Delegate tool, JsonSerializerOptions? options = null)
18+
=> WithTool(builder, null!, null!, tool, options);
19+
20+
/// <summary>
21+
/// Registers a specific method as a server tool.
22+
/// </summary>
23+
/// <param name="name">The name of the tool.</param>
24+
/// <remarks>
25+
/// The tool description will be set to the <see cref="DescriptionAttribute"/> on the method, if any.
26+
/// </remarks>
27+
public IMcpServerBuilder WithTool(string name, Delegate tool, JsonSerializerOptions? options = null)
28+
=> WithTool(builder, name, null!, null!, tool, options);
29+
30+
/// <summary>
31+
/// Registers a specific method as a server tool.
32+
/// </summary>
33+
/// <param name="name">The name of the tool.</param>
34+
/// <param name="title">A human-readable title for the tool that can be displayed to users.</param>
35+
/// <remarks>
36+
/// The tool description will be set to the <see cref="DescriptionAttribute"/> on the method, if any.
37+
/// </remarks>
38+
public IMcpServerBuilder WithTool(string name, string title, Delegate tool, JsonSerializerOptions? options = null)
39+
=> WithTool(builder, name, title, null!, tool, options);
40+
41+
/// <summary>
42+
/// Registers a specific method as a server tool.
43+
/// </summary>
44+
/// <param name="name">The name of the tool.</param>
45+
/// <param name="title">A human-readable title for the tool that can be displayed to users.</param>
46+
/// <param name="description">The tool description.</param>
47+
public IMcpServerBuilder WithTool(string name, string title, string description, Delegate tool, JsonSerializerOptions? options = null)
1848
{
1949
builder.Services.AddSingleton(services
20-
=> McpServerTool.Create(tool, new() { Services = services, SerializerOptions = options }));
50+
=> McpServerTool.Create(tool, new()
51+
{
52+
Name = name,
53+
Title = title,
54+
Description = description,
55+
Services = services,
56+
SerializerOptions = options
57+
}));
2158

2259
return builder;
2360
}

0 commit comments

Comments
 (0)