Skip to content

Commit 7507081

Browse files
Fix stack overflow by using McpServerDefaultOptions instead of resolving IOptions during tool creation
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
1 parent d56b8e5 commit 7507081

4 files changed

Lines changed: 67 additions & 11 deletions

File tree

src/ModelContextProtocol/McpServerBuilderExtensions.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,36 @@ public static partial class McpServerBuilderExtensions
4848
if (toolMethod.GetCustomAttribute<McpServerToolAttribute>() is not null)
4949
{
5050
builder.Services.AddSingleton((Func<IServiceProvider, McpServerTool>)(toolMethod.IsStatic ?
51-
services => McpServerTool.Create(toolMethod, options: new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService<IOptions<McpServerOptions>>()?.Value.JsonSerializerOptions, SchemaCreateOptions = schemaCreateOptions ?? services.GetService<IOptions<McpServerOptions>>()?.Value.SchemaCreateOptions }) :
52-
services => McpServerTool.Create(toolMethod, static r => CreateTarget(r.Services, typeof(TToolType)), new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService<IOptions<McpServerOptions>>()?.Value.JsonSerializerOptions, SchemaCreateOptions = schemaCreateOptions ?? services.GetService<IOptions<McpServerOptions>>()?.Value.SchemaCreateOptions })));
51+
services =>
52+
{
53+
var effectiveSerializerOptions = serializerOptions;
54+
var effectiveSchemaCreateOptions = schemaCreateOptions;
55+
56+
// Try to get server-wide defaults if not explicitly provided
57+
if (effectiveSerializerOptions is null || effectiveSchemaCreateOptions is null)
58+
{
59+
var defaultOptions = services.GetService<McpServerDefaultOptions>();
60+
effectiveSerializerOptions ??= defaultOptions?.JsonSerializerOptions;
61+
effectiveSchemaCreateOptions ??= defaultOptions?.SchemaCreateOptions;
62+
}
63+
64+
return McpServerTool.Create(toolMethod, options: new() { Services = services, SerializerOptions = effectiveSerializerOptions, SchemaCreateOptions = effectiveSchemaCreateOptions });
65+
} :
66+
services =>
67+
{
68+
var effectiveSerializerOptions = serializerOptions;
69+
var effectiveSchemaCreateOptions = schemaCreateOptions;
70+
71+
// Try to get server-wide defaults if not explicitly provided
72+
if (effectiveSerializerOptions is null || effectiveSchemaCreateOptions is null)
73+
{
74+
var defaultOptions = services.GetService<McpServerDefaultOptions>();
75+
effectiveSerializerOptions ??= defaultOptions?.JsonSerializerOptions;
76+
effectiveSchemaCreateOptions ??= defaultOptions?.SchemaCreateOptions;
77+
}
78+
79+
return McpServerTool.Create(toolMethod, static r => CreateTarget(r.Services, typeof(TToolType)), new() { Services = services, SerializerOptions = effectiveSerializerOptions, SchemaCreateOptions = effectiveSchemaCreateOptions });
80+
}));
5381
}
5482
}
5583

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Microsoft.Extensions.AI;
2+
using System.Text.Json;
3+
4+
namespace ModelContextProtocol.Server;
5+
6+
/// <summary>
7+
/// Holds default options for MCP server primitives (tools, prompts, resources).
8+
/// This is separate from McpServerOptions to avoid circular dependencies during service resolution.
9+
/// </summary>
10+
internal sealed class McpServerDefaultOptions
11+
{
12+
/// <summary>
13+
/// Gets or sets the default JSON serializer options.
14+
/// </summary>
15+
public JsonSerializerOptions? JsonSerializerOptions { get; set; }
16+
17+
/// <summary>
18+
/// Gets or sets the default JSON schema creation options.
19+
/// </summary>
20+
public AIJsonSchemaCreateOptions? SchemaCreateOptions { get; set; }
21+
}

src/ModelContextProtocol/McpServerServiceCollectionExtensions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,23 @@ public static IMcpServerBuilder AddMcpServer(this IServiceCollection services, A
2121
{
2222
services.AddOptions();
2323
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<McpServerOptions>, McpServerOptionsSetup>());
24+
25+
// Capture default options from the configuration callback to avoid circular dependencies
26+
// when resolving IOptions<McpServerOptions> from within tool/prompt/resource factories
27+
var defaultOptions = new McpServerDefaultOptions();
2428
if (configureOptions is not null)
2529
{
30+
var tempOptions = new McpServerOptions();
31+
configureOptions(tempOptions);
32+
defaultOptions.JsonSerializerOptions = tempOptions.JsonSerializerOptions;
33+
defaultOptions.SchemaCreateOptions = tempOptions.SchemaCreateOptions;
34+
2635
services.Configure(configureOptions);
2736
}
37+
38+
// Register the default options as a singleton that can be safely resolved
39+
// without circular dependencies
40+
services.TryAddSingleton(defaultOptions);
2841

2942
return new DefaultMcpServerBuilder(services);
3043
}

tests/ModelContextProtocol.Tests/Configuration/McpServerJsonSerializerOptionsTests.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,11 @@ public void WithTools_UsesServerWideOptions_WhenNoExplicitOptionsProvided()
4848
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
4949
};
5050

51-
services.Configure<McpServerOptions>(options =>
51+
var builder = services.AddMcpServer(options =>
5252
{
5353
options.JsonSerializerOptions = customOptions;
5454
});
5555

56-
var builder = services.AddMcpServer();
57-
5856
// Act - WithTools should pick up the server-wide options with snake_case naming policy
5957
builder.WithTools<TestTools>();
6058
var serviceProvider = services.BuildServiceProvider();
@@ -85,13 +83,11 @@ public void WithPrompts_UsesServerWideOptions_WhenNoExplicitOptionsProvided()
8583
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
8684
};
8785

88-
services.Configure<McpServerOptions>(options =>
86+
var builder = services.AddMcpServer(options =>
8987
{
9088
options.JsonSerializerOptions = customOptions;
9189
});
9290

93-
var builder = services.AddMcpServer();
94-
9591
// Act - WithPrompts should pick up the server-wide options with snake_case naming policy
9692
builder.WithPrompts<TestPrompts>();
9793
var serviceProvider = services.BuildServiceProvider();
@@ -120,13 +116,11 @@ public void WithResources_UsesServerWideOptions_WhenNoExplicitOptionsProvided()
120116
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
121117
};
122118

123-
services.Configure<McpServerOptions>(options =>
119+
var builder = services.AddMcpServer(options =>
124120
{
125121
options.JsonSerializerOptions = customOptions;
126122
});
127123

128-
var builder = services.AddMcpServer();
129-
130124
// Act - WithResources should pick up the server-wide options with snake_case naming policy
131125
builder.WithResources<TestResources>();
132126
var serviceProvider = services.BuildServiceProvider();

0 commit comments

Comments
 (0)