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
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<System9Version>9.0.5</System9Version>
<System10Version>10.0.0-preview.4.25258.110</System10Version>
<MicrosoftExtensionsAIVersion>9.5.0</MicrosoftExtensionsAIVersion>
<MicrosoftExtensionsAIVersion>9.6.0</MicrosoftExtensionsAIVersion>
</PropertyGroup>

<!-- Product dependencies netstandard -->
Expand Down Expand Up @@ -50,7 +50,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageVersion>
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.5.0-preview.1.25265.7" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.6.0-preview.1.25310.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="$(System9Version)" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="$(System9Version)" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="$(System9Version)" />
Expand Down
25 changes: 1 addition & 24 deletions src/ModelContextProtocol.Core/McpJsonUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,30 +85,6 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
return false; // No type keyword found.
}

internal static JsonElement? GetReturnSchema(this AIFunction function, AIJsonSchemaCreateOptions? schemaCreateOptions)
{
// TODO replace with https://github.com/dotnet/extensions/pull/6447 once merged.
if (function.UnderlyingMethod?.ReturnType is not Type returnType)
{
return null;
}

if (returnType == typeof(void) || returnType == typeof(Task) || returnType == typeof(ValueTask))
{
// Do not report an output schema for void or Task methods.
return null;
}

if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() is Type genericTypeDef &&
(genericTypeDef == typeof(Task<>) || genericTypeDef == typeof(ValueTask<>)))
{
// Extract the real type from Task<T> or ValueTask<T> if applicable.
returnType = returnType.GetGenericArguments()[0];
}

return AIJsonUtilities.CreateJsonSchema(returnType, serializerOptions: function.JsonSerializerOptions, inferenceOptions: schemaCreateOptions);
}

// Keep in sync with CreateDefaultOptions above.
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Expand Down Expand Up @@ -157,6 +133,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(SubscribeRequestParams))]
[JsonSerializable(typeof(UnsubscribeRequestParams))]
[JsonSerializable(typeof(IReadOnlyDictionary<string, object>))]
[JsonSerializable(typeof(PromptMessage[]))]

// Primitive types for use in consuming AIFunctions
[JsonSerializable(typeof(string))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
JsonSchemaCreateOptions = options?.SchemaCreateOptions,
ConfigureParameterBinding = pi =>
{
if (pi.ParameterType == typeof(RequestContext<GetPromptRequestParams>))
Expand Down Expand Up @@ -151,7 +152,6 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
return null;
}
},
JsonSchemaCreateOptions = options?.SchemaCreateOptions,
};

/// <summary>Creates an <see cref="McpServerPrompt"/> that wraps the specified <see cref="AIFunction"/>.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
Name = options?.Name ?? method.GetCustomAttribute<McpServerResourceAttribute>()?.Name,
Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
SerializerOptions = McpJsonUtilities.DefaultOptions,
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
JsonSchemaCreateOptions = options?.SchemaCreateOptions,
ConfigureParameterBinding = pi =>
{
if (pi.ParameterType == typeof(RequestContext<ReadResourceRequestParams>))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
JsonSchemaCreateOptions = options?.SchemaCreateOptions,
ConfigureParameterBinding = pi =>
{
if (pi.ParameterType == typeof(RequestContext<CallToolRequestParams>))
Expand Down Expand Up @@ -166,7 +167,6 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
return null;
}
},
JsonSchemaCreateOptions = options?.SchemaCreateOptions,
};

/// <summary>Creates an <see cref="McpServerTool"/> that wraps the specified <see cref="AIFunction"/>.</summary>
Expand Down Expand Up @@ -366,7 +366,7 @@ public override async ValueTask<CallToolResponse> InvokeAsync(
return null;
}

if (function.GetReturnSchema(toolCreateOptions?.SchemaCreateOptions) is not JsonElement outputSchema)
if (function.ReturnJsonSchema is not JsonElement outputSchema)
{
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Microsoft.Extensions.AI;
using System.ComponentModel;
using System.Text.Json;

namespace ModelContextProtocol.Server;

Expand Down Expand Up @@ -60,6 +62,22 @@ public sealed class McpServerResourceCreateOptions
/// </summary>
public string? MimeType { get; set; }

/// <summary>
/// Gets or sets the JSON serializer options to use when marshalling data to/from JSON.
/// </summary>
/// <remarks>
/// Defaults to <see cref="McpJsonUtilities.DefaultOptions"/> if left unspecified.
/// </remarks>
public JsonSerializerOptions? SerializerOptions { get; set; }

/// <summary>
/// Gets or sets the JSON schema options when creating <see cref="AIFunction"/> from a method.
/// </summary>
/// <remarks>
/// Defaults to <see cref="AIJsonSchemaCreateOptions.Default"/> if left unspecified.
/// </remarks>
public AIJsonSchemaCreateOptions? SchemaCreateOptions { get; set; }

/// <summary>
/// Creates a shallow clone of the current <see cref="McpServerResourceCreateOptions"/> instance.
/// </summary>
Expand All @@ -71,5 +89,7 @@ internal McpServerResourceCreateOptions Clone() =>
Name = Name,
Description = Description,
MimeType = MimeType,
SerializerOptions = SerializerOptions,
SchemaCreateOptions = SchemaCreateOptions,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using Moq;
using System.ComponentModel;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace ModelContextProtocol.Tests.Server;

Expand Down Expand Up @@ -324,6 +326,11 @@ public async Task SupportsSchemaCreateOptions()
{
TransformSchemaNode = (context, node) =>
{
if (node.GetValueKind() is not JsonValueKind.Object)
{
node = new JsonObject();
}

node["description"] = "1234";
return node;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ public async Task CanReturnResourceContents()
{
Assert.Same(mockServer.Object, server);
return new TextResourceContents() { Text = "hello" };
}, new() { Name = "Test" });
}, new() { Name = "Test", SerializerOptions = JsonContext6.Default.Options });
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Expand Down Expand Up @@ -507,7 +507,7 @@ public async Task CanReturnCollectionOfStrings()
{
Assert.Same(mockServer.Object, server);
return new List<string>() { "42", "43" };
}, new() { Name = "Test" });
}, new() { Name = "Test", SerializerOptions = JsonContext6.Default.Options });
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Expand Down Expand Up @@ -547,7 +547,7 @@ public async Task CanReturnCollectionOfAIContent()
new TextContent("hello!"),
new DataContent(new byte[] { 4, 5, 6 }, "application/json"),
};
}, new() { Name = "Test" });
}, new() { Name = "Test", SerializerOptions = JsonContext6.Default.Options });
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Expand Down Expand Up @@ -575,5 +575,8 @@ private class DisposableResourceType : IDisposable

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(DisposableResourceType))]
[JsonSerializable(typeof(List<AIContent>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(TextResourceContents))]
partial class JsonContext6 : JsonSerializerContext;
}
6 changes: 4 additions & 2 deletions tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ public async Task CanReturnCollectionOfAIContent()
new DataContent("data:image/png;base64,1234"),
new DataContent("data:audio/wav;base64,1234")
};
});
}, new() { SerializerOptions = JsonContext2.Default.Options });

var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object),
Expand Down Expand Up @@ -288,7 +288,7 @@ public async Task CanReturnCollectionOfStrings()
{
Assert.Same(mockServer.Object, server);
return new List<string>() { "42", "43" };
});
}, new() { SerializerOptions = JsonContext2.Default.Options });
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object),
TestContext.Current.CancellationToken);
Expand Down Expand Up @@ -632,5 +632,7 @@ record Person(string Name, int Age);
[JsonSerializable(typeof(AsyncDisposableToolType))]
[JsonSerializable(typeof(AsyncDisposableAndDisposableToolType))]
[JsonSerializable(typeof(JsonSchema))]
[JsonSerializable(typeof(List<AIContent>))]
[JsonSerializable(typeof(List<string>))]
partial class JsonContext2 : JsonSerializerContext;
}
Loading