Skip to content

Commit b5fbb91

Browse files
committed
Address feedback
1 parent 904a5d5 commit b5fbb91

8 files changed

+319
-69
lines changed

Diff for: Directory.Packages.props

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
3939
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
4040
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
41+
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="$(System10Version)" />
4142
<PackageVersion Include="xunit.v3" Version="1.1.0" />
4243
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2" />
4344
</ItemGroup>

Diff for: src/ModelContextProtocol/Client/McpClientExtensions.cs

+138-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using ModelContextProtocol.Utils.Json;
55
using Microsoft.Extensions.AI;
66
using System.Text.Json;
7+
using System.Runtime.CompilerServices;
78

89
namespace ModelContextProtocol.Client;
910

@@ -45,25 +46,25 @@ public static Task PingAsync(this IMcpClient client, CancellationToken cancellat
4546
}
4647

4748
/// <summary>
48-
/// Retrieves a sequence of available tools from the server.
49+
/// Retrieves a list of available tools from the server.
4950
/// </summary>
5051
/// <param name="client">The client.</param>
5152
/// <param name="cancellationToken">A token to cancel the operation.</param>
52-
/// <returns>An asynchronous sequence of tool information.</returns>
53+
/// <returns>A list of all available tools.</returns>
5354
public static async Task<IList<McpClientTool>> ListToolsAsync(
5455
this IMcpClient client, CancellationToken cancellationToken = default)
5556
{
5657
Throw.IfNull(client);
5758

58-
List<McpClientTool> tools = [];
59-
59+
List<McpClientTool>? tools = null;
6060
string? cursor = null;
6161
do
6262
{
6363
var toolResults = await client.SendRequestAsync<ListToolsResult>(
6464
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
6565
cancellationToken).ConfigureAwait(false);
6666

67+
tools ??= new List<McpClientTool>(toolResults.Tools.Count);
6768
foreach (var tool in toolResults.Tools)
6869
{
6970
tools.Add(new McpClientTool(client, tool));
@@ -76,12 +77,44 @@ public static async Task<IList<McpClientTool>> ListToolsAsync(
7677
return tools;
7778
}
7879

80+
/// <summary>
81+
/// Creates an enumerable for asynchronously enumerating all available tools from the server.
82+
/// </summary>
83+
/// <param name="client">The client.</param>
84+
/// <param name="cancellationToken">A token to cancel the operation.</param>
85+
/// <returns>An asynchronous sequence of all available tools.</returns>
86+
/// <remarks>
87+
/// Every iteration through the returned <see cref="IAsyncEnumerable{McpClientTool}"/>
88+
/// will result in requerying the server and yielding the sequence of available tools.
89+
/// </remarks>
90+
public static async IAsyncEnumerable<McpClientTool> EnumerateToolsAsync(
91+
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
92+
{
93+
Throw.IfNull(client);
94+
95+
string? cursor = null;
96+
do
97+
{
98+
var toolResults = await client.SendRequestAsync<ListToolsResult>(
99+
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
100+
cancellationToken).ConfigureAwait(false);
101+
102+
foreach (var tool in toolResults.Tools)
103+
{
104+
yield return new McpClientTool(client, tool);
105+
}
106+
107+
cursor = toolResults.NextCursor;
108+
}
109+
while (cursor is not null);
110+
}
111+
79112
/// <summary>
80113
/// Retrieves a list of available prompts from the server.
81114
/// </summary>
82115
/// <param name="client">The client.</param>
83116
/// <param name="cancellationToken">A token to cancel the operation.</param>
84-
/// <returns>An asynchronous sequence of prompt information.</returns>
117+
/// <returns>A list of all available prompts.</returns>
85118
public static async Task<IList<Prompt>> ListPromptsAsync(
86119
this IMcpClient client, CancellationToken cancellationToken = default)
87120
{
@@ -112,6 +145,38 @@ public static async Task<IList<Prompt>> ListPromptsAsync(
112145
return prompts;
113146
}
114147

148+
/// <summary>
149+
/// Creates an enumerable for asynchronously enumerating all available prompts from the server.
150+
/// </summary>
151+
/// <param name="client">The client.</param>
152+
/// <param name="cancellationToken">A token to cancel the operation.</param>
153+
/// <returns>An asynchronous sequence of all available prompts.</returns>
154+
/// <remarks>
155+
/// Every iteration through the returned <see cref="IAsyncEnumerable{Prompt}"/>
156+
/// will result in requerying the server and yielding the sequence of available prompts.
157+
/// </remarks>
158+
public static async IAsyncEnumerable<Prompt> EnumeratePromptsAsync(
159+
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
160+
{
161+
Throw.IfNull(client);
162+
163+
string? cursor = null;
164+
do
165+
{
166+
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
167+
CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
168+
cancellationToken).ConfigureAwait(false);
169+
170+
foreach (var prompt in promptResults.Prompts)
171+
{
172+
yield return prompt;
173+
}
174+
175+
cursor = promptResults.NextCursor;
176+
}
177+
while (cursor is not null);
178+
}
179+
115180
/// <summary>
116181
/// Retrieves a specific prompt with optional arguments.
117182
/// </summary>
@@ -132,11 +197,11 @@ public static Task<GetPromptResult> GetPromptAsync(
132197
}
133198

134199
/// <summary>
135-
/// Retrieves a sequence of available resource templates from the server.
200+
/// Retrieves a list of available resource templates from the server.
136201
/// </summary>
137202
/// <param name="client">The client.</param>
138203
/// <param name="cancellationToken">A token to cancel the operation.</param>
139-
/// <returns>An asynchronous sequence of resource template information.</returns>
204+
/// <returns>A list of all available resource templates.</returns>
140205
public static async Task<IList<ResourceTemplate>> ListResourceTemplatesAsync(
141206
this IMcpClient client, CancellationToken cancellationToken = default)
142207
{
@@ -168,11 +233,43 @@ public static async Task<IList<ResourceTemplate>> ListResourceTemplatesAsync(
168233
}
169234

170235
/// <summary>
171-
/// Retrieves a sequence of available resources from the server.
236+
/// Creates an enumerable for asynchronously enumerating all available resource templates from the server.
237+
/// </summary>
238+
/// <param name="client">The client.</param>
239+
/// <param name="cancellationToken">A token to cancel the operation.</param>
240+
/// <returns>An asynchronous sequence of all available resource templates.</returns>
241+
/// <remarks>
242+
/// Every iteration through the returned <see cref="IAsyncEnumerable{ResourceTemplate}"/>
243+
/// will result in requerying the server and yielding the sequence of available resource templates.
244+
/// </remarks>
245+
public static async IAsyncEnumerable<ResourceTemplate> EnumerateResourceTemplatesAsync(
246+
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
247+
{
248+
Throw.IfNull(client);
249+
250+
string? cursor = null;
251+
do
252+
{
253+
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>(
254+
CreateRequest("resources/templates/list", CreateCursorDictionary(cursor)),
255+
cancellationToken).ConfigureAwait(false);
256+
257+
foreach (var template in templateResults.ResourceTemplates)
258+
{
259+
yield return template;
260+
}
261+
262+
cursor = templateResults.NextCursor;
263+
}
264+
while (cursor is not null);
265+
}
266+
267+
/// <summary>
268+
/// Retrieves a list of available resources from the server.
172269
/// </summary>
173270
/// <param name="client">The client.</param>
174271
/// <param name="cancellationToken">A token to cancel the operation.</param>
175-
/// <returns>An asynchronous sequence of resource information.</returns>
272+
/// <returns>A list of all available resources.</returns>
176273
public static async Task<IList<Resource>> ListResourcesAsync(
177274
this IMcpClient client, CancellationToken cancellationToken = default)
178275
{
@@ -203,6 +300,38 @@ public static async Task<IList<Resource>> ListResourcesAsync(
203300
return resources;
204301
}
205302

303+
/// <summary>
304+
/// Creates an enumerable for asynchronously enumerating all available resources from the server.
305+
/// </summary>
306+
/// <param name="client">The client.</param>
307+
/// <param name="cancellationToken">A token to cancel the operation.</param>
308+
/// <returns>An asynchronous sequence of all available resources.</returns>
309+
/// <remarks>
310+
/// Every iteration through the returned <see cref="IAsyncEnumerable{Resource}"/>
311+
/// will result in requerying the server and yielding the sequence of available resources.
312+
/// </remarks>
313+
public static async IAsyncEnumerable<Resource> EnumerateResourcesAsync(
314+
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
315+
{
316+
Throw.IfNull(client);
317+
318+
string? cursor = null;
319+
do
320+
{
321+
var resourceResults = await client.SendRequestAsync<ListResourcesResult>(
322+
CreateRequest("resources/list", CreateCursorDictionary(cursor)),
323+
cancellationToken).ConfigureAwait(false);
324+
325+
foreach (var resource in resourceResults.Resources)
326+
{
327+
yield return resource;
328+
}
329+
330+
cursor = resourceResults.NextCursor;
331+
}
332+
while (cursor is not null);
333+
}
334+
206335
/// <summary>
207336
/// Reads a resource from the server.
208337
/// </summary>

Diff for: src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
using Microsoft.Extensions.AI;
22
using Microsoft.Extensions.DependencyInjection;
33
using ModelContextProtocol.Protocol.Types;
4-
using ModelContextProtocol.Server;
54
using ModelContextProtocol.Utils;
65
using ModelContextProtocol.Utils.Json;
76
using System.Reflection;
87
using System.Text.Json;
98

10-
namespace ModelContextProtocol;
9+
namespace ModelContextProtocol.Server;
1110

1211
/// <summary>Provides an <see cref="McpServerTool"/> that's implemented via an <see cref="AIFunction"/>.</summary>
1312
internal sealed class AIFunctionMcpServerTool : McpServerTool
@@ -19,17 +18,26 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
1918
/// <summary>
2019
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
2120
/// </summary>
22-
public static new AIFunctionMcpServerTool Create(Delegate method, IServiceProvider? services = null)
21+
public static new AIFunctionMcpServerTool Create(
22+
Delegate method,
23+
string? name,
24+
string? description,
25+
IServiceProvider? services)
2326
{
2427
Throw.IfNull(method);
2528

26-
return Create(method.Method, method.Target, services);
29+
return Create(method.Method, method.Target, name, description, services);
2730
}
2831

2932
/// <summary>
3033
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
3134
/// </summary>
32-
public static new AIFunctionMcpServerTool Create(MethodInfo method, object? target = null, IServiceProvider? services = null)
35+
public static new AIFunctionMcpServerTool Create(
36+
MethodInfo method,
37+
object? target,
38+
string? name,
39+
string? description,
40+
IServiceProvider? services)
3341
{
3442
Throw.IfNull(method);
3543

@@ -42,7 +50,8 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
4250

4351
return Create(TemporaryAIFunctionFactory.Create(method, target, new TemporaryAIFunctionFactoryOptions()
4452
{
45-
Name = method.GetCustomAttribute<McpServerToolAttribute>()?.Name,
53+
Name = name ?? method.GetCustomAttribute<McpServerToolAttribute>()?.Name,
54+
Description = description,
4655
MarshalResult = static (result, _, cancellationToken) => Task.FromResult(result),
4756
ConfigureParameterBinding = pi =>
4857
{

Diff for: src/ModelContextProtocol/Server/DelegatingMcpServerTool.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
using ModelContextProtocol.Protocol.Types;
2-
using ModelContextProtocol.Server;
32
using ModelContextProtocol.Utils;
43

5-
namespace ModelContextProtocol;
4+
namespace ModelContextProtocol.Server;
65

76
/// <summary>Provides an <see cref="McpServerTool"/> that delegates all operations to an inner <see cref="McpServerTool"/>.</summary>
87
/// <remarks>

Diff for: src/ModelContextProtocol/Server/McpServerTool.cs

+33-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
using Microsoft.Extensions.AI;
22
using ModelContextProtocol.Protocol.Types;
3-
using ModelContextProtocol.Server;
4-
using System.Diagnostics;
3+
using System.ComponentModel;
54
using System.Reflection;
65

7-
namespace ModelContextProtocol;
6+
namespace ModelContextProtocol.Server;
87

98
/// <summary>Represents an invocable tool used by Model Context Protocol clients and servers.</summary>
109
public abstract class McpServerTool
@@ -30,21 +29,43 @@ public abstract Task<CallToolResponse> InvokeAsync(
3029
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
3130
/// </summary>
3231
/// <param name="method">The method to be represented via the created <see cref="McpServerTool"/>.</param>
32+
/// <param name="name">
33+
/// The name to use for the <see cref="McpServerTool"/>. If <see langword="null"/>, but an <see cref="McpServerToolAttribute"/>
34+
/// is applied to <paramref name="method"/>, the name from the attribute will be used. If that's not present, the name based
35+
/// on <paramref name="method"/>'s name will be used.
36+
/// </param>
37+
/// <param name="description">
38+
/// The description to use for the <see cref="McpServerTool"/>. If <see langword="null"/>, but a <see cref="DescriptionAttribute"/>
39+
/// is applied to <paramref name="method"/>, the description from that attribute will be used.
40+
/// </param>
3341
/// <param name="services">
3442
/// Optional services used in the construction of the <see cref="McpServerTool"/>. These services will be
3543
/// used to determine which parameters should be satisifed from dependency injection, and so what services
3644
/// are satisfied via this provider should match what's satisfied via the provider passed in at invocation time.
3745
/// </param>
3846
/// <returns>The created <see cref="McpServerTool"/> for invoking <paramref name="method"/>.</returns>
3947
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
40-
public static McpServerTool Create(Delegate method, IServiceProvider? services = null) =>
41-
AIFunctionMcpServerTool.Create(method, services);
48+
public static McpServerTool Create(
49+
Delegate method,
50+
string? name = null,
51+
string? description = null,
52+
IServiceProvider? services = null) =>
53+
AIFunctionMcpServerTool.Create(method, name, description, services);
4254

4355
/// <summary>
4456
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
4557
/// </summary>
4658
/// <param name="method">The method to be represented via the created <see cref="McpServerTool"/>.</param>
4759
/// <param name="target">The instance if <paramref name="method"/> is an instance method; otherwise, <see langword="null"/>.</param>
60+
/// <param name="name">
61+
/// The name to use for the <see cref="McpServerTool"/>. If <see langword="null"/>, but an <see cref="McpServerToolAttribute"/>
62+
/// is applied to <paramref name="method"/>, the name from the attribute will be used. If that's not present, the name based
63+
/// on <paramref name="method"/>'s name will be used.
64+
/// </param>
65+
/// <param name="description">
66+
/// The description to use for the <see cref="McpServerTool"/>. If <see langword="null"/>, but a <see cref="DescriptionAttribute"/>
67+
/// is applied to <paramref name="method"/>, the description from that attribute will be used.
68+
/// </param>
4869
/// <param name="services">
4970
/// Optional services used in the construction of the <see cref="McpServerTool"/>. These services will be
5071
/// used to determine which parameters should be satisifed from dependency injection, and so what services
@@ -53,8 +74,13 @@ public static McpServerTool Create(Delegate method, IServiceProvider? services =
5374
/// <returns>The created <see cref="McpServerTool"/> for invoking <paramref name="method"/>.</returns>
5475
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
5576
/// <exception cref="ArgumentException"><paramref name="method"/> is an instance method but <paramref name="target"/> is <see langword="null"/>.</exception>
56-
public static McpServerTool Create(MethodInfo method, object? target = null, IServiceProvider? services = null) =>
57-
AIFunctionMcpServerTool.Create(method, target, services);
77+
public static McpServerTool Create(
78+
MethodInfo method,
79+
object? target = null,
80+
string? name = null,
81+
string? description = null,
82+
IServiceProvider? services = null) =>
83+
AIFunctionMcpServerTool.Create(method, target, name, description, services);
5884

5985
/// <summary>Creates an <see cref="McpServerTool"/> that wraps the specified <see cref="AIFunction"/>.</summary>
6086
/// <param name="function">The function to wrap.</param>

0 commit comments

Comments
 (0)