Skip to content

Commit 144e28e

Browse files
authored
[mcp] Validated MCP tool names (#8948)
1 parent b00f6e5 commit 144e28e

File tree

4 files changed

+81
-1
lines changed

4 files changed

+81
-1
lines changed

src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Properties/McpAdapterResources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Properties/McpAdapterResources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
<data name="OperationToolDefinition_DocumentMustContainSingleOperation" xml:space="preserve">
3131
<value>An operation tool document must have exactly one operation definition.</value>
3232
</data>
33+
<data name="OperationToolDefinition_InvalidToolName" xml:space="preserve">
34+
<value>The tool name '{0}' is invalid. Tool names must match the regular expression '{1}'.</value>
35+
</data>
3336
<data name="OperationToolFactory_OpenAiComponentResourceName" xml:space="preserve">
3437
<value>{0} OpenAI Component</value>
3538
</data>

src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Storage/OperationToolDefinition.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Security.Cryptography;
22
using System.Text;
3+
using System.Text.RegularExpressions;
34
using CaseConverter;
45
using HotChocolate.Language;
56
using static HotChocolate.Adapters.Mcp.Properties.McpAdapterResources;
@@ -10,7 +11,7 @@ namespace HotChocolate.Adapters.Mcp.Storage;
1011
/// Represents a GraphQL-operation-based MCP tool definition which is used by
1112
/// Hot Chocolate to create the actual MCP tool.
1213
/// </summary>
13-
public sealed class OperationToolDefinition
14+
public sealed partial class OperationToolDefinition
1415
{
1516
/// <summary>
1617
/// Initializes a new MCP tool definition from a GraphQL operation document.
@@ -67,6 +68,12 @@ public OperationToolDefinition(
6768
nameof(document));
6869
}
6970

71+
if (name is not null && !ValidateToolNameRegex().IsMatch(name))
72+
{
73+
throw new ArgumentException(
74+
string.Format(OperationToolDefinition_InvalidToolName, name, ValidateToolNameRegex()));
75+
}
76+
7077
Name = name ?? operation.Name?.Value.ToSnakeCase()!;
7178
Document = document;
7279
Title = title;
@@ -129,4 +136,8 @@ public OpenAiComponent? OpenAiComponent
129136
}
130137

131138
public string? OpenAiComponentOutputTemplate { get; private set; }
139+
140+
/// <summary>Regex that validates tool names.</summary>
141+
[GeneratedRegex(@"^[A-Za-z0-9_.-]{1,128}\z")]
142+
private static partial Regex ValidateToolNameRegex();
132143
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using HotChocolate.Language;
2+
3+
namespace HotChocolate.Adapters.Mcp.Storage;
4+
5+
public sealed class OperationToolDefinitionTests
6+
{
7+
[Theory]
8+
[InlineData(null)]
9+
[InlineData("valid")]
10+
[InlineData("valid_name")]
11+
[InlineData("valid.name")]
12+
[InlineData("valid-name")]
13+
public void OperationToolDefinition_WithValidName_Succeeds(string? name)
14+
{
15+
// arrange & act
16+
var exception =
17+
Record.Exception(
18+
() =>
19+
new OperationToolDefinition(Utf8GraphQLParser.Parse(OperationDocument), name));
20+
21+
// assert
22+
Assert.Null(exception);
23+
}
24+
25+
[Theory]
26+
[InlineData("")]
27+
[InlineData("invalid🔧")]
28+
[InlineData("invalid name")]
29+
[InlineData("invalid/name")]
30+
[InlineData("invalid\\name")]
31+
[InlineData("invalid", 20)] // 7 characters repeated 20 times = 140 characters
32+
public void OperationToolDefinition_WithInvalidName_ThrowsArgumentException(
33+
string name,
34+
int repeat = 0)
35+
{
36+
// arrange
37+
if (repeat > 0)
38+
{
39+
name = string.Concat(Enumerable.Repeat(name, repeat));
40+
}
41+
42+
// act
43+
var exception =
44+
Record.Exception(
45+
() =>
46+
new OperationToolDefinition(Utf8GraphQLParser.Parse(OperationDocument), name));
47+
48+
// assert
49+
Assert.IsType<ArgumentException>(exception);
50+
Assert.Equal(
51+
$"The tool name '{name}' is invalid. Tool names must match the regular expression "
52+
+ "'^[A-Za-z0-9_.-]{1,128}\\z'.",
53+
exception.Message);
54+
}
55+
56+
private const string OperationDocument = "query GetUsers { users { id } }";
57+
}

0 commit comments

Comments
 (0)