diff --git a/Directory.Packages.props b/Directory.Packages.props
index 75d61ac..8d22158 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -13,11 +13,12 @@
-
-
-
-
-
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 05e1307..5afd9cd 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ An extensible toolkit for [Microsoft Semantic Kernel](https://github.com/microso
- 📝 **Skills** — Parse `SKILL.md` files (YAML frontmatter + markdown) into `KernelFunction` or `PromptTemplate`
- 🔗 **Hooks** — Map Claude Code lifecycle events (`PreToolUse`, `PostToolUse`, etc.) to SK's `IFunctionInvocationFilter` and `IPromptRenderFilter`
- 📦 **Plugins** — Load `.claude-plugin/` directories with skills, hooks, and MCP configs
+- 🔌 **MCP** — Discover MCP servers from Claude Code, Claude Desktop, VS Code, Codex, Copilot, and JD canonical config with a PatternKit-backed provider chain
- 🗜️ **Compaction** — Transparent context window management with configurable triggers and hierarchical summarization
- 🧠 **Memory** — Semantic memory with MMR reranking, temporal decay scoring, and query expansion
- 💾 **Memory.Sqlite** — SQLite-backed persistent memory storage
diff --git a/src/JD.SemanticKernel.Extensions.Mcp/JD.SemanticKernel.Extensions.Mcp.csproj b/src/JD.SemanticKernel.Extensions.Mcp/JD.SemanticKernel.Extensions.Mcp.csproj
index 32ae6fc..cb7ffb8 100644
--- a/src/JD.SemanticKernel.Extensions.Mcp/JD.SemanticKernel.Extensions.Mcp.csproj
+++ b/src/JD.SemanticKernel.Extensions.Mcp/JD.SemanticKernel.Extensions.Mcp.csproj
@@ -9,10 +9,11 @@
-
-
-
-
+
+
+
+
+
diff --git a/src/JD.SemanticKernel.Extensions.Mcp/Registry/McpRegistry.cs b/src/JD.SemanticKernel.Extensions.Mcp/Registry/McpRegistry.cs
index 3db0842..92d9bea 100644
--- a/src/JD.SemanticKernel.Extensions.Mcp/Registry/McpRegistry.cs
+++ b/src/JD.SemanticKernel.Extensions.Mcp/Registry/McpRegistry.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using PatternKit.Behavioral.Chain;
namespace JD.SemanticKernel.Extensions.Mcp.Registry;
@@ -37,22 +38,23 @@ public async Task> GetAllAsync(
CancellationToken cancellationToken = default)
{
var merged = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var state = new McpDiscoveryState(merged);
+ var chainBuilder = AsyncActionChain.Create();
foreach (var provider in _providers)
{
- cancellationToken.ThrowIfCancellationRequested();
- var servers = await provider.DiscoverAsync(cancellationToken).ConfigureAwait(false);
-
- foreach (var server in servers)
+ chainBuilder.Use(async (current, token, next) =>
{
- if (!merged.TryGetValue(server.Name, out var existing) ||
- server.Scope > existing.Scope)
- {
- merged[server.Name] = server;
- }
- }
+ token.ThrowIfCancellationRequested();
+ var servers = await provider.DiscoverAsync(token).ConfigureAwait(false);
+ current.Merge(servers);
+ await next(current, token).ConfigureAwait(false);
+ });
}
+ var chain = chainBuilder.Build();
+ await chain.ExecuteAsync(state, cancellationToken).ConfigureAwait(false);
+
return new List(merged.Values).AsReadOnly();
}
@@ -76,4 +78,26 @@ public async Task> GetAllAsync(
return null;
}
+
+ private sealed class McpDiscoveryState
+ {
+ private readonly Dictionary _merged;
+
+ public McpDiscoveryState(Dictionary merged)
+ {
+ _merged = merged;
+ }
+
+ public void Merge(IEnumerable servers)
+ {
+ foreach (var server in servers)
+ {
+ if (!_merged.TryGetValue(server.Name, out var existing) ||
+ server.Scope > existing.Scope)
+ {
+ _merged[server.Name] = server;
+ }
+ }
+ }
+ }
}
diff --git a/tests/JD.SemanticKernel.Extensions.Mcp.Tests/McpRegistryTests.cs b/tests/JD.SemanticKernel.Extensions.Mcp.Tests/McpRegistryTests.cs
index 5091089..2728514 100644
--- a/tests/JD.SemanticKernel.Extensions.Mcp.Tests/McpRegistryTests.cs
+++ b/tests/JD.SemanticKernel.Extensions.Mcp.Tests/McpRegistryTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -105,6 +106,27 @@ public async Task GetAllAsync_SameScopeConflict_FirstWriterWins()
Assert.Single(results);
}
+ [Fact]
+ public async Task GetAllAsync_CancellationStopsPatternKitDiscoveryChain()
+ {
+ var cts = new CancellationTokenSource();
+ var p1 = Substitute.For();
+ p1.DiscoverAsync(Arg.Any()).Returns(_ =>
+ {
+ cts.Cancel();
+ return new List { MakeServer("server-a", "p1", McpScope.User) };
+ });
+
+ var p2 = Substitute.For();
+ p2.DiscoverAsync(Arg.Any()).Returns(
+ new List { MakeServer("server-b", "p2", McpScope.User) });
+
+ var registry = new McpRegistry(new[] { p1, p2 });
+
+ await Assert.ThrowsAsync(() => registry.GetAllAsync(cts.Token));
+ await p2.DidNotReceive().DiscoverAsync(Arg.Any());
+ }
+
[Fact]
public async Task GetAsync_ExistingServer_ReturnsDefinition()
{