Skip to content

Commit 3de19d3

Browse files
Change type of Tool.InputSchema to be JsonElement (#4)
* Change type of Tool.InputSchema to be JsonElement * Add schema validation in the InputSchema property setter. * Simplify AIFunction tool adapter. --------- Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>
1 parent d23e065 commit 3de19d3

File tree

12 files changed

+175
-128
lines changed

12 files changed

+175
-128
lines changed

samples/anthropic/tools/ToolsConsole/McpToolExtensions.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ public static class McpToolExtensions
1616
List<Anthropic.SDK.Common.Tool> result = [];
1717
foreach (var tool in tools)
1818
{
19-
var function = tool.InputSchema == null
20-
? new Function(tool.Name, tool.Description)
21-
: new Function(tool.Name, tool.Description, JsonSerializer.Serialize(tool.InputSchema));
19+
var function = new Function(tool.Name, tool.Description, JsonSerializer.SerializeToNode(tool.InputSchema));
2220
result.Add(function);
2321
}
2422
return result;

src/ModelContextProtocol/Client/McpClientExtensions.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -466,24 +466,14 @@ private static JsonRpcRequest CreateRequest(string method, Dictionary<string, ob
466466
/// <summary>Provides an AI function that calls a tool through <see cref="IMcpClient"/>.</summary>
467467
private sealed class McpAIFunction(IMcpClient client, Tool tool) : AIFunction
468468
{
469-
private JsonElement? _jsonSchema;
470-
471469
/// <inheritdoc/>
472470
public override string Name => tool.Name;
473471

474472
/// <inheritdoc/>
475473
public override string Description => tool.Description ?? string.Empty;
476474

477475
/// <inheritdoc/>
478-
public override JsonElement JsonSchema => _jsonSchema ??=
479-
JsonSerializer.SerializeToElement(new Dictionary<string, object>
480-
{
481-
["type"] = "object",
482-
["title"] = tool.Name,
483-
["description"] = tool.Description ?? string.Empty,
484-
["properties"] = tool.InputSchema?.Properties ?? [],
485-
["required"] = tool.InputSchema?.Required ?? []
486-
}, McpJsonUtilities.JsonContext.Default.DictionaryStringObject);
476+
public override JsonElement JsonSchema => tool.InputSchema;
487477

488478
/// <inheritdoc/>
489479
protected async override Task<object?> InvokeCoreAsync(

src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Tools.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params
9696
{
9797
Name = function.Name,
9898
Description = function.Description,
99-
InputSchema = JsonSerializer.Deserialize(function.JsonSchema, McpJsonUtilities.JsonContext.Default.JsonSchema),
99+
InputSchema = function.JsonSchema,
100100
});
101101

102102
callbacks.Add(function.Name, async (request, cancellationToken) =>

src/ModelContextProtocol/Protocol/Types/JsonSchema.cs

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/ModelContextProtocol/Protocol/Types/JsonSchemaProperty.cs

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/ModelContextProtocol/Protocol/Types/Tool.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.Text.Json.Serialization;
1+
using ModelContextProtocol.Utils.Json;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
24

35
namespace ModelContextProtocol.Protocol.Types;
46

@@ -23,6 +25,23 @@ public class Tool
2325
/// <summary>
2426
/// A JSON Schema object defining the expected parameters for the tool.
2527
/// </summary>
28+
/// <remarks>
29+
/// Needs to a valid JSON schema object that additionally is of type object.
30+
/// </remarks>
2631
[JsonPropertyName("inputSchema")]
27-
public JsonSchema? InputSchema { get; set; }
32+
public JsonElement InputSchema
33+
{
34+
get => _inputSchema;
35+
set
36+
{
37+
if (!McpJsonUtilities.IsValidMcpToolSchema(value))
38+
{
39+
throw new ArgumentException("The specified document is not a valid MPC tool JSON schema.", nameof(InputSchema));
40+
}
41+
42+
_inputSchema = value;
43+
}
44+
}
45+
46+
private JsonElement _inputSchema = McpJsonUtilities.DefaultMcpToolSchema;
2847
}

src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,31 @@ private static JsonSerializerOptions CreateDefaultOptions()
7777
internal static JsonTypeInfo<T> GetTypeInfo<T>(this JsonSerializerOptions options) =>
7878
(JsonTypeInfo<T>)options.GetTypeInfo(typeof(T));
7979

80+
internal static JsonElement DefaultMcpToolSchema = ParseJsonElement("{\"type\":\"object\"}"u8);
81+
internal static bool IsValidMcpToolSchema(JsonElement element)
82+
{
83+
if (element.ValueKind is not JsonValueKind.Object)
84+
{
85+
return false;
86+
}
87+
88+
foreach (JsonProperty property in element.EnumerateObject())
89+
{
90+
if (property.NameEquals("type"))
91+
{
92+
if (property.Value.ValueKind is not JsonValueKind.String ||
93+
!property.Value.ValueEquals("object"))
94+
{
95+
return false;
96+
}
97+
98+
return true; // No need to check other properties
99+
}
100+
}
101+
102+
return false; // No type keyword found.
103+
}
104+
80105
// Keep in sync with CreateDefaultOptions above.
81106
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
82107
UseStringEnumConverter = true,
@@ -96,7 +121,13 @@ internal static JsonTypeInfo<T> GetTypeInfo<T>(this JsonSerializerOptions option
96121
[JsonSerializable(typeof(CreateMessageResult))]
97122
[JsonSerializable(typeof(ListRootsResult))]
98123
[JsonSerializable(typeof(InitializeResult))]
99-
[JsonSerializable(typeof(JsonSchema))]
100124
[JsonSerializable(typeof(CallToolResponse))]
101125
internal sealed partial class JsonContext : JsonSerializerContext;
126+
127+
private static JsonElement ParseJsonElement(ReadOnlySpan<byte> utf8Json)
128+
{
129+
Utf8JsonReader reader = new(utf8Json);
130+
return JsonElement.ParseValue(ref reader);
131+
}
132+
102133
}

tests/ModelContextProtocol.TestServer/Program.cs

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
using System.Text;
1+
using Microsoft.Extensions.Logging;
22
using ModelContextProtocol.Protocol.Messages;
33
using ModelContextProtocol.Protocol.Transport;
44
using ModelContextProtocol.Protocol.Types;
55
using ModelContextProtocol.Server;
6-
using Microsoft.Extensions.Logging;
76
using Serilog;
7+
using System.Text;
8+
using System.Text.Json;
89

910
namespace ModelContextProtocol.TestServer;
1011

@@ -91,28 +92,39 @@ private static ToolsCapability ConfigureTools()
9192
{
9293
Name = "echo",
9394
Description = "Echoes the input back to the client.",
94-
InputSchema = new JsonSchema()
95-
{
96-
Type = "object",
97-
Properties = new Dictionary<string, JsonSchemaProperty>()
95+
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
9896
{
99-
["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
97+
"type": "object",
98+
"properties": {
99+
"message": {
100+
"type": "string",
101+
"description": "The input to echo back."
102+
}
103+
},
104+
"required": ["message"]
100105
}
101-
},
106+
"""),
102107
},
103108
new Tool()
104109
{
105110
Name = "sampleLLM",
106111
Description = "Samples from an LLM using MCP's sampling feature.",
107-
InputSchema = new JsonSchema()
108-
{
109-
Type = "object",
110-
Properties = new Dictionary<string, JsonSchemaProperty>()
112+
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
111113
{
112-
["prompt"] = new JsonSchemaProperty() { Type = "string", Description = "The prompt to send to the LLM" },
113-
["maxTokens"] = new JsonSchemaProperty() { Type = "number", Description = "Maximum number of tokens to generate" }
114+
"type": "object",
115+
"properties": {
116+
"prompt": {
117+
"type": "string",
118+
"description": "The prompt to send to the LLM"
119+
},
120+
"maxTokens": {
121+
"type": "number",
122+
"description": "Maximum number of tokens to generate"
123+
}
124+
},
125+
"required": ["prompt", "maxTokens"]
114126
}
115-
},
127+
"""),
116128
}
117129
]
118130
});

tests/ModelContextProtocol.TestSseServer/Program.cs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.Extensions.Logging;
55
using Serilog;
66
using System.Text;
7+
using System.Text.Json;
78

89
internal class Program
910
{
@@ -121,28 +122,39 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
121122
{
122123
Name = "echo",
123124
Description = "Echoes the input back to the client.",
124-
InputSchema = new JsonSchema()
125-
{
126-
Type = "object",
127-
Properties = new Dictionary<string, JsonSchemaProperty>()
125+
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
128126
{
129-
["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
127+
"type": "object",
128+
"properties": {
129+
"message": {
130+
"type": "string",
131+
"description": "The input to echo back."
132+
}
133+
},
134+
"required": ["message"]
130135
}
131-
},
136+
"""),
132137
},
133138
new Tool()
134139
{
135140
Name = "sampleLLM",
136141
Description = "Samples from an LLM using MCP's sampling feature.",
137-
InputSchema = new JsonSchema()
138-
{
139-
Type = "object",
140-
Properties = new Dictionary<string, JsonSchemaProperty>()
142+
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
141143
{
142-
["prompt"] = new JsonSchemaProperty() { Type = "string", Description = "The prompt to send to the LLM" },
143-
["maxTokens"] = new JsonSchemaProperty() { Type = "number", Description = "Maximum number of tokens to generate" }
144+
"type": "object",
145+
"properties": {
146+
"prompt": {
147+
"type": "string",
148+
"description": "The prompt to send to the LLM"
149+
},
150+
"maxTokens": {
151+
"type": "number",
152+
"description": "Maximum number of tokens to generate"
153+
}
154+
},
155+
"required": ["prompt", "maxTokens"]
144156
}
145-
},
157+
"""),
146158
}
147159
]
148160
});

tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs

Lines changed: 13 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,10 @@ public async Task Can_List_Registered_Tool()
4848
var tool = result.Tools[0];
4949
Assert.Equal("Echo", tool.Name);
5050
Assert.Equal("Echoes the input back to the client.", tool.Description);
51-
Assert.NotNull(tool.InputSchema);
52-
Assert.Equal("object", tool.InputSchema.Type);
53-
Assert.NotNull(tool.InputSchema.Properties);
54-
Assert.NotEmpty(tool.InputSchema.Properties);
55-
Assert.Contains("message", tool.InputSchema.Properties);
56-
Assert.Equal("string", tool.InputSchema.Properties["message"].Type);
57-
Assert.Equal("the echoes message", tool.InputSchema.Properties["message"].Description);
58-
Assert.NotNull(tool.InputSchema.Required);
59-
Assert.NotEmpty(tool.InputSchema.Required);
60-
Assert.Contains("message", tool.InputSchema.Required);
51+
Assert.Equal("object", tool.InputSchema.GetProperty("type").GetString());
52+
Assert.Equal(JsonValueKind.Object, tool.InputSchema.GetProperty("properties").GetProperty("message").ValueKind);
53+
Assert.Equal("the echoes message", tool.InputSchema.GetProperty("properties").GetProperty("message").GetProperty("description").GetString());
54+
Assert.Equal(1, tool.InputSchema.GetProperty("required").GetArrayLength());
6155

6256
tool = result.Tools[1];
6357
Assert.Equal("double_echo", tool.Name);
@@ -288,31 +282,15 @@ public async Task Recognizes_Parameter_Types()
288282
var tool = result.Tools.First(t => t.Name == "TestTool");
289283
Assert.Equal("TestTool", tool.Name);
290284
Assert.Empty(tool.Description!);
291-
Assert.NotNull(tool.InputSchema);
292-
Assert.Equal("object", tool.InputSchema.Type);
293-
Assert.NotNull(tool.InputSchema.Properties);
294-
Assert.NotEmpty(tool.InputSchema.Properties);
295-
296-
Assert.Contains("number", tool.InputSchema.Properties);
297-
Assert.Equal("integer", tool.InputSchema.Properties["number"].Type);
298-
299-
Assert.Contains("otherNumber", tool.InputSchema.Properties);
300-
Assert.Equal("number", tool.InputSchema.Properties["otherNumber"].Type);
301-
302-
Assert.Contains("someCheck", tool.InputSchema.Properties);
303-
Assert.Equal("boolean", tool.InputSchema.Properties["someCheck"].Type);
304-
305-
Assert.Contains("someDate", tool.InputSchema.Properties);
306-
Assert.Equal("string", tool.InputSchema.Properties["someDate"].Type);
307-
308-
Assert.Contains("someOtherDate", tool.InputSchema.Properties);
309-
Assert.Equal("string", tool.InputSchema.Properties["someOtherDate"].Type);
310-
311-
Assert.Contains("data", tool.InputSchema.Properties);
312-
Assert.Equal("array", tool.InputSchema.Properties["data"].Type);
313-
314-
Assert.Contains("complexObject", tool.InputSchema.Properties);
315-
Assert.Equal("object", tool.InputSchema.Properties["complexObject"].Type);
285+
Assert.Equal("object", tool.InputSchema.GetProperty("type").GetString());
286+
287+
Assert.Contains("integer", tool.InputSchema.GetProperty("properties").GetProperty("number").GetProperty("type").GetString());
288+
Assert.Contains("number", tool.InputSchema.GetProperty("properties").GetProperty("otherNumber").GetProperty("type").GetString());
289+
Assert.Contains("boolean", tool.InputSchema.GetProperty("properties").GetProperty("someCheck").GetProperty("type").GetString());
290+
Assert.Contains("string", tool.InputSchema.GetProperty("properties").GetProperty("someDate").GetProperty("type").GetString());
291+
Assert.Contains("string", tool.InputSchema.GetProperty("properties").GetProperty("someOtherDate").GetProperty("type").GetString());
292+
Assert.Contains("array", tool.InputSchema.GetProperty("properties").GetProperty("data").GetProperty("type").GetString());
293+
Assert.Contains("object", tool.InputSchema.GetProperty("properties").GetProperty("complexObject").GetProperty("type").GetString());
316294
}
317295

318296
[McpToolType]

0 commit comments

Comments
 (0)