Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Cellm.sln
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Cellm.Installer", "src\Cell
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cellm.Installer.CustomActions", "src\Cellm.Installers\CustomActions\Cellm.Installer.CustomActions.csproj", "{02AB9A3F-CF5E-8F75-666D-6D8AE65C85E1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cellm.Mcp", "src\Cellm.Mcp\Cellm.Mcp.csproj", "{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -90,6 +92,22 @@ Global
{02AB9A3F-CF5E-8F75-666D-6D8AE65C85E1}.Release|x64.Build.0 = Release|Any CPU
{02AB9A3F-CF5E-8F75-666D-6D8AE65C85E1}.Release|x86.ActiveCfg = Release|Any CPU
{02AB9A3F-CF5E-8F75-666D-6D8AE65C85E1}.Release|x86.Build.0 = Release|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Debug|ARM64.Build.0 = Debug|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Debug|x64.ActiveCfg = Debug|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Debug|x64.Build.0 = Debug|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Debug|x86.ActiveCfg = Debug|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Debug|x86.Build.0 = Debug|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Release|Any CPU.Build.0 = Release|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Release|ARM64.ActiveCfg = Release|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Release|ARM64.Build.0 = Release|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Release|x64.ActiveCfg = Release|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Release|x64.Build.0 = Release|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Release|x86.ActiveCfg = Release|Any CPU
{7F995EA7-C647-4323-9689-9FCBF3A2AE7A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
16 changes: 16 additions & 0 deletions src/Cellm.Mcp/Cellm.Mcp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
</ItemGroup>

</Project>
63 changes: 63 additions & 0 deletions src/Cellm.Mcp/CellmClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.IO.Pipes;
using System.Security.Principal;
using System.Text;
using System.Text.Json;

namespace Cellm.Mcp;

internal class CellmClient
{
private static readonly TimeSpan _connectTimeout = TimeSpan.FromSeconds(5);
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
private readonly string _pipeName;

public CellmClient()
: this(GetPipeName())
{
}

internal CellmClient(string pipeName)
{
_pipeName = pipeName;
}

public async Task<PromptResponse> PromptAsync(PromptRequest request, CancellationToken cancellationToken)
{
await using var pipe = new NamedPipeClientStream(
serverName: ".",
pipeName: _pipeName,
direction: PipeDirection.InOut,
options: PipeOptions.Asynchronous);

try
{
await pipe.ConnectAsync(_connectTimeout, cancellationToken).ConfigureAwait(false);
}
catch (TimeoutException ex)
{
throw new InvalidOperationException("Cellm is not running in Excel.", ex);
}

using var reader = new StreamReader(pipe, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
using var writer = new StreamWriter(pipe, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), leaveOpen: true)
{
AutoFlush = true
};

await writer.WriteLineAsync(JsonSerializer.Serialize(request, _jsonOptions)).ConfigureAwait(false);

var responseJson = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Cellm closed the connection without returning a response.");

return JsonSerializer.Deserialize<PromptResponse>(responseJson, _jsonOptions)
?? throw new InvalidOperationException("Cellm returned an invalid response.");
}

private static string GetPipeName()
{
var user = WindowsIdentity.GetCurrent().User?.Value
?? throw new InvalidOperationException("Unable to identify the current Windows user.");

return $"Cellm-{user}";
}
}
40 changes: 40 additions & 0 deletions src/Cellm.Mcp/CellmTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.ComponentModel;
using ModelContextProtocol.Server;

namespace Cellm.Mcp;

[McpServerToolType]
internal class CellmTools(CellmClient client)
{
[McpServerTool(
Name = "cellm_prompt",
Title = "Run a Cellm prompt in Excel",
Destructive = true,
Idempotent = false,
OpenWorld = true,
UseStructuredContent = true)]
[Description("Writes a PROMPT or PROMPTMODEL formula to an Excel cell and waits for Excel to return the result.")]
public Task<PromptResponse> PromptAsync(
[Description("The name of the open Excel workbook.")] string workbook,
[Description("The name of the worksheet in that workbook.")] string worksheet,
[Description("The target cell address, for example B2.")] string cell,
[Description("The prompt text sent to the configured model.")] string prompt,
[Description("Optional cell or range addresses on the same worksheet to include as context.")] string[]? ranges = null,
[Description("Optional provider and model in provider/model form. Omit this to use Cellm's configured default.")] string? providerAndModel = null,
[Description("Whether Cellm may replace an existing value or formula in the target cell.")] bool overwrite = false,
CancellationToken cancellationToken = default)
{
return client.PromptAsync(
new PromptRequest(
Version: 1,
Method: "prompt",
Workbook: workbook,
Worksheet: worksheet,
Cell: cell,
Prompt: prompt,
Ranges: ranges ?? [],
ProviderAndModel: providerAndModel,
Overwrite: overwrite),
cancellationToken);
}
}
19 changes: 19 additions & 0 deletions src/Cellm.Mcp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Cellm.Mcp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = Host.CreateApplicationBuilder(args);

builder.Logging.AddConsole(options =>
{
options.LogToStandardErrorThreshold = LogLevel.Trace;
});

builder.Services
.AddSingleton<CellmClient>()
.AddMcpServer()
.WithStdioServerTransport()
.WithTools<CellmTools>();

await builder.Build().RunAsync();
12 changes: 12 additions & 0 deletions src/Cellm.Mcp/PromptRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Cellm.Mcp;

internal record PromptRequest(
int Version,
string Method,
string Workbook,
string Worksheet,
string Cell,
string Prompt,
IReadOnlyList<string> Ranges,
string? ProviderAndModel,
bool Overwrite);
11 changes: 11 additions & 0 deletions src/Cellm.Mcp/PromptResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Cellm.Mcp;

public record PromptResponse(
int Version,
string Status,
string Workbook,
string Worksheet,
string Cell,
string? Formula = null,
object? Value = null,
string? Error = null);
3 changes: 3 additions & 0 deletions src/Cellm.Mcp/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Cellm.Tests")]
14 changes: 14 additions & 0 deletions src/Cellm.Mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Cellm MCP server

This project exposes Cellm to MCP clients over standard input/output. Excel must be running with the Cellm add-in loaded.

For a development build, configure the client to run:

```json
{
"command": "dotnet",
"args": ["C:\\path\\to\\Cellm.Mcp.dll"]
}
```

The first tool, `cellm_prompt`, writes a normal `PROMPT` or `PROMPTMODEL` formula to an explicit workbook, worksheet, and cell. It then waits for Excel's result. Cancelling the MCP call stops waiting but does not replace Cellm's existing Excel cancellation behavior.
Loading
Loading