Introduction
Modern software applications often require dynamic execution of code in multiple languages. This is particularly valuable in systems powered by large language models (LLMs), where users can request script execution directly from the application. F#—with its functional programming paradigm—offers an elegant and concise way to build an MCP (Model Context Protocol) server to enable this functionality.
In this blog, we will walk through how to write an MCP server in F# and integrate it into LLM applications for executing scripts dynamically. The process is surprisingly simple yet powerful, enabling users to run scripts in languages like F#, C#, JavaScript, and PowerShell.
Writing an MCP Server in F# with a single script file
The following example demonstrates how to create a fully functional MCP server in F#. The server provides tools for executing scripts and managing them dynamically.
Step 1: Setting Up the Environment
Before proceeding, ensure you have installed the necessary NuGet packages:
ModelContextProtocol
for MCP server implementationMicrosoft.Extensions.Hosting
for application hostingCliWrap
for script execution
These can be added using the following directives:
#r "nuget: ModelContextProtocol,0.3.0-preview.3"
#r "nuget: Microsoft.Extensions.Hosting"
#r "nuget: CliWrap"
Step 2: Writing the MCP Server
Here’s a complete implementation of the MCP server in F#:
#r "nuget: ModelContextProtocol,0.3.0-preview.3"
#r "nuget: Microsoft.Extensions.Hosting"
#r "nuget: CliWrap"
open System
open System.IO
open System.Text
open System.Threading
open System.ComponentModel
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.Logging
open Microsoft.Extensions.DependencyInjection
open ModelContextProtocol.Server
[<McpServerToolType>]
type RunScriptTool() as this =
let scriptsDir = Path.Combine(__SOURCE_DIRECTORY__, "Scripts")
do
if not (Directory.Exists scriptsDir) then
System.IO.Directory.CreateDirectory scriptsDir |> ignore
member _.RunScript(scriptName: string, ?scriptCode: string, ?cancellationToken: CancellationToken) = task {
let output = StringBuilder()
try
let command, args =
if scriptName.EndsWith(".fsx") then "dotnet", [| "fsi" |]
elif scriptName.EndsWith(".cs") then "dotnet", [| "run" |]
elif scriptName.EndsWith(".js") then "node", [||]
elif scriptName.EndsWith(".ps1") then "powershell", [| "-File" |]
else failwith "Unsupported script type"
let fileName =
match scriptCode with
| Some code ->
let fileName =
Path.GetFileNameWithoutExtension(scriptName).Replace(" ", "-")
+ "-"
+ DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")
+ Path.GetExtension(scriptName)
let filePath = Path.Combine(scriptsDir, fileName)
File.WriteAllText(filePath, code)
fileName
| None ->
let filePath = Path.Combine(scriptsDir, scriptName)
if File.Exists filePath |> not then
failwith (sprintf "Script '%s' is not found" scriptName)
scriptName
let! _ =
CliWrap.Cli
.Wrap(command)
.WithArguments([| yield! args; fileName |])
.WithWorkingDirectory(scriptsDir)
.WithStandardErrorPipe(CliWrap.PipeTarget.ToStringBuilder(output))
.WithStandardOutputPipe(CliWrap.PipeTarget.ToStringBuilder(output))
.WithValidation(CliWrap.CommandResultValidation.None)
.ExecuteAsync(?cancellationToken = cancellationToken)
()
with ex ->
output.Append("Error executing script: ").Append(ex) |> ignore
return output.ToString()
}
[<McpServerTool; Description("List all the available script file names")>]
member _.GetScriptFileNames() = Directory.GetFiles(scriptsDir, "*.*") |> Array.map Path.GetFileName
[<McpServerTool; Description("Run a script file by its file name")>]
member _.RunWithScriptFileName(scriptFileName: string, cancellationToken: CancellationToken) =
this.RunScript(scriptFileName, cancellationToken = cancellationToken)
[<McpServerTool;
Description("Run fsharp script code locally. Script name should be xxx.fsx, and should contain a brief description for recall in the future.")>]
member _.RunFsharpScript(scriptName: string, scriptCode: string, cancellationToken: CancellationToken) =
this.RunScript(scriptName, scriptCode, cancellationToken = cancellationToken)
[<McpServerTool;
Description("Run csharp script code locally. Script name should be xxx.cs, and should contain a brief description for recall in the future.")>]
member _.RunCsharpScript(scriptName: string, scriptCode: string, cancellationToken: CancellationToken) =
this.RunScript(scriptName, scriptCode, cancellationToken = cancellationToken)
[<McpServerTool;
Description("Run javascript code locally with node. Script name should be xxx.js, and should contain a brief description for recall in the future.")>]
member _.RunJavaScript(scriptName: string, scriptCode: string, cancellationToken: CancellationToken) =
this.RunScript(scriptName, scriptCode, cancellationToken = cancellationToken)
[<McpServerTool;
Description("Run powershell script locally. Script name should be xxx.ps1, and should contain a brief description for recall in the future.")>]
member _.RunPowershellScript(scriptName: string, scriptCode: string, cancellationToken: CancellationToken) =
this.RunScript(scriptName, scriptCode, cancellationToken = cancellationToken)
let builder = Host.CreateApplicationBuilder(Environment.GetCommandLineArgs())
builder.Logging.AddConsole(fun consoleLogOptions -> consoleLogOptions.LogToStandardErrorThreshold <- LogLevel.Trace)
builder.Services.AddMcpServer().WithStdioServerTransport().WithTools<RunScriptTool>()
builder.Build().RunAsync() |> Async.AwaitTask |> Async.RunSynchronously
Integration into LLM Applications
Once the MCP server is running, it can be integrated into any LLM application that supports the MCP protocol. Users can dynamically invoke tools provided by the server to execute scripts.
Example Workflow
Imagine a scenario where a user interacts with an LLM agent to execute an F# script. The following sequence showcases the process:
User Request: @Scripter write a fsharp loop to print from 1 to 10
Agent Response: The LLM agent creates or validates script and create proper script name for future re-execution.
MCP Invocation: The MCP tool is invoked to run the script.
Result Output: The output (numbers from 1 to 10) is returned to the user.
Screenshot of Execution
Below is an example of an execution recorded by the MCP tool integrated into Brainloop:
Key Benefits
- Multi-Language Support: Easily execute scripts in F#, C#, JavaScript, and PowerShell.
- Dynamic Integration: Seamlessly integrates with LLM applications to enable real-time script execution.
- Minimal Code Overhead: F#'s functional programming style simplifies server implementation.
- Scalability: Extensible architecture allows adding support for more scripting languages.
Conclusion
Creating an MCP server using F# is not only simple but also highly effective for enabling dynamic script execution. With its concise syntax and powerful features, F# simplifies server-side development, allowing seamless integration into LLM applications.
Whether you’re building automation tools, interactive applications, or educational platforms, this approach provides a robust foundation for handling scripts across multiple languages.