Skip to content

Commit 3ce1ad2

Browse files
committed
json converter, remove hand-writtent copy for obejcts
1 parent 29542b6 commit 3ce1ad2

18 files changed

Lines changed: 403 additions & 313 deletions

src/ManagedCode.MCPGateway/AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,7 @@ For this .NET project:
7575
- 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.
7676
- 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.
7777
- 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.
78+
- Use official `ModelContextProtocol` SDK protocol models as the source of truth at MCP protocol boundaries; introduce gateway-owned models only for gateway-owned catalog, search, routing, or invocation concerns.
79+
- When a gateway-owned model carries protocol data such as tool schemas, resource metadata, prompt content, or result metadata, type those members to match the corresponding MCP SDK model members instead of inventing parallel string or loosely typed representations.
80+
- Preserve official MCP SDK protocol metadata and JSON schemas as typed `JsonObject` or `JsonElement` values across descriptors and proxy layers; do not downgrade them to JSON strings and parse them back when the SDK already provides structured objects.
81+
- Treat `ManagedCode.MCPGateway` as an adapter layer over the official MCP SDK: prefer carrying SDK protocol objects such as `Tool`, `Prompt`, `Resource`, and `ResourceTemplate` through internal catalog boundaries instead of recreating protocol DTOs field-by-field in gateway-owned models.

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

Lines changed: 99 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
#pragma warning disable MCPEXP001
2+
13
using System.Text;
24
using System.Text.Json;
35
using Microsoft.Extensions.AI;
46
using ModelContextProtocol.Client;
7+
using ModelContextProtocol.Protocol;
8+
using ModelContextProtocol.Server;
59

610
namespace ManagedCode.MCPGateway;
711

@@ -21,20 +25,15 @@ McpGatewayLoadedTool loadedTool
2125
var toolName = tool.Name.Trim();
2226
var sourceKind = McpGatewaySourceKindMapper.Map(registration.Kind);
2327

24-
var inputSchema = ResolveInputSchema(tool);
25-
var outputSchema = ResolveOutputSchema(tool);
26-
var metaJson = ResolveMetaJson(tool);
2728
var searchHints = ResolveSearchHints(tool, loadedTool.SearchHints);
29+
var protocolTool = ResolveProtocolTool(tool, toolName, searchHints);
2830

2931
return new McpGatewayToolDescriptor(
3032
ToolId: $"{registration.SourceId}:{toolName}",
3133
SourceId: registration.SourceId,
3234
SourceKind: sourceKind,
33-
ToolName: toolName,
34-
DisplayName: ResolveDisplayName(tool),
35-
Description: tool.Description ?? string.Empty,
36-
RequiredArguments: inputSchema.RequiredArguments,
37-
InputSchemaJson: inputSchema.Json
35+
ProtocolTool: protocolTool,
36+
RequiredArguments: ExtractRequiredArguments(protocolTool.InputSchema)
3837
)
3938
{
4039
SearchAliases = searchHints.Aliases ?? [],
@@ -43,12 +42,6 @@ McpGatewayLoadedTool loadedTool
4342
Tags = searchHints.Tags ?? [],
4443
DataSources = searchHints.DataSources ?? [],
4544
UsageExamples = searchHints.UsageExamples ?? [],
46-
MetaJson = metaJson,
47-
OutputSchemaJson = outputSchema.Json,
48-
IsReadOnly = searchHints.ReadOnly,
49-
IsIdempotent = searchHints.Idempotent,
50-
IsDestructive = searchHints.Destructive,
51-
IsOpenWorld = searchHints.OpenWorld,
5245
CostTier = searchHints.CostTier,
5346
LatencyTier = searchHints.LatencyTier,
5447
IsEnabledByDefault = searchHints.EnabledByDefault ?? true,
@@ -112,7 +105,7 @@ int maxDescriptorLength
112105
builder.AppendLine(string.Join(", ", descriptor.RequiredArguments));
113106
}
114107

115-
AppendInputSchema(builder, descriptor.InputSchemaJson);
108+
AppendInputSchema(builder, descriptor.InputSchema);
116109
AppendUsageExamples(builder, descriptor.UsageExamples);
117110
var document = builder.ToString().Trim();
118111
var effectiveMaxLength = Math.Max(
@@ -158,30 +151,23 @@ private static void AppendDescriptorBoolean(StringBuilder builder, string label,
158151
AppendDescriptorValue(builder, label, value.Value ? bool.TrueString : bool.FalseString);
159152
}
160153

161-
private static void AppendInputSchema(StringBuilder builder, string? inputSchemaJson)
154+
private static void AppendInputSchema(StringBuilder builder, JsonElement? inputSchema)
162155
{
163-
if (string.IsNullOrWhiteSpace(inputSchemaJson))
156+
if (inputSchema is not { } schema || schema.ValueKind == JsonValueKind.Undefined)
164157
{
165158
return;
166159
}
167160

168-
try
161+
if (!TryGetSchemaProperties(schema, out var properties))
169162
{
170-
using var schemaDocument = JsonDocument.Parse(inputSchemaJson);
171-
if (!TryGetSchemaProperties(schemaDocument.RootElement, out var properties))
172-
{
173-
return;
174-
}
175-
176-
foreach (var property in properties.EnumerateObject())
177-
{
178-
AppendInputSchemaProperty(builder, property);
179-
}
163+
builder.Append(InputSchemaLabel);
164+
builder.AppendLine(schema.GetRawText());
165+
return;
180166
}
181-
catch (JsonException)
167+
168+
foreach (var property in properties.EnumerateObject())
182169
{
183-
builder.Append(InputSchemaLabel);
184-
builder.AppendLine(inputSchemaJson);
170+
AppendInputSchemaProperty(builder, property);
185171
}
186172
}
187173

@@ -358,74 +344,113 @@ out var displayName
358344
return null;
359345
}
360346

361-
private static SerializedSchema ResolveInputSchema(AITool tool)
347+
private static Tool ResolveProtocolTool(
348+
AITool tool,
349+
string toolName,
350+
McpGatewayToolSearchHints searchHints
351+
)
362352
{
363-
if (tool is McpClientTool mcpTool)
364-
{
365-
return SerializeSchema(mcpTool.ProtocolTool?.InputSchema);
366-
}
353+
var protocolTool = tool is McpClientTool { ProtocolTool: { } upstreamTool }
354+
? McpGatewayProtocolTool.Clone(upstreamTool)
355+
: CreateLocalProtocolTool(tool, toolName, searchHints);
367356

368-
var function = tool as AIFunction ?? tool.GetService<AIFunction>();
369-
if (function is null)
370-
{
371-
return SerializedSchema.Empty;
372-
}
373-
374-
return function.JsonSchema.ValueKind == JsonValueKind.Undefined
375-
? SerializedSchema.Empty
376-
: SerializeSchema(function.JsonSchema);
357+
ApplySearchHintAnnotations(protocolTool, searchHints);
358+
return protocolTool;
377359
}
378360

379-
private static SerializedSchema ResolveOutputSchema(AITool tool)
361+
private static Tool CreateLocalProtocolTool(
362+
AITool tool,
363+
string toolName,
364+
McpGatewayToolSearchHints searchHints
365+
)
380366
{
381-
if (tool is McpClientTool mcpTool)
382-
{
383-
return SerializeSchema(mcpTool.ProtocolTool?.OutputSchema);
384-
}
385-
386367
var function = tool as AIFunction ?? tool.GetService<AIFunction>();
387-
if (function?.ReturnJsonSchema is not { } returnSchema ||
388-
returnSchema.ValueKind == JsonValueKind.Undefined)
368+
if (function is null)
389369
{
390-
return SerializedSchema.Empty;
370+
return new Tool
371+
{
372+
Name = toolName,
373+
Title = ResolveDisplayName(tool),
374+
Description = tool.Description,
375+
InputSchema = McpGatewayProtocolTool.CreateDefaultObjectSchema(),
376+
};
391377
}
392378

393-
return SerializeSchema(returnSchema);
394-
}
379+
var outputSchema = ResolveOutputSchema(function);
380+
var serverTool = McpServerTool.Create(
381+
function,
382+
new McpServerToolCreateOptions
383+
{
384+
Name = toolName,
385+
Title = ResolveDisplayName(tool),
386+
Description = tool.Description,
387+
SerializerOptions = McpGatewayJsonSerializer.Options,
388+
UseStructuredContent = outputSchema.Schema is not null,
389+
OutputSchema = outputSchema.Schema,
390+
ReadOnly = searchHints.ReadOnly,
391+
Idempotent = searchHints.Idempotent,
392+
Destructive = searchHints.Destructive,
393+
OpenWorld = searchHints.OpenWorld,
394+
}
395+
);
395396

396-
private static string? ResolveMetaJson(AITool tool)
397-
{
398-
return tool is McpClientTool mcpTool
399-
? SerializeObjectJson(mcpTool.ProtocolTool?.Meta)
400-
: null;
397+
return McpGatewayProtocolTool.Clone(serverTool.ProtocolTool);
401398
}
402399

403-
private static string? SerializeObjectJson(object? value)
400+
private static void ApplySearchHintAnnotations(
401+
Tool protocolTool,
402+
McpGatewayToolSearchHints searchHints
403+
)
404404
{
405405
if (
406-
McpGatewayJsonSerializer.TrySerializeToElement(value)
407-
is not JsonElement element ||
408-
element.ValueKind != JsonValueKind.Object
406+
protocolTool.Title is null
407+
&& !string.IsNullOrWhiteSpace(protocolTool.Annotations?.Title)
409408
)
410409
{
411-
return null;
410+
protocolTool.Title = protocolTool.Annotations.Title;
412411
}
413412

414-
return element.GetRawText();
413+
if (
414+
searchHints.ReadOnly is null
415+
&& searchHints.Idempotent is null
416+
&& searchHints.Destructive is null
417+
&& searchHints.OpenWorld is null
418+
&& string.IsNullOrWhiteSpace(protocolTool.Title)
419+
)
420+
{
421+
return;
422+
}
423+
424+
var annotations =
425+
McpGatewayProtocolTool.CloneAnnotations(protocolTool.Annotations)
426+
?? new ToolAnnotations();
427+
annotations.Title ??= protocolTool.Title;
428+
annotations.ReadOnlyHint = searchHints.ReadOnly ?? annotations.ReadOnlyHint;
429+
annotations.IdempotentHint = searchHints.Idempotent ?? annotations.IdempotentHint;
430+
annotations.DestructiveHint = searchHints.Destructive ?? annotations.DestructiveHint;
431+
annotations.OpenWorldHint = searchHints.OpenWorld ?? annotations.OpenWorldHint;
432+
protocolTool.Annotations = annotations;
415433
}
416434

435+
private static SerializedSchema ResolveOutputSchema(AIFunction function) =>
436+
function.ReturnJsonSchema is not { } returnSchema
437+
|| returnSchema.ValueKind == JsonValueKind.Undefined
438+
? SerializedSchema.Empty
439+
: SerializeSchema(returnSchema);
440+
417441
private static SerializedSchema SerializeSchema(object? schema)
418442
{
419443
if (
420444
McpGatewayJsonSerializer.TrySerializeToElement(schema)
421445
is not JsonElement serializedSchema
446+
|| !McpGatewayProtocolSchema.IsToolObjectSchema(serializedSchema)
422447
)
423448
{
424449
return SerializedSchema.Empty;
425450
}
426451

427452
return new SerializedSchema(
428-
serializedSchema.GetRawText(),
453+
serializedSchema.Clone(),
429454
ExtractRequiredArguments(serializedSchema)
430455
);
431456
}
@@ -461,8 +486,13 @@ private static IReadOnlyList<string> ExtractRequiredArguments(JsonElement schema
461486
return requiredArguments;
462487
}
463488

464-
private sealed record SerializedSchema(string? Json, IReadOnlyList<string> RequiredArguments)
489+
private sealed record SerializedSchema(
490+
JsonElement? Schema,
491+
IReadOnlyList<string> RequiredArguments
492+
)
465493
{
466494
public static SerializedSchema Empty { get; } = new(null, []);
467495
}
468496
}
497+
498+
#pragma warning restore MCPEXP001

src/ManagedCode.MCPGateway/Catalog/Models/McpGatewayToolDescriptor.cs

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,50 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Nodes;
3+
using ModelContextProtocol.Protocol;
4+
15
namespace ManagedCode.MCPGateway;
26

37
public sealed record McpGatewayToolDescriptor(
48
string ToolId,
59
string SourceId,
610
McpGatewaySourceKind SourceKind,
7-
string ToolName,
8-
string? DisplayName,
9-
string Description,
10-
IReadOnlyList<string> RequiredArguments,
11-
string? InputSchemaJson
11+
Tool ProtocolTool,
12+
IReadOnlyList<string> RequiredArguments
1213
)
1314
{
14-
public IReadOnlyList<string> SearchAliases { get; init; } = [];
15+
public string ToolName => ProtocolTool.Name;
1516

16-
public IReadOnlyList<string> SearchKeywords { get; init; } = [];
17+
public string? DisplayName => ProtocolTool.Title;
1718

18-
public IReadOnlyList<string> Categories { get; init; } = [];
19+
public string Description => ProtocolTool.Description ?? string.Empty;
1920

20-
public IReadOnlyList<string> Tags { get; init; } = [];
21+
public JsonElement InputSchema => ProtocolTool.InputSchema;
2122

22-
public IReadOnlyList<string> DataSources { get; init; } = [];
23+
public JsonElement? OutputSchema => ProtocolTool.OutputSchema;
2324

24-
public IReadOnlyList<McpGatewayToolExample> UsageExamples { get; init; } = [];
25+
public ToolAnnotations? Annotations => ProtocolTool.Annotations;
26+
27+
public JsonObject? Meta => ProtocolTool.Meta;
28+
29+
public bool? IsReadOnly => ProtocolTool.Annotations?.ReadOnlyHint;
2530

26-
public string? MetaJson { get; init; }
31+
public bool? IsIdempotent => ProtocolTool.Annotations?.IdempotentHint;
2732

28-
public string? OutputSchemaJson { get; init; }
33+
public bool? IsDestructive => ProtocolTool.Annotations?.DestructiveHint;
2934

30-
public bool? IsReadOnly { get; init; }
35+
public bool? IsOpenWorld => ProtocolTool.Annotations?.OpenWorldHint;
3136

32-
public bool? IsIdempotent { get; init; }
37+
public IReadOnlyList<string> SearchAliases { get; init; } = [];
3338

34-
public bool? IsDestructive { get; init; }
39+
public IReadOnlyList<string> SearchKeywords { get; init; } = [];
3540

36-
public bool? IsOpenWorld { get; init; }
41+
public IReadOnlyList<string> Categories { get; init; } = [];
42+
43+
public IReadOnlyList<string> Tags { get; init; } = [];
44+
45+
public IReadOnlyList<string> DataSources { get; init; } = [];
46+
47+
public IReadOnlyList<McpGatewayToolExample> UsageExamples { get; init; } = [];
3748

3849
public McpGatewayToolCostTier? CostTier { get; init; }
3950

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Text.Json;
2+
3+
namespace ManagedCode.MCPGateway;
4+
5+
internal static class McpGatewayProtocolSchema
6+
{
7+
private const string TypePropertyName = "type";
8+
private const string ObjectTypeName = "object";
9+
10+
public static bool IsToolObjectSchema(JsonElement schema) =>
11+
schema.ValueKind == JsonValueKind.Object
12+
&& schema.TryGetProperty(TypePropertyName, out var type)
13+
&& type.ValueKind == JsonValueKind.String
14+
&& string.Equals(type.GetString(), ObjectTypeName, StringComparison.Ordinal);
15+
}

0 commit comments

Comments
 (0)