Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/ModelContextProtocol.Core/Client/McpClientPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,30 @@ public sealed class McpClientPrompt
{
private readonly McpClient _client;

internal McpClientPrompt(McpClient client, Prompt prompt)
/// <summary>
/// Initializes a new instance of the <see cref="McpClientPrompt"/> class.
/// </summary>
/// <param name="client">The <see cref="McpClient"/> instance to use for invoking the prompt.</param>
/// <param name="prompt">The protocol <see cref="Prompt"/> definition describing the prompt's metadata.</param>
/// <remarks>
/// <para>
/// This constructor enables reusing cached prompt definitions across different <see cref="McpClient"/> instances
/// without needing to call <see cref="McpClient.ListPromptsAsync"/> on every reconnect. This is particularly useful
/// in scenarios where prompt definitions are stable and network round-trips should be minimized.
/// </para>
/// <para>
/// The provided <paramref name="prompt"/> must represent a prompt that is actually available on the server
/// associated with the <paramref name="client"/>. Attempting to invoke a prompt that doesn't exist on the
/// server will result in an <see cref="McpException"/>.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="prompt"/> is <see langword="null"/>.</exception>
public McpClientPrompt(McpClient client, Prompt prompt)
{
Throw.IfNull(client);
Throw.IfNull(prompt);

_client = client;
ProtocolPrompt = prompt;
}
Expand Down
24 changes: 23 additions & 1 deletion src/ModelContextProtocol.Core/Client/McpClientResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,30 @@ public sealed class McpClientResource
{
private readonly McpClient _client;

internal McpClientResource(McpClient client, Resource resource)
/// <summary>
/// Initializes a new instance of the <see cref="McpClientResource"/> class.
/// </summary>
/// <param name="client">The <see cref="McpClient"/> instance to use for reading the resource.</param>
/// <param name="resource">The protocol <see cref="Resource"/> definition describing the resource's metadata.</param>
/// <remarks>
/// <para>
/// This constructor enables reusing cached resource definitions across different <see cref="McpClient"/> instances
/// without needing to call <see cref="McpClient.ListResourcesAsync"/> on every reconnect. This is particularly useful
/// in scenarios where resource definitions are stable and network round-trips should be minimized.
/// </para>
/// <para>
/// The provided <paramref name="resource"/> must represent a resource that is actually available on the server
/// associated with the <paramref name="client"/>. Attempting to read a resource that doesn't exist on the
/// server will result in an <see cref="McpException"/>.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="resource"/> is <see langword="null"/>.</exception>
public McpClientResource(McpClient client, Resource resource)
{
Throw.IfNull(client);
Throw.IfNull(resource);

_client = client;
ProtocolResource = resource;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,30 @@ public sealed class McpClientResourceTemplate
{
private readonly McpClient _client;

internal McpClientResourceTemplate(McpClient client, ResourceTemplate resourceTemplate)
/// <summary>
/// Initializes a new instance of the <see cref="McpClientResourceTemplate"/> class.
/// </summary>
/// <param name="client">The <see cref="McpClient"/> instance to use for reading the resource template.</param>
/// <param name="resourceTemplate">The protocol <see cref="ResourceTemplate"/> definition describing the resource template's metadata.</param>
/// <remarks>
/// <para>
/// This constructor enables reusing cached resource template definitions across different <see cref="McpClient"/> instances
/// without needing to call <see cref="McpClient.ListResourceTemplatesAsync"/> on every reconnect. This is particularly useful
/// in scenarios where resource template definitions are stable and network round-trips should be minimized.
/// </para>
/// <para>
/// The provided <paramref name="resourceTemplate"/> must represent a resource template that is actually available on the server
/// associated with the <paramref name="client"/>. Attempting to read a resource template that doesn't exist on the
/// server will result in an <see cref="McpException"/>.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="resourceTemplate"/> is <see langword="null"/>.</exception>
public McpClientResourceTemplate(McpClient client, ResourceTemplate resourceTemplate)
{
Throw.IfNull(client);
Throw.IfNull(resourceTemplate);

_client = client;
ProtocolResourceTemplate = resourceTemplate;
}
Expand Down
39 changes: 39 additions & 0 deletions src/ModelContextProtocol.Core/Client/McpClientTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,45 @@ public sealed class McpClientTool : AIFunction
private readonly string _description;
private readonly IProgress<ProgressNotificationValue>? _progress;

/// <summary>
/// Initializes a new instance of the <see cref="McpClientTool"/> class.
/// </summary>
/// <param name="client">The <see cref="McpClient"/> instance to use for invoking the tool.</param>
/// <param name="tool">The protocol <see cref="Tool"/> definition describing the tool's metadata and schema.</param>
/// <param name="serializerOptions">
/// The JSON serialization options governing argument serialization. If <see langword="null"/>,
/// <see cref="McpJsonUtilities.DefaultOptions"/> will be used.
/// </param>
/// <remarks>
/// <para>
/// This constructor enables reusing cached tool definitions across different <see cref="McpClient"/> instances
/// without needing to call <see cref="McpClient.ListToolsAsync"/> on every reconnect. This is particularly useful
/// in scenarios where tool definitions are stable and network round-trips should be minimized.
/// </para>
/// <para>
/// The provided <paramref name="tool"/> must represent a tool that is actually available on the server
/// associated with the <paramref name="client"/>. Attempting to invoke a tool that doesn't exist on the
/// server will result in an <see cref="McpException"/>.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="tool"/> is <see langword="null"/>.</exception>
public McpClientTool(
McpClient client,
Tool tool,
JsonSerializerOptions? serializerOptions = null)
{
Throw.IfNull(client);
Throw.IfNull(tool);

_client = client;
ProtocolTool = tool;
JsonSerializerOptions = serializerOptions ?? McpJsonUtilities.DefaultOptions;
_name = tool.Name;
_description = tool.Description ?? string.Empty;
_progress = null;
}

internal McpClientTool(
McpClient client,
Tool tool,
Expand Down
137 changes: 137 additions & 0 deletions tests/ModelContextProtocol.Tests/Client/McpClientPromptTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.ComponentModel;

namespace ModelContextProtocol.Tests.Client;

public class McpClientPromptTests : ClientServerTestBase
{
public McpClientPromptTests(ITestOutputHelper outputHelper)
: base(outputHelper)
{
}

protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
mcpServerBuilder.WithPrompts<GreetingPrompts>();
}

[McpServerPromptType]
private sealed class GreetingPrompts
{
[McpServerPrompt, Description("Generates a greeting prompt")]
public static ChatMessage Greeting([Description("The name to greet")] string name) =>
new(ChatRole.User, $"Hello, {name}!");
}

[Fact]
public async Task Constructor_WithValidParameters_CreatesInstance()
{
await using McpClient client = await CreateMcpClientForServer();

var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
var originalPrompt = prompts.First();
var promptDefinition = originalPrompt.ProtocolPrompt;

var newPrompt = new McpClientPrompt(client, promptDefinition);

Assert.NotNull(newPrompt);
Assert.Equal("greeting", newPrompt.Name);
Assert.Equal("Generates a greeting prompt", newPrompt.Description);
Assert.Same(promptDefinition, newPrompt.ProtocolPrompt);
}

[Fact]
public void Constructor_WithNullClient_ThrowsArgumentNullException()
{
var promptDefinition = new Prompt
{
Name = "test",
Description = "Test prompt"
};

Assert.Throws<ArgumentNullException>("client", () => new McpClientPrompt(null!, promptDefinition));
}

[Fact]
public async Task Constructor_WithNullPrompt_ThrowsArgumentNullException()
{
await using McpClient client = await CreateMcpClientForServer();

Assert.Throws<ArgumentNullException>("prompt", () => new McpClientPrompt(client, null!));
}

[Fact]
public async Task ReusePromptDefinition_AcrossDifferentClients_InvokesSuccessfully()
{
Prompt promptDefinition;
{
await using McpClient client1 = await CreateMcpClientForServer();
var prompts = await client1.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
var greetingPrompt = prompts.First(p => p.Name == "greeting");
promptDefinition = greetingPrompt.ProtocolPrompt;
}

await using McpClient client2 = await CreateMcpClientForServer();

var reusedPrompt = new McpClientPrompt(client2, promptDefinition);

var result = await reusedPrompt.GetAsync(
new Dictionary<string, object?> { ["name"] = "World" },
cancellationToken: TestContext.Current.CancellationToken);

Assert.NotNull(result);
Assert.NotNull(result.Messages);
var message = result.Messages.First();
Assert.NotNull(message.Content);
var textContent = message.Content as TextContentBlock;
Assert.NotNull(textContent);
Assert.Equal("Hello, World!", textContent.Text);
}

[Fact]
public async Task ReusePromptDefinition_PreservesPromptMetadata()
{
await using McpClient client = await CreateMcpClientForServer();

var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
var originalPrompt = prompts.First();
var promptDefinition = originalPrompt.ProtocolPrompt;

var reusedPrompt = new McpClientPrompt(client, promptDefinition);

Assert.Equal(originalPrompt.Name, reusedPrompt.Name);
Assert.Equal(originalPrompt.Description, reusedPrompt.Description);
Assert.Equal(originalPrompt.ProtocolPrompt.Name, reusedPrompt.ProtocolPrompt.Name);
Assert.Equal(originalPrompt.ProtocolPrompt.Description, reusedPrompt.ProtocolPrompt.Description);
}

[Fact]
public async Task ManuallyConstructedPrompt_CanBeInvoked()
{
await using McpClient client = await CreateMcpClientForServer();

var manualPrompt = new Prompt
{
Name = "greeting",
Description = "Generates a greeting prompt"
};

var clientPrompt = new McpClientPrompt(client, manualPrompt);

var result = await clientPrompt.GetAsync(
new Dictionary<string, object?> { ["name"] = "Test" },
cancellationToken: TestContext.Current.CancellationToken);

Assert.NotNull(result);
Assert.NotNull(result.Messages);
var message = result.Messages.First();
Assert.NotNull(message.Content);
var textContent = message.Content as TextContentBlock;
Assert.NotNull(textContent);
Assert.Equal("Hello, Test!", textContent.Text);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.ComponentModel;

namespace ModelContextProtocol.Tests.Client;

public class McpClientResourceTemplateConstructorTests : ClientServerTestBase
{
public McpClientResourceTemplateConstructorTests(ITestOutputHelper outputHelper)
: base(outputHelper)
{
}

protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
mcpServerBuilder.WithResources<FileTemplateResources>();
}

[McpServerResourceType]
private sealed class FileTemplateResources
{
[McpServerResource, Description("A file template")]
public static string FileTemplate([Description("The file path")] string path) => $"Content for {path}";
}

[Fact]
public async Task Constructor_WithValidParameters_CreatesInstance()
{
await using McpClient client = await CreateMcpClientForServer();

var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
var originalTemplate = templates.First();
var templateDefinition = originalTemplate.ProtocolResourceTemplate;

var newTemplate = new McpClientResourceTemplate(client, templateDefinition);

Assert.NotNull(newTemplate);
Assert.Equal("file_template", newTemplate.Name);
Assert.Equal("A file template", newTemplate.Description);
Assert.Same(templateDefinition, newTemplate.ProtocolResourceTemplate);
}

[Fact]
public void Constructor_WithNullClient_ThrowsArgumentNullException()
{
var templateDefinition = new ResourceTemplate
{
UriTemplate = "file:///{path}",
Name = "test",
Description = "Test template"
};

Assert.Throws<ArgumentNullException>("client", () => new McpClientResourceTemplate(null!, templateDefinition));
}

[Fact]
public async Task Constructor_WithNullResourceTemplate_ThrowsArgumentNullException()
{
await using McpClient client = await CreateMcpClientForServer();

Assert.Throws<ArgumentNullException>("resourceTemplate", () => new McpClientResourceTemplate(client, null!));
}

[Fact]
public async Task ReuseResourceTemplateDefinition_PreservesTemplateMetadata()
{
await using McpClient client = await CreateMcpClientForServer();

var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
var originalTemplate = templates.First();
var templateDefinition = originalTemplate.ProtocolResourceTemplate;

var reusedTemplate = new McpClientResourceTemplate(client, templateDefinition);

Assert.Equal(originalTemplate.Name, reusedTemplate.Name);
Assert.Equal(originalTemplate.Description, reusedTemplate.Description);
Assert.Equal(originalTemplate.UriTemplate, reusedTemplate.UriTemplate);
Assert.Equal(originalTemplate.ProtocolResourceTemplate.Name, reusedTemplate.ProtocolResourceTemplate.Name);
Assert.Equal(originalTemplate.ProtocolResourceTemplate.Description, reusedTemplate.ProtocolResourceTemplate.Description);
}

[Fact]
public async Task ManuallyConstructedResourceTemplate_CreatesValidInstance()
{
await using McpClient client = await CreateMcpClientForServer();

var manualTemplate = new ResourceTemplate
{
UriTemplate = "file:///{path}",
Name = "file_template",
Description = "A file template"
};

var clientTemplate = new McpClientResourceTemplate(client, manualTemplate);

Assert.NotNull(clientTemplate);
Assert.Equal("file_template", clientTemplate.Name);
Assert.Equal("A file template", clientTemplate.Description);
Assert.Equal("file:///{path}", clientTemplate.UriTemplate);
}
}
Loading
Loading