Skip to content

Commit d272e46

Browse files
authored
Merge branch 'main' into copilot/fix-handler-asymmetry-again
2 parents adfcd82 + 79b2dbe commit d272e46

11 files changed

Lines changed: 204 additions & 10 deletions

package-lock.json

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

src/ModelContextProtocol.Core/Client/McpClient.Methods.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ public ValueTask<GetPromptResult> GetPromptAsync(
280280
}
281281

282282
/// <summary>
283-
/// Retrieves a list of available prompts from the server.
283+
/// Retrieves a specific prompt from the MCP server.
284284
/// </summary>
285285
/// <param name="requestParams">The request parameters to send in the request.</param>
286286
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
@@ -549,9 +549,9 @@ public ValueTask<CompleteResult> CompleteAsync(
549549
}
550550

551551
/// <summary>
552-
/// Unsubscribes from a resource on the server to stop receiving notifications about its changes.
552+
/// Subscribes to a resource on the server to receive notifications when it changes.
553553
/// </summary>
554-
/// <param name="uri">The URI of the resource to which to subscribe.</param>
554+
/// <param name="uri">The URI of the resource to subscribe to.</param>
555555
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
556556
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
557557
/// <returns>A task that represents the asynchronous operation.</returns>
@@ -1036,6 +1036,7 @@ async ValueTask<McpTask> SendTaskAugmentedCallToolRequestWithProgressAsync(
10361036
/// <returns>The current state of the task.</returns>
10371037
/// <exception cref="ArgumentNullException"><paramref name="taskId"/> is <see langword="null"/>.</exception>
10381038
/// <exception cref="ArgumentException"><paramref name="taskId"/> is empty or composed entirely of whitespace.</exception>
1039+
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
10391040
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
10401041
public async ValueTask<McpTask> GetTaskAsync(
10411042
string taskId,
@@ -1073,6 +1074,7 @@ public async ValueTask<McpTask> GetTaskAsync(
10731074
/// <returns>The raw JSON result of the task.</returns>
10741075
/// <exception cref="ArgumentNullException"><paramref name="taskId"/> is <see langword="null"/>.</exception>
10751076
/// <exception cref="ArgumentException"><paramref name="taskId"/> is empty or composed entirely of whitespace.</exception>
1077+
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
10761078
/// <remarks>
10771079
/// This method sends a tasks/result request to the server, which will block until the task completes if it hasn't already.
10781080
/// The server handles all polling logic internally.
@@ -1099,6 +1101,7 @@ public ValueTask<JsonElement> GetTaskResultAsync(
10991101
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
11001102
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
11011103
/// <returns>A list of all tasks.</returns>
1104+
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
11021105
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
11031106
public async ValueTask<IList<McpTask>> ListTasksAsync(
11041107
RequestOptions? options = null,
@@ -1124,6 +1127,7 @@ public async ValueTask<IList<McpTask>> ListTasksAsync(
11241127
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
11251128
/// <returns>The result of the request as provided by the server.</returns>
11261129
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
1130+
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
11271131
/// <remarks>
11281132
/// The <see cref="ListTasksAsync(RequestOptions?, CancellationToken)"/> overload retrieves all tasks by automatically handling pagination.
11291133
/// This overload works with the lower-level <see cref="ListTasksRequestParams"/> and <see cref="ListTasksResult"/>, returning the raw result from the server.
@@ -1153,6 +1157,7 @@ public ValueTask<ListTasksResult> ListTasksAsync(
11531157
/// <returns>The updated state of the task after cancellation.</returns>
11541158
/// <exception cref="ArgumentNullException"><paramref name="taskId"/> is <see langword="null"/>.</exception>
11551159
/// <exception cref="ArgumentException"><paramref name="taskId"/> is empty or composed entirely of whitespace.</exception>
1160+
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
11561161
/// <remarks>
11571162
/// Cancelling a task requests that the server stop execution. The server may not immediately cancel the task,
11581163
/// and may choose to allow the task to complete if it's close to finishing.

src/ModelContextProtocol.Core/Protocol/UrlElicitationRequiredErrorData.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ public sealed class UrlElicitationRequiredErrorData
1212
/// Gets or sets the elicitations that must be completed before retrying the original request.
1313
/// </summary>
1414
[JsonPropertyName("elicitations")]
15-
public required IReadOnlyList<ElicitRequestParams> Elicitations { get; init; }
15+
public required IList<ElicitRequestParams> Elicitations { get; set; }
1616
}

src/ModelContextProtocol.Core/Server/DelegatingMcpServerPrompt.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ protected DelegatingMcpServerPrompt(McpServerPrompt innerPrompt)
2323
/// <inheritdoc />
2424
public override Prompt ProtocolPrompt => _innerPrompt.ProtocolPrompt;
2525

26+
/// <inheritdoc />
27+
public override IReadOnlyList<object> Metadata => _innerPrompt.Metadata;
28+
2629
/// <inheritdoc />
2730
public override ValueTask<GetPromptResult> GetAsync(
2831
RequestContext<GetPromptRequestParams> request,

src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ protected DelegatingMcpServerResource(McpServerResource innerResource)
2626
/// <inheritdoc />
2727
public override ResourceTemplate ProtocolResourceTemplate => _innerResource.ProtocolResourceTemplate;
2828

29+
/// <inheritdoc />
30+
public override IReadOnlyList<object> Metadata => _innerResource.Metadata;
31+
2932
/// <inheritdoc />
3033
public override bool IsMatch(string uri) => _innerResource.IsMatch(uri);
3134

src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ protected DelegatingMcpServerTool(McpServerTool innerTool)
2323
/// <inheritdoc />
2424
public override Tool ProtocolTool => _innerTool.ProtocolTool;
2525

26+
/// <inheritdoc />
27+
public override IReadOnlyList<object> Metadata => _innerTool.Metadata;
28+
2629
/// <inheritdoc />
2730
public override ValueTask<CallToolResult> InvokeAsync(
2831
RequestContext<CallToolRequestParams> request,

src/ModelContextProtocol.Core/Server/McpServer.Methods.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,7 @@ void IDisposable.Dispose() { } // nop
10661066
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
10671067
/// <returns>A task representing the asynchronous notification operation.</returns>
10681068
/// <exception cref="ArgumentNullException"><paramref name="task"/> is <see langword="null"/>.</exception>
1069+
/// <exception cref="McpException">The notification failed or the client returned an error response.</exception>
10691070
/// <remarks>
10701071
/// <para>
10711072
/// This method sends an optional status notification to inform the client of task state changes.

src/ModelContextProtocol.Core/UrlElicitationRequiredException.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace ModelContextProtocol;
1212
/// </summary>
1313
public sealed class UrlElicitationRequiredException : McpProtocolException
1414
{
15-
private readonly IReadOnlyList<ElicitRequestParams> _elicitations;
15+
private readonly List<ElicitRequestParams> _elicitations;
1616

1717
/// <summary>
1818
/// Initializes a new instance of the <see cref="UrlElicitationRequiredException"/> class with the specified message and pending elicitations.
@@ -66,7 +66,7 @@ internal static bool TryCreateFromError(
6666
return true;
6767
}
6868

69-
private static bool TryParseElicitations(JsonElement dataElement, out IReadOnlyList<ElicitRequestParams> elicitations)
69+
private static bool TryParseElicitations(JsonElement dataElement, out IList<ElicitRequestParams> elicitations)
7070
{
7171
elicitations = [];
7272

@@ -93,7 +93,7 @@ private static bool TryParseElicitations(JsonElement dataElement, out IReadOnlyL
9393
return true;
9494
}
9595

96-
private static IReadOnlyList<ElicitRequestParams> Validate(IEnumerable<ElicitRequestParams> elicitations)
96+
private static List<ElicitRequestParams> Validate(IEnumerable<ElicitRequestParams> elicitations)
9797
{
9898
var list = new List<ElicitRequestParams>();
9999
foreach (var elicitation in elicitations)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using ModelContextProtocol.Protocol;
2+
using ModelContextProtocol.Server;
3+
using System.Reflection;
4+
5+
namespace ModelContextProtocol.Tests.Server;
6+
7+
public class DelegatingMcpServerPromptTests
8+
{
9+
[Fact]
10+
public void Ctor_NullInnerPrompt_Throws()
11+
{
12+
Assert.Throws<ArgumentNullException>("innerPrompt", () => new TestDelegatingPrompt(null!));
13+
}
14+
15+
[Fact]
16+
public async Task AllMembers_DelegateToInnerPrompt()
17+
{
18+
Prompt expectedPrompt = new() { Name = "sentinel-prompt" };
19+
IReadOnlyList<object> expectedMetadata = new object[] { "m1" };
20+
GetPromptResult expectedResult = new() { Messages = [] };
21+
InnerPrompt inner = new(expectedPrompt, expectedMetadata, expectedResult);
22+
23+
TestDelegatingPrompt delegating = new(inner);
24+
25+
Assert.Same(expectedPrompt, delegating.ProtocolPrompt);
26+
Assert.Same(expectedMetadata, delegating.Metadata);
27+
Assert.Same(expectedResult, await delegating.GetAsync(null!, CancellationToken.None));
28+
Assert.Equal(inner.ToString(), delegating.ToString());
29+
}
30+
31+
[Fact]
32+
public void OverridesAllVirtualAndAbstractMembers()
33+
{
34+
MethodInfo[] baseMethods = typeof(McpServerPrompt).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
35+
.Where(m => m.IsVirtual || m.IsAbstract)
36+
.ToArray();
37+
38+
Assert.NotEmpty(baseMethods);
39+
40+
foreach (MethodInfo baseMethod in baseMethods)
41+
{
42+
Assert.True(
43+
typeof(DelegatingMcpServerPrompt).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
44+
.Any(m => m.Name == baseMethod.Name),
45+
$"DelegatingMcpServerPrompt does not override {baseMethod.Name} from McpServerPrompt.");
46+
}
47+
}
48+
49+
private sealed class TestDelegatingPrompt(McpServerPrompt innerPrompt) : DelegatingMcpServerPrompt(innerPrompt);
50+
51+
private sealed class InnerPrompt(Prompt protocolPrompt, IReadOnlyList<object> metadata, GetPromptResult result) : McpServerPrompt
52+
{
53+
public override Prompt ProtocolPrompt => protocolPrompt;
54+
public override IReadOnlyList<object> Metadata => metadata;
55+
public override ValueTask<GetPromptResult> GetAsync(RequestContext<GetPromptRequestParams> request, CancellationToken cancellationToken = default) => new(result);
56+
public override string ToString() => "inner-prompt";
57+
}
58+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using ModelContextProtocol.Protocol;
2+
using ModelContextProtocol.Server;
3+
using System.Reflection;
4+
5+
namespace ModelContextProtocol.Tests.Server;
6+
7+
public class DelegatingMcpServerResourceTests
8+
{
9+
[Fact]
10+
public void Ctor_NullInnerResource_Throws()
11+
{
12+
Assert.Throws<ArgumentNullException>("innerResource", () => new TestDelegatingResource(null!));
13+
}
14+
15+
[Fact]
16+
public async Task AllMembers_DelegateToInnerResource()
17+
{
18+
ResourceTemplate expectedTemplate = new() { Name = "sentinel-resource", UriTemplate = "test://resource" };
19+
Resource expectedResource = new() { Name = "sentinel-resource", Uri = "test://resource" };
20+
IReadOnlyList<object> expectedMetadata = new object[] { "m1" };
21+
ReadResourceResult expectedResult = new() { Contents = [] };
22+
InnerResource inner = new(expectedTemplate, expectedResource, expectedMetadata, expectedResult);
23+
24+
TestDelegatingResource delegating = new(inner);
25+
26+
Assert.Same(expectedTemplate, delegating.ProtocolResourceTemplate);
27+
Assert.Same(expectedResource, delegating.ProtocolResource);
28+
Assert.Same(expectedMetadata, delegating.Metadata);
29+
Assert.True(delegating.IsMatch("test://resource"));
30+
Assert.Same(expectedResult, await delegating.ReadAsync(null!, CancellationToken.None));
31+
Assert.Equal(inner.ToString(), delegating.ToString());
32+
}
33+
34+
[Fact]
35+
public void OverridesAllVirtualAndAbstractMembers()
36+
{
37+
MethodInfo[] baseMethods = typeof(McpServerResource).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
38+
.Where(m => m.IsVirtual || m.IsAbstract)
39+
.ToArray();
40+
41+
Assert.NotEmpty(baseMethods);
42+
43+
foreach (MethodInfo baseMethod in baseMethods)
44+
{
45+
Assert.True(
46+
typeof(DelegatingMcpServerResource).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
47+
.Any(m => m.Name == baseMethod.Name),
48+
$"DelegatingMcpServerResource does not override {baseMethod.Name} from McpServerResource.");
49+
}
50+
}
51+
52+
private sealed class TestDelegatingResource(McpServerResource innerResource) : DelegatingMcpServerResource(innerResource);
53+
54+
private sealed class InnerResource(ResourceTemplate protocolResourceTemplate, Resource protocolResource, IReadOnlyList<object> metadata, ReadResourceResult result) : McpServerResource
55+
{
56+
public override ResourceTemplate ProtocolResourceTemplate => protocolResourceTemplate;
57+
public override Resource? ProtocolResource => protocolResource;
58+
public override IReadOnlyList<object> Metadata => metadata;
59+
public override bool IsMatch(string uri) => true;
60+
public override ValueTask<ReadResourceResult> ReadAsync(RequestContext<ReadResourceRequestParams> request, CancellationToken cancellationToken = default) => new(result);
61+
public override string ToString() => "inner-resource";
62+
}
63+
}

0 commit comments

Comments
 (0)