Wrap each request in a service scope (#276)

* Wrap each request in a service scope

* Change request/notification delegates to return ValueTask

* Address feedback
This commit is contained in:
Stephen Toub 2025-04-11 19:19:12 -04:00 • committed by GitHub
commit f4c87f5e2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 477 additions and 306 deletions

View file

@ -21,16 +21,16 @@ public class McpServerHandlerTests
Assert.Null(handlers.SubscribeToResourcesHandler);
Assert.Null(handlers.UnsubscribeFromResourcesHandler);
handlers.ListToolsHandler = (p, c) => Task.FromResult(new ListToolsResult());
handlers.CallToolHandler = (p, c) => Task.FromResult(new CallToolResponse());
handlers.ListPromptsHandler = (p, c) => Task.FromResult(new ListPromptsResult());
handlers.GetPromptHandler = (p, c) => Task.FromResult(new GetPromptResult());
handlers.ListResourceTemplatesHandler = (p, c) => Task.FromResult(new ListResourceTemplatesResult());
handlers.ListResourcesHandler = (p, c) => Task.FromResult(new ListResourcesResult());
handlers.ReadResourceHandler = (p, c) => Task.FromResult(new ReadResourceResult());
handlers.CompleteHandler = (p, c) => Task.FromResult(new CompleteResult());
handlers.SubscribeToResourcesHandler = (s, c) => Task.FromResult(new EmptyResult());
handlers.UnsubscribeFromResourcesHandler = (s, c) => Task.FromResult(new EmptyResult());
handlers.ListToolsHandler = async (p, c) => new ListToolsResult();
handlers.CallToolHandler = async (p, c) => new CallToolResponse();
handlers.ListPromptsHandler = async (p, c) => new ListPromptsResult();
handlers.GetPromptHandler = async (p, c) => new GetPromptResult();
handlers.ListResourceTemplatesHandler = async (p, c) => new ListResourceTemplatesResult();
handlers.ListResourcesHandler = async (p, c) => new ListResourcesResult();
handlers.ReadResourceHandler = async (p, c) => new ReadResourceResult();
handlers.CompleteHandler = async (p, c) => new CompleteResult();
handlers.SubscribeToResourcesHandler = async (s, c) => new EmptyResult();
handlers.UnsubscribeFromResourcesHandler = async (s, c) => new EmptyResult();
Assert.NotNull(handlers.ListToolsHandler);
Assert.NotNull(handlers.CallToolHandler);

View file

@ -12,10 +12,7 @@ public class McpServerLoggingLevelTests
services.AddMcpServer()
.WithStdioServerTransport()
.WithSetLoggingLevelHandler((ctx, ct) =>
{
return Task.FromResult(new EmptyResult());
});
.WithSetLoggingLevelHandler(async (ctx, ct) => new EmptyResult());
var provider = services.BuildServiceProvider();
@ -29,10 +26,7 @@ public class McpServerLoggingLevelTests
services.AddMcpServer()
.WithStdioServerTransport()
.WithSetLoggingLevelHandler((ctx, ct) =>
{
return Task.FromResult(new EmptyResult());
});
.WithSetLoggingLevelHandler(async (ctx, ct) => new EmptyResult());
var provider = services.BuildServiceProvider();

View file

@ -33,7 +33,7 @@ public class McpServerPromptTests
Assert.DoesNotContain("server", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []);
var result = await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(mockServer.Object, null),
new RequestContext<GetPromptRequestParams>(mockServer.Object),
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.NotNull(result.Messages);
@ -59,16 +59,12 @@ public class McpServerPromptTests
Assert.Contains("something", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []);
Assert.DoesNotContain("actualMyService", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []);
Mock<IMcpServer> mockServer = new();
await Assert.ThrowsAsync<ArgumentException>(async () => await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(mockServer.Object, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken));
mockServer.SetupGet(x => x.Services).Returns(services);
var result = await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(mockServer.Object, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object) { Services = services },
TestContext.Current.CancellationToken);
Assert.Equal("Hello", result.Messages[0].Content.Text);
}
@ -89,7 +85,7 @@ public class McpServerPromptTests
}, new() { Services = services });
var result = await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(null!, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.Equal("Hello", result.Messages[0].Content.Text);
}
@ -102,7 +98,7 @@ public class McpServerPromptTests
typeof(DisposablePromptType));
var result = await prompt1.GetAsync(
new RequestContext<GetPromptRequestParams>(null!, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.Equal("disposals:1", result.Messages[0].Content.Text);
}
@ -115,7 +111,7 @@ public class McpServerPromptTests
typeof(AsyncDisposablePromptType));
var result = await prompt1.GetAsync(
new RequestContext<GetPromptRequestParams>(null!, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.Equal("asyncDisposals:1", result.Messages[0].Content.Text);
}
@ -128,7 +124,7 @@ public class McpServerPromptTests
typeof(AsyncDisposableAndDisposablePromptType));
var result = await prompt1.GetAsync(
new RequestContext<GetPromptRequestParams>(null!, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.Equal("disposals:0, asyncDisposals:1", result.Messages[0].Content.Text);
}
@ -144,7 +140,7 @@ public class McpServerPromptTests
});
var actual = await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(null!, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.Same(expected, actual);
@ -161,7 +157,7 @@ public class McpServerPromptTests
});
var actual = await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(null!, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.NotNull(actual);
@ -187,7 +183,7 @@ public class McpServerPromptTests
});
var actual = await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(null!, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.NotNull(actual);
@ -218,7 +214,7 @@ public class McpServerPromptTests
});
var actual = await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(null!, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.NotNull(actual);
@ -247,7 +243,7 @@ public class McpServerPromptTests
});
var actual = await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(null!, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.NotNull(actual);
@ -280,7 +276,7 @@ public class McpServerPromptTests
});
var actual = await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(null!, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.NotNull(actual);
@ -303,7 +299,7 @@ public class McpServerPromptTests
});
await Assert.ThrowsAsync<InvalidOperationException>(async () => await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(null!, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken));
}
@ -316,7 +312,7 @@ public class McpServerPromptTests
});
await Assert.ThrowsAsync<InvalidOperationException>(async () => await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(null!, null),
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken));
}

View file

@ -13,19 +13,19 @@ public class McpServerResourceTests
services.AddMcpServer()
.WithStdioServerTransport()
.WithListResourceTemplatesHandler((ctx, ct) =>
.WithListResourceTemplatesHandler(async (ctx, ct) =>
{
return Task.FromResult(new ListResourceTemplatesResult
return new ListResourceTemplatesResult
{
ResourceTemplates =
[
new ResourceTemplate { Name = "Static Resource", Description = "A static resource with a numeric ID", UriTemplate = "test://static/resource/{id}" }
]
});
};
})
.WithReadResourceHandler((ctx, ct) =>
.WithReadResourceHandler(async (ctx, ct) =>
{
return Task.FromResult(new ReadResourceResult
return new ReadResourceResult
{
Contents = [new TextResourceContents
{
@ -33,7 +33,7 @@ public class McpServerResourceTests
Text = "Static Resource",
MimeType = "text/plain",
}]
});
};
});
var provider = services.BuildServiceProvider();
@ -48,19 +48,19 @@ public class McpServerResourceTests
services.AddMcpServer()
.WithStdioServerTransport()
.WithListResourcesHandler((ctx, ct) =>
.WithListResourcesHandler(async (ctx, ct) =>
{
return Task.FromResult(new ListResourcesResult
return new ListResourcesResult
{
Resources =
[
new Resource { Name = "Static Resource", Description = "A static resource with a numeric ID", Uri = "test://static/resource/foo.txt" }
]
});
};
})
.WithReadResourceHandler((ctx, ct) =>
.WithReadResourceHandler(async (ctx, ct) =>
{
return Task.FromResult(new ReadResourceResult
return new ReadResourceResult
{
Contents = [new TextResourceContents
{
@ -68,7 +68,7 @@ public class McpServerResourceTests
Text = "Static Resource",
MimeType = "text/plain",
}]
});
};
});
var provider = services.BuildServiceProvider();
@ -82,9 +82,9 @@ public class McpServerResourceTests
var services = new ServiceCollection();
services.AddMcpServer()
.WithStdioServerTransport()
.WithReadResourceHandler((ctx, ct) =>
.WithReadResourceHandler(async (ctx, ct) =>
{
return Task.FromResult(new ReadResourceResult
return new ReadResourceResult
{
Contents = [new TextResourceContents
{
@ -92,7 +92,7 @@ public class McpServerResourceTests
Text = "Static Resource",
MimeType = "text/plain",
}]
});
};
});
var sp = services.BuildServiceProvider();
Assert.Throws<McpException>(() => sp.GetRequiredService<IMcpServer>());

View file

@ -202,8 +202,8 @@ public class McpServerTests : LoggedTest
{
Completions = new()
{
CompleteHandler = (request, ct) =>
Task.FromResult(new CompleteResult
CompleteHandler = async (request, ct) =>
new CompleteResult
{
Completion = new()
{
@ -211,7 +211,7 @@ public class McpServerTests : LoggedTest
Total = 2,
HasMore = true
}
})
}
}
},
method: RequestMethods.CompletionComplete,
@ -234,19 +234,19 @@ public class McpServerTests : LoggedTest
{
Resources = new()
{
ListResourceTemplatesHandler = (request, ct) =>
ListResourceTemplatesHandler = async (request, ct) =>
{
return Task.FromResult(new ListResourceTemplatesResult
return new ListResourceTemplatesResult
{
ResourceTemplates = [new() { UriTemplate = "test", Name = "Test Resource" }]
});
};
},
ListResourcesHandler = (request, ct) =>
ListResourcesHandler = async (request, ct) =>
{
return Task.FromResult(new ListResourcesResult
return new ListResourcesResult
{
Resources = [new() { Uri = "test", Name = "Test Resource" }]
});
};
},
ReadResourceHandler = (request, ct) => throw new NotImplementedException(),
}
@ -270,12 +270,12 @@ public class McpServerTests : LoggedTest
{
Resources = new()
{
ListResourcesHandler = (request, ct) =>
ListResourcesHandler = async (request, ct) =>
{
return Task.FromResult(new ListResourcesResult
return new ListResourcesResult
{
Resources = [new() { Uri = "test", Name = "Test Resource" }]
});
};
},
ReadResourceHandler = (request, ct) => throw new NotImplementedException(),
}
@ -305,12 +305,12 @@ public class McpServerTests : LoggedTest
{
Resources = new()
{
ReadResourceHandler = (request, ct) =>
ReadResourceHandler = async (request, ct) =>
{
return Task.FromResult(new ReadResourceResult
return new ReadResourceResult
{
Contents = [new TextResourceContents { Text = "test" }]
});
};
},
ListResourcesHandler = (request, ct) => throw new NotImplementedException(),
}
@ -342,12 +342,12 @@ public class McpServerTests : LoggedTest
{
Prompts = new()
{
ListPromptsHandler = (request, ct) =>
ListPromptsHandler = async (request, ct) =>
{
return Task.FromResult(new ListPromptsResult
return new ListPromptsResult
{
Prompts = [new() { Name = "test" }]
});
};
},
GetPromptHandler = (request, ct) => throw new NotImplementedException(),
},
@ -377,7 +377,7 @@ public class McpServerTests : LoggedTest
{
Prompts = new()
{
GetPromptHandler = (request, ct) => Task.FromResult(new GetPromptResult { Description = "test" }),
GetPromptHandler = async (request, ct) => new GetPromptResult { Description = "test" },
ListPromptsHandler = (request, ct) => throw new NotImplementedException(),
}
},
@ -405,12 +405,12 @@ public class McpServerTests : LoggedTest
{
Tools = new()
{
ListToolsHandler = (request, ct) =>
ListToolsHandler = async (request, ct) =>
{
return Task.FromResult(new ListToolsResult
return new ListToolsResult
{
Tools = [new() { Name = "test" }]
});
};
},
CallToolHandler = (request, ct) => throw new NotImplementedException(),
}
@ -440,12 +440,12 @@ public class McpServerTests : LoggedTest
{
Tools = new()
{
CallToolHandler = (request, ct) =>
CallToolHandler = async (request, ct) =>
{
return Task.FromResult(new CallToolResponse
return new CallToolResponse
{
Content = [new Content { Text = "test" }]
});
};
},
ListToolsHandler = (request, ct) => throw new NotImplementedException(),
}
@ -623,7 +623,7 @@ public class McpServerTests : LoggedTest
throw new NotImplementedException();
public Task RunAsync(CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, Task> handler) =>
public IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler) =>
throw new NotImplementedException();
}
@ -639,7 +639,7 @@ public class McpServerTests : LoggedTest
NotificationHandlers = [new(NotificationMethods.ProgressNotification, (notification, cancellationToken) =>
{
notificationReceived.TrySetResult(notification);
return Task.CompletedTask;
return default;
})],
};

View file

@ -40,7 +40,7 @@ public partial class McpServerToolTests
Assert.DoesNotContain("server", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, McpJsonUtilities.DefaultOptions));
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null),
new RequestContext<CallToolRequestParams>(mockServer.Object),
TestContext.Current.CancellationToken);
Assert.Equal("42", result.Content[0].Text);
}
@ -92,14 +92,12 @@ public partial class McpServerToolTests
Mock<IMcpServer> mockServer = new();
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null),
new RequestContext<CallToolRequestParams>(mockServer.Object),
TestContext.Current.CancellationToken);
Assert.True(result.IsError);
mockServer.SetupGet(x => x.Services).Returns(services);
result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null),
new RequestContext<CallToolRequestParams>(mockServer.Object) { Services = services },
TestContext.Current.CancellationToken);
Assert.Equal("42", result.Content[0].Text);
}
@ -120,7 +118,7 @@ public partial class McpServerToolTests
}, new() { Services = services });
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(null!, null),
new RequestContext<CallToolRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.Equal("42", result.Content[0].Text);
}
@ -135,7 +133,7 @@ public partial class McpServerToolTests
options);
var result = await tool1.InvokeAsync(
new RequestContext<CallToolRequestParams>(null!, null),
new RequestContext<CallToolRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.Equal("""{"disposals":1}""", result.Content[0].Text);
}
@ -150,7 +148,7 @@ public partial class McpServerToolTests
options);
var result = await tool1.InvokeAsync(
new RequestContext<CallToolRequestParams>(null!, null),
new RequestContext<CallToolRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.Equal("""{"asyncDisposals":1}""", result.Content[0].Text);
}
@ -165,7 +163,7 @@ public partial class McpServerToolTests
options);
var result = await tool1.InvokeAsync(
new RequestContext<CallToolRequestParams>(null!, null),
new RequestContext<CallToolRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken);
Assert.Equal("""{"asyncDisposals":1,"disposals":0}""", result.Content[0].Text);
}
@ -186,7 +184,7 @@ public partial class McpServerToolTests
});
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null),
new RequestContext<CallToolRequestParams>(mockServer.Object),
TestContext.Current.CancellationToken);
Assert.Equal(3, result.Content.Count);
@ -223,7 +221,7 @@ public partial class McpServerToolTests
});
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null),
new RequestContext<CallToolRequestParams>(mockServer.Object),
TestContext.Current.CancellationToken);
Assert.Single(result.Content);
@ -251,7 +249,7 @@ public partial class McpServerToolTests
return (string?)null;
});
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null),
new RequestContext<CallToolRequestParams>(mockServer.Object),
TestContext.Current.CancellationToken);
Assert.Empty(result.Content);
}
@ -266,7 +264,7 @@ public partial class McpServerToolTests
return "42";
});
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null),
new RequestContext<CallToolRequestParams>(mockServer.Object),
TestContext.Current.CancellationToken);
Assert.Single(result.Content);
Assert.Equal("42", result.Content[0].Text);
@ -283,7 +281,7 @@ public partial class McpServerToolTests
return new List<string>() { "42", "43" };
});
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null),
new RequestContext<CallToolRequestParams>(mockServer.Object),
TestContext.Current.CancellationToken);
Assert.Equal(2, result.Content.Count);
Assert.Equal("42", result.Content[0].Text);
@ -302,7 +300,7 @@ public partial class McpServerToolTests
return new Content { Text = "42", Type = "text" };
});
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null),
new RequestContext<CallToolRequestParams>(mockServer.Object),
TestContext.Current.CancellationToken);
Assert.Single(result.Content);
Assert.Equal("42", result.Content[0].Text);
@ -319,7 +317,7 @@ public partial class McpServerToolTests
return new List<Content>() { new() { Text = "42", Type = "text" }, new() { Data = "1234", Type = "image", MimeType = "image/png" } };
});
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null),
new RequestContext<CallToolRequestParams>(mockServer.Object),
TestContext.Current.CancellationToken);
Assert.Equal(2, result.Content.Count);
Assert.Equal("42", result.Content[0].Text);
@ -345,7 +343,7 @@ public partial class McpServerToolTests
return response;
});
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null),
new RequestContext<CallToolRequestParams>(mockServer.Object),
TestContext.Current.CancellationToken);
Assert.Same(response, result);