diff --git a/MCPSharp.Example.BackgroundServer/MCPSharp.Example.BackgroundServer.csproj b/MCPSharp.Example.BackgroundServer/MCPSharp.Example.BackgroundServer.csproj
new file mode 100644
index 0000000..c089391
--- /dev/null
+++ b/MCPSharp.Example.BackgroundServer/MCPSharp.Example.BackgroundServer.csproj
@@ -0,0 +1,41 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+ AnyCPU;x64
+ False
+
+
+
+ true
+
+
+
+ embedded
+
+
+
+ embedded
+
+
+
+ embedded
+
+
+
+ embedded
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MCPSharp.Example.BackgroundServer/Program.cs b/MCPSharp.Example.BackgroundServer/Program.cs
new file mode 100644
index 0000000..19e290c
--- /dev/null
+++ b/MCPSharp.Example.BackgroundServer/Program.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Threading.Tasks;
+using MCPSharp;
+using MCPSharp.Model;
+using MCPSharp.Model.Schemas;
+
+namespace MCPSharp.Example.BackgroundServer;
+
+///
+/// This example demonstrates how to run an MCP server in a background thread using MCPServerHost.
+/// The server is started asynchronously and will continue to run even after the main thread exits.
+///
+public class Program
+{
+ ///
+ /// The main entry point for the MCP server example.
+ ///
+ public static async Task Main()
+ {
+ // Start server in background thread
+ await using var server = await MCPServerHost.StartAsync("BackgroundServer", "1.0.0");
+
+ // Add a sample tool
+ MCPServer.AddToolHandler(new Tool()
+ {
+ Name = "greet",
+ Description = "A simple greeting tool",
+ InputSchema = new InputSchema
+ {
+ Type = "object",
+ Required = ["name"],
+ Properties = new Dictionary{
+ {"name", new ParameterSchema{Type="string", Description="Name to greet"}}
+ }
+ }
+ }, (string name) => $"Hello, {name}! (from background server)");
+
+ await Task.Delay(-1);
+ }
+}
diff --git a/MCPSharp.sln b/MCPSharp.sln
index 0c8531b..094647b 100644
--- a/MCPSharp.sln
+++ b/MCPSharp.sln
@@ -1,4 +1,4 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35707.178
@@ -13,54 +13,90 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCPSharp.Example.Import", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCPSharp.Example.OllamaChatCLI", "MCPSharp.Example.OllamaChatCLI\MCPSharp.Example.OllamaChatCLI.csproj", "{73FD6F85-CCEE-4D80-8E8B-154F94511DD2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCPSharp.Example.BackgroundServer", "MCPSharp.Example.BackgroundServer\MCPSharp.Example.BackgroundServer.csproj", "{DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1A4D1BB7-973C-49AD-9564-F45D5B7703EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A4D1BB7-973C-49AD-9564-F45D5B7703EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A4D1BB7-973C-49AD-9564-F45D5B7703EF}.Debug|x64.ActiveCfg = Debug|x64
{1A4D1BB7-973C-49AD-9564-F45D5B7703EF}.Debug|x64.Build.0 = Debug|x64
+ {1A4D1BB7-973C-49AD-9564-F45D5B7703EF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1A4D1BB7-973C-49AD-9564-F45D5B7703EF}.Debug|x86.Build.0 = Debug|Any CPU
{1A4D1BB7-973C-49AD-9564-F45D5B7703EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A4D1BB7-973C-49AD-9564-F45D5B7703EF}.Release|Any CPU.Build.0 = Release|Any CPU
{1A4D1BB7-973C-49AD-9564-F45D5B7703EF}.Release|x64.ActiveCfg = Release|x64
{1A4D1BB7-973C-49AD-9564-F45D5B7703EF}.Release|x64.Build.0 = Release|x64
+ {1A4D1BB7-973C-49AD-9564-F45D5B7703EF}.Release|x86.ActiveCfg = Release|Any CPU
+ {1A4D1BB7-973C-49AD-9564-F45D5B7703EF}.Release|x86.Build.0 = Release|Any CPU
{7FF8D00E-9DBA-435D-80E4-4F1F7E860CDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7FF8D00E-9DBA-435D-80E4-4F1F7E860CDA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7FF8D00E-9DBA-435D-80E4-4F1F7E860CDA}.Debug|x64.ActiveCfg = Debug|Any CPU
{7FF8D00E-9DBA-435D-80E4-4F1F7E860CDA}.Debug|x64.Build.0 = Debug|Any CPU
+ {7FF8D00E-9DBA-435D-80E4-4F1F7E860CDA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7FF8D00E-9DBA-435D-80E4-4F1F7E860CDA}.Debug|x86.Build.0 = Debug|Any CPU
{7FF8D00E-9DBA-435D-80E4-4F1F7E860CDA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7FF8D00E-9DBA-435D-80E4-4F1F7E860CDA}.Release|Any CPU.Build.0 = Release|Any CPU
{7FF8D00E-9DBA-435D-80E4-4F1F7E860CDA}.Release|x64.ActiveCfg = Release|Any CPU
{7FF8D00E-9DBA-435D-80E4-4F1F7E860CDA}.Release|x64.Build.0 = Release|Any CPU
+ {7FF8D00E-9DBA-435D-80E4-4F1F7E860CDA}.Release|x86.ActiveCfg = Release|Any CPU
+ {7FF8D00E-9DBA-435D-80E4-4F1F7E860CDA}.Release|x86.Build.0 = Release|Any CPU
{EAAC2C18-8A35-4CED-B407-582170A7A0F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EAAC2C18-8A35-4CED-B407-582170A7A0F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EAAC2C18-8A35-4CED-B407-582170A7A0F9}.Debug|x64.ActiveCfg = Debug|Any CPU
{EAAC2C18-8A35-4CED-B407-582170A7A0F9}.Debug|x64.Build.0 = Debug|Any CPU
+ {EAAC2C18-8A35-4CED-B407-582170A7A0F9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EAAC2C18-8A35-4CED-B407-582170A7A0F9}.Debug|x86.Build.0 = Debug|Any CPU
{EAAC2C18-8A35-4CED-B407-582170A7A0F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EAAC2C18-8A35-4CED-B407-582170A7A0F9}.Release|Any CPU.Build.0 = Release|Any CPU
{EAAC2C18-8A35-4CED-B407-582170A7A0F9}.Release|x64.ActiveCfg = Release|Any CPU
{EAAC2C18-8A35-4CED-B407-582170A7A0F9}.Release|x64.Build.0 = Release|Any CPU
+ {EAAC2C18-8A35-4CED-B407-582170A7A0F9}.Release|x86.ActiveCfg = Release|Any CPU
+ {EAAC2C18-8A35-4CED-B407-582170A7A0F9}.Release|x86.Build.0 = Release|Any CPU
{6B4E00C3-6CAF-4493-9A4D-9AE602FA9E00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B4E00C3-6CAF-4493-9A4D-9AE602FA9E00}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B4E00C3-6CAF-4493-9A4D-9AE602FA9E00}.Debug|x64.ActiveCfg = Debug|Any CPU
{6B4E00C3-6CAF-4493-9A4D-9AE602FA9E00}.Debug|x64.Build.0 = Debug|Any CPU
+ {6B4E00C3-6CAF-4493-9A4D-9AE602FA9E00}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6B4E00C3-6CAF-4493-9A4D-9AE602FA9E00}.Debug|x86.Build.0 = Debug|Any CPU
{6B4E00C3-6CAF-4493-9A4D-9AE602FA9E00}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B4E00C3-6CAF-4493-9A4D-9AE602FA9E00}.Release|Any CPU.Build.0 = Release|Any CPU
{6B4E00C3-6CAF-4493-9A4D-9AE602FA9E00}.Release|x64.ActiveCfg = Release|Any CPU
{6B4E00C3-6CAF-4493-9A4D-9AE602FA9E00}.Release|x64.Build.0 = Release|Any CPU
+ {6B4E00C3-6CAF-4493-9A4D-9AE602FA9E00}.Release|x86.ActiveCfg = Release|Any CPU
+ {6B4E00C3-6CAF-4493-9A4D-9AE602FA9E00}.Release|x86.Build.0 = Release|Any CPU
{73FD6F85-CCEE-4D80-8E8B-154F94511DD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{73FD6F85-CCEE-4D80-8E8B-154F94511DD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{73FD6F85-CCEE-4D80-8E8B-154F94511DD2}.Debug|x64.ActiveCfg = Debug|Any CPU
{73FD6F85-CCEE-4D80-8E8B-154F94511DD2}.Debug|x64.Build.0 = Debug|Any CPU
+ {73FD6F85-CCEE-4D80-8E8B-154F94511DD2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {73FD6F85-CCEE-4D80-8E8B-154F94511DD2}.Debug|x86.Build.0 = Debug|Any CPU
{73FD6F85-CCEE-4D80-8E8B-154F94511DD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{73FD6F85-CCEE-4D80-8E8B-154F94511DD2}.Release|Any CPU.Build.0 = Release|Any CPU
{73FD6F85-CCEE-4D80-8E8B-154F94511DD2}.Release|x64.ActiveCfg = Release|Any CPU
{73FD6F85-CCEE-4D80-8E8B-154F94511DD2}.Release|x64.Build.0 = Release|Any CPU
+ {73FD6F85-CCEE-4D80-8E8B-154F94511DD2}.Release|x86.ActiveCfg = Release|Any CPU
+ {73FD6F85-CCEE-4D80-8E8B-154F94511DD2}.Release|x86.Build.0 = Release|Any CPU
+ {DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}.Debug|x64.ActiveCfg = Debug|x64
+ {DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}.Debug|x64.Build.0 = Debug|x64
+ {DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}.Debug|x86.Build.0 = Debug|Any CPU
+ {DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}.Release|x64.ActiveCfg = Release|x64
+ {DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}.Release|x64.Build.0 = Release|x64
+ {DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}.Release|x86.ActiveCfg = Release|Any CPU
+ {DD4ABB3D-89CF-4C9C-AF62-94CEC22B779D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/MCPSharp/Core/MCPServer.cs b/MCPSharp/Core/MCPServer.cs
index d55bad9..c65f0fd 100644
--- a/MCPSharp/Core/MCPServer.cs
+++ b/MCPSharp/Core/MCPServer.cs
@@ -12,14 +12,22 @@ namespace MCPSharp
///
public class MCPServer
{
- private static readonly MCPServer _instance = new();
+ private static MCPServer _instance = new();
+
+ ///
+ /// Gets the current server instance.
+ ///
+ public static MCPServer Instance => _instance;
private readonly JsonRpc _rpc;
private readonly Stream StandardOutput;
private readonly ToolManager _toolManager = new()
{
- ToolChangeNotification = () => { if (EnableToolChangeNotification)
- _= _instance._rpc.InvokeWithParameterObjectAsync("notifications/tools/list_changed", null);}
+ ToolChangeNotification = () =>
+ {
+ if (EnableToolChangeNotification)
+ _ = _instance._rpc.InvokeWithParameterObjectAsync("notifications/tools/list_changed", null);
+ }
};
private readonly ResourceManager _resouceManager = new();
@@ -53,14 +61,17 @@ public class MCPServer
private MCPServer()
{
Implementation = new();
- _target = new(_toolManager, _resouceManager, Implementation);
+ _target = new(_toolManager, _resouceManager, Implementation);
Console.SetOut(RedirectedOutput);
- _rpc = new JsonRpc(new NewLineDelimitedMessageHandler(new StdioTransportPipe(),
- new SystemTextJsonFormatter() {
- JsonSerializerOptions = new System.Text.Json.JsonSerializerOptions {
- PropertyNameCaseInsensitive = true,
- PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
- } }), _target);
+ _rpc = new JsonRpc(new NewLineDelimitedMessageHandler(new StdioTransportPipe(),
+ new SystemTextJsonFormatter()
+ {
+ JsonSerializerOptions = new System.Text.Json.JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
+ }
+ }), _target);
_rpc.StartListening();
}
@@ -76,7 +87,7 @@ private MCPServer()
/// Registers a tool with the server.
///
///
- public static void Register() where T : class, new()=>_ = _instance.RegisterAsync();
+ public static void Register() where T : class, new() => _ = _instance.RegisterAsync();
public async Task RegisterAsync() where T : class, new() { _toolManager.Register(); _resouceManager.Register(); }
public static void AddToolHandler(Tool tool, Delegate func) => _instance._toolManager.AddToolHandler(new ToolHandler(tool, func.Method));
@@ -141,8 +152,11 @@ private async Task StartPingThreadAsync()
}
}
}
-
- internal void Dispose()
+
+ ///
+ /// Disposes the server and releases all resources.
+ ///
+ public void Dispose()
{
_cancellationTokenSource.Cancel();
_rpc.Dispose();
diff --git a/MCPSharp/Core/MCPServerHost.cs b/MCPSharp/Core/MCPServerHost.cs
new file mode 100644
index 0000000..1c34814
--- /dev/null
+++ b/MCPSharp/Core/MCPServerHost.cs
@@ -0,0 +1,198 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.Threading;
+
+#nullable enable
+
+namespace MCPSharp
+{
+ ///
+ /// Provides a background thread execution model for MCPServer.
+ /// This class allows running an MCP server in a background thread with proper lifecycle management.
+ ///
+ ///
+ /// The MCPServerHost class implements IAsyncDisposable for proper cleanup of resources.
+ /// It manages server startup, shutdown, and cancellation with timeouts to prevent deadlocks.
+ ///
+ /// Example usage:
+ ///
+ /// await using var server = await MCPServerHost.StartAsync("MyServer", "1.0.0");
+ /// // Server is now running in background
+ /// // When the using block exits, server will be properly disposed
+ ///
+ ///
+ public class MCPServerHost : System.IAsyncDisposable
+ {
+ private static readonly JoinableTaskFactory JoinableFactory = new(new JoinableTaskContext());
+
+ private static async Task TimeoutAfterAsync(Func operation, TimeSpan timeout)
+ {
+ using var cts = new CancellationTokenSource(timeout);
+ var timeoutTask = Task.Delay(timeout, cts.Token);
+ var operationTask = JoinableFactory.RunAsync(operation);
+
+ var completedTask = await Task.WhenAny(operationTask.Task, timeoutTask).ConfigureAwait(false);
+ if (completedTask == timeoutTask)
+ {
+ throw new TimeoutException();
+ }
+
+ await cts.CancelAsync(); // Cancel the timeout task
+ await operationTask.Task.ConfigureAwait(false); // Propagate any exceptions
+ }
+
+ private Task? _serverLoop;
+ private CancellationTokenSource? _cts;
+ private readonly AutoResetEvent _ready;
+ private bool _disposed;
+
+ private MCPServerHost()
+ {
+ _ready = new AutoResetEvent(false);
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(MCPServerHost));
+ }
+ }
+
+ ///
+ /// Creates and starts a new MCPServer instance running in a background thread.
+ ///
+ /// The name of the server.
+ /// The version of the server.
+ /// A running MCPServerHost instance that will manage the server's lifecycle.
+ /// Thrown when name or version is null or empty.
+ ///
+ /// The server is started in a background thread and will continue running until disposed.
+ /// The method waits for the server to be ready before returning, with a timeout to prevent deadlocks.
+ ///
+ public static async Task StartAsync(string name, string version)
+ {
+ if (string.IsNullOrEmpty(name))
+ throw new ArgumentException("Server name cannot be null or empty.", nameof(name));
+ if (string.IsNullOrEmpty(version))
+ throw new ArgumentException("Server version cannot be null or empty.", nameof(version));
+
+ var host = new MCPServerHost();
+ await host.InitializeAsync(name, version);
+ return host;
+ }
+
+ ///
+ /// Initializes the server host and starts the server in a background thread.
+ ///
+ /// The name of the server.
+ /// The version of the server.
+ /// A task representing the asynchronous initialization operation.
+ /// Thrown when the host has been disposed.
+ /// Thrown when the server fails to start within the timeout period.
+ ///
+ /// This method:
+ /// 1. Creates a cancellation token source for managing the server lifecycle
+ /// 2. Starts the server in a background thread
+ /// 3. Waits for the server to signal readiness with a timeout
+ ///
+ private async Task InitializeAsync(string name, string version)
+ {
+ ThrowIfDisposed();
+ const int timeoutSeconds = 5;
+ _cts = new CancellationTokenSource();
+
+ // Start server and wait for ready signal
+ try
+ {
+ var serverTask = JoinableFactory.RunAsync(async () =>
+ {
+ try
+ {
+ _ready.Set(); // Signal ready before starting server
+ await MCPServer.StartAsync(name, version).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) { }
+ });
+ _serverLoop = serverTask.Task;
+
+ // Wait for ready signal with timeout
+ await TimeoutAfterAsync(
+ async () => await JoinableFactory.RunAsync(() => Task.FromResult(_ready.WaitOne())),
+ TimeSpan.FromSeconds(timeoutSeconds)
+ ).ConfigureAwait(false);
+ }
+ catch (Exception)
+ {
+ await DisposeAsync(); // Clean up on timeout
+ throw new TimeoutException("Server failed to start within the timeout period.");
+ }
+ }
+
+ ///
+ /// Stops the server and releases all resources.
+ ///
+ ///
+ /// This method handles the graceful shutdown of the server:
+ /// 1. Cancels the server loop
+ /// 2. Waits for completion with a timeout to prevent deadlocks
+ /// 3. Cleans up resources including the cancellation token source
+ ///
+ /// If the server does not shut down within the timeout period (5 seconds),
+ /// the method will still proceed with cleanup to prevent hanging.
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ if (_cts != null && _serverLoop != null)
+ {
+ try
+ {
+ // Cancel server and wait for completion with timeout
+ try
+ {
+ await JoinableFactory.RunAsync(async () =>
+ {
+ await Task.Run(() => _cts?.Cancel()).ConfigureAwait(false);
+ if (_serverLoop != null)
+ {
+ await TimeoutAfterAsync(
+ () => _serverLoop,
+ TimeSpan.FromSeconds(5)
+ ).ConfigureAwait(false);
+ }
+ }).Task.ConfigureAwait(false);
+ }
+ catch (TimeoutException)
+ {
+ System.Diagnostics.Debug.WriteLine("Server shutdown timed out");
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ System.Diagnostics.Debug.WriteLine($"Server loop terminated with error: {ex}");
+ }
+ finally
+ {
+ MCPServer.Instance.Dispose();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when cancellation occurs
+ }
+ finally
+ {
+ _cts.Dispose();
+ _ready.Dispose();
+ }
+ }
+ }
+ }
+}