Skip to content

Commit 7a01fd3

Browse files
committed
more fixes for names
1 parent 0ac824c commit 7a01fd3

18 files changed

Lines changed: 1220 additions & 33 deletions

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<AnalysisLevel>latest-recommended</AnalysisLevel>
1212
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
1313
<NoWarn>$(NoWarn);CS1591;CA1707;CA1848;CA1859;CA1873</NoWarn>
14-
<Version>0.4.3</Version>
14+
<Version>0.4.4</Version>
1515
<PackageVersion>$(Version)</PackageVersion>
1616
</PropertyGroup>
1717

Directory.Packages.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
77
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="2.0.2" />
88
<PackageVersion Include="ManagedCode.MarkdownLd.Kb" Version="0.2.5" />
9-
<PackageVersion Include="Microsoft.Agents.AI" Version="1.7.0" />
9+
<PackageVersion Include="Microsoft.Agents.AI" Version="1.8.0" />
1010
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.6.0" />
1111
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.8" />
1212
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
@@ -20,6 +20,6 @@
2020
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
2121
<PackageVersion Include="ModelContextProtocol" Version="1.3.0" />
2222
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="1.3.0" />
23-
<PackageVersion Include="TUnit" Version="1.45.29" />
23+
<PackageVersion Include="TUnit" Version="1.48.6" />
2424
</ItemGroup>
2525
</Project>

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ services.AddMcpServer()
330330
- task-backed tool execution through MCP `tools/call` with `task` metadata plus MCP `tasks/list`, `tasks/get`, `tasks/result`, and `tasks/cancel`
331331
- forwarded `notifications/tasks/status` for exported gateway tasks
332332

333-
Exported MCP tool names are canonical lowercase MCP-safe gateway ids. A unique upstream tool such as `notion-fetch` is exported as `notion-fetch`; when multiple sources expose the same normalized tool name, the gateway uses a source-qualified safe id such as `docs_search_repository` or `ops_search_repository`. `McpGatewayToolDescriptor.ToolId`, downstream MCP `Tool.Name`, and downstream `tools/call` names are the same value, and gateway-owned name resolution is case-insensitive so callers can vary casing without creating a different tool identity. Prompt names and exported resource names use the same lowercase MCP-safe source qualification, for example `ops_deployment_review_system_prompt` or `local_release_review_bundle`, while raw upstream `SourceId`, prompt name, tool name, and resource URI remain available as routing metadata and `_meta` values. Exported MCP resource URIs and URI templates are rewritten into gateway-owned opaque URIs so downstream `resources/read` calls route back to the correct upstream source even when multiple servers expose overlapping URI spaces. The same source-aware rewrite is also used for `completion/complete`, forwarded prompt list changes, and forwarded resource update notifications, so downstream clients always talk in terms of gateway-owned prompt names and resource URIs while the gateway proxies the corresponding upstream MCP operations. When an upstream MCP tool already advertises task support, the gateway preserves that contract on the exported tool and proxies the corresponding upstream task flow. Local gateway tools are exported as optional task-capable tools and are executed through the gateway-owned task store.
333+
Exported MCP tool names are canonical lowercase MCP-safe gateway ids. A unique upstream tool such as `notion-fetch` is exported as `notion-fetch`; when multiple sources expose the same normalized tool name, the gateway uses a source-qualified safe id such as `docs_search_repository` or `ops_search_repository`. Hash suffixes are added only when length or collision handling requires them. Names that would exceed 64 characters keep a readable normalized prefix and receive a deterministic hash suffix instead of being blindly truncated. `McpGatewayToolDescriptor.ToolId`, downstream MCP `Tool.Name`, and downstream `tools/call` names are the same value, and gateway-owned name resolution is case-insensitive so callers can vary casing without creating a different tool identity. Prompt names and exported resource names use the same lowercase MCP-safe source qualification, for example `ops_deployment_review_system_prompt` or `local_release_review_bundle`, while raw upstream `SourceId`, prompt name, tool name, and resource URI remain available as routing metadata and `_meta` values. Exported MCP resource URIs and URI templates are rewritten into gateway-owned opaque URIs so downstream `resources/read` calls route back to the correct upstream source even when multiple servers expose overlapping URI spaces. The same source-aware rewrite is also used for `completion/complete`, forwarded prompt list changes, and forwarded resource update notifications, so downstream clients always talk in terms of gateway-owned prompt names and resource URIs while the gateway proxies the corresponding upstream MCP operations. When an upstream MCP tool already advertises task support, the gateway preserves that contract on the exported tool and proxies the corresponding upstream task flow. Local gateway tools are exported as optional task-capable tools and are executed through the gateway-owned task store.
334334

335335
The exported task store uses the official SDK `InMemoryMcpTaskStore` with MCPGateway-owned bounded defaults: task TTL 30 minutes, maximum task TTL 2 hours, cleanup every minute, maximum 10,000 tasks globally, and maximum 1,000 tasks per downstream session. Hosts can override those limits through `McpGatewayOptions.McpTaskStore`, or replace `McpServerOptions.TaskStore` with a durable production store when tasks must survive process restarts:
336336

docs/ADR/ADR-0013-mcp-safe-canonical-protocol-names.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Gateway-owned protocol names use one canonical lowercase MCP-safe identity.
1818
- Unique tools use the normalized upstream tool name, for example `notion-fetch`.
1919
- Colliding normalized tool names receive deterministic source-qualified safe ids, for example `docs_search_repository`.
2020
- Exact duplicate source/tool pairs receive a deterministic suffix instead of being skipped.
21+
- Hash suffixes are added only when length or collision handling requires them; normal short names such as `notion-fetch` remain unchanged.
22+
- Names that exceed the bounded host limit keep a readable normalized prefix and receive a deterministic hash suffix within 64 characters instead of being blindly truncated.
2123
- Prompt ids and exported resource names are lowercase MCP-safe source-qualified names.
2224
- Gateway-owned name resolution is case-insensitive for tools, prompts, task support maps, and embedding-store lookups.
2325
- Raw upstream `SourceId`, tool name, prompt name, resource name, and URI remain separate routing metadata and `_meta` values.

docs/Architecture/Overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Graph-specific search and indexing operations deliberately sit on `IMcpGatewayGr
3636

3737
The package also keeps chat-client and agent integration generic: `McpGatewayToolSet` is the source of reusable `AITool` search, route, invoke, graph schema-describe, tool index-build, graph schema-search, graph federated-search, and graph export meta-tools plus discovered proxy tools, `ChatOptions.AddMcpGatewayTools(...)` and `ChatOptions.AddMcpGatewayGraphTools(...)` remain the low-level bridges, and `McpGatewayAutoDiscoveryChatClient` plus `UseMcpGatewayAutoDiscovery(...)` provide the recommended staged host wrapper that starts with the gateway search/route/invoke tools and replaces the discovered proxy set on each new search result without introducing a hard Agent Framework dependency into the core package.
3838

39-
For MCP interoperability in the other direction, the package also exposes `WithMcpGatewayCatalog()` as a server-builder extension. Hosts can take one aggregated gateway catalog and re-export it as a downstream MCP server that exposes the combined tools, prompts, and resources from multiple upstream MCP sources plus gateway-owned prompts. That export also proxies MCP completions, forwarded prompt list-change notifications, resource subscriptions, forwarded resource-updated notifications, logging level changes, and task-backed tool execution through the MCP tasks surface. Tool ids, downstream MCP tool names, prompt ids, and exported resource names are normalized to lowercase MCP-safe names; unique tools keep their normalized upstream tool name, collisions get deterministic source-qualified ids, and gateway-owned name resolution is case-insensitive while raw `SourceId` and upstream protocol names stay in routing metadata. Resource URIs are rewritten into gateway-owned source-qualified URIs during export so downstream reads, completions, and forwarded update notifications stay unambiguous even when upstream servers reuse the same URI spaces. Exported task storage uses the official SDK in-memory task store with MCPGateway-owned bounded TTL, cleanup, and task-count limits by default. Gateway-owned per-session prompt notification, resource subscription, and active task binding state is removed both on protocol cleanup paths and, for Streamable HTTP hosting, through the official SDK `HttpServerTransportOptions.RunSessionHandler` lifecycle when the SDK-owned HTTP session ends. By default the export binds to the singleton gateway services in DI, but hosts can override that binding per request or per downstream MCP session through `IMcpGatewayServerBindingResolver`, which allows route-specific or tenant-specific gateway instances without forking the exported MCP handler stack.
39+
For MCP interoperability in the other direction, the package also exposes `WithMcpGatewayCatalog()` as a server-builder extension. Hosts can take one aggregated gateway catalog and re-export it as a downstream MCP server that exposes the combined tools, prompts, and resources from multiple upstream MCP sources plus gateway-owned prompts. That export also proxies MCP completions, forwarded prompt list-change notifications, resource subscriptions, forwarded resource-updated notifications, logging level changes, and task-backed tool execution through the MCP tasks surface. Tool ids, downstream MCP tool names, prompt ids, and exported resource names are normalized to lowercase MCP-safe names; unique tools keep their normalized upstream tool name, collisions get deterministic source-qualified ids, hash suffixes appear only when length or collision handling requires them, and gateway-owned name resolution is case-insensitive while raw `SourceId` and upstream protocol names stay in routing metadata. Resource URIs are rewritten into gateway-owned source-qualified URIs during export so downstream reads, completions, and forwarded update notifications stay unambiguous even when upstream servers reuse the same URI spaces. Exported task storage uses the official SDK in-memory task store with MCPGateway-owned bounded TTL, cleanup, and task-count limits by default. Gateway-owned per-session prompt notification, resource subscription, and active task binding state is removed both on protocol cleanup paths and, for Streamable HTTP hosting, through the official SDK `HttpServerTransportOptions.RunSessionHandler` lifecycle when the SDK-owned HTTP session ends. By default the export binds to the singleton gateway services in DI, but hosts can override that binding per request or per downstream MCP session through `IMcpGatewayServerBindingResolver`, which allows route-specific or tenant-specific gateway instances without forking the exported MCP handler stack.
4040

4141
The repository now uses feature-first package slices for `Gateway`, `Discovery`, `Catalog`, `Search`, `Invocation`, `Prompts`, `Resources`, and `Hosting`. The durable policy decision behind that structure is captured in [`docs/ADR/ADR-0007-vertical-slice-package-organization.md`](../ADR/ADR-0007-vertical-slice-package-organization.md).
4242

src/ManagedCode.MCPGateway/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ For this .NET project:
7272
- Keep `McpGateway` focused on search and invoke orchestration; registry mutation belongs in catalog and registration collaborators.
7373
- Keep transport-specific logic inside registration and source abstractions, not scattered through runtime code.
7474
- When production behavior changes here, update the integration-style tests under `tests/ManagedCode.MCPGateway.Tests/` in the same task.
75+
- Keep package-owned production string values such as source ids, fallback names, protocol metadata keys, tool kinds, and default runtime identifiers behind named constants instead of inline literals, because gateway naming contracts must stay auditable and reusable.
7576
- Do not hide shared runtime services such as `ILoggerFactory` inside options bags for public factory APIs; when custom gateway instances need host-owned shared dependencies, expose a DI-registered factory service that resolves those dependencies from the container and accepts only gateway-specific configuration at creation time.
7677
- For public runtime-mutation APIs in this package, use precise lifecycle verbs such as `Reconfigure` or `Reset`; avoid ambiguous or alarming names like `Replace` when the operation only rebuilds in-memory registry configuration.
7778
- For public factory APIs in this package, prefer explicit overloads over nullable optional delegate parameters; use `Create()` plus `Create(Action<T>)` when both cases are supported.

src/ManagedCode.MCPGateway/Catalog/Internal/Runtime/McpGatewayRuntime.Indexing.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,10 @@ IList<McpGatewayDiagnostic> diagnostics
213213
}
214214

215215
var toolName = loadedTool.Tool.Name.Trim();
216-
var normalizedToolName = McpGatewayProtocolName.Normalize(toolName, "gateway_tool");
216+
var normalizedToolName = McpGatewayProtocolName.Normalize(
217+
toolName,
218+
McpGatewayProtocolName.DefaultToolName
219+
);
217220
var requiresSourcePrefix =
218221
normalizedToolNameCounts.TryGetValue(normalizedToolName, out var count)
219222
&& count > 1;
@@ -269,7 +272,7 @@ private static IReadOnlyDictionary<string, int> CountNormalizedToolNames(
269272

270273
var normalizedToolName = McpGatewayProtocolName.Normalize(
271274
loadedTool.Tool.Name,
272-
"gateway_tool"
275+
McpGatewayProtocolName.DefaultToolName
273276
);
274277
counts[normalizedToolName] = counts.GetValueOrDefault(normalizedToolName) + 1;
275278
}

src/ManagedCode.MCPGateway/Discovery/Internal/McpGatewayDiscoveredToolNaming.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
using System.Globalization;
2+
13
namespace ManagedCode.MCPGateway;
24

35
internal static class McpGatewayDiscoveredToolNaming
46
{
5-
private const string DefaultToolName = "gateway_tool";
67
private const string NameSeparator = "_";
78
private const string DescriptionPrefix = "Direct proxy for gateway tool ";
89
private const string ToolIdLabel = " (";
@@ -65,7 +66,10 @@ public static string CreateName(McpGatewaySearchMatch match, ISet<string> reserv
6566
ArgumentNullException.ThrowIfNull(match);
6667
ArgumentNullException.ThrowIfNull(reservedNames);
6768

68-
var sanitizedToolName = McpGatewayProtocolName.Normalize(match.ToolName, DefaultToolName);
69+
var sanitizedToolName = McpGatewayProtocolName.Normalize(
70+
match.ToolName,
71+
McpGatewayProtocolName.DefaultToolName
72+
);
6973
if (reservedNames.Add(sanitizedToolName))
7074
{
7175
return sanitizedToolName;
@@ -74,7 +78,7 @@ public static string CreateName(McpGatewaySearchMatch match, ISet<string> reserv
7478
var compositeName = McpGatewayProtocolName.CreateSourceQualifiedName(
7579
match.SourceId,
7680
sanitizedToolName,
77-
DefaultToolName
81+
McpGatewayProtocolName.DefaultToolName
7882
);
7983
if (reservedNames.Add(compositeName))
8084
{
@@ -83,7 +87,10 @@ public static string CreateName(McpGatewaySearchMatch match, ISet<string> reserv
8387

8488
for (var suffix = 2; ; suffix++)
8589
{
86-
var uniqueName = $"{compositeName}{NameSeparator}{suffix}";
90+
var uniqueName = McpGatewayProtocolName.AppendSuffix(
91+
compositeName,
92+
string.Concat(NameSeparator, suffix.ToString(CultureInfo.InvariantCulture))
93+
);
8794
if (reservedNames.Add(uniqueName))
8895
{
8996
return uniqueName;

src/ManagedCode.MCPGateway/Discovery/Internal/McpGatewayGraphToolFactory.cs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,38 @@ string toolIndexBuildToolName
2828
)
2929
{
3030
_ = RequireGraphSearch();
31+
var reservedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
32+
var graphSchemaName = McpGatewayToolSet.CreateToolName(
33+
graphSchemaToolName,
34+
McpGatewayToolSet.DefaultGraphSchemaToolName,
35+
reservedNames
36+
);
37+
var toolIndexBuildName = McpGatewayToolSet.CreateToolName(
38+
toolIndexBuildToolName,
39+
McpGatewayToolSet.DefaultToolIndexBuildToolName,
40+
reservedNames
41+
);
42+
var graphSearchName = McpGatewayToolSet.CreateToolName(
43+
graphSearchToolName,
44+
McpGatewayToolSet.DefaultGraphSearchToolName,
45+
reservedNames
46+
);
47+
var graphFederatedSearchName = McpGatewayToolSet.CreateToolName(
48+
graphFederatedSearchToolName,
49+
McpGatewayToolSet.DefaultGraphFederatedSearchToolName,
50+
reservedNames
51+
);
52+
var graphExportName = McpGatewayToolSet.CreateToolName(
53+
graphExportToolName,
54+
McpGatewayToolSet.DefaultGraphExportToolName,
55+
reservedNames
56+
);
3157

3258
var graphSchemaTool = AIFunctionFactory.Create(
3359
DescribeGraphSchemaAsync,
3460
new AIFunctionFactoryOptions
3561
{
36-
Name = graphSchemaToolName,
62+
Name = graphSchemaName,
3763
Description = McpGatewayToolSet.GraphSchemaToolDescription,
3864
}
3965
);
@@ -42,7 +68,7 @@ string toolIndexBuildToolName
4268
BuildToolIndexAsync,
4369
new AIFunctionFactoryOptions
4470
{
45-
Name = toolIndexBuildToolName,
71+
Name = toolIndexBuildName,
4672
Description = McpGatewayToolSet.ToolIndexBuildToolDescription,
4773
}
4874
);
@@ -51,7 +77,7 @@ string toolIndexBuildToolName
5177
SchemaGraphSearchAsync,
5278
new AIFunctionFactoryOptions
5379
{
54-
Name = graphSearchToolName,
80+
Name = graphSearchName,
5581
Description = McpGatewayToolSet.GraphSearchToolDescription,
5682
}
5783
);
@@ -60,7 +86,7 @@ string toolIndexBuildToolName
6086
FederatedGraphSearchAsync,
6187
new AIFunctionFactoryOptions
6288
{
63-
Name = graphFederatedSearchToolName,
89+
Name = graphFederatedSearchName,
6490
Description = McpGatewayToolSet.GraphFederatedSearchToolDescription,
6591
}
6692
);
@@ -69,7 +95,7 @@ string toolIndexBuildToolName
6995
ExportGraphAsync,
7096
new AIFunctionFactoryOptions
7197
{
72-
Name = graphExportToolName,
98+
Name = graphExportName,
7399
Description = McpGatewayToolSet.GraphExportToolDescription,
74100
}
75101
);
@@ -100,6 +126,12 @@ string toolIndexBuildToolName
100126
foreach (var tool in targetTools)
101127
{
102128
toolNames.Add(tool.Name);
129+
toolNames.Add(
130+
McpGatewayToolSet.NormalizeToolName(
131+
tool.Name,
132+
McpGatewayToolSet.DefaultGraphSearchToolName
133+
)
134+
);
103135
}
104136

105137
foreach (

src/ManagedCode.MCPGateway/Discovery/McpGatewayAutoDiscoveryChatClient.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ IReadOnlyList<AITool> gatewayTools
274274
StringComparer.OrdinalIgnoreCase
275275
);
276276
McpGatewaySearchResult? latestSearchResult = null;
277+
var searchToolName = McpGatewayToolSet.NormalizeToolName(
278+
_options.SearchToolName,
279+
McpGatewayToolSet.DefaultSearchToolName
280+
);
277281

278282
foreach (var message in messages)
279283
{
@@ -291,7 +295,7 @@ out var functionName
291295
)
292296
&& string.Equals(
293297
functionName,
294-
_options.SearchToolName,
298+
searchToolName,
295299
StringComparison.OrdinalIgnoreCase
296300
):
297301
latestSearchResult = ReadSearchResult(functionResult.Result);

0 commit comments

Comments
 (0)