Skip to content

Commit 962fff9

Browse files
authored
[mcp] Added support for tool icons (#8949)
1 parent 144e28e commit 962fff9

File tree

9 files changed

+384
-76
lines changed

9 files changed

+384
-76
lines changed

src/HotChocolate/Adapters/src/Adapters.Mcp.Core/OperationToolFactory.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@ public OperationTool CreateTool(OperationToolDefinition toolDefinition)
5252
Meta = meta
5353
};
5454

55+
if (toolDefinition.Icons is { } icons)
56+
{
57+
tool.Icons =
58+
icons.Select(
59+
icon => new Icon
60+
{
61+
Source = icon.Source.OriginalString,
62+
MimeType = icon.MimeType,
63+
Sizes = icon.Sizes,
64+
Theme = icon.Theme
65+
}).ToList();
66+
}
67+
5568
return new OperationTool(toolDefinition.Document, tool)
5669
{
5770
OpenAiComponentResource = openAiComponentResource,

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

Lines changed: 27 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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@
3636
<data name="OperationToolFactory_OpenAiComponentResourceName" xml:space="preserve">
3737
<value>{0} OpenAI Component</value>
3838
</data>
39+
<data name="OperationToolIcon_InvalidIconMimeType" xml:space="preserve">
40+
<value>The MIME type must be a valid type/subtype string.</value>
41+
</data>
42+
<data name="OperationToolIcon_InvalidIconSize" xml:space="preserve">
43+
<value>Each size must have the format {WIDTH}x{HEIGHT} (e.g., 48x48) or be 'any'.</value>
44+
</data>
45+
<data name="OperationToolIcon_InvalidIconSourceScheme" xml:space="preserve">
46+
<value>The icon source URI must use the HTTP, HTTPS, or data scheme.</value>
47+
</data>
3948
<data name="ReadResourceHandler_ResourceWithUriNotFound" xml:space="preserve">
4049
<value>The resource with URI '{0}' was not found.</value>
4150
</data>

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

Lines changed: 33 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Immutable;
12
using System.Security.Cryptography;
23
using System.Text;
34
using System.Text.RegularExpressions;
@@ -13,104 +14,82 @@ namespace HotChocolate.Adapters.Mcp.Storage;
1314
/// </summary>
1415
public sealed partial class OperationToolDefinition
1516
{
17+
private readonly OperationDefinitionNode _operation;
18+
1619
/// <summary>
1720
/// Initializes a new MCP tool definition from a GraphQL operation document.
1821
/// </summary>
1922
/// <param name="document">
2023
/// GraphQL document containing exactly one operation definition.
2124
/// </param>
22-
/// <param name="name">
23-
/// The name of the MCP tool.
24-
/// </param>
25-
/// <param name="title">
26-
/// Optional tool title.
27-
/// </param>
28-
/// <param name="destructiveHint">
29-
/// Optional destructive operation hint.
30-
/// </param>
31-
/// <param name="idempotentHint">
32-
/// Optional idempotent operation hint.
33-
/// </param>
34-
/// <param name="openWorldHint">
35-
/// Optional open-world assumption hint.
36-
/// </param>
3725
/// <exception cref="ArgumentException">
3826
/// Thrown when document doesn't contain exactly one operation.
3927
/// </exception>
40-
public OperationToolDefinition(
41-
DocumentNode document,
42-
string? name = null,
43-
string? title = null,
44-
bool? destructiveHint = null,
45-
bool? idempotentHint = null,
46-
bool? openWorldHint = null)
28+
public OperationToolDefinition(DocumentNode document)
4729
{
4830
ArgumentNullException.ThrowIfNull(document);
4931

50-
OperationDefinitionNode? operation = null;
51-
52-
foreach (var current in document.Definitions.OfType<OperationDefinitionNode>())
32+
try
5333
{
54-
if (operation is not null)
55-
{
56-
throw new ArgumentException(
57-
OperationToolDefinition_DocumentMustContainSingleOperation,
58-
nameof(document));
59-
}
60-
61-
operation = current;
34+
_operation = document.Definitions.OfType<OperationDefinitionNode>().Single();
6235
}
63-
64-
if (operation is null)
36+
catch (InvalidOperationException)
6537
{
6638
throw new ArgumentException(
6739
OperationToolDefinition_DocumentMustContainSingleOperation,
6840
nameof(document));
6941
}
7042

71-
if (name is not null && !ValidateToolNameRegex().IsMatch(name))
72-
{
73-
throw new ArgumentException(
74-
string.Format(OperationToolDefinition_InvalidToolName, name, ValidateToolNameRegex()));
75-
}
76-
77-
Name = name ?? operation.Name?.Value.ToSnakeCase()!;
7843
Document = document;
79-
Title = title;
80-
DestructiveHint = destructiveHint;
81-
IdempotentHint = idempotentHint;
82-
OpenWorldHint = openWorldHint;
8344
}
8445

8546
/// <summary>
86-
/// Gets the name of the MCP tool.
47+
/// Gets the GraphQL document containing operation that represents the MCP tool.
8748
/// </summary>
88-
public string Name { get; }
49+
public DocumentNode Document { get; }
8950

9051
/// <summary>
91-
/// Gets the GraphQL document containing operation that represents the MCP tool.
52+
/// Gets the name of the MCP tool.
9253
/// </summary>
93-
public DocumentNode Document { get; }
54+
public string Name
55+
{
56+
get => field ??= _operation.Name!.Value.ToSnakeCase();
57+
init
58+
{
59+
if (!ValidateToolNameRegex().IsMatch(value))
60+
{
61+
throw new ArgumentException(
62+
string.Format(OperationToolDefinition_InvalidToolName, value, ValidateToolNameRegex()));
63+
}
64+
65+
field = value;
66+
}
67+
}
9468

9569
/// <summary>
9670
/// Gets the optional human-readable title for the tool.
9771
/// </summary>
98-
public string? Title { get; }
72+
public string? Title { get; init; }
73+
74+
/// <summary>
75+
/// Gets the optional icons for the tool.
76+
/// </summary>
77+
public ImmutableArray<OperationToolIcon>? Icons { get; init; }
9978

10079
/// <summary>
10180
/// Gets a hint indicating whether this operation may cause destructive side effects.
10281
/// </summary>
103-
public bool? DestructiveHint { get; }
82+
public bool? DestructiveHint { get; init; }
10483

10584
/// <summary>
10685
/// Gets a hint indicating whether this operation is idempotent (safe to retry).
10786
/// </summary>
108-
public bool? IdempotentHint { get; }
87+
public bool? IdempotentHint { get; init; }
10988

11089
/// <summary>
11190
/// Gets a hint indicating whether this operation assumes an open-world model.
11291
/// </summary>
113-
public bool? OpenWorldHint { get; }
92+
public bool? OpenWorldHint { get; init; }
11493

11594
/// <summary>
11695
/// Gets the optional OpenAI component configuration for this tool.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System.Text.RegularExpressions;
2+
using static HotChocolate.Adapters.Mcp.Properties.McpAdapterResources;
3+
4+
namespace HotChocolate.Adapters.Mcp.Storage;
5+
6+
/// <summary>
7+
/// Represents an icon that can be used to visually identify an operation tool.
8+
/// </summary>
9+
public sealed partial class OperationToolIcon
10+
{
11+
/// <summary>
12+
/// Represents an icon that can be used to visually identify an operation tool.
13+
/// </summary>
14+
/// <param name="source">
15+
/// The URI pointing to the icon resource. This can be an HTTP/HTTPS URL pointing to an image
16+
/// file or a data URI with base64-encoded image data.
17+
/// </param>
18+
public OperationToolIcon(Uri source)
19+
{
20+
Source = source;
21+
}
22+
23+
/// <summary>
24+
/// The URI pointing to the icon resource. This can be an HTTP/HTTPS URL pointing to an image
25+
/// file or a data URI with base64-encoded image data.
26+
/// </summary>
27+
public Uri Source
28+
{
29+
get;
30+
private set
31+
{
32+
if (value.Scheme != Uri.UriSchemeHttp
33+
&& value.Scheme != Uri.UriSchemeHttps
34+
&& value.Scheme != "data")
35+
{
36+
throw new ArgumentException(
37+
OperationToolIcon_InvalidIconSourceScheme,
38+
nameof(Source));
39+
}
40+
41+
field = value;
42+
}
43+
}
44+
45+
/// <summary>
46+
/// The optional MIME type of the icon. This can be used to override the server's MIME type if
47+
/// it's missing or generic. Common values include "image/png", "image/jpeg", "image/svg+xml",
48+
/// and "image/webp".
49+
/// </summary>
50+
public string? MimeType
51+
{
52+
get;
53+
init
54+
{
55+
if (value?.Contains('/') == false)
56+
{
57+
throw new ArgumentException(
58+
OperationToolIcon_InvalidIconMimeType,
59+
nameof(MimeType));
60+
}
61+
62+
field = value;
63+
}
64+
}
65+
66+
/// <summary>
67+
/// The optional size specifications for the icon. This can specify one or more sizes at which
68+
/// the icon file can be used. Examples include "48x48", or "any" for scalable formats like SVG.
69+
/// </summary>
70+
public IList<string>? Sizes
71+
{
72+
get;
73+
init
74+
{
75+
if (value?.Any(size => !IconSizeRegex().IsMatch(size)) == true)
76+
{
77+
throw new ArgumentException(
78+
OperationToolIcon_InvalidIconSize,
79+
nameof(Sizes));
80+
}
81+
82+
field = value;
83+
}
84+
}
85+
86+
/// <summary>
87+
/// The optional theme for this icon. Can be "light", "dark", or a custom theme identifier. Used
88+
/// to specify which UI theme the icon is designed for.
89+
/// </summary>
90+
public string? Theme { get; init; }
91+
92+
[GeneratedRegex(@"^([0-9]+x[0-9]+|any)\z")]
93+
private static partial Regex IconSizeRegex();
94+
}

0 commit comments

Comments
 (0)