1+ /*
2+ MCP server that can turn LaTeX formatted equations into their markdown
3+ image representation for rendering inline in an editor that supports markdown
4+ rendering (i.e. VS Code).
5+
6+ > dotnet run latex.cs
7+
8+ Example mcp.json for VS Code:
9+ {
10+ "servers": {
11+ "latex": {
12+ "type": "stdio",
13+ "command": "dotnet",
14+ "args": [
15+ "run",
16+ "${workspaceFolder}${/}mcp${/}latex.cs"
17+ ],
18+ // optionally hardcode preferences
19+ "env": {
20+ "LATEX__DARKMODE": "true",
21+ "LATEX__FONTSIZE": "tiny"
22+ }
23+ }
24+ }
25+ }
26+
27+ Showcases using elicitation for setting preferences:
28+ * #latex_setprefs: sets dark mode and font size preferences,
29+ * #latex_getprefs: reads the saved preferences.
30+ */
31+ #: package Smith @0.2 .5
32+ #: package DotNetConfig . Configuration@1.2 . *
33+ #: package ModelContextProtocol @0.3 .0 - preview . *
34+ #: package Microsoft . Extensions . Http @9 . *
35+ #: package SixLabors . ImageSharp @3.1 . *
36+
37+ using ModelContextProtocol . Protocol ;
38+ using ModelContextProtocol . Server ;
39+ using SixLabors . ImageSharp ;
40+ using SixLabors . ImageSharp . PixelFormats ;
41+
42+ var builder = App . CreateBuilder ( args ) ;
43+ builder . Configuration . AddDotNetConfig ( ) ;
44+
45+ var initialized = false ;
46+ bool ? darkMode = bool . TryParse ( builder . Configuration [ "latex:darkMode" ] , out var dm ) ? dm : null ;
47+ string ? fontSize = builder . Configuration [ "latex:fontSize" ] ;
48+ // See https://editor.codecogs.com/docs/4-LaTeX_rendering.php#overview_anchor
49+ var fonts = new Dictionary < string , string >
50+ {
51+ { "Tiny" , "tiny" } ,
52+ { "Small" , "small" } ,
53+ { "Large" , "large" } ,
54+ { "LARGE" , "LARGE" } ,
55+ { "Huge" , "huge" }
56+ } ;
57+
58+ builder . Services
59+ . AddHttpClient ( )
60+ . AddMcpServer ( )
61+ . WithStdioServerTransport ( )
62+ . WithTool (
63+ name : "latex" ,
64+ title : "LaTeX to Image" ,
65+ description : "Converts LaTeX equations into markdown-formatted images for inline display." ,
66+ tool : async ( IHttpClientFactory httpFactory , IMcpServer server ,
67+ [ Description ( "The LaTeX equation to render." ) ] string latex )
68+ =>
69+ {
70+ // On first tool run, we ask for preferences for dark mode and font size.
71+ if ( ! initialized )
72+ {
73+ initialized = true ;
74+ ( darkMode , fontSize ) = await SetPreferences ( server , darkMode , fontSize ) ;
75+ }
76+
77+ var colors = darkMode switch
78+ {
79+ true => @"\fg{white}" ,
80+ false => @"\fg{black}" ,
81+ null => @"\bg{white}\fg{black}"
82+ } ;
83+
84+ var query = WebUtility . UrlEncode ( @"\dpi{300}\" + ( fontSize ?? "small" ) + colors + new string ( [ .. latex . Where ( c => ! char . IsWhiteSpace ( c ) ) ] ) ) ;
85+ var url = $ "https://latex.codecogs.com/png.image?{ query } ";
86+
87+ using var client = httpFactory . CreateClient ( ) ;
88+ using var response = await client . GetAsync ( url ) ;
89+ response . EnsureSuccessStatusCode ( ) ;
90+
91+ using var image = Image . Load < Rgba32 > ( await response . Content . ReadAsStreamAsync ( ) ) ;
92+ using var ms = new MemoryStream ( ) ;
93+ image . SaveAsPng ( ms ) ;
94+ var base64 = Convert . ToBase64String ( ms . ToArray ( ) ) ;
95+ return $ "> ";
96+ } )
97+ . WithTool (
98+ name : "latex_getprefs" ,
99+ title : "Get LaTeX Preferences" ,
100+ description : "Gets the saved LaTeX rendering preferences for dark mode and font size." ,
101+ tool : ( ) => new { darkMode , fontSize } ,
102+ options : ToolJsonOptions . Default )
103+ . WithTool (
104+ name : "latex_setprefs" ,
105+ title : "Set LaTeX Preferences" ,
106+ description : "Sets the LaTeX rendering preferences for dark mode and font size." ,
107+ tool : async ( IMcpServer server ,
108+ [ Description ( "Use dark mode by inverting the colors in the output." ) ] bool ? darkMode = null ,
109+ [ Description ( "Font size to use in the output: tiny=5pt, small=9pt, large=12pt, LARGE=18pt, huge=20pt" ) ] string ? fontSize = null )
110+ => ( darkMode , fontSize ) = await SetPreferences ( server , darkMode , fontSize ) ,
111+ options : ToolJsonOptions . Default ) ;
112+
113+ await builder . Build ( ) . RunAsync ( ) ;
114+
115+ /// <summary>Saves the LaTeX rendering preferences to configuration.</summary>
116+ async ValueTask < ( bool ? darkMode , string ? fontSize ) > SetPreferences ( IMcpServer server , bool ? darkMode , string ? fontSize )
117+ {
118+ if ( ( darkMode is null || fontSize is null || ! fonts . ContainsValue ( fontSize ) ) && server . ClientCapabilities ? . Elicitation != null )
119+ {
120+ var result = await server . ElicitAsync ( new ( )
121+ {
122+ Message = "Specify LaTeX rendering preferences" ,
123+ RequestedSchema = new ( )
124+ {
125+ Required = [ "darkMode" , "fontSize" ] ,
126+ Properties =
127+ {
128+ { "darkMode" , new ElicitRequestParams . BooleanSchema ( )
129+ {
130+ Title = "Dark Mode" ,
131+ Description = "Use dark mode?" ,
132+ Default = darkMode
133+ }
134+ } ,
135+ { "fontSize" , new ElicitRequestParams . EnumSchema ( )
136+ {
137+ Title = "Font Size" ,
138+ Description = "Font size to use for the LaTeX rendering." ,
139+ Enum = [ .. fonts . Values ] ,
140+ EnumNames = [ .. fonts . Keys ] ,
141+ }
142+ } ,
143+ } ,
144+ }
145+ } ) ;
146+
147+ if ( result . Action == "accept" && result . Content is { } content )
148+ {
149+ darkMode = content [ "darkMode" ] . GetBoolean ( ) ;
150+ fontSize = content [ "fontSize" ] . GetString ( ) ?? "tiny" ;
151+
152+ DotNetConfig . Config . Build ( DotNetConfig . ConfigLevel . Global )
153+ . GetSection ( "latex" )
154+ . SetBoolean ( "darkMode" , darkMode . Value )
155+ . SetString ( "fontSize" , fontSize ) ;
156+ }
157+ // action == cancel is not supported in vscode
158+ // actoin == decline would be equal to "ignore" so we just don't set anything.
159+ return ( darkMode , fontSize ) ;
160+ }
161+ else
162+ {
163+ // We persist to ~/.netconfig
164+ var config = DotNetConfig . Config . Build ( DotNetConfig . ConfigLevel . Global ) . GetSection ( "latex" ) ;
165+ if ( darkMode != null )
166+ config = config . SetBoolean ( "darkMode" , darkMode . Value ) ;
167+ if ( fontSize != null && fonts . ContainsValue ( fontSize ) )
168+ config = config . SetString ( "fontSize" , fontSize ) ;
169+ else
170+ fontSize = null ;
171+
172+ return ( darkMode , fontSize ) ;
173+ }
174+ }
0 commit comments