Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<Version>0.2.0</Version>
<Version>0.3.0</Version>
</PropertyGroup>

</Project>
6 changes: 6 additions & 0 deletions src/McpAggregator.Core/Models/RegisteredServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
14 changes: 14 additions & 0 deletions src/McpAggregator.Core/Models/ServiceIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ToolSummary> Tools { get; set; } = [];
}

Expand All @@ -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<ToolDetail> Tools { get; set; } = [];
public List<PromptDetail> Prompts { get; set; } = [];
}
Expand Down
16 changes: 16 additions & 0 deletions src/McpAggregator.Core/Services/ConnectionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,22 @@ private async Task<ConnectionState> 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;
}

Expand Down
25 changes: 25 additions & 0 deletions src/McpAggregator.Core/Services/ServerRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
38 changes: 30 additions & 8 deletions src/McpAggregator.Core/Services/SummaryGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions src/McpAggregator.Core/Services/ToolIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ public async Task<List<ServiceIndex>> 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);
}

Expand Down Expand Up @@ -126,6 +131,10 @@ public async Task<ServiceDetails> 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
};
Expand Down
36 changes: 36 additions & 0 deletions test/McpAggregator.Core.Tests/Services/SummaryGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatMessage>? capturedMessages = null;
var expectations = new IChatClientCreateExpectations();
expectations.Setups.GetResponseAsync(
Arg.Any<IEnumerable<ChatMessage>>(),
Arg.IsDefault<ChatOptions?>(),
Arg.Any<CancellationToken>())
.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");
}
}
Loading