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() {