slaveOftime

Single fsharp script to write a MCP server - Simplified Integration for Dynamic Script Execution

2025-07-29

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 implementation
  • Microsoft.Extensions.Hosting for application hosting
  • CliWrap 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:

  1. User Request: @Scripter write a fsharp loop to print from 1 to 10

  2. Agent Response: The LLM agent creates or validates script and create proper script name for future re-execution.

  3. MCP Invocation: The MCP tool is invoked to run the script.

  4. 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:

Execution Screenshot


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.

Do you like this post?