From c6b4e516ef0cdec1c14824f72d59636b3ae31603 Mon Sep 17 00:00:00 2001 From: Rockford Lhotka Date: Tue, 12 May 2026 12:39:01 +0200 Subject: [PATCH 1/2] Surface downstream ServerInfo and instructions to consumers Capture each downstream MCP server's ServerInfo (name/title/version) and ServerInstructions on connection, persist them on RegisteredServer, and expose them through list_services and get_service_details. AI summary generation now incorporates server-supplied instructions as the authoritative description of the server's purpose. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Models/RegisteredServer.cs | 6 +++ src/McpAggregator.Core/Models/ServiceIndex.cs | 14 +++++++ .../Services/ConnectionManager.cs | 16 ++++++++ .../Services/ServerRegistry.cs | 25 ++++++++++++ .../Services/SummaryGenerator.cs | 38 +++++++++++++++---- src/McpAggregator.Core/Services/ToolIndex.cs | 9 +++++ .../Services/SummaryGeneratorTests.cs | 36 ++++++++++++++++++ 7 files changed, 136 insertions(+), 8 deletions(-) diff --git a/src/McpAggregator.Core/Models/RegisteredServer.cs b/src/McpAggregator.Core/Models/RegisteredServer.cs index 9fcc392..10ccbec 100644 --- a/src/McpAggregator.Core/Models/RegisteredServer.cs +++ b/src/McpAggregator.Core/Models/RegisteredServer.cs @@ -10,4 +10,10 @@ public class RegisteredServer public DateTimeOffset RegisteredAt { get; set; } = DateTimeOffset.UtcNow; public bool HasSkillDocument { get; set; } public string? AiSummary { get; set; } + + // Metadata captured from the downstream MCP server on connection. + public string? RemoteName { get; set; } + public string? RemoteTitle { get; set; } + public string? RemoteVersion { get; set; } + public string? RemoteInstructions { get; set; } } diff --git a/src/McpAggregator.Core/Models/ServiceIndex.cs b/src/McpAggregator.Core/Models/ServiceIndex.cs index 5be69b7..d852664 100644 --- a/src/McpAggregator.Core/Models/ServiceIndex.cs +++ b/src/McpAggregator.Core/Models/ServiceIndex.cs @@ -8,6 +8,13 @@ public class ServiceIndex public bool Enabled { get; set; } public bool Available { get; set; } public bool HasSkillDocument { get; set; } + + // Identity reported by the downstream MCP server's ServerInfo (compact identity only; + // the full server-supplied instructions are surfaced via ServiceDetails). + public string? RemoteName { get; set; } + public string? RemoteTitle { get; set; } + public string? RemoteVersion { get; set; } + public List Tools { get; set; } = []; } @@ -25,6 +32,13 @@ public class ServiceDetails public bool Enabled { get; set; } public bool Available { get; set; } public bool HasSkillDocument { get; set; } + + // Metadata supplied by the downstream MCP server itself during the initialize handshake. + public string? RemoteName { get; set; } + public string? RemoteTitle { get; set; } + public string? RemoteVersion { get; set; } + public string? RemoteInstructions { get; set; } + public List Tools { get; set; } = []; public List Prompts { get; set; } = []; } diff --git a/src/McpAggregator.Core/Services/ConnectionManager.cs b/src/McpAggregator.Core/Services/ConnectionManager.cs index 05b88fc..e91d682 100644 --- a/src/McpAggregator.Core/Services/ConnectionManager.cs +++ b/src/McpAggregator.Core/Services/ConnectionManager.cs @@ -204,6 +204,22 @@ private async Task ConnectAsync(RegisteredServer server, Cancel _connections[server.Name] = state; AggregatorTelemetry.ActiveConnections.Add(1, new TagList { { "server_name", server.Name } }); _logger.LogInformation("Connected to '{Server}'", server.Name); + + try + { + await _registry.UpdateRemoteMetadataAsync( + server.Name, + client.ServerInfo?.Name, + client.ServerInfo?.Title, + client.ServerInfo?.Version, + client.ServerInstructions, + ct); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to capture remote metadata from '{Server}'", server.Name); + } + return state; } diff --git a/src/McpAggregator.Core/Services/ServerRegistry.cs b/src/McpAggregator.Core/Services/ServerRegistry.cs index 0fe7d9b..0a6b120 100644 --- a/src/McpAggregator.Core/Services/ServerRegistry.cs +++ b/src/McpAggregator.Core/Services/ServerRegistry.cs @@ -105,6 +105,31 @@ public async Task UpdateSummaryAsync(string name, string summary, CancellationTo await PersistAsync(ct); } + public async Task UpdateRemoteMetadataAsync( + string name, + string? remoteName, + string? remoteTitle, + string? remoteVersion, + string? remoteInstructions, + CancellationToken ct = default) + { + var server = Get(name); + if (server.RemoteName == remoteName + && server.RemoteTitle == remoteTitle + && server.RemoteVersion == remoteVersion + && server.RemoteInstructions == remoteInstructions) + { + return; + } + + server.RemoteName = remoteName; + server.RemoteTitle = remoteTitle; + server.RemoteVersion = remoteVersion; + server.RemoteInstructions = remoteInstructions; + await PersistAsync(ct); + _logger.LogDebug("Updated remote metadata for '{Name}' (version: {Version})", name, remoteVersion ?? "unknown"); + } + public async Task SetEnabledAsync(string name, bool enabled, CancellationToken ct = default) { var server = Get(name); diff --git a/src/McpAggregator.Core/Services/SummaryGenerator.cs b/src/McpAggregator.Core/Services/SummaryGenerator.cs index 78bd72e..1364110 100644 --- a/src/McpAggregator.Core/Services/SummaryGenerator.cs +++ b/src/McpAggregator.Core/Services/SummaryGenerator.cs @@ -58,13 +58,29 @@ public SummaryGenerator( """; } + var serverIdentity = string.IsNullOrWhiteSpace(server.RemoteTitle) + ? server.RemoteName + : $"{server.RemoteTitle} ({server.RemoteName})"; + + var instructionsSection = string.IsNullOrWhiteSpace(server.RemoteInstructions) + ? "" + : $""" + + + Server-supplied instructions (authoritative — these come from the server itself + and describe how it expects to be used; weight this heavily over names alone): + {server.RemoteInstructions} + """; + var userContent = $""" Server name: {server.Name} Display name: {server.DisplayName ?? "(none)"} - Description: {server.Description ?? "(none)"} + Registered description: {server.Description ?? "(none)"} + Remote identity: {serverIdentity ?? "(none)"} + Remote version: {server.RemoteVersion ?? "(none)"} Tools: - {toolList}{promptSection} + {toolList}{promptSection}{instructionsSection} """; using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); @@ -74,12 +90,18 @@ public SummaryGenerator( [ new(ChatRole.System, """ You are a technical indexer building a capability index for an AI agent orchestrator. - Given an MCP server's metadata, tool catalog, and prompt templates, produce a concise - summary (2-4 sentences, max 300 characters) that describes what this server does and - what capabilities it offers. The summary will be read by another AI agent deciding - whether to route a request to this server, so use precise technical language and - emphasize what kinds of tasks or domains this server handles. If prompt templates - are present, mention them as first-class capabilities alongside tools. + Given an MCP server's metadata, tool catalog, prompt templates, and any + server-supplied instructions, produce a concise summary (2-4 sentences, + max 300 characters) that describes what this server does and what capabilities + it offers. The summary will be read by another AI agent deciding whether to + route a request to this server, so use precise technical language and emphasize + what kinds of tasks or domains this server handles. + + When server-supplied instructions are present, treat them as the authoritative + description of the server's purpose and recommended use — they override + guesses inferred from tool names. If prompt templates are present, mention them + as first-class capabilities alongside tools. + Output ONLY the summary text, no quotes or labels. """), new(ChatRole.User, userContent) diff --git a/src/McpAggregator.Core/Services/ToolIndex.cs b/src/McpAggregator.Core/Services/ToolIndex.cs index 5fb51d3..972e9ac 100644 --- a/src/McpAggregator.Core/Services/ToolIndex.cs +++ b/src/McpAggregator.Core/Services/ToolIndex.cs @@ -96,6 +96,11 @@ public async Task> GetIndexAsync(CancellationToken ct = defau } } + // Populate after the tools call so a fresh connect has refreshed the cached metadata. + index.RemoteName = server.RemoteName; + index.RemoteTitle = server.RemoteTitle; + index.RemoteVersion = server.RemoteVersion; + results.Add(index); } @@ -126,6 +131,10 @@ public async Task GetDetailsAsync(string serverName, Cancellatio Enabled = server.Enabled, Available = true, HasSkillDocument = server.HasSkillDocument, + RemoteName = server.RemoteName, + RemoteTitle = server.RemoteTitle, + RemoteVersion = server.RemoteVersion, + RemoteInstructions = server.RemoteInstructions, Tools = tools, Prompts = prompts }; diff --git a/test/McpAggregator.Core.Tests/Services/SummaryGeneratorTests.cs b/test/McpAggregator.Core.Tests/Services/SummaryGeneratorTests.cs index e410221..a9976db 100644 --- a/test/McpAggregator.Core.Tests/Services/SummaryGeneratorTests.cs +++ b/test/McpAggregator.Core.Tests/Services/SummaryGeneratorTests.cs @@ -171,4 +171,40 @@ public async Task GenerateSummaryAsync_SendsCorrectPromptContent() Assert.IsTrue(userText.Contains("tool_a"), "User prompt should contain tool names"); Assert.IsTrue(userText.Contains("tool_b"), "User prompt should contain tool names"); } + + [TestMethod] + public async Task GenerateSummaryAsync_IncludesRemoteMetadataAndInstructions() + { + IEnumerable? capturedMessages = null; + var expectations = new IChatClientCreateExpectations(); + expectations.Setups.GetResponseAsync( + Arg.Any>(), + Arg.IsDefault(), + Arg.Any()) + .Callback((messages, _, _) => + { + capturedMessages = messages; + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary"))); + }); + var client = expectations.Instance(); + var gen = CreateGenerator(chatClient: client, enabled: true); + + var server = SampleServer(); + server.RemoteName = "weather-api"; + server.RemoteTitle = "Weather API"; + server.RemoteVersion = "2.3.1"; + server.RemoteInstructions = "Use get_forecast for multi-day predictions and get_current for now-only queries."; + + await gen.GenerateSummaryAsync(server, SampleTools()); + + Assert.IsNotNull(capturedMessages); + var userText = capturedMessages!.ToList()[1].Text; + Assert.IsNotNull(userText); + Assert.IsTrue(userText!.Contains("Weather API"), "User prompt should contain remote title"); + Assert.IsTrue(userText.Contains("weather-api"), "User prompt should contain remote name"); + Assert.IsTrue(userText.Contains("2.3.1"), "User prompt should contain remote version"); + Assert.IsTrue(userText.Contains("get_forecast"), "User prompt should contain remote instructions verbatim"); + Assert.IsTrue(userText.Contains("Server-supplied instructions"), + "User prompt should label the instructions section"); + } } From bb9d108d230da1404f091f8c23c8ca73c37813cf Mon Sep 17 00:00:00 2001 From: Rockford Lhotka Date: Tue, 12 May 2026 12:40:58 +0200 Subject: [PATCH 2/2] Bump version to 0.3.0 for downstream metadata surfacing Co-Authored-By: Claude Opus 4.7 (1M context) --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 1d5a8e4..0817cc4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 0.2.0 + 0.3.0