Skip to content

Commit d8cb920

Browse files
committed
refactoring
1 parent 5104565 commit d8cb920

19 files changed

Lines changed: 505 additions & 287 deletions

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ If no new rule is detected -> do not update the file.
168168
- If a user adds or corrects a persistent workflow rule, update `AGENTS.md` first and only then continue with the task.
169169
- When a search-quality fix is requested as a concrete architectural change, finish the intended runtime behavior in the same task instead of stopping at a partial scoring-only step, because partial search fixes leave the real retrieval defect unresolved.
170170
- When the user asks for a shipped feature set, implement the runtime behavior end-to-end with production-ready code and tests instead of leaving placeholder, mocked, or temporary execution paths, because partial delivery is explicitly rejected in this repository.
171+
- During stabilization and release-hardening work, proactively search for analogous fragile patterns after fixing the first defect, and fix every confirmed issue with the correct SDK, platform, or .NET primitive plus tests instead of hacks, suppressions, or narrow one-off reactions.
172+
- Do not hide runtime or transport timeouts as magic numbers in package code; when a timeout is required, expose it as an explicit `TimeSpan` option with a clear default, caller override, and validation. Test-only bounded waits are acceptable only as harness hang guards, not as product behavior.
171173
- When adopting new upstream graph/search capabilities such as `ManagedCode.MarkdownLd.Kb` schema-aware search, implement the real hybrid runtime benefits with tests and docs instead of limiting the task to dependency bumps or export helpers, because the user expects the package to capture the upstream search value end-to-end.
172174
- When adding federated graph search, do not reject federation as a category; expose it through explicit allowlisted APIs or built-in tools with diagnostics and tests, because the user wants powerful hybrid/federated search while keeping hidden unconfigured network calls out of the default path.
173175
- Keep graph/search/index-specific capabilities on the appropriate search or indexing public surface instead of forcing them directly onto the MCP-facing `IMcpGateway` facade, because graph federation and graph export are search/index concerns rather than generic MCP gateway operations.

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ services.AddMcpGateway(options =>
141141

142142
`AddHttpServer(...)` uses the official MCP C# SDK Streamable HTTP transport for modern remote MCP endpoints and keeps the source registered as an HTTP MCP source in gateway descriptors and downstream export metadata.
143143
Use the overload with `HttpTransportMode` only when a legacy endpoint requires `AutoDetect` or `Sse`.
144-
Use `McpGatewayHttpServerOptions` when a host needs the SDK HTTP transport knobs such as additional headers, connection timeout, known session id, session ownership, OAuth options, or SSE reconnection settings.
144+
Use `McpGatewayHttpServerOptions` when a host needs the SDK HTTP transport knobs such as additional headers, connection timeout, known session id, session ownership, OAuth options, or SSE reconnection settings. The package does not set a transport timeout by default; hosts can pass one explicitly or own deadline policy through cancellation tokens and hosting infrastructure.
145145

146146
You can also register:
147147

@@ -633,6 +633,8 @@ services.AddMcpGateway(options =>
633633
{
634634
options.AddMarkdownLdFederatedServiceEndpoint(
635635
new Uri("https://knowledge.example.com/sparql"));
636+
637+
options.MarkdownLdFederatedSparqlQueryTimeout = TimeSpan.FromSeconds(30);
636638
});
637639
```
638640

@@ -649,6 +651,7 @@ var federatedResult = await graphSearch.SearchGraphAsync(
649651
```
650652

651653
The gateway never discovers remote SPARQL endpoints on its own. It uses the configured allowlist, can bind the local gateway graph as a federated service, and reports diagnostics when a requested endpoint is invalid or blocked.
654+
Federated SPARQL execution defaults to `McpGatewayOptions.DefaultMarkdownLdFederatedSparqlQueryTimeout`, which is 30 seconds. Set `MarkdownLdFederatedSparqlQueryTimeout` to a larger `TimeSpan` for slower trusted endpoints, or set it to `null` when the host owns the deadline entirely through cancellation tokens or infrastructure policy.
652655

653656
## Graph Sources
654657

docs/ADR/ADR-0012-schema-aware-sparql-graph-search.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Negative / risks:
142142
- graph search now depends on more upstream `ManagedCode.MarkdownLd.Kb` behavior
143143
- schema-profile drift can reduce result quality if generated graph predicates change
144144
- large-catalog schema search still materializes a per-query candidate graph until upstream schema search exposes a candidate filter such as `CandidateNodeIds`
145-
- federated queries add endpoint and timeout configuration that operators must understand
145+
- federated queries add endpoint and configurable timeout policy that operators must understand
146146

147147
Mitigations:
148148

@@ -161,15 +161,15 @@ Mitigations:
161161

162162
- `Search/Abstractions/IMcpGatewayGraphSearch.cs` adds the graph search/export boundary.
163163
- `McpGateway` and `McpGatewayRuntime` implement `IMcpGatewayGraphSearch`.
164-
- `McpGatewayOptions` adds graph search mode and federated endpoint configuration.
164+
- `McpGatewayOptions` adds graph search mode, federated endpoint configuration, and a configurable federated SPARQL query timeout.
165165
- `Search/Internal/Graph/*` owns schema profile creation, schema/profile description, schema/federated operations, graph export, and hybrid graph ranking.
166166
- `McpGatewayToolSet` adds graph schema/profile, index-build, search, federation, and export meta-tools while keeping basic meta-tools usable with a plain `IMcpGateway`.
167167

168168
### Data / Configuration
169169

170170
- `ManagedCode.MarkdownLd.Kb` is upgraded to `0.2.5`.
171171
- `MarkdownLdGraphSearchMode` defaults to `Hybrid`.
172-
- Federated endpoints are configured through `AddMarkdownLdFederatedServiceEndpoint(...)`.
172+
- Federated endpoints are configured through `AddMarkdownLdFederatedServiceEndpoint(...)`; federated query timeout defaults to `McpGatewayOptions.DefaultMarkdownLdFederatedSparqlQueryTimeout` and can be overridden or disabled through `MarkdownLdFederatedSparqlQueryTimeout`.
173173

174174
### Documentation
175175

docs/Architecture/Overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ flowchart LR
201201
- The built-in process-local embedding store may depend on `IMemoryCache`, but cross-instance persistence and cache replication must stay behind host-provided `IMcpGatewayToolEmbeddingStore` implementations.
202202
- Markdown-LD graph search is the default internal retrieval strategy. It may depend on `ManagedCode.MarkdownLd.Kb`, uses schema-aware SPARQL as the primary graph path in hybrid/schema-aware mode, uses gateway-built ranked candidate selection to bound large-catalog schema search, uses ranked candidate/fuzzy graph search only as focused hybrid support or fallback, still returns the same public `McpGatewaySearchMatch` contracts, calibrates user-facing confidence at the gateway layer, and must not create a separate invocation surface.
203203
- Graph-specific schema/profile inspection, evidence, generated SPARQL, explicit federated search, explicit index-build tooling, and runtime graph export belong to `IMcpGatewayGraphSearch` and `McpGatewayToolSet` graph tools, not to the MCP-facing `IMcpGateway` facade.
204-
- Federated graph search must use explicit configured endpoint allowlists and local graph bindings; the runtime must not perform hidden remote SPARQL discovery from the normal search path.
204+
- Federated graph search must use explicit configured endpoint allowlists, local graph bindings, and the `McpGatewayOptions.MarkdownLdFederatedSparqlQueryTimeout` `TimeSpan` option; the runtime must not perform hidden remote SPARQL discovery from the normal search path.
205205
- Markdown-LD graph sources may be generated from the live catalog at index build time, loaded from a file-system path, or provided through a host-supplied document factory configured in `McpGatewayOptions`. All modes must still map graph documents back to the current catalog before returning matches.
206206
- Tool metadata used for search enrichment must stay explicit and developer-controlled through registration hints or tool annotations; multilingual improvement should come from metadata plus scoring, not from one-off hardcoded phrase rules in runtime code.
207207
- Warmup remains optional. The package must work correctly with lazy indexing and must not require manual initialization for every host.

docs/Features/SearchQueryNormalizationAndRanking.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ flowchart LR
118118
- keyed optional search normalizer client from DI
119119
- optional embedding generator and embedding store from DI when vector strategy is selected
120120
- search and graph-source options from `McpGatewayOptions`
121-
- schema-aware graph search mode and federated endpoint allowlist from `McpGatewayOptions`
121+
- schema-aware graph search mode, federated endpoint allowlist, and federated SPARQL query timeout from `McpGatewayOptions`
122122
- file-system Markdown-LD graph source when `MarkdownLdGraphSource.FileSystem` is selected
123123
- host-supplied Markdown-LD graph documents when `UseMarkdownLdGraphDocuments(...)` is selected
124124
- Writes:

src/ManagedCode.MCPGateway/Gateway/Configuration/McpGatewayOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ namespace ManagedCode.MCPGateway;
55

66
public sealed class McpGatewayOptions
77
{
8+
public static TimeSpan DefaultMarkdownLdFederatedSparqlQueryTimeout { get; } =
9+
TimeSpan.FromSeconds(30);
10+
811
private readonly McpGatewayRegistrationCollection _sourceRegistrations = new();
912
private readonly List<Uri> _markdownLdFederatedServiceEndpoints = [];
1013

@@ -35,6 +38,9 @@ public Func<
3538

3639
public int MaxDescriptorLength { get; set; } = 4096;
3740

41+
public TimeSpan? MarkdownLdFederatedSparqlQueryTimeout { get; set; } =
42+
DefaultMarkdownLdFederatedSparqlQueryTimeout;
43+
3844
internal IReadOnlyList<McpGatewayToolSourceRegistration> SourceRegistrations =>
3945
_sourceRegistrations.Snapshot();
4046

src/ManagedCode.MCPGateway/Gateway/Internal/Runtime/McpGatewayRuntime.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,6 @@ internal sealed partial class McpGatewayRuntime : IMcpGateway, IMcpGatewayGraphS
345345
private const int GraphPluralNormalizationMinimumLength = 3;
346346
private const int GraphDefaultTermCollectionCapacity = 8;
347347
private const int GraphMaxCapabilityTerms = 4;
348-
private const int FederatedSparqlQueryTimeoutMilliseconds = 15000;
349348
private const double SearchScoreMinimum = 0d;
350349
private const double SearchScoreMaximum = 1d;
351350
private const double GraphSchemaNameTextWeight = 1.2d;
@@ -573,6 +572,7 @@ private readonly Func<
573572
private readonly int _defaultSearchLimit;
574573
private readonly int _maxSearchResults;
575574
private readonly int _maxDescriptorLength;
575+
private readonly TimeSpan? _markdownLdFederatedSparqlQueryTimeout;
576576
private readonly IReadOnlyList<Uri> _markdownLdFederatedServiceEndpoints;
577577
private readonly Uri _graphLocalFederationEndpoint;
578578
private readonly IMcpGatewaySearchCache _searchRuntimeCache;
@@ -608,6 +608,10 @@ ILoggerFactory loggerFactory
608608
_defaultSearchLimit = Math.Max(1, resolvedOptions.DefaultSearchLimit);
609609
_maxSearchResults = Math.Max(1, resolvedOptions.MaxSearchResults);
610610
_maxDescriptorLength = Math.Max(256, resolvedOptions.MaxDescriptorLength);
611+
_markdownLdFederatedSparqlQueryTimeout = ValidateOptionalTimeout(
612+
resolvedOptions.MarkdownLdFederatedSparqlQueryTimeout,
613+
nameof(McpGatewayOptions.MarkdownLdFederatedSparqlQueryTimeout)
614+
);
611615
_markdownLdFederatedServiceEndpoints = resolvedOptions.MarkdownLdFederatedServiceEndpoints;
612616
_graphLocalFederationEndpoint = new Uri(
613617
$"{GraphLocalFederationEndpointUriText}/{Guid.NewGuid():N}",
@@ -654,6 +658,34 @@ is IMcpGatewayCatalogSource registryCatalogSource
654658
throw new InvalidOperationException(CatalogSourceMissingMessage);
655659
}
656660

661+
private static TimeSpan? ValidateOptionalTimeout(TimeSpan? value, string paramName)
662+
{
663+
if (value is null)
664+
{
665+
return null;
666+
}
667+
668+
if (value.Value <= TimeSpan.Zero)
669+
{
670+
throw new ArgumentOutOfRangeException(
671+
paramName,
672+
value,
673+
"Timeout must be greater than zero."
674+
);
675+
}
676+
677+
if (value.Value.TotalMilliseconds > int.MaxValue)
678+
{
679+
throw new ArgumentOutOfRangeException(
680+
paramName,
681+
value,
682+
"Timeout must fit into Int32 milliseconds for the Markdown-LD federated SPARQL executor."
683+
);
684+
}
685+
686+
return value;
687+
}
688+
657689
private void ThrowIfDisposed()
658690
{
659691
ObjectDisposedException.ThrowIf(Volatile.Read(ref _state).IsDisposed, this);

src/ManagedCode.MCPGateway/Hosting/Internal/Server/McpGatewayMcpServerBindingManager.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,15 +196,18 @@ CancellationToken cancellationToken
196196

197197
public async ValueTask DisposeAsync()
198198
{
199+
IMcpGatewayServerBinding binding;
199200
try
200201
{
201-
var binding = await BindingTask;
202-
await binding.DisposeAsync();
202+
binding = await BindingTask;
203203
}
204204
catch
205205
{
206206
// Resolution failures have nothing to dispose and should not fail cleanup.
207+
return;
207208
}
209+
210+
await binding.DisposeAsync();
208211
}
209212
}
210213
}

src/ManagedCode.MCPGateway/Hosting/Internal/Server/McpGatewayMcpServerProtocolMapper.cs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ internal static class McpGatewayMcpServerProtocolMapper
1616
"' input schema must be a JSON object.";
1717
private const string InvalidPromptMessageContentMessage =
1818
"Prompt message content is not a valid MCP content block.";
19+
private const string InvalidPromptMessageRolePrefix = "Prompt message role '";
20+
private const string InvalidPromptMessageRoleSuffix = "' is not supported.";
1921

2022
public static Tool ToProtocolTool(
2123
McpGatewayToolDescriptor descriptor,
@@ -226,13 +228,7 @@ public static GetPromptResult CreateErrorPromptResult(string message) =>
226228
{
227229
return new PromptMessage
228230
{
229-
Role = Enum.TryParse<Role>(
230-
message.Role,
231-
ignoreCase: true,
232-
out var parsedRole
233-
)
234-
? parsedRole
235-
: Role.User,
231+
Role = ParsePromptRole(message.Role),
236232
Content = content,
237233
};
238234
}
@@ -254,9 +250,7 @@ out var parsedRole
254250

255251
return new PromptMessage
256252
{
257-
Role = Enum.TryParse<Role>(message.Role, ignoreCase: true, out var fallbackRole)
258-
? fallbackRole
259-
: Role.User,
253+
Role = ParsePromptRole(message.Role),
260254
Content = new TextContentBlock { Text = message.Text },
261255
};
262256
}
@@ -328,6 +322,21 @@ private static JsonElement ParseToolInputSchema(McpGatewayToolDescriptor descrip
328322
private static string CreateToolMessage(string toolId, string suffix) =>
329323
string.Concat(ToolMessagePrefix, toolId, suffix);
330324

325+
private static Role ParsePromptRole(string role)
326+
{
327+
if (
328+
Enum.TryParse<Role>(role, ignoreCase: true, out var parsedRole)
329+
&& Enum.IsDefined(parsedRole)
330+
)
331+
{
332+
return parsedRole;
333+
}
334+
335+
throw new InvalidOperationException(
336+
string.Concat(InvalidPromptMessageRolePrefix, role, InvalidPromptMessageRoleSuffix)
337+
);
338+
}
339+
331340
private static JsonElement CreateDefaultObjectSchema() =>
332341
JsonSerializer.SerializeToElement(
333342
new { type = "object", properties = new { } },

src/ManagedCode.MCPGateway/Hosting/Internal/Server/McpGatewayMcpServerTaskStore.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -925,8 +925,10 @@ private async Task<JsonElement> WaitForStoredTaskResultAsync(
925925
CancellationToken cancellationToken
926926
)
927927
{
928-
while (!cancellationToken.IsCancellationRequested)
928+
while (true)
929929
{
930+
cancellationToken.ThrowIfCancellationRequested();
931+
930932
var currentTask = await _innerStore.GetTaskAsync(taskId, sessionId, cancellationToken);
931933
switch (currentTask?.Status)
932934
{
@@ -946,9 +948,6 @@ CancellationToken cancellationToken
946948
break;
947949
}
948950
}
949-
950-
cancellationToken.ThrowIfCancellationRequested();
951-
return default;
952951
}
953952

954953
private static string CreateTaskMessage(string taskId, string suffix) =>
@@ -960,8 +959,10 @@ private static string CreateTaskMessage(string taskId, string suffix) =>
960959
CancellationToken cancellationToken
961960
)
962961
{
963-
while (!cancellationToken.IsCancellationRequested)
962+
while (true)
964963
{
964+
cancellationToken.ThrowIfCancellationRequested();
965+
965966
var result = await TryGetProxyTaskResultAsync(binding, cancellationToken);
966967
if (result is { } proxiedResult)
967968
{
@@ -975,9 +976,6 @@ CancellationToken cancellationToken
975976

976977
await Task.Delay(TaskResultPollDelay, cancellationToken);
977978
}
978-
979-
cancellationToken.ThrowIfCancellationRequested();
980-
return null;
981979
}
982980

983981
private async Task<JsonElement?> TryGetProxyTaskResultAsync(

0 commit comments

Comments
 (0)