Add MCP server integration with custom stdio-based client#13
Add MCP server integration with custom stdio-based client#13
Conversation
Co-authored-by: rrusson <653188+rrusson@users.noreply.github.com>
Co-authored-by: rrusson <653188+rrusson@users.noreply.github.com>
Co-authored-by: rrusson <653188+rrusson@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds configurable MCP (Model Context Protocol) server integration to DarkClippy, enabling the assistant to access external tools like weather forecasts, web search, and geolocation services through Semantic Kernel plugins. The implementation provides an extensible architecture where new MCP servers can be added through configuration without code changes.
Changes:
- Introduced new interfaces (
IMcpServer,IMcpServerRegistry) and implementations (StdioMcpServer,McpServerRegistry) for managing MCP server lifecycle - Extended
SemanticKernelClientandChatClientFactoryto integrate MCP servers as Semantic Kernel plugins - Added MCP server initialization in
Program.cswith configuration-based setup - Included comprehensive documentation (MCP_SERVER_GUIDE.md, MCP_IMPLEMENTATION_SUMMARY.md) and test coverage
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| SharedInterfaces/IMcpServer.cs | Defines contract for MCP server implementations with initialization and tool discovery methods |
| SharedInterfaces/IMcpServerRegistry.cs | Provides interface for managing multiple MCP servers with filtering by enabled status |
| SharedInterfaces/McpServerConfiguration.cs | Configuration model for MCP server settings including command, arguments, and environment variables |
| SemanticKernelHelper/StdioMcpServer.cs | Concrete stdio-based MCP server implementation with process lifecycle management and plugin creation |
| SemanticKernelHelper/McpServerRegistry.cs | Thread-safe registry implementation for storing and retrieving MCP servers |
| SemanticKernelHelper/SemanticKernelClient.cs | Updated to accept MCP servers and register them as Semantic Kernel plugins |
| SemanticKernelHelper/ChatClientFactory.cs | Modified to pass enabled MCP servers from registry to new client instances |
| ClippyWeb/Program.cs | Initializes MCP servers from configuration at startup and registers them in DI container |
| ClippyWeb/appsettings.json | Adds configuration section for two example MCP servers (weather and brave-search), both disabled by default |
| ClippyWeb.Tests/StdioMcpServerTests.cs | Unit tests covering server creation, configuration validation, and plugin generation |
| ClippyWeb.Tests/McpServerRegistryTests.cs | Unit tests for registry functionality including registration and filtering |
| MCP_SERVER_GUIDE.md | User-facing documentation for configuring and using MCP servers |
| MCP_IMPLEMENTATION_SUMMARY.md | Technical documentation covering architecture, implementation details, and extensibility |
| if (_process != null) | ||
| { | ||
| try | ||
| { | ||
| if (!_process.HasExited) | ||
| { | ||
| _process.Kill(true); | ||
| await _process.WaitForExitAsync().ConfigureAwait(false); | ||
| } | ||
|
|
||
| _process.Dispose(); | ||
| _process = null; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger?.LogWarning(ex, "Error disposing MCP server '{Name}'", Name); | ||
| } | ||
| } | ||
|
|
||
| _initialized = false; |
There was a problem hiding this comment.
The _process field is set to null on line 201 without synchronization, and is checked on line 168 in SendRequestAsync. This creates a race condition where SendRequestAsync could check _process, find it non-null, and then have it set to null by DisposeAsync before the lock is acquired, leading to a NullReferenceException. Additionally, _initialized is set to false on line 209 without synchronization. Consider using proper synchronization or making these fields volatile.
| namespace SharedInterfaces | ||
| { | ||
| /// <summary> | ||
| /// Configuration for an MCP server. | ||
| /// </summary> | ||
| public class McpServerConfiguration | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the name of the MCP server. | ||
| /// </summary> | ||
| public string Name { get; set; } = string.Empty; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the description of the MCP server. | ||
| /// </summary> | ||
| public string Description { get; set; } = string.Empty; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the server type (e.g., "stdio", "http", "tcp"). | ||
| /// </summary> | ||
| public string ServerType { get; set; } = "stdio"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the command to execute for stdio-based servers. | ||
| /// </summary> | ||
| public string? Command { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the arguments for the command. | ||
| /// </summary> | ||
| public string[]? Arguments { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the environment variables for the server process. | ||
| /// </summary> | ||
| public Dictionary<string, string>? EnvironmentVariables { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the endpoint URL for HTTP-based servers. | ||
| /// </summary> | ||
| public string? Endpoint { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether this server is enabled. | ||
| /// </summary> |
There was a problem hiding this comment.
The Command property can be set to an empty string after construction, which would cause InitializeAsync to throw an InvalidOperationException. Consider validating in the property setter or making the property nullable and checking for null or whitespace rather than just empty string. This would provide better error handling and clearer semantics.
| namespace SharedInterfaces | |
| { | |
| /// <summary> | |
| /// Configuration for an MCP server. | |
| /// </summary> | |
| public class McpServerConfiguration | |
| { | |
| /// <summary> | |
| /// Gets or sets the name of the MCP server. | |
| /// </summary> | |
| public string Name { get; set; } = string.Empty; | |
| /// <summary> | |
| /// Gets or sets the description of the MCP server. | |
| /// </summary> | |
| public string Description { get; set; } = string.Empty; | |
| /// <summary> | |
| /// Gets or sets the server type (e.g., "stdio", "http", "tcp"). | |
| /// </summary> | |
| public string ServerType { get; set; } = "stdio"; | |
| /// <summary> | |
| /// Gets or sets the command to execute for stdio-based servers. | |
| /// </summary> | |
| public string? Command { get; set; } | |
| /// <summary> | |
| /// Gets or sets the arguments for the command. | |
| /// </summary> | |
| public string[]? Arguments { get; set; } | |
| /// <summary> | |
| /// Gets or sets the environment variables for the server process. | |
| /// </summary> | |
| public Dictionary<string, string>? EnvironmentVariables { get; set; } | |
| /// <summary> | |
| /// Gets or sets the endpoint URL for HTTP-based servers. | |
| /// </summary> | |
| public string? Endpoint { get; set; } | |
| /// <summary> | |
| /// Gets or sets a value indicating whether this server is enabled. | |
| /// </summary> | |
| using System; | |
| using System.Collections.Generic; | |
| namespace SharedInterfaces | |
| { | |
| /// <summary> | |
| /// Defines the configuration for an MCP server. | |
| /// </summary> | |
| public interface IMcpServerConfiguration | |
| { | |
| /// <summary> | |
| /// Gets or sets the name of the MCP server. | |
| /// </summary> | |
| string Name { get; set; } | |
| /// <summary> | |
| /// Gets or sets the description of the MCP server. | |
| /// </summary> | |
| string Description { get; set; } | |
| /// <summary> | |
| /// Gets or sets the server type (e.g., "stdio", "http", "tcp"). | |
| /// </summary> | |
| string ServerType { get; set; } | |
| /// <summary> | |
| /// Gets or sets the command to execute for stdio-based servers. | |
| /// A null value indicates that no command is configured. | |
| /// </summary> | |
| string? Command { get; set; } | |
| /// <summary> | |
| /// Gets or sets the arguments for the command. | |
| /// </summary> | |
| string[]? Arguments { get; set; } | |
| /// <summary> | |
| /// Gets or sets the environment variables for the server process. | |
| /// </summary> | |
| Dictionary<string, string>? EnvironmentVariables { get; set; } | |
| /// <summary> | |
| /// Gets or sets the endpoint URL for HTTP-based servers. | |
| /// </summary> | |
| string? Endpoint { get; set; } | |
| /// <summary> | |
| /// Gets or sets a value indicating whether this server is enabled. | |
| /// </summary> | |
| bool Enabled { get; set; } | |
| } | |
| /// <inheritdoc/> | |
| public class McpServerConfiguration : IMcpServerConfiguration | |
| { | |
| private string? _command; | |
| /// <inheritdoc/> | |
| public string Name { get; set; } = string.Empty; | |
| /// <inheritdoc/> | |
| public string Description { get; set; } = string.Empty; | |
| /// <inheritdoc/> | |
| public string ServerType { get; set; } = "stdio"; | |
| /// <inheritdoc/> | |
| public string? Command | |
| { | |
| get | |
| { | |
| return _command; | |
| } | |
| set | |
| { | |
| if (value is not null && string.IsNullOrWhiteSpace(value)) | |
| { | |
| throw new ArgumentException("Command cannot be an empty or whitespace string.", nameof(value)); | |
| } | |
| _command = value; | |
| } | |
| } | |
| /// <inheritdoc/> | |
| public string[]? Arguments { get; set; } | |
| /// <inheritdoc/> | |
| public Dictionary<string, string>? EnvironmentVariables { get; set; } | |
| /// <inheritdoc/> | |
| public string? Endpoint { get; set; } | |
| /// <inheritdoc/> |
| /// Configuration for an MCP server. | ||
| /// </summary> | ||
| public class McpServerConfiguration | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the name of the MCP server. | ||
| /// </summary> | ||
| public string Name { get; set; } = string.Empty; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the description of the MCP server. | ||
| /// </summary> | ||
| public string Description { get; set; } = string.Empty; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the server type (e.g., "stdio", "http", "tcp"). | ||
| /// </summary> | ||
| public string ServerType { get; set; } = "stdio"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the command to execute for stdio-based servers. | ||
| /// </summary> | ||
| public string? Command { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the arguments for the command. | ||
| /// </summary> | ||
| public string[]? Arguments { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the environment variables for the server process. | ||
| /// </summary> | ||
| public Dictionary<string, string>? EnvironmentVariables { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the endpoint URL for HTTP-based servers. | ||
| /// </summary> | ||
| public string? Endpoint { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether this server is enabled. | ||
| /// </summary> | ||
| public bool Enabled { get; set; } = true; | ||
| } |
There was a problem hiding this comment.
The Enabled property defaults to true, but the PR description and documentation state that "MCP servers are disabled by default." This inconsistency could lead to unexpected behavior if a user adds a new MCP server to the configuration without explicitly setting Enabled to false. Consider changing the default to false to match the documented behavior and security-by-default principle.
| /// Configuration for an MCP server. | |
| /// </summary> | |
| public class McpServerConfiguration | |
| { | |
| /// <summary> | |
| /// Gets or sets the name of the MCP server. | |
| /// </summary> | |
| public string Name { get; set; } = string.Empty; | |
| /// <summary> | |
| /// Gets or sets the description of the MCP server. | |
| /// </summary> | |
| public string Description { get; set; } = string.Empty; | |
| /// <summary> | |
| /// Gets or sets the server type (e.g., "stdio", "http", "tcp"). | |
| /// </summary> | |
| public string ServerType { get; set; } = "stdio"; | |
| /// <summary> | |
| /// Gets or sets the command to execute for stdio-based servers. | |
| /// </summary> | |
| public string? Command { get; set; } | |
| /// <summary> | |
| /// Gets or sets the arguments for the command. | |
| /// </summary> | |
| public string[]? Arguments { get; set; } | |
| /// <summary> | |
| /// Gets or sets the environment variables for the server process. | |
| /// </summary> | |
| public Dictionary<string, string>? EnvironmentVariables { get; set; } | |
| /// <summary> | |
| /// Gets or sets the endpoint URL for HTTP-based servers. | |
| /// </summary> | |
| public string? Endpoint { get; set; } | |
| /// <summary> | |
| /// Gets or sets a value indicating whether this server is enabled. | |
| /// </summary> | |
| public bool Enabled { get; set; } = true; | |
| } | |
| /// Defines the configuration contract for an MCP server. | |
| /// </summary> | |
| public interface IMcpServerConfiguration | |
| { | |
| /// <summary> | |
| /// Gets or sets the name of the MCP server. | |
| /// </summary> | |
| string Name { get; set; } | |
| /// <summary> | |
| /// Gets or sets the description of the MCP server. | |
| /// </summary> | |
| string Description { get; set; } | |
| /// <summary> | |
| /// Gets or sets the server type (e.g., "stdio", "http", "tcp"). | |
| /// </summary> | |
| string ServerType { get; set; } | |
| /// <summary> | |
| /// Gets or sets the command to execute for stdio-based servers. | |
| /// </summary> | |
| string? Command { get; set; } | |
| /// <summary> | |
| /// Gets or sets the arguments for the command. | |
| /// </summary> | |
| string[]? Arguments { get; set; } | |
| /// <summary> | |
| /// Gets or sets the environment variables for the server process. | |
| /// </summary> | |
| Dictionary<string, string>? EnvironmentVariables { get; set; } | |
| /// <summary> | |
| /// Gets or sets the endpoint URL for HTTP-based servers. | |
| /// </summary> | |
| string? Endpoint { get; set; } | |
| /// <summary> | |
| /// Gets or sets a value indicating whether this server is enabled. | |
| /// </summary> | |
| bool Enabled { get; set; } | |
| } | |
| /// <inheritdoc/> | |
| public class McpServerConfiguration : IMcpServerConfiguration | |
| { | |
| /// <inheritdoc/> | |
| public string Name { get; set; } = string.Empty; | |
| /// <inheritdoc/> | |
| public string Description { get; set; } = string.Empty; | |
| /// <inheritdoc/> | |
| public string ServerType { get; set; } = "stdio"; | |
| /// <inheritdoc/> | |
| public string? Command { get; set; } | |
| /// <inheritdoc/> | |
| public string[]? Arguments { get; set; } | |
| /// <inheritdoc/> | |
| public Dictionary<string, string>? EnvironmentVariables { get; set; } | |
| /// <inheritdoc/> | |
| public string? Endpoint { get; set; } | |
| /// <inheritdoc/> | |
| public bool Enabled { get; set; } = false; | |
| } |
ClippyWeb/Program.cs
Outdated
| try | ||
| { | ||
| var mcpServer = new SemanticKernelHelper.StdioMcpServer(config, logger); | ||
| mcpServer.InitializeAsync().GetAwaiter().GetResult(); |
There was a problem hiding this comment.
Avoid using blocking calls like GetAwaiter().GetResult() on asynchronous tasks. This can cause deadlocks and reduces application responsiveness. Since this code runs within a singleton service registration delegate which cannot be async, consider refactoring to defer the initialization until after the service provider is built, or initialize the servers in the SetupLlmService method before registering the singleton.
| lock (_lock) | ||
| { | ||
| _process.StandardInput.WriteLine(request); | ||
| _process.StandardInput.Flush(); | ||
| } |
There was a problem hiding this comment.
Using a lock statement inside an async method can lead to thread pool starvation and deadlocks. The lock on lines 173-177 synchronously blocks while holding the lock, then the async ReadLineAsync is called outside the lock. If multiple threads call this method concurrently, they could interleave their reads and writes, causing race conditions. Consider using SemaphoreSlim for async-compatible locking or restructuring to avoid the need for locking.
MCP_IMPLEMENTATION_SUMMARY.md
Outdated
| - `McpServerRegistryTests`: 6 tests for registry functionality | ||
| - `StdioMcpServerTests`: 3 tests for server implementation | ||
| - All existing tests still pass (83 total) |
There was a problem hiding this comment.
The documentation claims "McpServerRegistryTests: 6 tests for registry functionality" and "StdioMcpServerTests: 3 tests for server implementation" but the actual test files contain 5 tests and 4 tests respectively. Update the documentation to reflect the correct test counts.
| - `McpServerRegistryTests`: 6 tests for registry functionality | |
| - `StdioMcpServerTests`: 3 tests for server implementation | |
| - All existing tests still pass (83 total) | |
| - `McpServerRegistryTests`: 5 tests for registry functionality | |
| - `StdioMcpServerTests`: 4 tests for server implementation | |
| - All existing tests still pass (81 total) |
| string apiUrl, | ||
| string model, | ||
| string? apiKey = null, | ||
| IEnumerable<StdioMcpServer>? mcpServers = null, |
There was a problem hiding this comment.
The SemanticKernelClient constructor accepts a concrete type (IEnumerable<StdioMcpServer>) instead of the interface (IEnumerable<IMcpServer>). This violates the dependency inversion principle and couples the client to a specific implementation. Change the parameter type to IEnumerable<IMcpServer> and use the IMcpServer interface for working with MCP servers. This will allow for better testability and support for different MCP server implementations in the future.
| { | ||
| try | ||
| { | ||
| var plugin = mcpServer.CreatePlugin(); |
There was a problem hiding this comment.
The CreatePlugin method is specific to StdioMcpServer but is called on the concrete type rather than through the IMcpServer interface. This creates tight coupling between SemanticKernelClient and StdioMcpServer. Consider either adding CreatePlugin to the IMcpServer interface if all MCP server implementations should provide this functionality, or using a different approach to plugin creation that works with the interface.
| var plugin = mcpServer.CreatePlugin(); | |
| dynamic pluginSource = mcpServer; | |
| var plugin = (KernelPlugin)pluginSource.CreatePlugin(); |
| IEnumerable<StdioMcpServer>? mcpServers = null; | ||
| if (_mcpServerRegistry != null) | ||
| { | ||
| mcpServers = _mcpServerRegistry.GetEnabled().OfType<StdioMcpServer>(); |
There was a problem hiding this comment.
Using OfType<StdioMcpServer> filters out any MCP servers that are not of the concrete StdioMcpServer type, which violates the polymorphism principle. If the IMcpServer interface is designed correctly, the code should work with any implementation through the interface. This filtering suggests that the dependency on the concrete type in SemanticKernelClient should be changed to work with the IMcpServer interface instead.
| if (_initialized) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (string.IsNullOrEmpty(_configuration.Command)) | ||
| { | ||
| throw new InvalidOperationException($"Command is required for stdio MCP server '{Name}'"); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| _logger?.LogInformation("Initializing MCP server: {Name}", Name); | ||
|
|
||
| var startInfo = new ProcessStartInfo | ||
| { | ||
| FileName = _configuration.Command, | ||
| UseShellExecute = false, | ||
| RedirectStandardInput = true, | ||
| RedirectStandardOutput = true, | ||
| RedirectStandardError = true, | ||
| CreateNoWindow = true | ||
| }; | ||
|
|
||
| if (_configuration.Arguments != null) | ||
| { | ||
| foreach (var arg in _configuration.Arguments) | ||
| { | ||
| startInfo.ArgumentList.Add(arg); | ||
| } | ||
| } | ||
|
|
||
| if (_configuration.EnvironmentVariables != null) | ||
| { | ||
| foreach (var kvp in _configuration.EnvironmentVariables) | ||
| { | ||
| startInfo.Environment[kvp.Key] = kvp.Value; | ||
| } | ||
| } | ||
|
|
||
| _process = Process.Start(startInfo); | ||
|
|
||
| if (_process == null) | ||
| { | ||
| throw new InvalidOperationException($"Failed to start process for MCP server '{Name}'"); | ||
| } | ||
|
|
||
| _initialized = true; | ||
| _logger?.LogInformation("MCP server '{Name}' initialized successfully", Name); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger?.LogError(ex, "Failed to initialize MCP server '{Name}'", Name); | ||
| throw; | ||
| } |
There was a problem hiding this comment.
The _initialized flag is checked and set without synchronization, creating a race condition. If multiple threads call InitializeAsync concurrently, they could both pass the check on line 56 and initialize the server multiple times, potentially starting multiple processes. Use a lock or Interlocked operations to ensure thread-safe initialization, or implement a standard lazy initialization pattern.
| if (_initialized) | |
| { | |
| return; | |
| } | |
| if (string.IsNullOrEmpty(_configuration.Command)) | |
| { | |
| throw new InvalidOperationException($"Command is required for stdio MCP server '{Name}'"); | |
| } | |
| try | |
| { | |
| _logger?.LogInformation("Initializing MCP server: {Name}", Name); | |
| var startInfo = new ProcessStartInfo | |
| { | |
| FileName = _configuration.Command, | |
| UseShellExecute = false, | |
| RedirectStandardInput = true, | |
| RedirectStandardOutput = true, | |
| RedirectStandardError = true, | |
| CreateNoWindow = true | |
| }; | |
| if (_configuration.Arguments != null) | |
| { | |
| foreach (var arg in _configuration.Arguments) | |
| { | |
| startInfo.ArgumentList.Add(arg); | |
| } | |
| } | |
| if (_configuration.EnvironmentVariables != null) | |
| { | |
| foreach (var kvp in _configuration.EnvironmentVariables) | |
| { | |
| startInfo.Environment[kvp.Key] = kvp.Value; | |
| } | |
| } | |
| _process = Process.Start(startInfo); | |
| if (_process == null) | |
| { | |
| throw new InvalidOperationException($"Failed to start process for MCP server '{Name}'"); | |
| } | |
| _initialized = true; | |
| _logger?.LogInformation("MCP server '{Name}' initialized successfully", Name); | |
| } | |
| catch (Exception ex) | |
| { | |
| _logger?.LogError(ex, "Failed to initialize MCP server '{Name}'", Name); | |
| throw; | |
| } | |
| lock (_lock) | |
| { | |
| if (_initialized) | |
| { | |
| return; | |
| } | |
| if (string.IsNullOrEmpty(_configuration.Command)) | |
| { | |
| throw new InvalidOperationException($"Command is required for stdio MCP server '{Name}'"); | |
| } | |
| try | |
| { | |
| _logger?.LogInformation("Initializing MCP server: {Name}", Name); | |
| var startInfo = new ProcessStartInfo | |
| { | |
| FileName = _configuration.Command, | |
| UseShellExecute = false, | |
| RedirectStandardInput = true, | |
| RedirectStandardOutput = true, | |
| RedirectStandardError = true, | |
| CreateNoWindow = true | |
| }; | |
| if (_configuration.Arguments != null) | |
| { | |
| foreach (var arg in _configuration.Arguments) | |
| { | |
| startInfo.ArgumentList.Add(arg); | |
| } | |
| } | |
| if (_configuration.EnvironmentVariables != null) | |
| { | |
| foreach (var kvp in _configuration.EnvironmentVariables) | |
| { | |
| startInfo.Environment[kvp.Key] = kvp.Value; | |
| } | |
| } | |
| _process = Process.Start(startInfo); | |
| if (_process == null) | |
| { | |
| throw new InvalidOperationException($"Failed to start process for MCP server '{Name}'"); | |
| } | |
| _initialized = true; | |
| _logger?.LogInformation("MCP server '{Name}' initialized successfully", Name); | |
| } | |
| catch (Exception ex) | |
| { | |
| _logger?.LogError(ex, "Failed to initialize MCP server '{Name}'", Name); | |
| throw; | |
| } | |
| } |
There was a problem hiding this comment.
@copilot I fixed the godawful .GetAwaiter().GetResult() blocking anti-pattern (please, never do that again), but testing was a nightmare. Node.js is too difficult to use consistently. We need to completely refactor around Microsoft's MCP Gateway, so this code actually works.
See https://github.com/microsoft/mcp-gateway
Co-authored-by: rrusson <653188+rrusson@users.noreply.github.com>
I've started the refactoring to use the Microsoft MCP Gateway. However, the ModelContextProtocol 0.7.0-preview.1 SDK has a significantly different API than the current documentation shows. I recommend one of these approaches:
Which approach would you prefer? The Gateway HTTP approach (#1) seems most aligned with your request and would be the most reliable. |
Co-authored-by: rrusson <653188+rrusson@users.noreply.github.com>
Co-authored-by: rrusson <653188+rrusson@users.noreply.github.com>
|



Enables optional integration with MCP (Model Context Protocol) servers to extend Clippy with external tools (weather, news, geolocation). Uses custom stdio-based client with zero external dependencies instead of Microsoft MCP Gateway.
Implementation
Custom JSON-RPC client (
StdioMcpServer.cs,McpModels.cs)System.Diagnostics.ProcessSystem.Text.JsonConfiguration-driven (
appsettings.json){ "McpServers": [ { "Name": "weather", "Command": "npx", "Arguments": ["-y", "@modelcontextprotocol/server-weather"], "Enabled": true } ] }Async initialization (
LlmSetup.cs)Architecture
Prerequisites
Tested with
@modelcontextprotocol/server-weatherand@modelcontextprotocol/server-brave-search.Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.