using System.Text.Json;
using Sbroenne.ExcelMcp.Service;
namespace Sbroenne.ExcelMcp.McpServer.ServiceBridge;
/// <summary>
/// Bridge that holds the in-process ExcelMCP Service for direct method calls.
/// No named pipe — MCP tools call the service directly (same process).
/// </summary>
public static class ServiceBridge
{
private static readonly SemaphoreSlim _initLock = new(1, 1);
private static Service.ExcelMcpService? _service;
/// <summary>
/// JSON serializer options for deserializing service responses.
/// </summary>
public static readonly JsonSerializerOptions JsonOptions = ServiceProtocol.JsonOptions;
/// <summary>
/// Ensures the in-process ExcelMCP Service is created.
/// Called automatically on first request.
/// </summary>
public static async Task<bool> EnsureServiceAsync(CancellationToken cancellationToken = default)
{
if (_service != null)
{
return true;
}
await _initLock.WaitAsync(cancellationToken);
try
{
if (_service != null)
{
return true;
}
_service = new Service.ExcelMcpService();
return true;
}
catch (Exception)
{
return false;
}
finally
{
_initLock.Release();
}
}
/// <summary>
/// Sends a command to the ExcelMCP Service directly (in-process, no pipe).
/// </summary>
public static async Task<ServiceResponse> SendAsync(
string command,
string? sessionId = null,
object? args = null,
int? timeoutSeconds = null,
CancellationToken cancellationToken = default)
{
if (!await EnsureServiceAsync(cancellationToken))
{
return new ServiceResponse
{
Success = false,
ErrorMessage = "Failed to start ExcelMCP Service in-process."
};
}
var request = new ServiceRequest
{
Command = command,
SessionId = sessionId,
Args = args != null ? JsonSerializer.Serialize(args, JsonOptions) : null
};
// Apply timeout if specified
if (timeoutSeconds.HasValue)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds.Value));
try
{
return await _service!.ProcessAsync(request);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
return new ServiceResponse
{
Success = false,
ErrorMessage = $"Operation timed out after {timeoutSeconds} seconds."
};
}
}
return await _service!.ProcessAsync(request);
}
/// <summary>
/// Sends a session-scoped command to the service.
/// </summary>
public static async Task<ServiceResponse> WithSessionAsync(
string sessionId,
string command,
object? args = null,
int? timeoutSeconds = null,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(sessionId))
{
return new ServiceResponse
{
Success = false,
ErrorMessage = "sessionId is required. Use file 'open' action to start a session."
};
}
return await SendAsync(command, sessionId, args, timeoutSeconds, cancellationToken);
}
/// <summary>
/// Opens a session via the service.
/// </summary>
public static async Task<ServiceResponse> OpenSessionAsync(
string excelPath,
bool show = false,
int? timeoutSeconds = null,
CancellationToken cancellationToken = default)
{
return await SendAsync("session.open", null, new
{
filePath = excelPath,
show,
timeoutSeconds
}, timeoutSeconds, cancellationToken);
}
/// <summary>
/// Creates a new file and opens a session via the service.
/// </summary>
public static async Task<ServiceResponse> CreateSessionAsync(
string excelPath,
bool macroEnabled = false,
bool show = false,
int? timeoutSeconds = null,
CancellationToken cancellationToken = default)
{
return await SendAsync("session.create", null, new
{
filePath = excelPath,
macroEnabled,
show,
timeoutSeconds
}, timeoutSeconds, cancellationToken);
}
/// <summary>
/// Closes a session via the service.
/// </summary>
public static async Task<ServiceResponse> CloseSessionAsync(
string sessionId,
bool save = true,
CancellationToken cancellationToken = default)
{
return await SendAsync("session.close", sessionId, new { save }, cancellationToken: cancellationToken);
}
/// <summary>
/// Lists active sessions via the service.
/// </summary>
public static async Task<ServiceResponse> ListSessionsAsync(CancellationToken cancellationToken = default)
{
return await SendAsync("session.list", cancellationToken: cancellationToken);
}
/// <summary>
/// Saves a session via the service.
/// </summary>
public static async Task<ServiceResponse> SaveSessionAsync(
string sessionId,
CancellationToken cancellationToken = default)
{
return await SendAsync("session.save", sessionId, cancellationToken: cancellationToken);
}
/// <summary>
/// Tests if a file can be opened via the service.
/// </summary>
public static async Task<ServiceResponse> TestFileAsync(
string excelPath,
CancellationToken cancellationToken = default)
{
return await SendAsync("session.test", null, new { filePath = excelPath }, cancellationToken: cancellationToken);
}
/// <summary>
/// Disposes the in-process ExcelMCP Service, auto-saving all sessions before shutdown.
/// Must be called when the MCP server process exits to prevent silent data loss.
/// </summary>
public static void Dispose()
{
var service = Interlocked.Exchange(ref _service, null);
service?.Dispose();
}
}