Skip to content

Commit 79b2dbe

Browse files
Copilotstephentoubjeffhandley
authored
Fix Metadata delegation in DelegatingMcpServerTool/Prompt/Resource (#1338)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com>
1 parent 34d4cd7 commit 79b2dbe

File tree

6 files changed

+188
-0
lines changed

6 files changed

+188
-0
lines changed

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,
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+
}
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 DelegatingMcpServerToolTests
8+
{
9+
[Fact]
10+
public void Ctor_NullInnerTool_Throws()
11+
{
12+
Assert.Throws<ArgumentNullException>("innerTool", () => new TestDelegatingTool(null!));
13+
}
14+
15+
[Fact]
16+
public async Task AllMembers_DelegateToInnerTool()
17+
{
18+
Tool expectedTool = new() { Name = "sentinel-tool" };
19+
IReadOnlyList<object> expectedMetadata = new object[] { "m1" };
20+
CallToolResult expectedResult = new() { Content = [] };
21+
InnerTool inner = new(expectedTool, expectedMetadata, expectedResult);
22+
23+
TestDelegatingTool delegating = new(inner);
24+
25+
Assert.Same(expectedTool, delegating.ProtocolTool);
26+
Assert.Same(expectedMetadata, delegating.Metadata);
27+
Assert.Same(expectedResult, await delegating.InvokeAsync(null!, CancellationToken.None));
28+
Assert.Equal(inner.ToString(), delegating.ToString());
29+
}
30+
31+
[Fact]
32+
public void OverridesAllVirtualAndAbstractMembers()
33+
{
34+
MethodInfo[] baseMethods = typeof(McpServerTool).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(DelegatingMcpServerTool).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
44+
.Any(m => m.Name == baseMethod.Name),
45+
$"DelegatingMcpServerTool does not override {baseMethod.Name} from McpServerTool.");
46+
}
47+
}
48+
49+
private sealed class TestDelegatingTool(McpServerTool innerTool) : DelegatingMcpServerTool(innerTool);
50+
51+
private sealed class InnerTool(Tool protocolTool, IReadOnlyList<object> metadata, CallToolResult result) : McpServerTool
52+
{
53+
public override Tool ProtocolTool => protocolTool;
54+
public override IReadOnlyList<object> Metadata => metadata;
55+
public override ValueTask<CallToolResult> InvokeAsync(RequestContext<CallToolRequestParams> request, CancellationToken cancellationToken = default) => new(result);
56+
public override string ToString() => "inner-tool";
57+
}
58+
}

0 commit comments

Comments
 (0)