Add ModelContextProtocol.Core package (#428)

This commit is contained in:
Stephen Halter 2025-06-02 15:16:45 -07:00 • committed by GitHub
commit 703b0fe50b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 239 additions and 94 deletions

View file

@ -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."),
};
}
}

View file

@ -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."),
};
}
}

View 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);
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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);
}
}

View 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);
}

View 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; }
}

View 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,
};
}

View 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,
});
}
}
}
}
}

View 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);
}
}

View 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; }
}

View file

@ -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;
}

View 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;
}

View 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; }
}

View file

@ -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,
};
}

View file

@ -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;

View 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;
}

View file

@ -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