Add McpServerPrompt / McpClientPrompt and friends (#126)

This commit is contained in:
Stephen Toub 2025-03-31 09:54:40 -04:00 • committed by GitHub
commit 5d3fb65b11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1950 additions and 550 deletions

View file

@ -0,0 +1,256 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
namespace ModelContextProtocol.Server;
/// <summary>Provides an <see cref="McpServerPrompt"/> that's implemented via an <see cref="AIFunction"/>.</summary>
internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
{
/// <summary>Key used temporarily for flowing request context into an AIFunction.</summary>
/// <remarks>This will be replaced with use of AIFunctionArguments.Context.</remarks>
internal const string RequestContextKey = "__temporary_RequestContext";
/// <summary>
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="Delegate"/> instance.
/// </summary>
public static new AIFunctionMcpServerPrompt Create(
Delegate method,
McpServerPromptCreateOptions? options)
{
Throw.IfNull(method);
options = DeriveOptions(method.Method, options);
return Create(method.Method, method.Target, options);
}
/// <summary>
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="Delegate"/> instance.
/// </summary>
public static new AIFunctionMcpServerPrompt Create(
MethodInfo method,
object? target,
McpServerPromptCreateOptions? options)
{
Throw.IfNull(method);
// TODO: Once this repo consumes a new build of Microsoft.Extensions.AI containing
// https://github.com/dotnet/extensions/pull/6158,
// https://github.com/dotnet/extensions/pull/6162, and
// https://github.com/dotnet/extensions/pull/6175, switch over to using the real
// AIFunctionFactory, delete the TemporaryXx types, and fix-up the mechanism by
// which the arguments are passed.
options = DeriveOptions(method, options);
return Create(
TemporaryAIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, options)),
options);
}
/// <summary>
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="Delegate"/> instance.
/// </summary>
public static new AIFunctionMcpServerPrompt Create(
MethodInfo method,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
McpServerPromptCreateOptions? options)
{
Throw.IfNull(method);
options = DeriveOptions(method, options);
return Create(
TemporaryAIFunctionFactory.Create(method, targetType, CreateAIFunctionFactoryOptions(method, options)),
options);
}
private static TemporaryAIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
MethodInfo method, McpServerPromptCreateOptions? options) =>
new()
{
Name = options?.Name ?? method.GetCustomAttribute<McpServerPromptAttribute>()?.Name,
Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => Task.FromResult(result),
ConfigureParameterBinding = pi =>
{
if (pi.ParameterType == typeof(RequestContext<GetPromptRequestParams>))
{
return new()
{
ExcludeFromSchema = true,
BindParameter = (pi, args) => GetRequestContext(args),
};
}
if (pi.ParameterType == typeof(IMcpServer))
{
return new()
{
ExcludeFromSchema = true,
BindParameter = (pi, args) => GetRequestContext(args)?.Server,
};
}
// We assume that if the services used to create the prompt support a particular type,
// so too do the services associated with the server. This is the same basic assumption
// made in ASP.NET.
if (options?.Services is { } services &&
services.GetService<IServiceProviderIsService>() is { } ispis &&
ispis.IsService(pi.ParameterType))
{
return new()
{
ExcludeFromSchema = true,
BindParameter = (pi, args) =>
GetRequestContext(args)?.Server?.Services?.GetService(pi.ParameterType) ??
(pi.HasDefaultValue ? null :
throw new ArgumentException("No service of the requested type was found.")),
};
}
if (pi.GetCustomAttribute<FromKeyedServicesAttribute>() is { } keyedAttr)
{
return new()
{
ExcludeFromSchema = true,
BindParameter = (pi, args) =>
(GetRequestContext(args)?.Server?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
(pi.HasDefaultValue ? null :
throw new ArgumentException("No service of the requested type was found.")),
};
}
return default;
static RequestContext<GetPromptRequestParams>? GetRequestContext(IReadOnlyDictionary<string, object?> args)
{
if (args.TryGetValue(RequestContextKey, out var orc) &&
orc is RequestContext<GetPromptRequestParams> requestContext)
{
return requestContext;
}
return null;
}
},
};
/// <summary>Creates an <see cref="McpServerPrompt"/> that wraps the specified <see cref="AIFunction"/>.</summary>
public static new AIFunctionMcpServerPrompt Create(AIFunction function, McpServerPromptCreateOptions? options)
{
Throw.IfNull(function);
List<PromptArgument> args = [];
if (function.JsonSchema.TryGetProperty("properties", out JsonElement properties))
{
foreach (var param in properties.EnumerateObject())
{
args.Add(new()
{
Name = param.Name,
Description = param.Value.TryGetProperty("description", out JsonElement description) ? description.GetString() : null,
Required = param.Value.TryGetProperty("required", out JsonElement required) && required.GetBoolean(),
});
}
}
Prompt prompt = new()
{
Name = options?.Name ?? function.Name,
Description = options?.Description ?? function.Description,
Arguments = args,
};
return new AIFunctionMcpServerPrompt(function, prompt);
}
private static McpServerPromptCreateOptions? DeriveOptions(MethodInfo method, McpServerPromptCreateOptions? options)
{
McpServerPromptCreateOptions newOptions = options?.Clone() ?? new();
if (method.GetCustomAttribute<McpServerPromptAttribute>() is { } attr)
{
newOptions.Name ??= attr.Name;
}
return newOptions;
}
/// <summary>Gets the <see cref="AIFunction"/> wrapped by this prompt.</summary>
internal AIFunction AIFunction { get; }
/// <summary>Initializes a new instance of the <see cref="McpServerPrompt"/> class.</summary>
private AIFunctionMcpServerPrompt(AIFunction function, Prompt prompt)
{
AIFunction = function;
ProtocolPrompt = prompt;
}
/// <inheritdoc />
public override string ToString() => AIFunction.ToString();
/// <inheritdoc />
public override Prompt ProtocolPrompt { get; }
/// <inheritdoc />
public override async Task<GetPromptResult> GetAsync(
RequestContext<GetPromptRequestParams> request, CancellationToken cancellationToken = default)
{
Throw.IfNull(request);
cancellationToken.ThrowIfCancellationRequested();
// TODO: Once we shift to the real AIFunctionFactory, the request should be passed via AIFunctionArguments.Context.
Dictionary<string, object?> arguments = request.Params?.Arguments is IDictionary<string, object?> existingArgs ?
new(existingArgs) :
[];
arguments[RequestContextKey] = request;
object? result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
return result switch
{
GetPromptResult getPromptResult => getPromptResult,
string text => new()
{
Description = ProtocolPrompt.Description,
Messages = [new() { Role = Role.User, Content = new() { Text = text, Type = "text" } }],
},
PromptMessage promptMessage => new()
{
Description = ProtocolPrompt.Description,
Messages = [promptMessage],
},
IEnumerable<PromptMessage> promptMessages => new()
{
Description = ProtocolPrompt.Description,
Messages = [.. promptMessages],
},
ChatMessage chatMessage => new()
{
Description = ProtocolPrompt.Description,
Messages = [.. chatMessage.ToPromptMessages()],
},
IEnumerable<ChatMessage> chatMessages => new()
{
Description = ProtocolPrompt.Description,
Messages = [.. chatMessages.SelectMany(chatMessage => chatMessage.ToPromptMessages())],
},
null => throw new InvalidOperationException($"Null result returned from prompt function."),
_ => throw new InvalidOperationException($"Unknown result type '{result.GetType()}' returned from prompt function."),
};
}
}

View file

@ -0,0 +1,34 @@
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils;
namespace ModelContextProtocol.Server;
/// <summary>Provides an <see cref="McpServerPrompt"/> that delegates all operations to an inner <see cref="McpServerPrompt"/>.</summary>
/// <remarks>
/// This is recommended as a base type when building prompts that can be chained around an underlying <see cref="McpServerPrompt"/>.
/// The default implementation simply passes each call to the inner prompt instance.
/// </remarks>
public abstract class DelegatingMcpServerPrompt : McpServerPrompt
{
private readonly McpServerPrompt _innerPrompt;
/// <summary>Initializes a new instance of the <see cref="DelegatingMcpServerPrompt"/> class around the specified <paramref name="innerPrompt"/>.</summary>
/// <param name="innerPrompt">The inner prompt wrapped by this delegating prompt.</param>
protected DelegatingMcpServerPrompt(McpServerPrompt innerPrompt)
{
Throw.IfNull(innerPrompt);
_innerPrompt = innerPrompt;
}
/// <inheritdoc />
public override Prompt ProtocolPrompt => _innerPrompt.ProtocolPrompt;
/// <inheritdoc />
public override Task<GetPromptResult> GetAsync(
RequestContext<GetPromptRequestParams> request,
CancellationToken cancellationToken = default) =>
_innerPrompt.GetAsync(request, cancellationToken);
/// <inheritdoc />
public override string ToString() => _innerPrompt.ToString();
}

View file

@ -0,0 +1,8 @@
namespace ModelContextProtocol.Server;
/// <summary>Represents an MCP server primitive, like a tool or a prompt.</summary>
public interface IMcpServerPrimitive
{
/// <summary>Gets the name of the primitive.</summary>
string Name { get; }
}

View file

@ -14,6 +14,7 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
{
private readonly IServerTransport? _serverTransport;
private readonly EventHandler? _toolsChangedDelegate;
private readonly EventHandler? _promptsChangedDelegate;
private ITransport? _sessionTransport;
private string _endpointName;
@ -77,6 +78,13 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
Method = NotificationMethods.ToolListChangedNotification,
});
};
_promptsChangedDelegate = delegate
{
_ = SendMessageAsync(new JsonRpcNotification()
{
Method = NotificationMethods.PromptListChangedNotification,
});
};
AddNotificationHandler(NotificationMethods.InitializedNotification, _ =>
{
@ -85,6 +93,11 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
tools.Changed += _toolsChangedDelegate;
}
if (ServerOptions.Capabilities?.Prompts?.PromptCollection is { } prompts)
{
prompts.Changed += _promptsChangedDelegate;
}
return Task.CompletedTask;
});
@ -160,6 +173,11 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
tools.Changed -= _toolsChangedDelegate;
}
if (ServerOptions.Capabilities?.Prompts?.PromptCollection is { } prompts)
{
prompts.Changed -= _promptsChangedDelegate;
}
try
{
await base.DisposeUnsynchronizedAsync().ConfigureAwait(false);
@ -253,15 +271,97 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
private void SetPromptsHandler(McpServerOptions options)
{
if (options.Capabilities?.Prompts is not { } promptsCapability)
PromptsCapability? promptsCapability = options.Capabilities?.Prompts;
var listPromptsHandler = promptsCapability?.ListPromptsHandler;
var getPromptHandler = promptsCapability?.GetPromptHandler;
var prompts = promptsCapability?.PromptCollection;
if (listPromptsHandler is null != getPromptHandler is null)
{
return;
throw new McpServerException("ListPrompts and GetPrompt handlers should be specified together.");
}
if (promptsCapability.ListPromptsHandler is not { } listPromptsHandler ||
promptsCapability.GetPromptHandler is not { } getPromptHandler)
// Handle tools provided via DI.
if (prompts is { IsEmpty: false })
{
throw new McpServerException("Prompts capability was enabled, but ListPrompts and/or GetPrompt handlers were not specified.");
var originalListPromptsHandler = listPromptsHandler;
var originalGetPromptHandler = getPromptHandler;
// Synthesize the handlers, making sure a ToolsCapability is specified.
listPromptsHandler = async (request, cancellationToken) =>
{
ListPromptsResult result = new();
foreach (McpServerPrompt prompt in prompts)
{
result.Prompts.Add(prompt.ProtocolPrompt);
}
if (originalListPromptsHandler is not null)
{
string? nextCursor = null;
do
{
ListPromptsResult extraResults = await originalListPromptsHandler(request, cancellationToken).ConfigureAwait(false);
result.Prompts.AddRange(extraResults.Prompts);
nextCursor = extraResults.NextCursor;
if (nextCursor is not null)
{
request = request with { Params = new() { Cursor = nextCursor } };
}
}
while (nextCursor is not null);
}
return result;
};
getPromptHandler = (request, cancellationToken) =>
{
if (request.Params is null ||
!prompts.TryGetPrimitive(request.Params.Name, out var prompt))
{
if (originalGetPromptHandler is not null)
{
return originalGetPromptHandler(request, cancellationToken);
}
throw new McpServerException($"Unknown prompt '{request.Params?.Name}'");
}
return prompt.GetAsync(request, cancellationToken);
};
ServerCapabilities = new()
{
Experimental = options.Capabilities?.Experimental,
Logging = options.Capabilities?.Logging,
Tools = options.Capabilities?.Tools,
Resources = options.Capabilities?.Resources,
Prompts = new()
{
ListPromptsHandler = listPromptsHandler,
GetPromptHandler = getPromptHandler,
PromptCollection = prompts,
ListChanged = true,
}
};
}
else
{
ServerCapabilities = options.Capabilities;
if (promptsCapability is null)
{
// No prompts, and no prompts capability was declared, so nothing to do.
return;
}
// Make sure the handlers are provided if the capability is enabled.
if (listPromptsHandler is null || getPromptHandler is null)
{
throw new McpServerException("ListPrompts and/or GetPrompt handlers were not specified but the Prompts capability was enabled.");
}
}
SetRequestHandler<ListPromptsRequestParams, ListPromptsResult>(RequestMethods.PromptsList, (request, ct) => listPromptsHandler(new(this, request), ct));
@ -318,7 +418,7 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
callToolHandler = (request, cancellationToken) =>
{
if (request.Params is null ||
!tools.TryGetTool(request.Params.Name, out var tool))
!tools.TryGetPrimitive(request.Params.Name, out var tool))
{
if (originalCallToolHandler is not null)
{

View file

@ -0,0 +1,163 @@
using ModelContextProtocol.Utils;
using System.Collections;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
namespace ModelContextProtocol.Server;
/// <summary>Provides a thread-safe collection of <typeparamref name="T"/> instances, indexed by their names.</summary>
/// <typeparam name="T">Specifies the type of primitive stored in the collection.</typeparam>
public class McpServerPrimitiveCollection<T> : ICollection<T>, IReadOnlyCollection<T>
where T : IMcpServerPrimitive
{
/// <summary>Concurrent dictionary of primitives, indexed by their names.</summary>
private readonly ConcurrentDictionary<string, T> _primitives = [];
/// <summary>
/// Initializes a new instance of the <see cref="McpServerPrimitiveCollection{T}"/> class.
/// </summary>
public McpServerPrimitiveCollection()
{
}
/// <summary>Occurs when the collection is changed.</summary>
/// <remarks>
/// By default, this is raised when a primitive is added or removed. However, a derived implementation
/// may raise this event for other reasons, such as when a primitive is modified.
/// </remarks>
public event EventHandler? Changed;
/// <summary>Gets the number of primitives in the collection.</summary>
public int Count => _primitives.Count;
/// <summary>Gets whether there are any primitives in the collection.</summary>
public bool IsEmpty => _primitives.IsEmpty;
/// <summary>Raises <see cref="Changed"/> if there are registered handlers.</summary>
protected void RaiseChanged() => Changed?.Invoke(this, EventArgs.Empty);
/// <summary>Gets the <typeparamref name="T"/> with the specified <paramref name="name"/> from the collection.</summary>
/// <param name="name">The name of the primitive to retrieve.</param>
/// <returns>The <typeparamref name="T"/> with the specified name.</returns>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
/// <exception cref="KeyNotFoundException">An primitive with the specified name does not exist in the collection.</exception>
public T this[string name]
{
get
{
Throw.IfNull(name);
return _primitives[name];
}
}
/// <summary>Clears all primitives from the collection.</summary>
public virtual void Clear()
{
_primitives.Clear();
RaiseChanged();
}
/// <summary>Adds the specified <typeparamref name="T"/> to the collection.</summary>
/// <param name="primitive">The primitive to be added.</param>
/// <exception cref="ArgumentNullException"><paramref name="primitive"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">A primitive with the same name as <paramref name="primitive"/> already exists in the collection.</exception>
public void Add(T primitive)
{
if (!TryAdd(primitive))
{
throw new ArgumentException($"A primitive with the same name '{primitive.Name}' already exists in the collection.", nameof(primitive));
}
}
/// <summary>Adds the specified <typeparamref name="T"/> to the collection.</summary>
/// <param name="primitive">The primitive to be added.</param>
/// <returns><see langword="true"/> if the primitive was added; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="primitive"/> is <see langword="null"/>.</exception>
public virtual bool TryAdd(T primitive)
{
Throw.IfNull(primitive);
bool added = _primitives.TryAdd(primitive.Name, primitive);
if (added)
{
RaiseChanged();
}
return added;
}
/// <summary>Removes the specified primitivefrom the collection.</summary>
/// <param name="primitive">The primitive to be removed from the collection.</param>
/// <returns>
/// <see langword="true"/> if the primitive was found in the collection and removed; otherwise, <see langword="false"/> if it couldn't be found.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="primitive"/> is <see langword="null"/>.</exception>
public virtual bool Remove(T primitive)
{
Throw.IfNull(primitive);
bool removed = ((ICollection<KeyValuePair<string, T>>)_primitives).Remove(new(primitive.Name, primitive));
if (removed)
{
RaiseChanged();
}
return removed;
}
/// <summary>Attempts to get the primitive with the specified name from the collection.</summary>
/// <param name="name">The name of the primitive to retrieve.</param>
/// <param name="primitive">The primitive, if found; otherwise, <see langword="null"/>.</param>
/// <returns>
/// <see langword="true"/> if the primitive was found in the collection and return; otherwise, <see langword="false"/> if it couldn't be found.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
public virtual bool TryGetPrimitive(string name, [NotNullWhen(true)] out T? primitive)
{
Throw.IfNull(name);
return _primitives.TryGetValue(name, out primitive);
}
/// <summary>Checks if a specific primitive is present in the collection of primitives.</summary>
/// <param name="primitive">The primitive to search for in the collection.</param>
/// <see langword="true"/> if the primitive was found in the collection and return; otherwise, <see langword="false"/> if it couldn't be found.
/// <exception cref="ArgumentNullException"><paramref name="primitive"/> is <see langword="null"/>.</exception>
public virtual bool Contains(T primitive)
{
Throw.IfNull(primitive);
return ((ICollection<KeyValuePair<string, T>>)_primitives).Contains(new(primitive.Name, primitive));
}
/// <summary>Gets the names of all of the primitives in the collection.</summary>
public virtual ICollection<string> PrimitiveNames => _primitives.Keys;
/// <summary>Creates an array containing all of the primitives in the collection.</summary>
/// <returns>An array containing all of the primitives in the collection.</returns>
public virtual T[] ToArray() => _primitives.Values.ToArray();
/// <inheritdoc/>
public virtual void CopyTo(T[] array, int arrayIndex)
{
Throw.IfNull(array);
_primitives.Values.CopyTo(array, arrayIndex);
}
/// <inheritdoc/>
public virtual IEnumerator<T> GetEnumerator()
{
foreach (var entry in _primitives)
{
yield return entry.Value;
}
}
/// <inheritdoc/>
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <inheritdoc/>
bool ICollection<T>.IsReadOnly => false;
}

View file

@ -0,0 +1,95 @@
using Microsoft.Extensions.AI;
using ModelContextProtocol.Protocol.Types;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace ModelContextProtocol.Server;
/// <summary>Represents an invocable prompt used by Model Context Protocol servers.</summary>
public abstract class McpServerPrompt : IMcpServerPrimitive
{
/// <summary>Initializes a new instance of the <see cref="McpServerPrompt"/> class.</summary>
protected McpServerPrompt()
{
}
/// <summary>Gets the protocol <see cref="Prompt"/> type for this instance.</summary>
public abstract Prompt ProtocolPrompt { get; }
/// <summary>Invokes the <see cref="McpServerPrompt"/>.</summary>
/// <param name="request">The request information resulting in the invocation of this tool.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The call response from invoking the tool.</returns>
/// <exception cref="ArgumentNullException"><paramref name="request"/> is <see langword="null"/>.</exception>
public abstract Task<GetPromptResult> GetAsync(
RequestContext<GetPromptRequestParams> request,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="Delegate"/> instance.
/// </summary>
/// <param name="method">The method to be represented via the created <see cref="McpServerPrompt"/>.</param>
/// <param name="options">Optional options used in the creation of the <see cref="McpServerPrompt"/> to control its behavior.</param>
/// <returns>The created <see cref="McpServerPrompt"/> for invoking <paramref name="method"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
public static McpServerPrompt Create(
Delegate method,
McpServerPromptCreateOptions? options = null) =>
AIFunctionMcpServerPrompt.Create(method, options);
/// <summary>
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="Delegate"/> instance.
/// </summary>
/// <param name="method">The method to be represented via the created <see cref="McpServerPrompt"/>.</param>
/// <param name="target">The instance if <paramref name="method"/> is an instance method; otherwise, <see langword="null"/>.</param>
/// <param name="options">Optional options used in the creation of the <see cref="McpServerPrompt"/> to control its behavior.</param>
/// <returns>The created <see cref="McpServerPrompt"/> for invoking <paramref name="method"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="method"/> is an instance method but <paramref name="target"/> is <see langword="null"/>.</exception>
public static McpServerPrompt Create(
MethodInfo method,
object? target = null,
McpServerPromptCreateOptions? options = null) =>
AIFunctionMcpServerPrompt.Create(method, target, options);
/// <summary>
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via an <see cref="MethodInfo"/> for
/// and instance method, along with a <see cref="Type"/> representing the type of the target object to
/// instantiate each time the method is invoked.
/// </summary>
/// <param name="method">The instance method to be represented via the created <see cref="AIFunction"/>.</param>
/// <param name="targetType">
/// The <see cref="Type"/> to construct an instance of on which to invoke <paramref name="method"/> when
/// the resulting <see cref="AIFunction"/> is invoked. If services are provided,
/// ActivatorUtilities.CreateInstance will be used to construct the instance using those services; otherwise,
/// <see cref="Activator.CreateInstance(Type)"/> is used, utilizing the type's public parameterless constructor.
/// If an instance can't be constructed, an exception is thrown during the function's invocation.
/// </param>
/// <param name="options">Optional options used in the creation of the <see cref="McpServerPrompt"/> to control its behavior.</param>
/// <returns>The created <see cref="AIFunction"/> for invoking <paramref name="method"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
public static McpServerPrompt Create(
MethodInfo method,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
McpServerPromptCreateOptions? options = null) =>
AIFunctionMcpServerPrompt.Create(method, targetType, options);
/// <summary>Creates an <see cref="McpServerPrompt"/> that wraps the specified <see cref="AIFunction"/>.</summary>
/// <param name="function">The function to wrap.</param>
/// <param name="options">Optional options used in the creation of the <see cref="McpServerPrompt"/> to control its behavior.</param>
/// <exception cref="ArgumentNullException"><paramref name="function"/> is <see langword="null"/>.</exception>
/// <remarks>
/// Unlike the other overloads of Create, the <see cref="McpServerPrompt"/> created by <see cref="Create(AIFunction, McpServerPromptCreateOptions)"/>
/// does not provide all of the special parameter handling for MCP-specific concepts, like <see cref="IMcpServer"/>.
/// </remarks>
public static McpServerPrompt Create(
AIFunction function,
McpServerPromptCreateOptions? options = null) =>
AIFunctionMcpServerPrompt.Create(function, options);
/// <inheritdoc />
public override string ToString() => ProtocolPrompt.Name;
/// <inheritdoc />
string IMcpServerPrimitive.Name => ProtocolPrompt.Name;
}

View file

@ -0,0 +1,19 @@
namespace ModelContextProtocol.Server;
/// <summary>
/// Used to indicate that a method should be considered an MCP prompt and describe it.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class McpServerPromptAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="McpServerPromptAttribute"/> class.
/// </summary>
public McpServerPromptAttribute()
{
}
/// <summary>Gets the name of the prompt.</summary>
/// <remarks>If <see langword="null"/>, the method name will be used.</remarks>
public string? Name { get; set; }
}

View file

@ -0,0 +1,45 @@
using System.ComponentModel;
namespace ModelContextProtocol.Server;
/// <summary>Provides options for controlling the creation of an <see cref="McpServerPrompt"/>.</summary>
public sealed class McpServerPromptCreateOptions
{
/// <summary>
/// Gets or sets optional services used in the construction of the <see cref="McpServerPrompt"/>.
/// </summary>
/// <remarks>
/// These services will be used to determine which parameters should be satisifed from dependency injection; what services
/// are satisfied via this provider should match what's satisfied via the provider passed in at invocation time.
/// </remarks>
public IServiceProvider? Services { get; set; }
/// <summary>
/// Gets or sets the name to use for the <see cref="McpServerPrompt"/>.
/// </summary>
/// <remarks>
/// If <see langword="null"/>, but an <see cref="McpServerPromptAttribute"/> is applied to the method,
/// the name from the attribute will be used. If that's not present, a name based on the method's name will be used.
/// </remarks>
public string? Name { get; set; }
/// <summary>
/// Gets or set the description to use for the <see cref="McpServerPrompt"/>.
/// </summary>
/// <remarks>
/// If <see langword="null"/>, but a <see cref="DescriptionAttribute"/> is applied to the method,
/// the description from that attribute will be used.
/// </remarks>
public string? Description { get; set; }
/// <summary>
/// Creates a shallow clone of the current <see cref="McpServerPromptCreateOptions"/> instance.
/// </summary>
internal McpServerPromptCreateOptions Clone() =>
new McpServerPromptCreateOptions()
{
Services = Services,
Name = Name,
Description = Description,
};
}

View file

@ -0,0 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
namespace ModelContextProtocol.Server;
/// <summary>
/// Used to attribute a type containing methods that should be exposed as MCP prompts.
/// </summary>
/// <remarks>
/// This is primarily relevant to methods that scan types in an assembly looking for methods
/// to expose, such as <see cref="McpServerBuilderExtensions.WithPromptsFromAssembly"/>. It is not
/// necessary to attribute types explicitly provided to a method like <see cref="McpServerBuilderExtensions.WithPrompts{TPrompt}"/>.
/// </remarks>
[AttributeUsage(AttributeTargets.Class)]
public sealed class McpServerPromptTypeAttribute : Attribute;

View file

@ -6,7 +6,7 @@ using System.Reflection;
namespace ModelContextProtocol.Server;
/// <summary>Represents an invocable tool used by Model Context Protocol clients and servers.</summary>
public abstract class McpServerTool
public abstract class McpServerTool : IMcpServerPrimitive
{
/// <summary>Initializes a new instance of the <see cref="McpServerTool"/> class.</summary>
protected McpServerTool()
@ -89,4 +89,7 @@ public abstract class McpServerTool
/// <inheritdoc />
public override string ToString() => ProtocolTool.Name;
/// <inheritdoc />
string IMcpServerPrimitive.Name => ProtocolTool.Name;
}

View file

@ -19,7 +19,7 @@ public sealed class McpServerToolAttribute : Attribute
internal bool? _readOnly;
/// <summary>
/// Initializes a new instance of the <see cref="McpServerToolTypeAttribute"/> class.
/// Initializes a new instance of the <see cref="McpServerToolAttribute"/> class.
/// </summary>
public McpServerToolAttribute()
{

View file

@ -1,164 +0,0 @@
using ModelContextProtocol.Utils;
using System.Collections;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
namespace ModelContextProtocol.Server;
/// <summary>Provides a thread-safe collection of <see cref="McpServerTool"/> instances, indexed by their names.</summary>
public class McpServerToolCollection : ICollection<McpServerTool>, IReadOnlyCollection<McpServerTool>
{
/// <summary>Concurrent dictionary of tools, indexed by their names.</summary>
private readonly ConcurrentDictionary<string, McpServerTool> _tools = [];
/// <summary>
/// Initializes a new instance of the <see cref="McpServerToolCollection"/> class.
/// </summary>
public McpServerToolCollection()
{
}
/// <summary>Occurs when the collection is changed.</summary>
/// <remarks>
/// By default, this is raised when a tool is added or removed. However, a derived implementation
/// may raise this event for other reasons, such as when a tool is modified.
/// </remarks>
public event EventHandler? Changed;
/// <summary>Gets the number of tools in the collection.</summary>
public int Count => _tools.Count;
/// <summary>Gets whether there are any tools in the collection.</summary>
public bool IsEmpty => _tools.IsEmpty;
/// <summary>Raises <see cref="Changed"/> if there are registered handlers.</summary>
protected void RaiseChanged() => Changed?.Invoke(this, EventArgs.Empty);
/// <summary>Gets the <see cref="McpServerTool"/> with the specified <paramref name="name"/> from the collection.</summary>
/// <param name="name">The name of the tool to retrieve.</param>
/// <returns>The <see cref="McpServerTool"/> with the specified name.</returns>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
/// <exception cref="KeyNotFoundException">A tool with the specified name does not exist in the collection.</exception>
public McpServerTool this[string name]
{
get
{
Throw.IfNull(name);
return _tools[name];
}
}
/// <summary>Clears all tools from the collection.</summary>
public virtual void Clear()
{
_tools.Clear();
RaiseChanged();
}
/// <summary>Adds the specified <see cref="McpServerTool"/> to the collection.</summary>
/// <param name="tool">The tool to be added.</param>
/// <exception cref="ArgumentNullException"><paramref name="tool"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">A tool with the same name as <paramref name="tool"/> already exists in the collection.</exception>
public void Add(McpServerTool tool)
{
if (!TryAdd(tool))
{
throw new ArgumentException($"A tool with the same name '{tool.ProtocolTool.Name}' already exists in the collection.", nameof(tool));
}
}
/// <summary>Adds the specified <see cref="McpServerTool"/> to the collection.</summary>
/// <param name="tool">The tool to be added.</param>
/// <returns><see langword="true"/> if the tool was added; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="tool"/> is <see langword="null"/>.</exception>
public virtual bool TryAdd(McpServerTool tool)
{
Throw.IfNull(tool);
bool added = _tools.TryAdd(tool.ProtocolTool.Name, tool);
if (added)
{
RaiseChanged();
}
return added;
}
/// <summary>Removes the specified toolfrom the collection.</summary>
/// <param name="tool">The tool to be removed from the collection.</param>
/// <returns>
/// <see langword="true"/> if the tool was found in the collection and removed; otherwise, <see langword="false"/> if it couldn't be found.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="tool"/> is <see langword="null"/>.</exception>
public virtual bool Remove(McpServerTool tool)
{
Throw.IfNull(tool);
bool removed = ((ICollection<KeyValuePair<string, McpServerTool>>)_tools).Remove(new(tool.ProtocolTool.Name, tool));
if (removed)
{
RaiseChanged();
}
return removed;
}
/// <summary>Attempts to get the tool with the specified name from the collection.</summary>
/// <param name="name">The name of the tool to retrieve.</param>
/// <param name="tool">The tool, if found; otherwise, <see langword="null"/>.</param>
/// <returns>
/// <see langword="true"/> if the tool was found in the collection and return; otherwise, <see langword="false"/> if it couldn't be found.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
public virtual bool TryGetTool(string name, [NotNullWhen(true)] out McpServerTool? tool)
{
Throw.IfNull(name);
return _tools.TryGetValue(name, out tool);
}
/// <summary>Checks if a specific tool is present in the collection of tools.</summary>
/// <param name="tool">The tool to search for in the collection.</param>
/// <see langword="true"/> if the tool was found in the collection and return; otherwise, <see langword="false"/> if it couldn't be found.
/// <exception cref="ArgumentNullException"><paramref name="tool"/> is <see langword="null"/>.</exception>
public virtual bool Contains(McpServerTool tool)
{
Throw.IfNull(tool);
return ((ICollection<KeyValuePair<string, McpServerTool>>)_tools).Contains(new(tool.ProtocolTool.Name, tool));
}
/// <summary>Gets the names of all of the tools in the collection.</summary>
public virtual ICollection<string> ToolNames => _tools.Keys;
/// <summary>Creates an array containing all of the tools in the collection.</summary>
/// <returns>An array containing all of the tools in the collection.</returns>
public virtual McpServerTool[] ToArray() => _tools.Values.ToArray();
/// <inheritdoc/>
public virtual void CopyTo(McpServerTool[] array, int arrayIndex)
{
Throw.IfNull(array);
foreach (var entry in _tools)
{
array[arrayIndex++] = entry.Value;
}
}
/// <inheritdoc/>
public virtual IEnumerator<McpServerTool> GetEnumerator()
{
foreach (var entry in _tools)
{
yield return entry.Value;
}
}
/// <inheritdoc/>
IEnumerator<McpServerTool> IEnumerable<McpServerTool>.GetEnumerator() => GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <inheritdoc/>
bool ICollection<McpServerTool>.IsReadOnly => false;
}

View file

@ -201,9 +201,16 @@ internal static partial class TemporaryAIFunctionFactory
throw new ArgumentException("Open generic methods are not supported", nameof(method));
}
if (!method.IsStatic && target is null)
if (method.IsStatic)
{
throw new ArgumentNullException("Target must not be null for an instance method.", nameof(target));
if (target is not null)
{
throw new ArgumentException("The specified method is a static method but the specified target is non-null.", nameof(target));
}
}
else if (target is null)
{
throw new ArgumentNullException(nameof(target), "The specified method is an instance method but the specified target is null.");
}
var functionDescriptor = ReflectionAIFunctionDescriptor.GetOrCreate(method, options);