Add ModelContextProtocol.Core package (#428)
This commit is contained in:
parent
57458241b9
commit
703b0fe50b
33 changed files with 239 additions and 94 deletions
|
|
@ -0,0 +1,283 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.ComponentModel;
|
||||
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>
|
||||
/// 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="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerPrompt Create(
|
||||
MethodInfo method,
|
||||
object? target,
|
||||
McpServerPromptCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
options = DeriveOptions(method, options);
|
||||
|
||||
return Create(
|
||||
AIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, options)),
|
||||
options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerPrompt Create(
|
||||
MethodInfo method,
|
||||
Func<RequestContext<GetPromptRequestParams>, object> createTargetFunc,
|
||||
McpServerPromptCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
Throw.IfNull(createTargetFunc);
|
||||
|
||||
options = DeriveOptions(method, options);
|
||||
|
||||
return Create(
|
||||
AIFunctionFactory.Create(method, args =>
|
||||
{
|
||||
var request = (RequestContext<GetPromptRequestParams>)args.Context![typeof(RequestContext<GetPromptRequestParams>)]!;
|
||||
return createTargetFunc(request);
|
||||
}, CreateAIFunctionFactoryOptions(method, options)),
|
||||
options);
|
||||
}
|
||||
|
||||
private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
|
||||
MethodInfo method, McpServerPromptCreateOptions? options) =>
|
||||
new()
|
||||
{
|
||||
Name = options?.Name ?? method.GetCustomAttribute<McpServerPromptAttribute>()?.Name,
|
||||
Description = options?.Description,
|
||||
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
|
||||
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IProgress<ProgressNotificationValue>))
|
||||
{
|
||||
// Bind IProgress<ProgressNotificationValue> to the progress token in the request,
|
||||
// if there is one. If we can't get one, return a nop progress.
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
{
|
||||
var requestContent = GetRequestContext(args);
|
||||
if (requestContent?.Server is { } server &&
|
||||
requestContent?.Params?.Meta?.ProgressToken is { } progressToken)
|
||||
{
|
||||
return new TokenProgress(server, progressToken);
|
||||
}
|
||||
|
||||
return NullProgress.Instance;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (options?.Services is { } services &&
|
||||
services.GetService<IServiceProviderIsService>() is { } ispis &&
|
||||
ispis.IsService(pi.ParameterType))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
GetRequestContext(args)?.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)?.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(AIFunctionArguments args)
|
||||
{
|
||||
if (args.Context?.TryGetValue(typeof(RequestContext<GetPromptRequestParams>), out var orc) is true &&
|
||||
orc is RequestContext<GetPromptRequestParams> requestContext)
|
||||
{
|
||||
return requestContext;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
JsonSchemaCreateOptions = options?.SchemaCreateOptions,
|
||||
};
|
||||
|
||||
/// <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 = [];
|
||||
HashSet<string>? requiredProps = function.JsonSchema.TryGetProperty("required", out JsonElement required)
|
||||
? new(required.EnumerateArray().Select(p => p.GetString()!), StringComparer.Ordinal)
|
||||
: null;
|
||||
|
||||
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 = requiredProps?.Contains(param.Name) ?? false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 { } promptAttr)
|
||||
{
|
||||
newOptions.Name ??= promptAttr.Name;
|
||||
}
|
||||
|
||||
if (method.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)
|
||||
{
|
||||
newOptions.Description ??= descAttr.Description;
|
||||
}
|
||||
|
||||
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 Prompt ProtocolPrompt { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<GetPromptResult> GetAsync(
|
||||
RequestContext<GetPromptRequestParams> request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(request);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
AIFunctionArguments arguments = new()
|
||||
{
|
||||
Services = request.Services,
|
||||
Context = new Dictionary<object, object?>() { [typeof(RequestContext<GetPromptRequestParams>)] = request }
|
||||
};
|
||||
|
||||
var argDict = request.Params?.Arguments;
|
||||
if (argDict is not null)
|
||||
{
|
||||
foreach (var kvp in argDict)
|
||||
{
|
||||
arguments[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
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."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,454 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>Provides an <see cref="AIFunctionMcpServerResource"/> that's implemented via an <see cref="AIFunction"/>.</summary>
|
||||
internal sealed class AIFunctionMcpServerResource : McpServerResource
|
||||
{
|
||||
private readonly Regex? _uriParser;
|
||||
private readonly string[] _templateVariableNames = [];
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AIFunctionMcpServerResource"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerResource Create(
|
||||
Delegate method,
|
||||
McpServerResourceCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
options = DeriveOptions(method.Method, options);
|
||||
|
||||
return Create(method.Method, method.Target, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerResource"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerResource Create(
|
||||
MethodInfo method,
|
||||
object? target,
|
||||
McpServerResourceCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
options = DeriveOptions(method, options);
|
||||
|
||||
return Create(
|
||||
AIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, options)),
|
||||
options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerResource"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerResource Create(
|
||||
MethodInfo method,
|
||||
Func<RequestContext<ReadResourceRequestParams>, object> createTargetFunc,
|
||||
McpServerResourceCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
Throw.IfNull(createTargetFunc);
|
||||
|
||||
options = DeriveOptions(method, options);
|
||||
|
||||
return Create(
|
||||
AIFunctionFactory.Create(method, args =>
|
||||
{
|
||||
var request = (RequestContext<ReadResourceRequestParams>)args.Context![typeof(RequestContext<ReadResourceRequestParams>)]!;
|
||||
return createTargetFunc(request);
|
||||
}, CreateAIFunctionFactoryOptions(method, options)),
|
||||
options);
|
||||
}
|
||||
|
||||
private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
|
||||
MethodInfo method, McpServerResourceCreateOptions? options) =>
|
||||
new()
|
||||
{
|
||||
Name = options?.Name ?? method.GetCustomAttribute<McpServerResourceAttribute>()?.Name,
|
||||
Description = options?.Description,
|
||||
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
|
||||
SerializerOptions = McpJsonUtilities.DefaultOptions,
|
||||
ConfigureParameterBinding = pi =>
|
||||
{
|
||||
if (pi.ParameterType == typeof(RequestContext<ReadResourceRequestParams>))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) => GetRequestContext(args),
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IMcpServer))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) => GetRequestContext(args)?.Server,
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IProgress<ProgressNotificationValue>))
|
||||
{
|
||||
// Bind IProgress<ProgressNotificationValue> to the progress token in the request,
|
||||
// if there is one. If we can't get one, return a nop progress.
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
{
|
||||
var requestContent = GetRequestContext(args);
|
||||
if (requestContent?.Server is { } server &&
|
||||
requestContent?.Params?.Meta?.ProgressToken is { } progressToken)
|
||||
{
|
||||
return new TokenProgress(server, progressToken);
|
||||
}
|
||||
|
||||
return NullProgress.Instance;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (options?.Services is { } services &&
|
||||
services.GetService<IServiceProviderIsService>() is { } ispis &&
|
||||
ispis.IsService(pi.ParameterType))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
GetRequestContext(args)?.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)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
|
||||
(pi.HasDefaultValue ? null :
|
||||
throw new ArgumentException("No service of the requested type was found.")),
|
||||
};
|
||||
}
|
||||
|
||||
// These parameters are the ones and only ones to include in the schema. The schema
|
||||
// won't be consumed by anyone other than this instance, which will use it to determine
|
||||
// which properties should show up in the URI template.
|
||||
if (pi.Name is not null && GetConverter(pi.ParameterType) is { } converter)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = false,
|
||||
BindParameter = (pi, args) =>
|
||||
{
|
||||
if (args.TryGetValue(pi.Name!, out var value))
|
||||
{
|
||||
return
|
||||
value is null || pi.ParameterType.IsInstanceOfType(value) ? value :
|
||||
value is string stringValue ? converter(stringValue) :
|
||||
throw new ArgumentException($"Parameter '{pi.Name}' is of type '{pi.ParameterType}', but value '{value}' is of type '{value.GetType()}'.");
|
||||
}
|
||||
|
||||
return
|
||||
pi.HasDefaultValue ? pi.DefaultValue :
|
||||
throw new ArgumentException($"Missing a value for the required parameter '{pi.Name}'.");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
static RequestContext<ReadResourceRequestParams>? GetRequestContext(AIFunctionArguments args)
|
||||
{
|
||||
if (args.Context?.TryGetValue(typeof(RequestContext<ReadResourceRequestParams>), out var rc) is true &&
|
||||
rc is RequestContext<ReadResourceRequestParams> requestContext)
|
||||
{
|
||||
return requestContext;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, Func<string, object?>> s_convertersCache = [];
|
||||
|
||||
private static Func<string, object?>? GetConverter(Type type)
|
||||
{
|
||||
Type key = type;
|
||||
|
||||
if (s_convertersCache.TryGetValue(key, out var converter))
|
||||
{
|
||||
return converter;
|
||||
}
|
||||
|
||||
if (Nullable.GetUnderlyingType(type) is { } underlyingType)
|
||||
{
|
||||
// We will have already screened out null values by the time the converter is used,
|
||||
// so we can parse just the underlying type.
|
||||
type = underlyingType;
|
||||
}
|
||||
|
||||
if (type == typeof(string) || type == typeof(object)) converter = static s => s;
|
||||
if (type == typeof(bool)) converter = static s => bool.Parse(s);
|
||||
if (type == typeof(char)) converter = static s => char.Parse(s);
|
||||
if (type == typeof(byte)) converter = static s => byte.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(sbyte)) converter = static s => sbyte.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(ushort)) converter = static s => ushort.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(short)) converter = static s => short.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(uint)) converter = static s => uint.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(int)) converter = static s => int.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(ulong)) converter = static s => ulong.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(long)) converter = static s => long.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(float)) converter = static s => float.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(double)) converter = static s => double.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(decimal)) converter = static s => decimal.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(TimeSpan)) converter = static s => TimeSpan.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(DateTime)) converter = static s => DateTime.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(DateTimeOffset)) converter = static s => DateTimeOffset.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(Uri)) converter = static s => new Uri(s, UriKind.RelativeOrAbsolute);
|
||||
if (type == typeof(Guid)) converter = static s => Guid.Parse(s);
|
||||
if (type == typeof(Version)) converter = static s => Version.Parse(s);
|
||||
#if NET
|
||||
if (type == typeof(Half)) converter = static s => Half.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(Int128)) converter = static s => Int128.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(UInt128)) converter = static s => UInt128.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(IntPtr)) converter = static s => IntPtr.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(UIntPtr)) converter = static s => UIntPtr.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(DateOnly)) converter = static s => DateOnly.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(TimeOnly)) converter = static s => TimeOnly.Parse(s, CultureInfo.InvariantCulture);
|
||||
#endif
|
||||
if (type.IsEnum) converter = s => Enum.Parse(type, s);
|
||||
|
||||
if (type.GetCustomAttribute<TypeConverterAttribute>() is TypeConverterAttribute tca &&
|
||||
Type.GetType(tca.ConverterTypeName, throwOnError: false) is { } converterType &&
|
||||
Activator.CreateInstance(converterType) is TypeConverter typeConverter &&
|
||||
typeConverter.CanConvertFrom(typeof(string)))
|
||||
{
|
||||
converter = s => typeConverter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
|
||||
}
|
||||
|
||||
if (converter is not null)
|
||||
{
|
||||
s_convertersCache.TryAdd(key, converter);
|
||||
}
|
||||
|
||||
return converter;
|
||||
}
|
||||
|
||||
/// <summary>Creates an <see cref="McpServerResource"/> that wraps the specified <see cref="AIFunction"/>.</summary>
|
||||
public static new AIFunctionMcpServerResource Create(AIFunction function, McpServerResourceCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(function);
|
||||
|
||||
string name = options?.Name ?? function.Name;
|
||||
|
||||
ResourceTemplate resource = new()
|
||||
{
|
||||
UriTemplate = options?.UriTemplate ?? DeriveUriTemplate(name, function),
|
||||
Name = name,
|
||||
Description = options?.Description,
|
||||
MimeType = options?.MimeType,
|
||||
};
|
||||
|
||||
return new AIFunctionMcpServerResource(function, resource);
|
||||
}
|
||||
|
||||
private static McpServerResourceCreateOptions DeriveOptions(MemberInfo member, McpServerResourceCreateOptions? options)
|
||||
{
|
||||
McpServerResourceCreateOptions newOptions = options?.Clone() ?? new();
|
||||
|
||||
if (member.GetCustomAttribute<McpServerResourceAttribute>() is { } resourceAttr)
|
||||
{
|
||||
newOptions.UriTemplate ??= resourceAttr.UriTemplate;
|
||||
newOptions.Name ??= resourceAttr.Name;
|
||||
newOptions.MimeType ??= resourceAttr.MimeType;
|
||||
}
|
||||
|
||||
if (member.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)
|
||||
{
|
||||
newOptions.Description ??= descAttr.Description;
|
||||
}
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
/// <summary>Derives a name to be used as a resource name.</summary>
|
||||
private static string DeriveUriTemplate(string name, AIFunction function)
|
||||
{
|
||||
StringBuilder template = new();
|
||||
|
||||
template.Append("resource://").Append(Uri.EscapeDataString(name));
|
||||
|
||||
if (function.JsonSchema.TryGetProperty("properties", out JsonElement properties))
|
||||
{
|
||||
string separator = "{?";
|
||||
foreach (var prop in properties.EnumerateObject())
|
||||
{
|
||||
template.Append(separator).Append(prop.Name);
|
||||
separator = ",";
|
||||
}
|
||||
|
||||
if (separator == ",")
|
||||
{
|
||||
template.Append('}');
|
||||
}
|
||||
}
|
||||
|
||||
return template.ToString();
|
||||
}
|
||||
|
||||
/// <summary>Gets the <see cref="AIFunction"/> wrapped by this resource.</summary>
|
||||
internal AIFunction AIFunction { get; }
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="McpServerResource"/> class.</summary>
|
||||
private AIFunctionMcpServerResource(AIFunction function, ResourceTemplate resourceTemplate)
|
||||
{
|
||||
AIFunction = function;
|
||||
ProtocolResourceTemplate = resourceTemplate;
|
||||
ProtocolResource = resourceTemplate.AsResource();
|
||||
|
||||
if (ProtocolResource is null)
|
||||
{
|
||||
_uriParser = UriTemplate.CreateParser(resourceTemplate.UriTemplate);
|
||||
_templateVariableNames = _uriParser.GetGroupNames().Where(n => n != "0").ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ResourceTemplate ProtocolResourceTemplate { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Resource? ProtocolResource { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<ReadResourceResult?> ReadAsync(
|
||||
RequestContext<ReadResourceRequestParams> request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(request);
|
||||
Throw.IfNull(request.Params);
|
||||
Throw.IfNull(request.Params.Uri);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Check to see if this URI template matches the request URI. If it doesn't, return null.
|
||||
// For templates, use the Regex to parse. For static resources, we can just compare the URIs.
|
||||
Match? match = null;
|
||||
if (_uriParser is not null)
|
||||
{
|
||||
match = _uriParser.Match(request.Params.Uri);
|
||||
if (!match.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (request.Params.Uri != ProtocolResource!.Uri)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build up the arguments for the AIFunction call, including all of the name/value pairs from the URI.
|
||||
AIFunctionArguments arguments = new()
|
||||
{
|
||||
Services = request.Services,
|
||||
Context = new Dictionary<object, object?>() { [typeof(RequestContext<ReadResourceRequestParams>)] = request }
|
||||
};
|
||||
|
||||
// For templates, populate the arguments from the URI template.
|
||||
if (match is not null)
|
||||
{
|
||||
foreach (string varName in _templateVariableNames)
|
||||
{
|
||||
if (match.Groups[varName] is { Success: true } value)
|
||||
{
|
||||
arguments[varName] = Uri.UnescapeDataString(value.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke the function.
|
||||
object? result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// And process the result.
|
||||
return result switch
|
||||
{
|
||||
ReadResourceResult readResourceResult => readResourceResult,
|
||||
|
||||
ResourceContents content => new()
|
||||
{
|
||||
Contents = [content],
|
||||
},
|
||||
|
||||
TextContent tc => new()
|
||||
{
|
||||
Contents = [new TextResourceContents() { Uri = request.Params!.Uri, MimeType = ProtocolResourceTemplate.MimeType, Text = tc.Text }],
|
||||
},
|
||||
|
||||
DataContent dc => new()
|
||||
{
|
||||
Contents = [new BlobResourceContents() { Uri = request.Params!.Uri, MimeType = dc.MediaType, Blob = dc.Base64Data.ToString() }],
|
||||
},
|
||||
|
||||
string text => new()
|
||||
{
|
||||
Contents = [new TextResourceContents() { Uri = request.Params!.Uri, MimeType = ProtocolResourceTemplate.MimeType, Text = text }],
|
||||
},
|
||||
|
||||
IEnumerable<ResourceContents> contents => new()
|
||||
{
|
||||
Contents = contents.ToList(),
|
||||
},
|
||||
|
||||
IEnumerable<AIContent> aiContents => new()
|
||||
{
|
||||
Contents = aiContents.Select<AIContent, ResourceContents>(
|
||||
ac => ac switch
|
||||
{
|
||||
TextContent tc => new TextResourceContents()
|
||||
{
|
||||
Uri = request.Params!.Uri,
|
||||
MimeType = ProtocolResourceTemplate.MimeType,
|
||||
Text = tc.Text
|
||||
},
|
||||
|
||||
DataContent dc => new BlobResourceContents()
|
||||
{
|
||||
Uri = request.Params!.Uri,
|
||||
MimeType = dc.MediaType,
|
||||
Blob = dc.Base64Data.ToString()
|
||||
},
|
||||
|
||||
_ => throw new InvalidOperationException($"Unsupported AIContent type '{ac.GetType()}' returned from resource function."),
|
||||
}).ToList(),
|
||||
},
|
||||
|
||||
IEnumerable<string> strings => new()
|
||||
{
|
||||
Contents = strings.Select<string, ResourceContents>(text => new TextResourceContents()
|
||||
{
|
||||
Uri = request.Params!.Uri,
|
||||
MimeType = ProtocolResourceTemplate.MimeType,
|
||||
Text = text
|
||||
}).ToList(),
|
||||
},
|
||||
|
||||
null => throw new InvalidOperationException("Null result returned from resource function."),
|
||||
|
||||
_ => throw new InvalidOperationException($"Unsupported result type '{result.GetType()}' returned from resource function."),
|
||||
};
|
||||
}
|
||||
}
|
||||
372
src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
Normal file
372
src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>Provides an <see cref="McpServerTool"/> that's implemented via an <see cref="AIFunction"/>.</summary>
|
||||
internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerTool Create(
|
||||
Delegate method,
|
||||
McpServerToolCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
options = DeriveOptions(method.Method, options);
|
||||
|
||||
return Create(method.Method, method.Target, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerTool Create(
|
||||
MethodInfo method,
|
||||
object? target,
|
||||
McpServerToolCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
options = DeriveOptions(method, options);
|
||||
|
||||
return Create(
|
||||
AIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, options)),
|
||||
options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerTool Create(
|
||||
MethodInfo method,
|
||||
Func<RequestContext<CallToolRequestParams>, object> createTargetFunc,
|
||||
McpServerToolCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
Throw.IfNull(createTargetFunc);
|
||||
|
||||
options = DeriveOptions(method, options);
|
||||
|
||||
return Create(
|
||||
AIFunctionFactory.Create(method, args =>
|
||||
{
|
||||
var request = (RequestContext<CallToolRequestParams>)args.Context![typeof(RequestContext<CallToolRequestParams>)]!;
|
||||
return createTargetFunc(request);
|
||||
}, CreateAIFunctionFactoryOptions(method, options)),
|
||||
options);
|
||||
}
|
||||
|
||||
// TODO: Fix the need for this suppression.
|
||||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111:ReflectionToDynamicallyAccessedMembers",
|
||||
Justification = "AIFunctionFactory ensures that the Type passed to AIFunctionFactoryOptions.CreateInstance has public constructors preserved")]
|
||||
internal static Func<Type, AIFunctionArguments, object> GetCreateInstanceFunc() =>
|
||||
static ([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] type, args) => args.Services is { } services ?
|
||||
ActivatorUtilities.CreateInstance(services, type) :
|
||||
Activator.CreateInstance(type)!;
|
||||
|
||||
private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
|
||||
MethodInfo method, McpServerToolCreateOptions? options) =>
|
||||
new()
|
||||
{
|
||||
Name = options?.Name ?? method.GetCustomAttribute<McpServerToolAttribute>()?.Name,
|
||||
Description = options?.Description,
|
||||
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
|
||||
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
|
||||
ConfigureParameterBinding = pi =>
|
||||
{
|
||||
if (pi.ParameterType == typeof(RequestContext<CallToolRequestParams>))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) => GetRequestContext(args),
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IMcpServer))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) => GetRequestContext(args)?.Server,
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IProgress<ProgressNotificationValue>))
|
||||
{
|
||||
// Bind IProgress<ProgressNotificationValue> to the progress token in the request,
|
||||
// if there is one. If we can't get one, return a nop progress.
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
{
|
||||
var requestContent = GetRequestContext(args);
|
||||
if (requestContent?.Server is { } server &&
|
||||
requestContent?.Params?.Meta?.ProgressToken is { } progressToken)
|
||||
{
|
||||
return new TokenProgress(server, progressToken);
|
||||
}
|
||||
|
||||
return NullProgress.Instance;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (options?.Services is { } services &&
|
||||
services.GetService<IServiceProviderIsService>() is { } ispis &&
|
||||
ispis.IsService(pi.ParameterType))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
GetRequestContext(args)?.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)?.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<CallToolRequestParams>? GetRequestContext(AIFunctionArguments args)
|
||||
{
|
||||
if (args.Context?.TryGetValue(typeof(RequestContext<CallToolRequestParams>), out var orc) is true &&
|
||||
orc is RequestContext<CallToolRequestParams> requestContext)
|
||||
{
|
||||
return requestContext;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
JsonSchemaCreateOptions = options?.SchemaCreateOptions,
|
||||
};
|
||||
|
||||
/// <summary>Creates an <see cref="McpServerTool"/> that wraps the specified <see cref="AIFunction"/>.</summary>
|
||||
public static new AIFunctionMcpServerTool Create(AIFunction function, McpServerToolCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(function);
|
||||
|
||||
Tool tool = new()
|
||||
{
|
||||
Name = options?.Name ?? function.Name,
|
||||
Description = options?.Description ?? function.Description,
|
||||
InputSchema = function.JsonSchema,
|
||||
};
|
||||
|
||||
if (options is not null)
|
||||
{
|
||||
if (options.Title is not null ||
|
||||
options.Idempotent is not null ||
|
||||
options.Destructive is not null ||
|
||||
options.OpenWorld is not null ||
|
||||
options.ReadOnly is not null)
|
||||
{
|
||||
tool.Annotations = new()
|
||||
{
|
||||
Title = options?.Title,
|
||||
IdempotentHint = options?.Idempotent,
|
||||
DestructiveHint = options?.Destructive,
|
||||
OpenWorldHint = options?.OpenWorld,
|
||||
ReadOnlyHint = options?.ReadOnly,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new AIFunctionMcpServerTool(function, tool, options?.Services);
|
||||
}
|
||||
|
||||
private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options)
|
||||
{
|
||||
McpServerToolCreateOptions newOptions = options?.Clone() ?? new();
|
||||
|
||||
if (method.GetCustomAttribute<McpServerToolAttribute>() is { } toolAttr)
|
||||
{
|
||||
newOptions.Name ??= toolAttr.Name;
|
||||
newOptions.Title ??= toolAttr.Title;
|
||||
|
||||
if (toolAttr._destructive is bool destructive)
|
||||
{
|
||||
newOptions.Destructive ??= destructive;
|
||||
}
|
||||
|
||||
if (toolAttr._idempotent is bool idempotent)
|
||||
{
|
||||
newOptions.Idempotent ??= idempotent;
|
||||
}
|
||||
|
||||
if (toolAttr._openWorld is bool openWorld)
|
||||
{
|
||||
newOptions.OpenWorld ??= openWorld;
|
||||
}
|
||||
|
||||
if (toolAttr._readOnly is bool readOnly)
|
||||
{
|
||||
newOptions.ReadOnly ??= readOnly;
|
||||
}
|
||||
}
|
||||
|
||||
if (method.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)
|
||||
{
|
||||
newOptions.Description ??= descAttr.Description;
|
||||
}
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
/// <summary>Gets the <see cref="AIFunction"/> wrapped by this tool.</summary>
|
||||
internal AIFunction AIFunction { get; }
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="McpServerTool"/> class.</summary>
|
||||
private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider)
|
||||
{
|
||||
AIFunction = function;
|
||||
ProtocolTool = tool;
|
||||
_logger = serviceProvider?.GetService<ILoggerFactory>()?.CreateLogger<McpServerTool>() ?? (ILogger)NullLogger.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Tool ProtocolTool { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<CallToolResponse> InvokeAsync(
|
||||
RequestContext<CallToolRequestParams> request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(request);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
AIFunctionArguments arguments = new()
|
||||
{
|
||||
Services = request.Services,
|
||||
Context = new Dictionary<object, object?>() { [typeof(RequestContext<CallToolRequestParams>)] = request }
|
||||
};
|
||||
|
||||
var argDict = request.Params?.Arguments;
|
||||
if (argDict is not null)
|
||||
{
|
||||
foreach (var kvp in argDict)
|
||||
{
|
||||
arguments[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
object? result;
|
||||
try
|
||||
{
|
||||
result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e) when (e is not OperationCanceledException)
|
||||
{
|
||||
ToolCallError(request.Params?.Name ?? string.Empty, e);
|
||||
|
||||
string errorMessage = e is McpException ?
|
||||
$"An error occurred invoking '{request.Params?.Name}': {e.Message}" :
|
||||
$"An error occurred invoking '{request.Params?.Name}'.";
|
||||
|
||||
return new()
|
||||
{
|
||||
IsError = true,
|
||||
Content = [new() { Text = errorMessage, Type = "text" }],
|
||||
};
|
||||
}
|
||||
|
||||
return result switch
|
||||
{
|
||||
AIContent aiContent => new()
|
||||
{
|
||||
Content = [aiContent.ToContent()],
|
||||
IsError = aiContent is ErrorContent
|
||||
},
|
||||
|
||||
null => new()
|
||||
{
|
||||
Content = []
|
||||
},
|
||||
|
||||
string text => new()
|
||||
{
|
||||
Content = [new() { Text = text, Type = "text" }]
|
||||
},
|
||||
|
||||
Content content => new()
|
||||
{
|
||||
Content = [content]
|
||||
},
|
||||
|
||||
IEnumerable<string> texts => new()
|
||||
{
|
||||
Content = [.. texts.Select(x => new Content() { Type = "text", Text = x ?? string.Empty })]
|
||||
},
|
||||
|
||||
IEnumerable<AIContent> contentItems => ConvertAIContentEnumerableToCallToolResponse(contentItems),
|
||||
|
||||
IEnumerable<Content> contents => new()
|
||||
{
|
||||
Content = [.. contents]
|
||||
},
|
||||
|
||||
CallToolResponse callToolResponse => callToolResponse,
|
||||
|
||||
_ => new()
|
||||
{
|
||||
Content = [new()
|
||||
{
|
||||
Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))),
|
||||
Type = "text"
|
||||
}]
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static CallToolResponse ConvertAIContentEnumerableToCallToolResponse(IEnumerable<AIContent> contentItems)
|
||||
{
|
||||
List<Content> contentList = [];
|
||||
bool allErrorContent = true;
|
||||
bool hasAny = false;
|
||||
|
||||
foreach (var item in contentItems)
|
||||
{
|
||||
contentList.Add(item.ToContent());
|
||||
hasAny = true;
|
||||
|
||||
if (allErrorContent && item is not ErrorContent)
|
||||
{
|
||||
allErrorContent = false;
|
||||
}
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
Content = contentList,
|
||||
IsError = allErrorContent && hasAny
|
||||
};
|
||||
}
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "\"{ToolName}\" threw an unhandled exception.")]
|
||||
private partial void ToolCallError(string toolName, Exception exception);
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
using ModelContextProtocol.Protocol;
|
||||
|
||||
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 ValueTask<GetPromptResult> GetAsync(
|
||||
RequestContext<GetPromptRequestParams> request,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
_innerPrompt.GetAsync(request, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => _innerPrompt.ToString();
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>Provides an <see cref="McpServerResource"/> that delegates all operations to an inner <see cref="McpServerResource"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This is recommended as a base type when building resources that can be chained around an underlying <see cref="McpServerResource"/>.
|
||||
/// The default implementation simply passes each call to the inner resource instance.
|
||||
/// </remarks>
|
||||
public abstract class DelegatingMcpServerResource : McpServerResource
|
||||
{
|
||||
private readonly McpServerResource _innerResource;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="DelegatingMcpServerResource"/> class around the specified <paramref name="innerResource"/>.</summary>
|
||||
/// <param name="innerResource">The inner resource wrapped by this delegating resource.</param>
|
||||
protected DelegatingMcpServerResource(McpServerResource innerResource)
|
||||
{
|
||||
Throw.IfNull(innerResource);
|
||||
_innerResource = innerResource;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Resource? ProtocolResource => _innerResource.ProtocolResource;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ResourceTemplate ProtocolResourceTemplate => _innerResource.ProtocolResourceTemplate;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueTask<ReadResourceResult?> ReadAsync(RequestContext<ReadResourceRequestParams> request, CancellationToken cancellationToken = default) =>
|
||||
_innerResource.ReadAsync(request, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => _innerResource.ToString();
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>Provides an <see cref="McpServerTool"/> that delegates all operations to an inner <see cref="McpServerTool"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This is recommended as a base type when building tools that can be chained around an underlying <see cref="McpServerTool"/>.
|
||||
/// The default implementation simply passes each call to the inner tool instance.
|
||||
/// </remarks>
|
||||
public abstract class DelegatingMcpServerTool : McpServerTool
|
||||
{
|
||||
private readonly McpServerTool _innerTool;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="DelegatingMcpServerTool"/> class around the specified <paramref name="innerTool"/>.</summary>
|
||||
/// <param name="innerTool">The inner tool wrapped by this delegating tool.</param>
|
||||
protected DelegatingMcpServerTool(McpServerTool innerTool)
|
||||
{
|
||||
Throw.IfNull(innerTool);
|
||||
_innerTool = innerTool;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Tool ProtocolTool => _innerTool.ProtocolTool;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueTask<CallToolResponse> InvokeAsync(
|
||||
RequestContext<CallToolRequestParams> request,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
_innerTool.InvokeAsync(request, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => _innerTool.ToString();
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
using ModelContextProtocol.Protocol;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
internal sealed class DestinationBoundMcpServer(McpServer server, ITransport? transport) : IMcpServer
|
||||
{
|
||||
public string EndpointName => server.EndpointName;
|
||||
public ClientCapabilities? ClientCapabilities => server.ClientCapabilities;
|
||||
public Implementation? ClientInfo => server.ClientInfo;
|
||||
public McpServerOptions ServerOptions => server.ServerOptions;
|
||||
public IServiceProvider? Services => server.Services;
|
||||
public LoggingLevel? LoggingLevel => server.LoggingLevel;
|
||||
|
||||
public ValueTask DisposeAsync() => server.DisposeAsync();
|
||||
|
||||
public IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler) => server.RegisterNotificationHandler(method, handler);
|
||||
|
||||
// This will throw because the server must already be running for this class to be constructed, but it should give us a good Exception message.
|
||||
public Task RunAsync(CancellationToken cancellationToken) => server.RunAsync(cancellationToken);
|
||||
|
||||
public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Debug.Assert(message.RelatedTransport is null);
|
||||
message.RelatedTransport = transport;
|
||||
return server.SendMessageAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Debug.Assert(request.RelatedTransport is null);
|
||||
request.RelatedTransport = transport;
|
||||
return server.SendRequestAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
62
src/ModelContextProtocol.Core/Server/IMcpServer.cs
Normal file
62
src/ModelContextProtocol.Core/Server/IMcpServer.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client.
|
||||
/// </summary>
|
||||
public interface IMcpServer : IMcpEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the capabilities supported by the client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These capabilities are established during the initialization handshake and indicate
|
||||
/// which features the client supports, such as sampling, roots, and other
|
||||
/// protocol-specific functionality.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Server implementations can check these capabilities to determine which features
|
||||
/// are available when interacting with the client.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
ClientCapabilities? ClientCapabilities { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version and implementation information of the connected client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property contains identification information about the client that has connected to this server,
|
||||
/// including its name and version. This information is provided by the client during initialization.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Server implementations can use this information for logging, tracking client versions,
|
||||
/// or implementing client-specific behaviors.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
Implementation? ClientInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the options used to construct this server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These options define the server's capabilities, protocol version, and other configuration
|
||||
/// settings that were used to initialize the server.
|
||||
/// </remarks>
|
||||
McpServerOptions ServerOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service provider for the server.
|
||||
/// </summary>
|
||||
IServiceProvider? Services { get; }
|
||||
|
||||
/// <summary>Gets the last logging level set by the client, or <see langword="null"/> if it's never been set.</summary>
|
||||
LoggingLevel? LoggingLevel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Runs the server, listening for and handling client requests.
|
||||
/// </summary>
|
||||
Task RunAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
10
src/ModelContextProtocol.Core/Server/IMcpServerPrimitive.cs
Normal file
10
src/ModelContextProtocol.Core/Server/IMcpServerPrimitive.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an MCP server primitive, like a tool or a prompt.
|
||||
/// </summary>
|
||||
public interface IMcpServerPrimitive
|
||||
{
|
||||
/// <summary>Gets the unique identifier of the primitive.</summary>
|
||||
string Id { get; }
|
||||
}
|
||||
564
src/ModelContextProtocol.Core/Server/McpServer.cs
Normal file
564
src/ModelContextProtocol.Core/Server/McpServer.cs
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <inheritdoc />
|
||||
internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||
{
|
||||
internal static Implementation DefaultImplementation { get; } = new()
|
||||
{
|
||||
Name = DefaultAssemblyName.Name ?? nameof(McpServer),
|
||||
Version = DefaultAssemblyName.Version?.ToString() ?? "1.0.0",
|
||||
};
|
||||
|
||||
private readonly ITransport _sessionTransport;
|
||||
private readonly bool _servicesScopePerRequest;
|
||||
private readonly List<Action> _disposables = [];
|
||||
|
||||
private readonly string _serverOnlyEndpointName;
|
||||
private string? _endpointName;
|
||||
private int _started;
|
||||
|
||||
/// <summary>Holds a boxed <see cref="LoggingLevel"/> value for the server.</summary>
|
||||
/// <remarks>
|
||||
/// Initialized to non-null the first time SetLevel is used. This is stored as a strong box
|
||||
/// rather than a nullable to be able to manipulate it atomically.
|
||||
/// </remarks>
|
||||
private StrongBox<LoggingLevel>? _loggingLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="McpServer"/>.
|
||||
/// </summary>
|
||||
/// <param name="transport">Transport to use for the server representing an already-established session.</param>
|
||||
/// <param name="options">Configuration options for this server, including capabilities.
|
||||
/// Make sure to accurately reflect exactly what capabilities the server supports and does not support.</param>
|
||||
/// <param name="loggerFactory">Logger factory to use for logging</param>
|
||||
/// <param name="serviceProvider">Optional service provider to use for dependency injection</param>
|
||||
/// <exception cref="McpException">The server was incorrectly configured.</exception>
|
||||
public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory? loggerFactory, IServiceProvider? serviceProvider)
|
||||
: base(loggerFactory)
|
||||
{
|
||||
Throw.IfNull(transport);
|
||||
Throw.IfNull(options);
|
||||
|
||||
options ??= new();
|
||||
|
||||
_sessionTransport = transport;
|
||||
ServerOptions = options;
|
||||
Services = serviceProvider;
|
||||
_serverOnlyEndpointName = $"Server ({options.ServerInfo?.Name ?? DefaultImplementation.Name} {options.ServerInfo?.Version ?? DefaultImplementation.Version})";
|
||||
_servicesScopePerRequest = options.ScopeRequests;
|
||||
|
||||
ClientInfo = options.KnownClientInfo;
|
||||
UpdateEndpointNameWithClientInfo();
|
||||
|
||||
// Configure all request handlers based on the supplied options.
|
||||
ServerCapabilities = new();
|
||||
ConfigureInitialize(options);
|
||||
ConfigureTools(options);
|
||||
ConfigurePrompts(options);
|
||||
ConfigureResources(options);
|
||||
ConfigureLogging(options);
|
||||
ConfigureCompletion(options);
|
||||
ConfigureExperimental(options);
|
||||
ConfigurePing();
|
||||
|
||||
// Register any notification handlers that were provided.
|
||||
if (options.Capabilities?.NotificationHandlers is { } notificationHandlers)
|
||||
{
|
||||
NotificationHandlers.RegisterRange(notificationHandlers);
|
||||
}
|
||||
|
||||
// Now that everything has been configured, subscribe to any necessary notifications.
|
||||
if (transport is not StreamableHttpServerTransport streamableHttpTransport || streamableHttpTransport.Stateless is false)
|
||||
{
|
||||
Register(ServerOptions.Capabilities?.Tools?.ToolCollection, NotificationMethods.ToolListChangedNotification);
|
||||
Register(ServerOptions.Capabilities?.Prompts?.PromptCollection, NotificationMethods.PromptListChangedNotification);
|
||||
Register(ServerOptions.Capabilities?.Resources?.ResourceCollection, NotificationMethods.ResourceListChangedNotification);
|
||||
|
||||
void Register<TPrimitive>(McpServerPrimitiveCollection<TPrimitive>? collection, string notificationMethod)
|
||||
where TPrimitive : IMcpServerPrimitive
|
||||
{
|
||||
if (collection is not null)
|
||||
{
|
||||
EventHandler changed = (sender, e) => _ = this.SendNotificationAsync(notificationMethod);
|
||||
collection.Changed += changed;
|
||||
_disposables.Add(() => collection.Changed -= changed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And initialize the session.
|
||||
InitializeSession(transport);
|
||||
}
|
||||
|
||||
public ServerCapabilities ServerCapabilities { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ClientCapabilities? ClientCapabilities { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Implementation? ClientInfo { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public McpServerOptions ServerOptions { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IServiceProvider? Services { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string EndpointName => _endpointName ?? _serverOnlyEndpointName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LoggingLevel? LoggingLevel => _loggingLevel?.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RunAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _started, 1) != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(RunAsync)} must only be called once.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StartSession(_sessionTransport, fullSessionCancellationToken: cancellationToken);
|
||||
await MessageProcessingTask.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeUnsynchronizedAsync()
|
||||
{
|
||||
_disposables.ForEach(d => d());
|
||||
await base.DisposeUnsynchronizedAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void ConfigurePing()
|
||||
{
|
||||
SetHandler(RequestMethods.Ping,
|
||||
async (request, _) => new PingResult(),
|
||||
McpJsonUtilities.JsonContext.Default.JsonNode,
|
||||
McpJsonUtilities.JsonContext.Default.PingResult);
|
||||
}
|
||||
|
||||
private void ConfigureInitialize(McpServerOptions options)
|
||||
{
|
||||
RequestHandlers.Set(RequestMethods.Initialize,
|
||||
async (request, _, _) =>
|
||||
{
|
||||
ClientCapabilities = request?.Capabilities ?? new();
|
||||
ClientInfo = request?.ClientInfo;
|
||||
|
||||
// Use the ClientInfo to update the session EndpointName for logging.
|
||||
UpdateEndpointNameWithClientInfo();
|
||||
GetSessionOrThrow().EndpointName = EndpointName;
|
||||
|
||||
return new InitializeResult
|
||||
{
|
||||
ProtocolVersion = options.ProtocolVersion,
|
||||
Instructions = options.ServerInstructions,
|
||||
ServerInfo = options.ServerInfo ?? DefaultImplementation,
|
||||
Capabilities = ServerCapabilities ?? new(),
|
||||
};
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.InitializeResult);
|
||||
}
|
||||
|
||||
private void ConfigureCompletion(McpServerOptions options)
|
||||
{
|
||||
if (options.Capabilities?.Completions is not { } completionsCapability)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCapabilities.Completions = new()
|
||||
{
|
||||
CompleteHandler = completionsCapability.CompleteHandler ?? (static async (_, __) => new CompleteResult())
|
||||
};
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.CompletionComplete,
|
||||
ServerCapabilities.Completions.CompleteHandler,
|
||||
McpJsonUtilities.JsonContext.Default.CompleteRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CompleteResult);
|
||||
}
|
||||
|
||||
private void ConfigureExperimental(McpServerOptions options)
|
||||
{
|
||||
ServerCapabilities.Experimental = options.Capabilities?.Experimental;
|
||||
}
|
||||
|
||||
private void ConfigureResources(McpServerOptions options)
|
||||
{
|
||||
if (options.Capabilities?.Resources is not { } resourcesCapability)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCapabilities.Resources = new();
|
||||
|
||||
var listResourcesHandler = resourcesCapability.ListResourcesHandler ?? (static async (_, __) => new ListResourcesResult());
|
||||
var listResourceTemplatesHandler = resourcesCapability.ListResourceTemplatesHandler ?? (static async (_, __) => new ListResourceTemplatesResult());
|
||||
var readResourceHandler = resourcesCapability.ReadResourceHandler ?? (static async (request, _) => throw new McpException($"Unknown resource URI: '{request.Params?.Uri}'", McpErrorCode.InvalidParams));
|
||||
var subscribeHandler = resourcesCapability.SubscribeToResourcesHandler ?? (static async (_, __) => new EmptyResult());
|
||||
var unsubscribeHandler = resourcesCapability.UnsubscribeFromResourcesHandler ?? (static async (_, __) => new EmptyResult());
|
||||
var resources = resourcesCapability.ResourceCollection;
|
||||
var listChanged = resourcesCapability.ListChanged;
|
||||
var subcribe = resourcesCapability.Subscribe;
|
||||
|
||||
// Handle resources provided via DI.
|
||||
if (resources is { IsEmpty: false })
|
||||
{
|
||||
var originalListResourcesHandler = listResourcesHandler;
|
||||
listResourcesHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
ListResourcesResult result = originalListResourcesHandler is not null ?
|
||||
await originalListResourcesHandler(request, cancellationToken).ConfigureAwait(false) :
|
||||
new();
|
||||
|
||||
if (request.Params?.Cursor is null)
|
||||
{
|
||||
result.Resources.AddRange(resources.Select(t => t.ProtocolResource).OfType<Resource>());
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
var originalListResourceTemplatesHandler = listResourceTemplatesHandler;
|
||||
listResourceTemplatesHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
ListResourceTemplatesResult result = originalListResourceTemplatesHandler is not null ?
|
||||
await originalListResourceTemplatesHandler(request, cancellationToken).ConfigureAwait(false) :
|
||||
new();
|
||||
|
||||
if (request.Params?.Cursor is null)
|
||||
{
|
||||
result.ResourceTemplates.AddRange(resources.Where(t => t.IsTemplated).Select(t => t.ProtocolResourceTemplate));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Synthesize read resource handler, which covers both resources and resource templates.
|
||||
var originalReadResourceHandler = readResourceHandler;
|
||||
readResourceHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
if (request.Params?.Uri is string uri)
|
||||
{
|
||||
// First try an O(1) lookup by exact match.
|
||||
if (resources.TryGetPrimitive(uri, out var resource))
|
||||
{
|
||||
if (await resource.ReadAsync(request, cancellationToken).ConfigureAwait(false) is { } result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to an O(N) lookup, trying to match against each URI template.
|
||||
// The number of templates is controlled by the server developer, and the number is expected to be
|
||||
// not terribly large. If that changes, this can be tweaked to enable a more efficient lookup.
|
||||
foreach (var resourceTemplate in resources)
|
||||
{
|
||||
if (await resourceTemplate.ReadAsync(request, cancellationToken).ConfigureAwait(false) is { } result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally fall back to the handler.
|
||||
return await originalReadResourceHandler(request, cancellationToken).ConfigureAwait(false);
|
||||
};
|
||||
|
||||
listChanged = true;
|
||||
|
||||
// TODO: Implement subscribe/unsubscribe logic for resource and resource template collections.
|
||||
// subcribe = true;
|
||||
}
|
||||
|
||||
ServerCapabilities.Resources.ListResourcesHandler = listResourcesHandler;
|
||||
ServerCapabilities.Resources.ListResourceTemplatesHandler = listResourceTemplatesHandler;
|
||||
ServerCapabilities.Resources.ReadResourceHandler = readResourceHandler;
|
||||
ServerCapabilities.Resources.ResourceCollection = resources;
|
||||
ServerCapabilities.Resources.SubscribeToResourcesHandler = subscribeHandler;
|
||||
ServerCapabilities.Resources.UnsubscribeFromResourcesHandler = unsubscribeHandler;
|
||||
ServerCapabilities.Resources.ListChanged = listChanged;
|
||||
ServerCapabilities.Resources.Subscribe = subcribe;
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesList,
|
||||
listResourcesHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesTemplatesList,
|
||||
listResourceTemplatesHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesRead,
|
||||
readResourceHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesSubscribe,
|
||||
subscribeHandler,
|
||||
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesUnsubscribe,
|
||||
unsubscribeHandler,
|
||||
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||
}
|
||||
|
||||
private void ConfigurePrompts(McpServerOptions options)
|
||||
{
|
||||
if (options.Capabilities?.Prompts is not { } promptsCapability)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCapabilities.Prompts = new();
|
||||
|
||||
var listPromptsHandler = promptsCapability.ListPromptsHandler ?? (static async (_, __) => new ListPromptsResult());
|
||||
var getPromptHandler = promptsCapability.GetPromptHandler ?? (static async (request, _) => throw new McpException($"Unknown prompt: '{request.Params?.Name}'", McpErrorCode.InvalidParams));
|
||||
var prompts = promptsCapability.PromptCollection;
|
||||
var listChanged = promptsCapability.ListChanged;
|
||||
|
||||
// Handle tools provided via DI by augmenting the handlers to incorporate them.
|
||||
if (prompts is { IsEmpty: false })
|
||||
{
|
||||
var originalListPromptsHandler = listPromptsHandler;
|
||||
listPromptsHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
ListPromptsResult result = originalListPromptsHandler is not null ?
|
||||
await originalListPromptsHandler(request, cancellationToken).ConfigureAwait(false) :
|
||||
new();
|
||||
|
||||
if (request.Params?.Cursor is null)
|
||||
{
|
||||
result.Prompts.AddRange(prompts.Select(t => t.ProtocolPrompt));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
var originalGetPromptHandler = getPromptHandler;
|
||||
getPromptHandler = (request, cancellationToken) =>
|
||||
{
|
||||
if (request.Params is not null &&
|
||||
prompts.TryGetPrimitive(request.Params.Name, out var prompt))
|
||||
{
|
||||
return prompt.GetAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
return originalGetPromptHandler(request, cancellationToken);
|
||||
};
|
||||
|
||||
listChanged = true;
|
||||
}
|
||||
|
||||
ServerCapabilities.Prompts.ListPromptsHandler = listPromptsHandler;
|
||||
ServerCapabilities.Prompts.GetPromptHandler = getPromptHandler;
|
||||
ServerCapabilities.Prompts.PromptCollection = prompts;
|
||||
ServerCapabilities.Prompts.ListChanged = listChanged;
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.PromptsList,
|
||||
listPromptsHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.PromptsGet,
|
||||
getPromptHandler,
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptResult);
|
||||
}
|
||||
|
||||
private void ConfigureTools(McpServerOptions options)
|
||||
{
|
||||
if (options.Capabilities?.Tools is not { } toolsCapability)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCapabilities.Tools = new();
|
||||
|
||||
var listToolsHandler = toolsCapability.ListToolsHandler ?? (static async (_, __) => new ListToolsResult());
|
||||
var callToolHandler = toolsCapability.CallToolHandler ?? (static async (request, _) => throw new McpException($"Unknown tool: '{request.Params?.Name}'", McpErrorCode.InvalidParams));
|
||||
var tools = toolsCapability.ToolCollection;
|
||||
var listChanged = toolsCapability.ListChanged;
|
||||
|
||||
// Handle tools provided via DI by augmenting the handlers to incorporate them.
|
||||
if (tools is { IsEmpty: false })
|
||||
{
|
||||
var originalListToolsHandler = listToolsHandler;
|
||||
listToolsHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
ListToolsResult result = originalListToolsHandler is not null ?
|
||||
await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false) :
|
||||
new();
|
||||
|
||||
if (request.Params?.Cursor is null)
|
||||
{
|
||||
result.Tools.AddRange(tools.Select(t => t.ProtocolTool));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
var originalCallToolHandler = callToolHandler;
|
||||
callToolHandler = (request, cancellationToken) =>
|
||||
{
|
||||
if (request.Params is not null &&
|
||||
tools.TryGetPrimitive(request.Params.Name, out var tool))
|
||||
{
|
||||
return tool.InvokeAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
return originalCallToolHandler(request, cancellationToken);
|
||||
};
|
||||
|
||||
listChanged = true;
|
||||
}
|
||||
|
||||
ServerCapabilities.Tools.ListToolsHandler = listToolsHandler;
|
||||
ServerCapabilities.Tools.CallToolHandler = callToolHandler;
|
||||
ServerCapabilities.Tools.ToolCollection = tools;
|
||||
ServerCapabilities.Tools.ListChanged = listChanged;
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ToolsList,
|
||||
listToolsHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ToolsCall,
|
||||
callToolHandler,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolResponse);
|
||||
}
|
||||
|
||||
private void ConfigureLogging(McpServerOptions options)
|
||||
{
|
||||
// We don't require that the handler be provided, as we always store the provided log level to the server.
|
||||
var setLoggingLevelHandler = options.Capabilities?.Logging?.SetLoggingLevelHandler;
|
||||
|
||||
ServerCapabilities.Logging = new();
|
||||
ServerCapabilities.Logging.SetLoggingLevelHandler = setLoggingLevelHandler;
|
||||
|
||||
RequestHandlers.Set(
|
||||
RequestMethods.LoggingSetLevel,
|
||||
(request, destinationTransport, cancellationToken) =>
|
||||
{
|
||||
// Store the provided level.
|
||||
if (request is not null)
|
||||
{
|
||||
if (_loggingLevel is null)
|
||||
{
|
||||
Interlocked.CompareExchange(ref _loggingLevel, new(request.Level), null);
|
||||
}
|
||||
|
||||
_loggingLevel.Value = request.Level;
|
||||
}
|
||||
|
||||
// If a handler was provided, now delegate to it.
|
||||
if (setLoggingLevelHandler is not null)
|
||||
{
|
||||
return InvokeHandlerAsync(setLoggingLevelHandler, request, destinationTransport, cancellationToken);
|
||||
}
|
||||
|
||||
// Otherwise, consider it handled.
|
||||
return new ValueTask<EmptyResult>(EmptyResult.Instance);
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.SetLevelRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||
}
|
||||
|
||||
private ValueTask<TResult> InvokeHandlerAsync<TParams, TResult>(
|
||||
Func<RequestContext<TParams>, CancellationToken, ValueTask<TResult>> handler,
|
||||
TParams? args,
|
||||
ITransport? destinationTransport = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _servicesScopePerRequest ?
|
||||
InvokeScopedAsync(handler, args, cancellationToken) :
|
||||
handler(new(new DestinationBoundMcpServer(this, destinationTransport)) { Params = args }, cancellationToken);
|
||||
|
||||
async ValueTask<TResult> InvokeScopedAsync(
|
||||
Func<RequestContext<TParams>, CancellationToken, ValueTask<TResult>> handler,
|
||||
TParams? args,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scope = Services?.GetService<IServiceScopeFactory>()?.CreateAsyncScope();
|
||||
try
|
||||
{
|
||||
return await handler(
|
||||
new RequestContext<TParams>(new DestinationBoundMcpServer(this, destinationTransport))
|
||||
{
|
||||
Services = scope?.ServiceProvider ?? Services,
|
||||
Params = args
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (scope is not null)
|
||||
{
|
||||
await scope.Value.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetHandler<TRequest, TResponse>(
|
||||
string method,
|
||||
Func<RequestContext<TRequest>, CancellationToken, ValueTask<TResponse>> handler,
|
||||
JsonTypeInfo<TRequest> requestTypeInfo,
|
||||
JsonTypeInfo<TResponse> responseTypeInfo)
|
||||
{
|
||||
RequestHandlers.Set(method,
|
||||
(request, destinationTransport, cancellationToken) =>
|
||||
InvokeHandlerAsync(handler, request, destinationTransport, cancellationToken),
|
||||
requestTypeInfo, responseTypeInfo);
|
||||
}
|
||||
|
||||
private void UpdateEndpointNameWithClientInfo()
|
||||
{
|
||||
if (ClientInfo is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_endpointName = $"{_serverOnlyEndpointName}, Client ({ClientInfo.Name} {ClientInfo.Version})";
|
||||
}
|
||||
|
||||
/// <summary>Maps a <see cref="LogLevel"/> to a <see cref="LoggingLevel"/>.</summary>
|
||||
internal static LoggingLevel ToLoggingLevel(LogLevel level) =>
|
||||
level switch
|
||||
{
|
||||
LogLevel.Trace => Protocol.LoggingLevel.Debug,
|
||||
LogLevel.Debug => Protocol.LoggingLevel.Debug,
|
||||
LogLevel.Information => Protocol.LoggingLevel.Info,
|
||||
LogLevel.Warning => Protocol.LoggingLevel.Warning,
|
||||
LogLevel.Error => Protocol.LoggingLevel.Error,
|
||||
LogLevel.Critical => Protocol.LoggingLevel.Critical,
|
||||
_ => Protocol.LoggingLevel.Emergency,
|
||||
};
|
||||
}
|
||||
354
src/ModelContextProtocol.Core/Server/McpServerExtensions.cs
Normal file
354
src/ModelContextProtocol.Core/Server/McpServerExtensions.cs
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for interacting with an <see cref="IMcpServer"/> instance.
|
||||
/// </summary>
|
||||
public static class McpServerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Requests to sample an LLM via the client using the specified request parameters.
|
||||
/// </summary>
|
||||
/// <param name="server">The server instance initiating the request.</param>
|
||||
/// <param name="request">The parameters for the sampling request.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A task containing the sampling result from the client.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The client does not support sampling.</exception>
|
||||
/// <remarks>
|
||||
/// This method requires the client to support sampling capabilities.
|
||||
/// It allows detailed control over sampling parameters including messages, system prompt, temperature,
|
||||
/// and token limits.
|
||||
/// </remarks>
|
||||
public static ValueTask<CreateMessageResult> SampleAsync(
|
||||
this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
ThrowIfSamplingUnsupported(server);
|
||||
|
||||
return server.SendRequestAsync(
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
request,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests to sample an LLM via the client using the provided chat messages and options.
|
||||
/// </summary>
|
||||
/// <param name="server">The server initiating the request.</param>
|
||||
/// <param name="messages">The messages to send as part of the request.</param>
|
||||
/// <param name="options">The options to use for the request, including model parameters and constraints.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task containing the chat response from the model.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The client does not support sampling.</exception>
|
||||
/// <remarks>
|
||||
/// This method converts the provided chat messages into a format suitable for the sampling API,
|
||||
/// handling different content types such as text, images, and audio.
|
||||
/// </remarks>
|
||||
public static async Task<ChatResponse> SampleAsync(
|
||||
this IMcpServer server,
|
||||
IEnumerable<ChatMessage> messages, ChatOptions? options = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
Throw.IfNull(messages);
|
||||
|
||||
StringBuilder? systemPrompt = null;
|
||||
|
||||
List<SamplingMessage> samplingMessages = [];
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (message.Role == ChatRole.System)
|
||||
{
|
||||
if (systemPrompt is null)
|
||||
{
|
||||
systemPrompt = new();
|
||||
}
|
||||
else
|
||||
{
|
||||
systemPrompt.AppendLine();
|
||||
}
|
||||
|
||||
systemPrompt.Append(message.Text);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.Role == ChatRole.User || message.Role == ChatRole.Assistant)
|
||||
{
|
||||
Role role = message.Role == ChatRole.User ? Role.User : Role.Assistant;
|
||||
|
||||
foreach (var content in message.Contents)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case TextContent textContent:
|
||||
samplingMessages.Add(new()
|
||||
{
|
||||
Role = role,
|
||||
Content = new()
|
||||
{
|
||||
Type = "text",
|
||||
Text = textContent.Text,
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
case DataContent dataContent when dataContent.HasTopLevelMediaType("image") || dataContent.HasTopLevelMediaType("audio"):
|
||||
samplingMessages.Add(new()
|
||||
{
|
||||
Role = role,
|
||||
Content = new()
|
||||
{
|
||||
Type = dataContent.HasTopLevelMediaType("image") ? "image" : "audio",
|
||||
MimeType = dataContent.MediaType,
|
||||
Data = dataContent.Base64Data.ToString(),
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModelPreferences? modelPreferences = null;
|
||||
if (options?.ModelId is { } modelId)
|
||||
{
|
||||
modelPreferences = new() { Hints = [new() { Name = modelId }] };
|
||||
}
|
||||
|
||||
var result = await server.SampleAsync(new()
|
||||
{
|
||||
Messages = samplingMessages,
|
||||
MaxTokens = options?.MaxOutputTokens,
|
||||
StopSequences = options?.StopSequences?.ToArray(),
|
||||
SystemPrompt = systemPrompt?.ToString(),
|
||||
Temperature = options?.Temperature,
|
||||
ModelPreferences = modelPreferences,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, [result.Content.ToAIContent()]))
|
||||
{
|
||||
ModelId = result.Model,
|
||||
FinishReason = result.StopReason switch
|
||||
{
|
||||
"maxTokens" => ChatFinishReason.Length,
|
||||
"endTurn" or "stopSequence" or _ => ChatFinishReason.Stop,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="IChatClient"/> wrapper that can be used to send sampling requests to the client.
|
||||
/// </summary>
|
||||
/// <param name="server">The server to be wrapped as an <see cref="IChatClient"/>.</param>
|
||||
/// <returns>The <see cref="IChatClient"/> that can be used to issue sampling requests to the client.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The client does not support sampling.</exception>
|
||||
public static IChatClient AsSamplingChatClient(this IMcpServer server)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
ThrowIfSamplingUnsupported(server);
|
||||
|
||||
return new SamplingChatClient(server);
|
||||
}
|
||||
|
||||
/// <summary>Gets an <see cref="ILogger"/> on which logged messages will be sent as notifications to the client.</summary>
|
||||
/// <param name="server">The server to wrap as an <see cref="ILogger"/>.</param>
|
||||
/// <returns>An <see cref="ILogger"/> that can be used to log to the client..</returns>
|
||||
public static ILoggerProvider AsClientLoggerProvider(this IMcpServer server)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
|
||||
return new ClientLoggerProvider(server);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests the client to list the roots it exposes.
|
||||
/// </summary>
|
||||
/// <param name="server">The server initiating the request.</param>
|
||||
/// <param name="request">The parameters for the list roots request.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A task containing the list of roots exposed by the client.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The client does not support roots.</exception>
|
||||
/// <remarks>
|
||||
/// This method requires the client to support the roots capability.
|
||||
/// Root resources allow clients to expose a hierarchical structure of resources that can be
|
||||
/// navigated and accessed by the server. These resources might include file systems, databases,
|
||||
/// or other structured data sources that the client makes available through the protocol.
|
||||
/// </remarks>
|
||||
public static ValueTask<ListRootsResult> RequestRootsAsync(
|
||||
this IMcpServer server, ListRootsRequestParams request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
ThrowIfRootsUnsupported(server);
|
||||
|
||||
return server.SendRequestAsync(
|
||||
RequestMethods.RootsList,
|
||||
request,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests additional information from the user via the client, allowing the server to elicit structured data.
|
||||
/// </summary>
|
||||
/// <param name="server">The server initiating the request.</param>
|
||||
/// <param name="request">The parameters for the elicitation request.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A task containing the elicitation result.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The client does not support elicitation.</exception>
|
||||
/// <remarks>
|
||||
/// This method requires the client to support the elicitation capability.
|
||||
/// </remarks>
|
||||
public static ValueTask<ElicitResult> ElicitAsync(
|
||||
this IMcpServer server, ElicitRequestParams request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
ThrowIfElicitationUnsupported(server);
|
||||
|
||||
return server.SendRequestAsync(
|
||||
RequestMethods.ElicitationCreate,
|
||||
request,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private static void ThrowIfSamplingUnsupported(IMcpServer server)
|
||||
{
|
||||
if (server.ClientCapabilities?.Sampling is null)
|
||||
{
|
||||
if (server.ServerOptions.KnownClientInfo is not null)
|
||||
{
|
||||
throw new InvalidOperationException("Sampling is not supported in stateless mode.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Client does not support sampling.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowIfRootsUnsupported(IMcpServer server)
|
||||
{
|
||||
if (server.ClientCapabilities?.Roots is null)
|
||||
{
|
||||
if (server.ServerOptions.KnownClientInfo is not null)
|
||||
{
|
||||
throw new InvalidOperationException("Roots are not supported in stateless mode.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Client does not support roots.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowIfElicitationUnsupported(IMcpServer server)
|
||||
{
|
||||
if (server.ClientCapabilities?.Elicitation is null)
|
||||
{
|
||||
if (server.ServerOptions.KnownClientInfo is not null)
|
||||
{
|
||||
throw new InvalidOperationException("Elicitation is not supported in stateless mode.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Client does not support elicitation requests.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Provides an <see cref="IChatClient"/> implementation that's implemented via client sampling.</summary>
|
||||
private sealed class SamplingChatClient(IMcpServer server) : IChatClient
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
server.SampleAsync(messages, options, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
async IAsyncEnumerable<ChatResponseUpdate> IChatClient.GetStreamingResponseAsync(
|
||||
IEnumerable<ChatMessage> messages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var update in response.ToChatResponseUpdates())
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
object? IChatClient.GetService(Type serviceType, object? serviceKey)
|
||||
{
|
||||
Throw.IfNull(serviceType);
|
||||
|
||||
return
|
||||
serviceKey is not null ? null :
|
||||
serviceType.IsInstanceOfType(this) ? this :
|
||||
serviceType.IsInstanceOfType(server) ? server :
|
||||
null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IDisposable.Dispose() { } // nop
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides an <see cref="ILoggerProvider"/> implementation for creating loggers
|
||||
/// that send logging message notifications to the client for logged messages.
|
||||
/// </summary>
|
||||
private sealed class ClientLoggerProvider(IMcpServer server) : ILoggerProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
Throw.IfNull(categoryName);
|
||||
|
||||
return new ClientLogger(server, categoryName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IDisposable.Dispose() { }
|
||||
|
||||
private sealed class ClientLogger(IMcpServer server, string categoryName) : ILogger
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull =>
|
||||
null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) =>
|
||||
server?.LoggingLevel is { } loggingLevel &&
|
||||
McpServer.ToLoggingLevel(logLevel) >= loggingLevel;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Throw.IfNull(formatter);
|
||||
|
||||
Log(logLevel, formatter(state, exception));
|
||||
|
||||
void Log(LogLevel logLevel, string message)
|
||||
{
|
||||
_ = server.SendNotificationAsync(NotificationMethods.LoggingMessageNotification, new LoggingMessageNotificationParams()
|
||||
{
|
||||
Level = McpServer.ToLoggingLevel(logLevel),
|
||||
Data = JsonSerializer.SerializeToElement(message, McpJsonUtilities.JsonContext.Default.String),
|
||||
Logger = categoryName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/ModelContextProtocol.Core/Server/McpServerFactory.cs
Normal file
36
src/ModelContextProtocol.Core/Server/McpServerFactory.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a factory for creating <see cref="IMcpServer"/> instances.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the recommended way to create <see cref="IMcpServer"/> instances.
|
||||
/// The factory handles proper initialization of server instances with the required dependencies.
|
||||
/// </remarks>
|
||||
public static class McpServerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of an <see cref="IMcpServer"/>.
|
||||
/// </summary>
|
||||
/// <param name="transport">Transport to use for the server representing an already-established MCP session.</param>
|
||||
/// <param name="serverOptions">Configuration options for this server, including capabilities. </param>
|
||||
/// <param name="loggerFactory">Logger factory to use for logging. If null, logging will be disabled.</param>
|
||||
/// <param name="serviceProvider">Optional service provider to create new instances of tools and other dependencies.</param>
|
||||
/// <returns>An <see cref="IMcpServer"/> instance that should be disposed when no longer needed.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="transport"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="serverOptions"/> is <see langword="null"/>.</exception>
|
||||
public static IMcpServer Create(
|
||||
ITransport transport,
|
||||
McpServerOptions serverOptions,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
IServiceProvider? serviceProvider = null)
|
||||
{
|
||||
Throw.IfNull(transport);
|
||||
Throw.IfNull(serverOptions);
|
||||
|
||||
return new McpServer(transport, serverOptions, loggerFactory, serviceProvider);
|
||||
}
|
||||
}
|
||||
79
src/ModelContextProtocol.Core/Server/McpServerOptions.cs
Normal file
79
src/ModelContextProtocol.Core/Server/McpServerOptions.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration options for the MCP server.
|
||||
/// </summary>
|
||||
public class McpServerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets information about this server implementation, including its name and version.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This information is sent to the client during initialization to identify the server.
|
||||
/// It's displayed in client logs and can be used for debugging and compatibility checks.
|
||||
/// </remarks>
|
||||
public Implementation? ServerInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets server capabilities to advertise to the client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These determine which features will be available when a client connects.
|
||||
/// Capabilities can include "tools", "prompts", "resources", "logging", and other
|
||||
/// protocol-specific functionality.
|
||||
/// </remarks>
|
||||
public ServerCapabilities? Capabilities { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the protocol version supported by this server, using a date-based versioning scheme.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The protocol version defines which features and message formats this server supports.
|
||||
/// This uses a date-based versioning scheme in the format "YYYY-MM-DD".
|
||||
/// </remarks>
|
||||
public string ProtocolVersion { get; set; } = "2024-11-05";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a timeout used for the client-server initialization handshake sequence.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This timeout determines how long the server will wait for client responses during
|
||||
/// the initialization protocol handshake. If the client doesn't respond within this timeframe,
|
||||
/// the initialization process will be aborted.
|
||||
/// </remarks>
|
||||
public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(60);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets optional server instructions to send to clients.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These instructions are sent to clients during the initialization handshake and provide
|
||||
/// guidance on how to effectively use the server's capabilities. They can include details
|
||||
/// about available tools, expected input formats, limitations, or other helpful information.
|
||||
/// Client applications typically use these instructions as system messages for LLM interactions
|
||||
/// to provide context about available functionality.
|
||||
/// </remarks>
|
||||
public string? ServerInstructions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to create a new service provider scope for each handled request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default is <see langword="true"/>. When <see langword="true"/>, each invocation of a request
|
||||
/// handler will be invoked within a new service scope.
|
||||
/// </remarks>
|
||||
public bool ScopeRequests { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets preexisting knowledge about the client including its name and version to help support
|
||||
/// stateless Streamable HTTP servers that encode this knowledge in the mcp-session-id header.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When not specified, this information is sourced from the client's initialize request.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public Implementation? KnownClientInfo { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
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.Id}' 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.Id, 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.Id, 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.Id, 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;
|
||||
}
|
||||
216
src/ModelContextProtocol.Core/Server/McpServerPrompt.cs
Normal file
216
src/ModelContextProtocol.Core/Server/McpServerPrompt.cs
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an invocable prompt used by Model Context Protocol clients and servers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="McpServerPrompt"/> is an abstract base class that represents an MCP prompt for use in the server (as opposed
|
||||
/// to <see cref="Prompt"/>, which provides the protocol representation of a prompt, and <see cref="McpClientPrompt"/>, which
|
||||
/// provides a client-side representation of a prompt). Instances of <see cref="McpServerPrompt"/> can be added into a
|
||||
/// <see cref="IServiceCollection"/> to be picked up automatically when <see cref="McpServerFactory"/> is used to create
|
||||
/// an <see cref="IMcpServer"/>, or added into a <see cref="McpServerPrimitiveCollection{McpServerPrompt}"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Most commonly, <see cref="McpServerPrompt"/> instances are created using the static <see cref="M:McpServerPrompt.Create"/> methods.
|
||||
/// These methods enable creating an <see cref="McpServerPrompt"/> for a method, specified via a <see cref="Delegate"/> or
|
||||
/// <see cref="MethodInfo"/>, and are what are used implicitly by WithPromptsFromAssembly and WithPrompts. The <see cref="M:McpServerPrompt.Create"/> methods
|
||||
/// create <see cref="McpServerPrompt"/> instances capable of working with a large variety of .NET method signatures, automatically handling
|
||||
/// how parameters are marshaled into the method from the JSON received from the MCP client, and how the return value is marshaled back
|
||||
/// into the <see cref="GetPromptResult"/> that's then serialized and sent back to the client.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// By default, parameters are sourced from the <see cref="GetPromptRequestParams.Arguments"/> dictionary, which is a collection
|
||||
/// of key/value pairs. Those parameters are deserialized from the
|
||||
/// <see cref="JsonElement"/> values in that collection. There are a few exceptions to this:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="CancellationToken"/> parameters are automatically bound to a <see cref="CancellationToken"/> provided by the
|
||||
/// <see cref="IMcpServer"/> and that respects any <see cref="CancelledNotification"/>s sent by the client for this operation's
|
||||
/// <see cref="RequestId"/>.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IServiceProvider"/> parameters are bound from the <see cref="RequestContext{GetPromptRequestParams}"/> for this request.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IMcpServer"/> parameters are bound directly to the <see cref="IMcpServer"/> instance associated
|
||||
/// with this request's <see cref="RequestContext{CallPromptRequestParams}"/>. Such parameters may be used to understand
|
||||
/// what server is being used to process the request, and to interact with the client issuing the request to that server.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IProgress{ProgressNotificationValue}"/> parameters accepting <see cref="ProgressNotificationValue"/> values
|
||||
/// are bound to an <see cref="IProgress{ProgressNotificationValue}"/> instance manufactured to forward progress notifications
|
||||
/// from the prompt to the client. If the client included a <see cref="ProgressToken"/> in their request, progress reports issued
|
||||
/// to this instance will propagate to the client as <see cref="NotificationMethods.ProgressNotification"/> notifications with
|
||||
/// that token. If the client did not include a <see cref="ProgressToken"/>, the instance will ignore any progress reports issued to it.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// When the <see cref="McpServerPrompt"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
|
||||
/// <see cref="McpServerPromptCreateOptions.Services"/>. Any parameter that can be satisfied by that <see cref="IServiceProvider"/>
|
||||
/// according to <see cref="IServiceProviderIsService"/> will be resolved from the <see cref="IServiceProvider"/> provided to
|
||||
/// <see cref="GetAsync"/> rather than from the argument collection.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Any parameter attributed with <see cref="FromKeyedServicesAttribute"/> will similarly be resolved from the
|
||||
/// <see cref="IServiceProvider"/> provided to <see cref="GetAsync"/> rather than from the argument collection.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// All other parameters are deserialized from the <see cref="JsonElement"/>s in the <see cref="GetPromptRequestParams.Arguments"/> dictionary.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In general, the data supplied via the <see cref="GetPromptRequestParams.Arguments"/>'s dictionary is passed along from the caller and
|
||||
/// should thus be considered unvalidated and untrusted. To provide validated and trusted data to the invocation of the prompt, consider having
|
||||
/// the prompt be an instance method, referring to data stored in the instance, or using an instance or parameters resolved from the <see cref="IServiceProvider"/>
|
||||
/// to provide data to the method.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Return values from a method are used to create the <see cref="GetPromptResult"/> that is sent back to the client:
|
||||
/// </para>
|
||||
/// <list type="table">
|
||||
/// <item>
|
||||
/// <term><see cref="string"/></term>
|
||||
/// <description>Converted to a list containing a single <see cref="PromptMessage"/> with its <see cref="PromptMessage.Content"/> set to contain the <see cref="string"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="PromptMessage"/></term>
|
||||
/// <description>Converted to a list containing the single <see cref="PromptMessage"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="IEnumerable{PromptMessage}"/> of <see cref="PromptMessage"/></term>
|
||||
/// <description>Converted to a list containing all of the returned <see cref="PromptMessage"/> instances.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="ChatMessage"/></term>
|
||||
/// <description>Converted to a list of <see cref="PromptMessage"/> instances derived from the <see cref="ChatMessage"/> with <see cref="AIContentExtensions.ToPromptMessages"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="IEnumerable{PromptMessage}"/> of <see cref="PromptMessage"/></term>
|
||||
/// <description>Converted to a list of <see cref="PromptMessage"/> instances derived from all of the <see cref="ChatMessage"/> instances with <see cref="AIContentExtensions.ToPromptMessages"/>.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
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>
|
||||
/// <remarks>
|
||||
/// The ProtocolPrompt property represents the underlying prompt definition as defined in the
|
||||
/// Model Context Protocol specification. It contains metadata like the prompt's name,
|
||||
/// description, and acceptable arguments.
|
||||
/// </remarks>
|
||||
public abstract Prompt ProtocolPrompt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the prompt, rendering it with the provided request parameters and returning the prompt result.
|
||||
/// </summary>
|
||||
/// <param name="request">
|
||||
/// The request context containing information about the prompt invocation, including any arguments
|
||||
/// passed to the prompt. This object provides access to both the request parameters and the server context.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">
|
||||
/// The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task"/> representing the asynchronous operation, containing a <see cref="GetPromptResult"/> with
|
||||
/// the prompt content and messages.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="request"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The prompt implementation returns <see langword="null"/> or an unsupported result type.</exception>
|
||||
public abstract ValueTask<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="createTargetFunc">
|
||||
/// Callback used on each function invocation to create an instance of the type on which the instance method <paramref name="method"/>
|
||||
/// will be invoked. If the returned instance is <see cref="IAsyncDisposable"/> or <see cref="IDisposable"/>, it will
|
||||
/// be disposed of after method completes its 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,
|
||||
Func<RequestContext<GetPromptRequestParams>, object> createTargetFunc,
|
||||
McpServerPromptCreateOptions? options = null) =>
|
||||
AIFunctionMcpServerPrompt.Create(method, createTargetFunc, 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.Id => ProtocolPrompt.Name;
|
||||
}
|
||||
120
src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs
Normal file
120
src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Used to indicate that a method should be considered an <see cref="McpServerPrompt"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This attribute is applied to methods that should be exposed as prompts in the Model Context Protocol. When a class
|
||||
/// containing methods marked with this attribute is registered with McpServerBuilderExtensions,
|
||||
/// these methods become available as prompts that can be called by MCP clients.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When methods are provided directly to <see cref="M:McpServerPrompt.Create"/>, the attribute is not required.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// By default, parameters are sourced from the <see cref="GetPromptRequestParams.Arguments"/> dictionary, which is a collection
|
||||
/// of key/value pairs. Those parameters are deserialized from the
|
||||
/// <see cref="JsonElement"/> values in that collection. There are a few exceptions to this:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="CancellationToken"/> parameters are automatically bound to a <see cref="CancellationToken"/> provided by the
|
||||
/// <see cref="IMcpServer"/> and that respects any <see cref="CancelledNotification"/>s sent by the client for this operation's
|
||||
/// <see cref="RequestId"/>.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IServiceProvider"/> parameters are bound from the <see cref="RequestContext{GetPromptRequestParams}"/> for this request.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IMcpServer"/> parameters are bound directly to the <see cref="IMcpServer"/> instance associated
|
||||
/// with this request's <see cref="RequestContext{CallPromptRequestParams}"/>. Such parameters may be used to understand
|
||||
/// what server is being used to process the request, and to interact with the client issuing the request to that server.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IProgress{ProgressNotificationValue}"/> parameters accepting <see cref="ProgressNotificationValue"/> values
|
||||
/// are bound to an <see cref="IProgress{ProgressNotificationValue}"/> instance manufactured to forward progress notifications
|
||||
/// from the prompt to the client. If the client included a <see cref="ProgressToken"/> in their request, progress reports issued
|
||||
/// to this instance will propagate to the client as <see cref="NotificationMethods.ProgressNotification"/> notifications with
|
||||
/// that token. If the client did not include a <see cref="ProgressToken"/>, the instance will ignore any progress reports issued to it.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// When the <see cref="McpServerPrompt"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
|
||||
/// <see cref="McpServerPromptCreateOptions.Services"/>. Any parameter that can be satisfied by that <see cref="IServiceProvider"/>
|
||||
/// according to <see cref="IServiceProviderIsService"/> will be resolved from the <see cref="IServiceProvider"/> provided to the
|
||||
/// prompt invocation rather than from the argument collection.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Any parameter attributed with <see cref="FromKeyedServicesAttribute"/> will similarly be resolved from the
|
||||
/// <see cref="IServiceProvider"/> provided to the prompt invocation rather than from the argument collection.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// All other parameters are deserialized from the <see cref="JsonElement"/>s in the <see cref="GetPromptRequestParams.Arguments"/> dictionary.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In general, the data supplied via the <see cref="GetPromptRequestParams.Arguments"/>'s dictionary is passed along from the caller and
|
||||
/// should thus be considered unvalidated and untrusted. To provide validated and trusted data to the invocation of the prompt, consider having
|
||||
/// the prompt be an instance method, referring to data stored in the instance, or using an instance or parameters resolved from the <see cref="IServiceProvider"/>
|
||||
/// to provide data to the method.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Return values from a method are used to create the <see cref="GetPromptResult"/> that is sent back to the client:
|
||||
/// </para>
|
||||
/// <list type="table">
|
||||
/// <item>
|
||||
/// <term><see cref="string"/></term>
|
||||
/// <description>Converted to a list containing a single <see cref="PromptMessage"/> with its <see cref="PromptMessage.Content"/> set to contain the <see cref="string"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="PromptMessage"/></term>
|
||||
/// <description>Converted to a list containing the single <see cref="PromptMessage"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="IEnumerable{PromptMessage}"/> of <see cref="PromptMessage"/></term>
|
||||
/// <description>Converted to a list containing all of the returned <see cref="PromptMessage"/> instances.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="ChatMessage"/></term>
|
||||
/// <description>Converted to a list of <see cref="PromptMessage"/> instances derived from the <see cref="ChatMessage"/> with <see cref="AIContentExtensions.ToPromptMessages"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="IEnumerable{ChatMessage}"/> of <see cref="ChatMessage"/></term>
|
||||
/// <description>Converted to a list of <see cref="PromptMessage"/> instances derived from all of the <see cref="ChatMessage"/> instances with <see cref="AIContentExtensions.ToPromptMessages"/>.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[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; }
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Provides options for controlling the creation of an <see cref="McpServerPrompt"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These options allow for customizing the behavior and metadata of prompts created with
|
||||
/// <see cref="M:McpServerPrompt.Create"/>. They provide control over naming, description,
|
||||
/// and dependency injection integration.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When creating prompts programmatically rather than using attributes, these options
|
||||
/// provide the same level of configuration flexibility.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
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. As such,
|
||||
/// 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>
|
||||
/// Gets or sets the JSON serializer options to use when marshalling data to/from JSON.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Defaults to <see cref="McpJsonUtilities.DefaultOptions"/> if left unspecified.
|
||||
/// </remarks>
|
||||
public JsonSerializerOptions? SerializerOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the JSON schema options when creating <see cref="AIFunction"/> from a method.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Defaults to <see cref="AIJsonSchemaCreateOptions.Default"/> if left unspecified.
|
||||
/// </remarks>
|
||||
public AIJsonSchemaCreateOptions? SchemaCreateOptions { 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,
|
||||
SerializerOptions = SerializerOptions,
|
||||
SchemaCreateOptions = SchemaCreateOptions,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Used to attribute a type containing methods that should be exposed as <see cref="McpServerPrompt"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This attribute is used to mark a class containing methods that should be automatically
|
||||
/// discovered and registered as <see cref="McpServerPrompt"/>s. When combined with discovery methods like
|
||||
/// WithPromptsFromAssembly, it enables automatic registration of prompts without explicitly listing each prompt class.
|
||||
/// The attribute is not necessary when a reference to the type is provided directly to a method like WithPrompts.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Within a class marked with this attribute, individual methods that should be exposed as
|
||||
/// prompts must be marked with the <see cref="McpServerPromptAttribute"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class McpServerPromptTypeAttribute : Attribute;
|
||||
238
src/ModelContextProtocol.Core/Server/McpServerResource.cs
Normal file
238
src/ModelContextProtocol.Core/Server/McpServerResource.cs
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an invocable resource used by Model Context Protocol clients and servers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="McpServerResource"/> is an abstract base class that represents an MCP resource for use in the server (as opposed
|
||||
/// to <see cref="Resource"/> or <see cref="ResourceTemplate"/>, which provide the protocol representations of a resource). Instances of
|
||||
/// <see cref="McpServerResource"/> can be added into a <see cref="IServiceCollection"/> to be picked up automatically when
|
||||
/// <see cref="McpServerFactory"/> is used to create an <see cref="IMcpServer"/>, or added into a <see cref="McpServerPrimitiveCollection{McpServerResource}"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Most commonly, <see cref="McpServerResource"/> instances are created using the static <see cref="M:McpServerResource.Create"/> methods.
|
||||
/// These methods enable creating an <see cref="McpServerResource"/> for a method, specified via a <see cref="Delegate"/> or
|
||||
/// <see cref="MethodInfo"/>, and are what are used implicitly by WithResourcesFromAssembly and
|
||||
/// <see cref="M:McpServerBuilderExtensions.WithResources"/>. The <see cref="M:McpServerResource.Create"/> methods
|
||||
/// create <see cref="McpServerResource"/> instances capable of working with a large variety of .NET method signatures, automatically handling
|
||||
/// how parameters are marshaled into the method from the URI received from the MCP client, and how the return value is marshaled back
|
||||
/// into the <see cref="ReadResourceResult"/> that's then serialized and sent back to the client.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="McpServerResource"/> is used to represent both direct resources (e.g. "resource://example") and templated
|
||||
/// resources (e.g. "resource://example/{id}").
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Read resource requests do not contain separate arguments, only a URI. However, for templated resources, portions of that URI may be considered
|
||||
/// as arguments and may be bound to parameters. Further, resource methods may accept parameters that will be bound to arguments based on their type.
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="CancellationToken"/> parameters are automatically bound to a <see cref="CancellationToken"/> provided by the
|
||||
/// <see cref="IMcpServer"/> and that respects any <see cref="CancelledNotification"/>s sent by the client for this operation's
|
||||
/// <see cref="RequestId"/>.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IServiceProvider"/> parameters are bound from the <see cref="RequestContext{ReadResourceRequestParams}"/> for this request.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IMcpServer"/> parameters are bound directly to the <see cref="IMcpServer"/> instance associated
|
||||
/// with this request's <see cref="RequestContext{ReadResourceRequestParams}"/>. Such parameters may be used to understand
|
||||
/// what server is being used to process the request, and to interact with the client issuing the request to that server.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IProgress{ProgressNotificationValue}"/> parameters accepting <see cref="ProgressNotificationValue"/> values
|
||||
/// are bound to an <see cref="IProgress{ProgressNotificationValue}"/> instance manufactured to forward progress notifications
|
||||
/// from the resource to the client. If the client included a <see cref="ProgressToken"/> in their request, progress reports issued
|
||||
/// to this instance will propagate to the client as <see cref="NotificationMethods.ProgressNotification"/> notifications with
|
||||
/// that token. If the client did not include a <see cref="ProgressToken"/>, the instance will ignore any progress reports issued to it.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// When the <see cref="McpServerResource"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
|
||||
/// <see cref="McpServerResourceCreateOptions.Services"/>. Any parameter that can be satisfied by that <see cref="IServiceProvider"/>
|
||||
/// according to <see cref="IServiceProviderIsService"/> will be resolved from the <see cref="IServiceProvider"/> provided to the
|
||||
/// resource invocation rather than from the argument collection.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Any parameter attributed with <see cref="FromKeyedServicesAttribute"/> will similarly be resolved from the
|
||||
/// <see cref="IServiceProvider"/> provided to the resource invocation rather than from the argument collection.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// All other parameters are bound from the data in the URI.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Return values from a method are used to create the <see cref="ReadResourceResult"/> that is sent back to the client:
|
||||
/// </para>
|
||||
/// <list type="table">
|
||||
/// <item>
|
||||
/// <term><see cref="ResourceContents"/></term>
|
||||
/// <description>Wrapped in a list containing the single <see cref="ResourceContents"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="TextContent"/></term>
|
||||
/// <description>Converted to a list containing a single <see cref="TextResourceContents"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="DataContent"/></term>
|
||||
/// <description>Converted to a list containing a single <see cref="BlobResourceContents"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="string"/></term>
|
||||
/// <description>Converted to a list containing a single <see cref="TextResourceContents"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="IEnumerable{ResourceContents}"/> of <see cref="ResourceContents"/></term>
|
||||
/// <description>Returned directly as a list of <see cref="ResourceContents"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="IEnumerable{AIContent}"/> of <see cref="AIContent"/></term>
|
||||
/// <description>Converted to a list containing a <see cref="TextResourceContents"/> for each <see cref="TextContent"/> and a <see cref="BlobResourceContents"/> for each <see cref="DataContent"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="IEnumerable{String}"/> of <see cref="string"/></term>
|
||||
/// <description>Converted to a list containing a <see cref="TextResourceContents"/>, one for each <see cref="string"/>.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class McpServerResource : IMcpServerPrimitive
|
||||
{
|
||||
/// <summary>Initializes a new instance of the <see cref="McpServerResource"/> class.</summary>
|
||||
protected McpServerResource()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Gets whether this resource is a URI template with parameters as opposed to a direct resource.</summary>
|
||||
public bool IsTemplated => ProtocolResourceTemplate.UriTemplate.Contains('{');
|
||||
|
||||
/// <summary>Gets the protocol <see cref="ResourceTemplate"/> type for this instance.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="ProtocolResourceTemplate"/> property represents the underlying resource template definition as defined in the
|
||||
/// Model Context Protocol specification. It contains metadata like the resource templates's URI template, name, and description.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Every valid resource URI is a valid resource URI template, and thus this property always returns an instance.
|
||||
/// In contrast, the <see cref="ProtocolResource"/> property may return <see langword="null"/> if the resource template
|
||||
/// contains a parameter, in which case the resource template URI is not a valid resource URI.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract ResourceTemplate ProtocolResourceTemplate { get; }
|
||||
|
||||
/// <summary>Gets the protocol <see cref="Resource"/> type for this instance.</summary>
|
||||
/// <remarks>
|
||||
/// The ProtocolResourceTemplate property represents the underlying resource template definition as defined in the
|
||||
/// Model Context Protocol specification. It contains metadata like the resource templates's URI template, name, and description.
|
||||
/// </remarks>
|
||||
public virtual Resource? ProtocolResource => ProtocolResourceTemplate.AsResource();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resource, rendering it with the provided request parameters and returning the resource result.
|
||||
/// </summary>
|
||||
/// <param name="request">
|
||||
/// The request context containing information about the resource invocation, including any arguments
|
||||
/// passed to the resource. This object provides access to both the request parameters and the server context.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">
|
||||
/// The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A <see cref="ValueTask{ReadResourceResult}"/> representing the asynchronous operation, containing a <see cref="ReadResourceResult"/> with
|
||||
/// the resource content and messages. If and only if this <see cref="McpServerResource"/> doesn't match the <see cref="ReadResourceRequestParams.Uri"/>,
|
||||
/// the method returns <see langword="null"/>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="request"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The resource implementation returned <see langword="null"/> or an unsupported result type.</exception>
|
||||
public abstract ValueTask<ReadResourceResult?> ReadAsync(
|
||||
RequestContext<ReadResourceRequestParams> request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerResource"/> 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="McpServerResource"/>.</param>
|
||||
/// <param name="options">Optional options used in the creation of the <see cref="McpServerResource"/> to control its behavior.</param>
|
||||
/// <returns>The created <see cref="McpServerResource"/> for invoking <paramref name="method"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
|
||||
public static McpServerResource Create(
|
||||
Delegate method,
|
||||
McpServerResourceCreateOptions? options = null) =>
|
||||
AIFunctionMcpServerResource.Create(method, options);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerResource"/> 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="McpServerResource"/>.</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="McpServerResource"/> to control its behavior.</param>
|
||||
/// <returns>The created <see cref="McpServerResource"/> 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 McpServerResource Create(
|
||||
MethodInfo method,
|
||||
object? target = null,
|
||||
McpServerResourceCreateOptions? options = null) =>
|
||||
AIFunctionMcpServerResource.Create(method, target, options);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerResource"/> 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="createTargetFunc">
|
||||
/// Callback used on each function invocation to create an instance of the type on which the instance method <paramref name="method"/>
|
||||
/// will be invoked. If the returned instance is <see cref="IAsyncDisposable"/> or <see cref="IDisposable"/>, it will
|
||||
/// be disposed of after method completes its invocation.
|
||||
/// </param>
|
||||
/// <param name="options">Optional options used in the creation of the <see cref="McpServerResource"/> 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 McpServerResource Create(
|
||||
MethodInfo method,
|
||||
Func<RequestContext<ReadResourceRequestParams>, object> createTargetFunc,
|
||||
McpServerResourceCreateOptions? options = null) =>
|
||||
AIFunctionMcpServerResource.Create(method, createTargetFunc, options);
|
||||
|
||||
/// <summary>Creates an <see cref="McpServerResource"/> 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="McpServerResource"/> 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="McpServerResource"/> created by <see cref="Create(AIFunction, McpServerResourceCreateOptions)"/>
|
||||
/// does not provide all of the special parameter handling for MCP-specific concepts, like <see cref="IMcpServer"/>.
|
||||
/// </remarks>
|
||||
public static McpServerResource Create(
|
||||
AIFunction function,
|
||||
McpServerResourceCreateOptions? options = null) =>
|
||||
AIFunctionMcpServerResource.Create(function, options);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => ProtocolResourceTemplate.UriTemplate;
|
||||
|
||||
/// <inheritdoc />
|
||||
string IMcpServerPrimitive.Id => ProtocolResourceTemplate.UriTemplate;
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Used to indicate that a method or property should be considered an <see cref="McpServerResource"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This attribute is applied to methods or properties that should be exposed as resources in the Model Context Protocol. When a class
|
||||
/// containing methods marked with this attribute is registered with McpServerBuilderExtensions,
|
||||
/// these methods or properties become available as resources that can be called by MCP clients.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When methods are provided directly to <see cref="M:McpServerResource.Create"/>, the attribute is not required.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Read resource requests do not contain separate arguments, only a URI. However, for templated resources, portions of that URI may be considered
|
||||
/// as arguments and may be bound to parameters. Further, resource methods may accept parameters that will be bound to arguments based on their type.
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="CancellationToken"/> parameters are automatically bound to a <see cref="CancellationToken"/> provided by the
|
||||
/// <see cref="IMcpServer"/> and that respects any <see cref="CancelledNotification"/>s sent by the client for this operation's
|
||||
/// <see cref="RequestId"/>.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IServiceProvider"/> parameters are bound from the <see cref="RequestContext{ReadResourceRequestParams}"/> for this request.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IMcpServer"/> parameters are bound directly to the <see cref="IMcpServer"/> instance associated
|
||||
/// with this request's <see cref="RequestContext{ReadResourceRequestParams}"/>. Such parameters may be used to understand
|
||||
/// what server is being used to process the request, and to interact with the client issuing the request to that server.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IProgress{ProgressNotificationValue}"/> parameters accepting <see cref="ProgressNotificationValue"/> values
|
||||
/// are bound to an <see cref="IProgress{ProgressNotificationValue}"/> instance manufactured to forward progress notifications
|
||||
/// from the resource to the client. If the client included a <see cref="ProgressToken"/> in their request, progress reports issued
|
||||
/// to this instance will propagate to the client as <see cref="NotificationMethods.ProgressNotification"/> notifications with
|
||||
/// that token. If the client did not include a <see cref="ProgressToken"/>, the instance will ignore any progress reports issued to it.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// When the <see cref="McpServerResource"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
|
||||
/// <see cref="McpServerResourceCreateOptions.Services"/>. Any parameter that can be satisfied by that <see cref="IServiceProvider"/>
|
||||
/// according to <see cref="IServiceProviderIsService"/> will be resolved from the <see cref="IServiceProvider"/> provided to the
|
||||
/// resource invocation rather than from the argument collection.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Any parameter attributed with <see cref="FromKeyedServicesAttribute"/> will similarly be resolved from the
|
||||
/// <see cref="IServiceProvider"/> provided to the resource invocation rather than from the argument collection.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// All other parameters are bound from the data in the URI.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Return values from a method are used to create the <see cref="ReadResourceResult"/> that is sent back to the client:
|
||||
/// </para>
|
||||
/// <list type="table">
|
||||
/// <item>
|
||||
/// <term><see cref="ResourceContents"/></term>
|
||||
/// <description>Wrapped in a list containing the single <see cref="ResourceContents"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="TextContent"/></term>
|
||||
/// <description>Converted to a list containing a single <see cref="TextResourceContents"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="DataContent"/></term>
|
||||
/// <description>Converted to a list containing a single <see cref="BlobResourceContents"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="string"/></term>
|
||||
/// <description>Converted to a list containing a single <see cref="TextResourceContents"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="IEnumerable{ResourceContents}"/> of <see cref="ResourceContents"/></term>
|
||||
/// <description>Returned directly as a list of <see cref="ResourceContents"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="IEnumerable{AIContent}"/> of <see cref="AIContent"/></term>
|
||||
/// <description>Converted to a list containing a <see cref="TextResourceContents"/> for each <see cref="TextContent"/> and a <see cref="BlobResourceContents"/> for each <see cref="DataContent"/>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="IEnumerable{String}"/> of <see cref="string"/></term>
|
||||
/// <description>Converted to a list containing a <see cref="TextResourceContents"/>, one for each <see cref="string"/>.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class McpServerResourceAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpServerResourceAttribute"/> class.
|
||||
/// </summary>
|
||||
public McpServerResourceAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the URI template of the resource.</summary>
|
||||
/// <remarks>
|
||||
/// If <see langword="null"/>, a URI will be derived from <see cref="Name"/> and the method's parameter names.
|
||||
/// This template may, but doesn't have to, include parameters; if it does, this <see cref="McpServerResource"/>
|
||||
/// will be considered a "resource template", and if it doesn't, it will be considered a "direct resource".
|
||||
/// The former will be listed with <see cref="RequestMethods.ResourcesTemplatesList"/> requests and the latter
|
||||
/// with <see cref="RequestMethods.ResourcesList"/> requests.
|
||||
/// </remarks>
|
||||
public string? UriTemplate { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the name of the resource.</summary>
|
||||
/// <remarks>If <see langword="null"/>, the method name will be used.</remarks>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the MIME (media) type of the resource.</summary>
|
||||
public string? MimeType { get; set; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue