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
Original file line number Diff line number Diff line change
Expand Up @@ -289,24 +289,49 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js
objSchema.InsertAtStart(TypePropertyName, "string");
}

// Include the type keyword in nullable enum types
if (Nullable.GetUnderlyingType(ctx.TypeInfo.Type)?.IsEnum is true && objSchema.ContainsKey(EnumPropertyName) && !objSchema.ContainsKey(TypePropertyName))
{
objSchema.InsertAtStart(TypePropertyName, new JsonArray { (JsonNode)"string", (JsonNode)"null" });
}

// Some consumers of the JSON schema, including Ollama as of v0.3.13, don't understand
// schemas with "type": [...], and only understand "type" being a single value.
// In certain configurations STJ represents .NET numeric types as ["string", "number"], which will then lead to an error.
if (TypeIsIntegerWithStringNumberHandling(ctx, objSchema, out string? numericType))
if (TypeIsIntegerWithStringNumberHandling(ctx, objSchema, out string? numericType, out bool isNullable))
{
// We don't want to emit any array for "type". In this case we know it contains "integer" or "number",
// so reduce the type to that alone, assuming it's the most specific type.
// This makes schemas for Int32 (etc) work with Ollama.
JsonObject obj = ConvertSchemaToObject(ref schema);
obj[TypePropertyName] = numericType;
if (isNullable)
{
// If the type is nullable, we still need use a type array
obj[TypePropertyName] = new JsonArray { (JsonNode)numericType, (JsonNode)"null" };
}
else
{
obj[TypePropertyName] = (JsonNode)numericType;
}

_ = obj.Remove(PatternPropertyName);
}

if (Nullable.GetUnderlyingType(ctx.TypeInfo.Type) is Type nullableElement)
{
// Account for bug https://github.com/dotnet/runtime/issues/117493
// To be removed once System.Text.Json v10 becomes the lowest supported version.
// null not inserted in the type keyword for root-level Nullable<T> types.
if (objSchema.TryGetPropertyValue(TypePropertyName, out JsonNode? typeKeyWord) &&
typeKeyWord?.GetValueKind() is JsonValueKind.String)
{
string typeValue = typeKeyWord.GetValue<string>()!;
if (typeValue is not "null")
{
objSchema[TypePropertyName] = new JsonArray { (JsonNode)typeValue, (JsonNode)"null" };
}
}

// Include the type keyword in nullable enum types
if (nullableElement.IsEnum && objSchema.ContainsKey(EnumPropertyName) && !objSchema.ContainsKey(TypePropertyName))
{
objSchema.InsertAtStart(TypePropertyName, new JsonArray { (JsonNode)"string", (JsonNode)"null" });
}
}
}

if (ctx.Path.IsEmpty && hasDefaultValue)
Expand Down Expand Up @@ -601,11 +626,12 @@ static JsonArray CreateJsonArray(object?[] values, JsonSerializerOptions seriali
}
}

private static bool TypeIsIntegerWithStringNumberHandling(AIJsonSchemaCreateContext ctx, JsonObject schema, [NotNullWhen(true)] out string? numericType)
private static bool TypeIsIntegerWithStringNumberHandling(AIJsonSchemaCreateContext ctx, JsonObject schema, [NotNullWhen(true)] out string? numericType, out bool isNullable)
{
numericType = null;
isNullable = false;

if (ctx.TypeInfo.NumberHandling is not JsonNumberHandling.Strict && schema["type"] is JsonArray { Count: 2 } typeArray)
if (ctx.TypeInfo.NumberHandling is not JsonNumberHandling.Strict && schema["type"] is JsonArray typeArray)
{
bool allowString = false;

Expand All @@ -617,11 +643,23 @@ private static bool TypeIsIntegerWithStringNumberHandling(AIJsonSchemaCreateCont
switch (type)
{
case "integer" or "number":
if (numericType is not null)
{
// Conflicting numeric type
return false;
}

numericType = type;
break;
case "string":
allowString = true;
break;
case "null":
isNullable = true;
break;
default:
// keyword is not valid in the context of numeric types.
return false;
}
}
}
Expand Down Expand Up @@ -665,7 +703,7 @@ private static JsonElement ParseJsonElement(ReadOnlySpan<byte> utf8Json)

if (defaultValue is null || (defaultValue == DBNull.Value && parameterType != typeof(DBNull)))
{
return parameterType.IsValueType
return parameterType.IsValueType && Nullable.GetUnderlyingType(parameterType) is null
#if NET
? RuntimeHelpers.GetUninitializedObject(parameterType)
#else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public async Task<ChatResponse> GetResponseAsync(
{
_ = Throw.IfNull(messages);

var openAIChatMessages = ToOpenAIChatMessages(messages, options, AIJsonUtilities.DefaultOptions);
var openAIChatMessages = ToOpenAIChatMessages(messages, options);
var openAIOptions = ToOpenAIOptions(options);

// Make the call to OpenAI.
Expand All @@ -85,7 +85,7 @@ public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
{
_ = Throw.IfNull(messages);

var openAIChatMessages = ToOpenAIChatMessages(messages, options, AIJsonUtilities.DefaultOptions);
var openAIChatMessages = ToOpenAIChatMessages(messages, options);
var openAIOptions = ToOpenAIOptions(options);

// Make the call to OpenAI.
Expand Down Expand Up @@ -115,7 +115,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? op
}

/// <summary>Converts an Extensions chat message enumerable to an OpenAI chat message enumerable.</summary>
private static IEnumerable<OpenAI.Chat.ChatMessage> ToOpenAIChatMessages(IEnumerable<ChatMessage> inputs, ChatOptions? chatOptions, JsonSerializerOptions jsonOptions)
internal static IEnumerable<OpenAI.Chat.ChatMessage> ToOpenAIChatMessages(IEnumerable<ChatMessage> inputs, ChatOptions? chatOptions)
{
// Maps all of the M.E.AI types to the corresponding OpenAI types.
// Unrecognized or non-processable content is ignored.
Expand Down Expand Up @@ -148,7 +148,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? op
{
try
{
result = JsonSerializer.Serialize(resultContent.Result, jsonOptions.GetTypeInfo(typeof(object)));
result = JsonSerializer.Serialize(resultContent.Result, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
}
catch (NotSupportedException)
{
Expand Down Expand Up @@ -176,7 +176,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? op
case FunctionCallContent fc:
(toolCalls ??= []).Add(
ChatToolCall.CreateFunctionToolCall(fc.CallId, fc.Name, new(JsonSerializer.SerializeToUtf8Bytes(
fc.Arguments, jsonOptions.GetTypeInfo(typeof(IDictionary<string, object?>))))));
fc.Arguments, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary<string, object?>))))));
break;

default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,18 @@ public static ResponseTool AsOpenAIResponseTool(this AIFunction function) =>
public static ConversationFunctionTool AsOpenAIConversationFunctionTool(this AIFunction function) =>
OpenAIRealtimeConversationClient.ToOpenAIConversationFunctionTool(Throw.IfNull(function));

/// <summary>Creates a sequence of OpenAI <see cref="OpenAI.Chat.ChatMessage"/> instances from the specified input messages.</summary>
/// <param name="messages">The input messages to convert.</param>
/// <returns>A sequence of OpenAI chat messages.</returns>
public static IEnumerable<OpenAI.Chat.ChatMessage> AsOpenAIChatMessages(this IEnumerable<ChatMessage> messages) =>
OpenAIChatClient.ToOpenAIChatMessages(Throw.IfNull(messages), chatOptions: null);

/// <summary>Creates a sequence of OpenAI <see cref="OpenAI.Responses.ResponseItem"/> instances from the specified input messages.</summary>
/// <param name="messages">The input messages to convert.</param>
/// <returns>A sequence of OpenAI response items.</returns>
public static IEnumerable<OpenAI.Responses.ResponseItem> AsOpenAIResponseItems(this IEnumerable<ChatMessage> messages) =>
OpenAIResponseChatClient.ToOpenAIResponseItems(Throw.IfNull(messages));

// TODO: Once we're ready to rely on C# 14 features, add an extension property ChatOptions.Strict.

/// <summary>Gets whether the properties specify that strict schema handling is desired.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#pragma warning disable S1067 // Expressions should not be too complex
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
#pragma warning disable S3604 // Member initializer values should not be redundant
#pragma warning disable SA1202 // Elements should be ordered by access
#pragma warning disable SA1204 // Static elements should appear before instance elements

namespace Microsoft.Extensions.AI;
Expand Down Expand Up @@ -466,8 +467,7 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
}

/// <summary>Convert a sequence of <see cref="ChatMessage"/>s to <see cref="ResponseItem"/>s.</summary>
private static IEnumerable<ResponseItem> ToOpenAIResponseItems(
IEnumerable<ChatMessage> inputs)
internal static IEnumerable<ResponseItem> ToOpenAIResponseItems(IEnumerable<ChatMessage> inputs)
{
foreach (ChatMessage input in inputs)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public partial class FunctionInvokingChatClient : DelegatingChatClient
private readonly ActivitySource? _activitySource;

/// <summary>Maximum number of roundtrips allowed to the inner client.</summary>
private int _maximumIterationsPerRequest = 10;
private int _maximumIterationsPerRequest = 40; // arbitrary default to prevent runaway execution

/// <summary>Maximum number of consecutive iterations that are allowed contain at least one exception result. If the limit is exceeded, we rethrow the exception instead of continuing.</summary>
private int _maximumConsecutiveErrorsPerRequest = 3;
Expand Down Expand Up @@ -142,7 +142,7 @@ public static FunctionInvocationContext? CurrentContext
/// </summary>
/// <value>
/// The maximum number of iterations per request.
/// The default value is 10.
/// The default value is 40.
/// </value>
/// <remarks>
/// <para>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<PackageTags>dotnet-new;templates;ai</PackageTags>

<Stage>preview</Stage>
<PreReleaseVersionIteration>2</PreReleaseVersionIteration>
<PreReleaseVersionIteration>3</PreReleaseVersionIteration>
<Workstream>AI</Workstream>
<MinCodeCoverage>0</MinCodeCoverage>
<MinMutationScore>0</MinMutationScore>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json",
"description": "<your description here>",
"name": "io.github.<your GitHub username here>/<your repo name>",
"packages": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# MCP Server

This README was created using the C# MCP server template project. It demonstrates how you can easily create an MCP server using C# and then package it in a NuGet package.
This README was created using the C# MCP server project template. It demonstrates how you can easily create an MCP server using C# and publish it as a NuGet package.

See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide.

Please note that this template is currently in an early preview stage. If you have feedback, please take a [brief survey](http://aka.ms/dotnet-mcp-template-survey).

## Checklist before publishing to NuGet.org

- Test the MCP server locally using the steps below.
Expand All @@ -14,67 +16,70 @@ See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide.

The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package).

## Using the MCP Server in VS Code
## Developing locally

Once the MCP server package is published to NuGet.org, you can use the following VS Code user configuration to download and install the MCP server package. See [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more information about using MCP servers in VS Code.
To test this MCP server from source code (locally) without using a built MCP server package, you can configure your IDE to run the project directly using `dotnet run`.

```json
{
"mcp": {
"servers": {
"McpServer-CSharp": {
"type": "stdio",
"command": "dnx",
"args": [
"<your package ID here>",
"--version",
"<your package version here>",
"--yes"
]
}
"servers": {
"McpServer-CSharp": {
"type": "stdio",
"command": "dotnet",
"args": [
"run",
"--project",
"<PATH TO PROJECT DIRECTORY>"
]
}
}
}
```

Now you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `McpServer-CSharp` MCP server and show you the results.
## Testing the MCP Server

Once configured, you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `McpServer-CSharp` MCP server and show you the results.

## Publishing to NuGet.org

1. Run `dotnet pack -c Release` to create the NuGet package
2. Publish to NuGet.org with `dotnet nuget push bin/Release/*.nupkg --api-key <your-api-key> --source https://api.nuget.org/v3/index.json`

## Using the MCP Server from NuGet.org

## Developing locally in VS Code
Once the MCP server package is published to NuGet.org, you can configure it in your preferred IDE. Both VS Code and Visual Studio use the `dnx` command to download and install the MCP server package from NuGet.org.

To test this MCP server from source code (locally) without using a built MCP server package, create a `.vscode/mcp.json` file (a VS Code workspace settings file) in your project directory and add the following configuration:
- **VS Code**: Create a `<WORKSPACE DIRECTORY>/.vscode/mcp.json` file
- **Visual Studio**: Create a `<SOLUTION DIRECTORY>\.mcp.json` file

For both VS Code and Visual Studio, the configuration file uses the following server definition:

```json
{
"servers": {
"McpServer-CSharp": {
"type": "stdio",
"command": "dotnet",
"command": "dnx",
"args": [
"run",
"--project",
"<RELATIVE PATH TO PROJECT DIRECTORY>"
"<your package ID here>",
"--version",
"<your package version here>",
"--yes"
]
}
}
}
```

Alternatively, you can configure your VS Code user settings to use your local project:
## More information

```json
{
"mcp": {
"servers": {
"McpServer-CSharp": {
"type": "stdio",
"command": "dotnet",
"args": [
"run",
"--project",
"<FULL PATH TO PROJECT DIRECTORY>"
]
}
}
}
}
```
.NET MCP servers use the [ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol) C# SDK. For more information about MCP:

- [Official Documentation](https://modelcontextprotocol.io/)
- [Protocol Specification](https://spec.modelcontextprotocol.io/)
- [GitHub Organization](https://github.com/modelcontextprotocol)

Refer to the VS Code or Visual Studio documentation for more information on configuring and using MCP servers:

- [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers)
- [Use MCP servers in Visual Studio (Preview)](https://learn.microsoft.com/visualstudio/ide/mcp-servers)
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ private static class ReflectionHelpers
public static bool IsBuiltInConverter(JsonConverter converter) =>
converter.GetType().Assembly == typeof(JsonConverter).Assembly;

public static bool CanBeNull(Type type) => !type.IsValueType || Nullable.GetUnderlyingType(type) is not null;

public static Type GetElementType(JsonTypeInfo typeInfo)
{
Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary, "TypeInfo must be of collection type");
Expand Down
16 changes: 10 additions & 6 deletions src/Shared/JsonSchemaExporter/JsonSchemaExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,20 +452,24 @@ JsonSchema CompleteSchema(ref GenerationState state, JsonSchema schema)

bool IsNullableSchema(ref GenerationState state)
{
// A schema is marked as nullable if either
// A schema is marked as nullable if either:
// 1. We have a schema for a property where either the getter or setter are marked as nullable.
// 2. We have a schema for a reference type, unless we're explicitly treating null-oblivious types as non-nullable
// 2. We have a schema for a Nullable<T> type.
// 3. We have a schema for a reference type, unless we're explicitly treating null-oblivious types as non-nullable.

if (propertyInfo != null || parameterInfo != null)
{
return !isNonNullableType;
}
else

if (Nullable.GetUnderlyingType(typeInfo.Type) is not null)
{
return ReflectionHelpers.CanBeNull(typeInfo.Type) &&
!parentPolymorphicTypeIsNonNullable &&
!state.ExporterOptions.TreatNullObliviousAsNonNullable;
return true;
}

return !typeInfo.Type.IsValueType &&
!parentPolymorphicTypeIsNonNullable &&
!state.ExporterOptions.TreatNullObliviousAsNonNullable;
}
}

Expand Down
Loading
Loading