Add McpServer/ClientResource{Template} and friends (#391)
* Add McpServer/ClientResource{Template} and friends
This PR copies the same design used by tools and prompts to add McpServerResource, McpServerResourceAttribute, McpServerResourceTypeAttribute, McpClientResource, and then all of those for ResourceTemplate as well.
Unlike the others, [McpServerResource] is permitted not only on methods but also on properties. This convenience is afforded because such resources aren't parameterized, and thus can be easily modeled as properties, likely auto-props, e.g.
```C#
[McpServerResource]
public static BlobResourceContents MyAmazingData { get; } = Create();
```
Details about resources can be encoded into the attributes or provided via McpServerResource{Template}.Create methods. If not URI is explicitly defined, one is derived from the member name and arguments. For templates, this means manufacturing a URI template that includes variables for all non-DI parameters.
This also fixes a few issues discovered along the way:
- McpClientExtensions are updated to use `ValueTask<T>` instead of `Task<T>` where relevant.
- McpServerPrompt was missing handling for progress notifications; that's been rectified. It was also missing configuring the description on the ProtocolPrompt.
- Renamed IMcpServerPrimitive.Name to Id. For resources, names aren't unique.
- After going back and forth, updated McpServer to not fail for missing handlers and to instead register default handlers. It simplifies the implementation and makes it more consistent.
- Added a bunch of primitive types into the McpJsonUtilities JSON source generator context so that such primitives are usable with tools/prompts/resources without folks being required to add them into their own context for Native AOT. I think we should consider pushing this down to AIJsonUtilities, instead, and that will then flow up to McpJsonUtilities automatically.
* Address feedback
This commit is contained in:
parent
47de86ab91
commit
d86b3277d4
48 changed files with 4146 additions and 308 deletions
|
|
@ -59,3 +59,20 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
License notice for URI Template Tests
|
||||
-------------------------------------
|
||||
|
||||
Copyright 2011- The Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,617 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
// Copied from:
|
||||
// https://github.com/dotnet/runtime/blob/dd75c45c123055baacd7aa4418f425f412797a29/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler.cs
|
||||
// and then modified to build on netstandard2.0.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
/// <summary>Provides a handler used by the language compiler to process interpolated strings into <see cref="string"/> instances.</summary>
|
||||
public ref struct DefaultInterpolatedStringHandler
|
||||
{
|
||||
// Implementation note:
|
||||
// As this type lives in CompilerServices and is only intended to be targeted by the compiler,
|
||||
// public APIs eschew argument validation logic in a variety of places, e.g. allowing a null input
|
||||
// when one isn't expected to produce a NullReferenceException rather than an ArgumentNullException.
|
||||
|
||||
/// <summary>Expected average length of formatted data used for an individual interpolation expression result.</summary>
|
||||
/// <remarks>
|
||||
/// This is inherited from string.Format, and could be changed based on further data.
|
||||
/// string.Format actually uses `format.Length + args.Length * 8`, but format.Length
|
||||
/// includes the format items themselves, e.g. "{0}", and since it's rare to have double-digit
|
||||
/// numbers of items, we bump the 8 up to 11 to account for the three extra characters in "{d}",
|
||||
/// since the compiler-provided base length won't include the equivalent character count.
|
||||
/// </remarks>
|
||||
private const int GuessedLengthPerHole = 11;
|
||||
/// <summary>Minimum size array to rent from the pool.</summary>
|
||||
/// <remarks>Same as stack-allocation size used today by string.Format.</remarks>
|
||||
private const int MinimumArrayPoolLength = 256;
|
||||
|
||||
/// <summary>Optional provider to pass to IFormattable.ToString or ISpanFormattable.TryFormat calls.</summary>
|
||||
private readonly IFormatProvider? _provider;
|
||||
/// <summary>Array rented from the array pool and used to back <see cref="_chars"/>.</summary>
|
||||
private char[]? _arrayToReturnToPool;
|
||||
/// <summary>The span to write into.</summary>
|
||||
private Span<char> _chars;
|
||||
/// <summary>Position at which to write the next character.</summary>
|
||||
private int _pos;
|
||||
/// <summary>Whether <see cref="_provider"/> provides an ICustomFormatter.</summary>
|
||||
/// <remarks>
|
||||
/// Custom formatters are very rare. We want to support them, but it's ok if we make them more expensive
|
||||
/// in order to make them as pay-for-play as possible. So, we avoid adding another reference type field
|
||||
/// to reduce the size of the handler and to reduce required zero'ing, by only storing whether the provider
|
||||
/// provides a formatter, rather than actually storing the formatter. This in turn means, if there is a
|
||||
/// formatter, we pay for the extra interface call on each AppendFormatted that needs it.
|
||||
/// </remarks>
|
||||
private readonly bool _hasCustomFormatter;
|
||||
|
||||
/// <summary>Creates a handler used to translate an interpolated string into a <see cref="string"/>.</summary>
|
||||
/// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
|
||||
/// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
|
||||
/// <remarks>This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.</remarks>
|
||||
public DefaultInterpolatedStringHandler(int literalLength, int formattedCount)
|
||||
{
|
||||
_provider = null;
|
||||
_chars = _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(GetDefaultLength(literalLength, formattedCount));
|
||||
_pos = 0;
|
||||
_hasCustomFormatter = false;
|
||||
}
|
||||
|
||||
/// <summary>Creates a handler used to translate an interpolated string into a <see cref="string"/>.</summary>
|
||||
/// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
|
||||
/// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
|
||||
/// <param name="provider">An object that supplies culture-specific formatting information.</param>
|
||||
/// <remarks>This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.</remarks>
|
||||
public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider)
|
||||
{
|
||||
_provider = provider;
|
||||
_chars = _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(GetDefaultLength(literalLength, formattedCount));
|
||||
_pos = 0;
|
||||
_hasCustomFormatter = provider is not null && HasCustomFormatter(provider);
|
||||
}
|
||||
|
||||
/// <summary>Creates a handler used to translate an interpolated string into a <see cref="string"/>.</summary>
|
||||
/// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
|
||||
/// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
|
||||
/// <param name="provider">An object that supplies culture-specific formatting information.</param>
|
||||
/// <param name="initialBuffer">A buffer temporarily transferred to the handler for use as part of its formatting. Contents may be overwritten.</param>
|
||||
/// <remarks>This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.</remarks>
|
||||
public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider, Span<char> initialBuffer)
|
||||
{
|
||||
_provider = provider;
|
||||
_chars = initialBuffer;
|
||||
_arrayToReturnToPool = null;
|
||||
_pos = 0;
|
||||
_hasCustomFormatter = provider is not null && HasCustomFormatter(provider);
|
||||
}
|
||||
|
||||
/// <summary>Derives a default length with which to seed the handler.</summary>
|
||||
/// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
|
||||
/// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] // becomes a constant when inputs are constant
|
||||
internal static int GetDefaultLength(int literalLength, int formattedCount) =>
|
||||
Math.Max(MinimumArrayPoolLength, literalLength + (formattedCount * GuessedLengthPerHole));
|
||||
|
||||
/// <summary>Gets the built <see cref="string"/>.</summary>
|
||||
/// <returns>The built string.</returns>
|
||||
public override string ToString() => Text.ToString();
|
||||
|
||||
/// <summary>Gets the built <see cref="string"/> and clears the handler.</summary>
|
||||
/// <returns>The built string.</returns>
|
||||
/// <remarks>
|
||||
/// This releases any resources used by the handler. The method should be invoked only
|
||||
/// once and as the last thing performed on the handler. Subsequent use is erroneous, ill-defined,
|
||||
/// and may destabilize the process, as may using any other copies of the handler after
|
||||
/// <see cref="ToStringAndClear" /> is called on any one of them.
|
||||
/// </remarks>
|
||||
public string ToStringAndClear()
|
||||
{
|
||||
string result = Text.ToString();
|
||||
Clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Clears the handler.</summary>
|
||||
/// <remarks>
|
||||
/// This releases any resources used by the handler. The method should be invoked only
|
||||
/// once and as the last thing performed on the handler. Subsequent use is erroneous, ill-defined,
|
||||
/// and may destabilize the process, as may using any other copies of the handler after <see cref="Clear"/>
|
||||
/// is called on any one of them.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Clear()
|
||||
{
|
||||
char[]? toReturn = _arrayToReturnToPool;
|
||||
|
||||
// Defensive clear
|
||||
_arrayToReturnToPool = null;
|
||||
_chars = default;
|
||||
_pos = 0;
|
||||
|
||||
if (toReturn is not null)
|
||||
{
|
||||
ArrayPool<char>.Shared.Return(toReturn);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets a span of the characters appended to the handler.</summary>
|
||||
public ReadOnlySpan<char> Text => _chars.Slice(0, _pos);
|
||||
|
||||
/// <summary>Writes the specified string to the handler.</summary>
|
||||
/// <param name="value">The string to write.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void AppendLiteral(string value)
|
||||
{
|
||||
if (value.AsSpan().TryCopyTo(_chars.Slice(_pos)))
|
||||
{
|
||||
_pos += value.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
GrowThenCopyString(value);
|
||||
}
|
||||
}
|
||||
|
||||
#region AppendFormatted
|
||||
// Design note:
|
||||
// The compiler requires a AppendFormatted overload for anything that might be within an interpolation expression;
|
||||
// if it can't find an appropriate overload, for handlers in general it'll simply fail to compile.
|
||||
// (For target-typing to string where it uses DefaultInterpolatedStringHandler implicitly, it'll instead fall back to
|
||||
// its other mechanisms, e.g. using string.Format. This fallback has the benefit that if we miss a case,
|
||||
// interpolated strings will still work, but it has the downside that a developer generally won't know
|
||||
// if the fallback is happening and they're paying more.)
|
||||
//
|
||||
// At a minimum, then, we would need an overload that accepts:
|
||||
// (object value, int alignment = 0, string? format = null)
|
||||
// Such an overload would provide the same expressiveness as string.Format. However, this has several
|
||||
// shortcomings:
|
||||
// - Every value type in an interpolation expression would be boxed.
|
||||
// - ReadOnlySpan<char> could not be used in interpolation expressions.
|
||||
// - Every AppendFormatted call would have three arguments at the call site, bloating the IL further.
|
||||
// - Every invocation would be more expensive, due to lack of specialization, every call needing to account
|
||||
// for alignment and format, etc.
|
||||
//
|
||||
// To address that, we could just have overloads for T and ReadOnlySpan<char>:
|
||||
// (T)
|
||||
// (T, int alignment)
|
||||
// (T, string? format)
|
||||
// (T, int alignment, string? format)
|
||||
// (ReadOnlySpan<char>)
|
||||
// (ReadOnlySpan<char>, int alignment)
|
||||
// (ReadOnlySpan<char>, string? format)
|
||||
// (ReadOnlySpan<char>, int alignment, string? format)
|
||||
// but this also has shortcomings:
|
||||
// - Some expressions that would have worked with an object overload will now force a fallback to string.Format
|
||||
// (or fail to compile if the handler is used in places where the fallback isn't provided), because the compiler
|
||||
// can't always target type to T, e.g. `b switch { true => 1, false => null }` where `b` is a bool can successfully
|
||||
// be passed as an argument of type `object` but not of type `T`.
|
||||
// - Reference types get no benefit from going through the generic code paths, and actually incur some overheads
|
||||
// from doing so.
|
||||
// - Nullable value types also pay a heavy price, in particular around interface checks that would generally evaporate
|
||||
// at compile time for value types but don't (currently) if the Nullable<T> goes through the same code paths
|
||||
// (see https://github.com/dotnet/runtime/issues/50915).
|
||||
//
|
||||
// We could try to take a more elaborate approach for DefaultInterpolatedStringHandler, since it is the most common handler
|
||||
// and we want to minimize overheads both at runtime and in IL size, e.g. have a complete set of overloads for each of:
|
||||
// (T, ...) where T : struct
|
||||
// (T?, ...) where T : struct
|
||||
// (object, ...)
|
||||
// (ReadOnlySpan<char>, ...)
|
||||
// (string, ...)
|
||||
// but this also has shortcomings, most importantly:
|
||||
// - If you have an unconstrained T that happens to be a value type, it'll now end up getting boxed to use the object overload.
|
||||
// This also necessitates the T? overload, since nullable value types don't meet a T : struct constraint, so without those
|
||||
// they'd all map to the object overloads as well.
|
||||
// - Any reference type with an implicit cast to ROS<char> will fail to compile due to ambiguities between the overloads. string
|
||||
// is one such type, hence needing dedicated overloads for it that can be bound to more tightly.
|
||||
//
|
||||
// A middle ground we've settled on, which is likely to be the right approach for most other handlers as well, would be the set:
|
||||
// (T, ...) with no constraint
|
||||
// (ReadOnlySpan<char>) and (ReadOnlySpan<char>, int)
|
||||
// (object, int alignment = 0, string? format = null)
|
||||
// (string) and (string, int)
|
||||
// This would address most of the concerns, at the expense of:
|
||||
// - Most reference types going through the generic code paths and so being a bit more expensive.
|
||||
// - Nullable types being more expensive until https://github.com/dotnet/runtime/issues/50915 is addressed.
|
||||
// We could choose to add a T? where T : struct set of overloads if necessary.
|
||||
// Strings don't require their own overloads here, but as they're expected to be very common and as we can
|
||||
// optimize them in several ways (can copy the contents directly, don't need to do any interface checks, don't
|
||||
// need to pay the shared generic overheads, etc.) we can add overloads specifically to optimize for them.
|
||||
//
|
||||
// Hole values are formatted according to the following policy:
|
||||
// 1. If an IFormatProvider was supplied and it provides an ICustomFormatter, use ICustomFormatter.Format (even if the value is null).
|
||||
// 2. If the type implements ISpanFormattable, use ISpanFormattable.TryFormat.
|
||||
// 3. If the type implements IFormattable, use IFormattable.ToString.
|
||||
// 4. Otherwise, use object.ToString.
|
||||
// This matches the behavior of string.Format, StringBuilder.AppendFormat, etc. The only overloads for which this doesn't
|
||||
// apply is ReadOnlySpan<char>, which isn't supported by either string.Format nor StringBuilder.AppendFormat, but more
|
||||
// importantly which can't be boxed to be passed to ICustomFormatter.Format.
|
||||
|
||||
#region AppendFormatted T
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <typeparam name="T">The type of the value to write.</typeparam>
|
||||
public void AppendFormatted<T>(T value)
|
||||
{
|
||||
// This method could delegate to AppendFormatted with a null format, but explicitly passing
|
||||
// default as the format to TryFormat helps to improve code quality in some cases when TryFormat is inlined,
|
||||
// e.g. for Int32 it enables the JIT to eliminate code in the inlined method based on a length check on the format.
|
||||
|
||||
// If there's a custom formatter, always use it.
|
||||
if (_hasCustomFormatter)
|
||||
{
|
||||
AppendCustomFormatter(value, format: null);
|
||||
return;
|
||||
}
|
||||
|
||||
string? s;
|
||||
if (value is IFormattable)
|
||||
{
|
||||
s = ((IFormattable)value).ToString(format: null, _provider); // constrained call avoiding boxing for value types
|
||||
}
|
||||
else
|
||||
{
|
||||
s = value?.ToString();
|
||||
}
|
||||
|
||||
if (s is not null)
|
||||
{
|
||||
AppendLiteral(s);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="format">The format string.</param>
|
||||
/// <typeparam name="T">The type of the value to write.</typeparam>
|
||||
public void AppendFormatted<T>(T value, string? format)
|
||||
{
|
||||
// If there's a custom formatter, always use it.
|
||||
if (_hasCustomFormatter)
|
||||
{
|
||||
AppendCustomFormatter(value, format);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check first for IFormattable, even though we'll prefer to use ISpanFormattable, as the latter
|
||||
// requires the former. For value types, it won't matter as the type checks devolve into
|
||||
// JIT-time constants. For reference types, they're more likely to implement IFormattable
|
||||
// than they are to implement ISpanFormattable: if they don't implement either, we save an
|
||||
// interface check over first checking for ISpanFormattable and then for IFormattable, and
|
||||
// if it only implements IFormattable, we come out even: only if it implements both do we
|
||||
// end up paying for an extra interface check.
|
||||
string? s;
|
||||
if (value is IFormattable)
|
||||
{
|
||||
s = ((IFormattable)value).ToString(format, _provider); // constrained call avoiding boxing for value types
|
||||
}
|
||||
else
|
||||
{
|
||||
s = value?.ToString();
|
||||
}
|
||||
|
||||
if (s is not null)
|
||||
{
|
||||
AppendLiteral(s);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="alignment">Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
|
||||
/// <typeparam name="T">The type of the value to write.</typeparam>
|
||||
public void AppendFormatted<T>(T value, int alignment)
|
||||
{
|
||||
int startingPos = _pos;
|
||||
AppendFormatted(value);
|
||||
if (alignment != 0)
|
||||
{
|
||||
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="format">The format string.</param>
|
||||
/// <param name="alignment">Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
|
||||
/// <typeparam name="T">The type of the value to write.</typeparam>
|
||||
public void AppendFormatted<T>(T value, int alignment, string? format)
|
||||
{
|
||||
int startingPos = _pos;
|
||||
AppendFormatted(value, format);
|
||||
if (alignment != 0)
|
||||
{
|
||||
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region AppendFormatted ReadOnlySpan<char>
|
||||
/// <summary>Writes the specified character span to the handler.</summary>
|
||||
/// <param name="value">The span to write.</param>
|
||||
public void AppendFormatted(scoped ReadOnlySpan<char> value)
|
||||
{
|
||||
// Fast path for when the value fits in the current buffer
|
||||
if (value.TryCopyTo(_chars.Slice(_pos)))
|
||||
{
|
||||
_pos += value.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
GrowThenCopySpan(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Writes the specified string of chars to the handler.</summary>
|
||||
/// <param name="value">The span to write.</param>
|
||||
/// <param name="alignment">Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
|
||||
/// <param name="format">The format string.</param>
|
||||
public void AppendFormatted(scoped ReadOnlySpan<char> value, int alignment = 0, string? format = null)
|
||||
{
|
||||
bool leftAlign = false;
|
||||
if (alignment < 0)
|
||||
{
|
||||
leftAlign = true;
|
||||
alignment = -alignment;
|
||||
}
|
||||
|
||||
int paddingRequired = alignment - value.Length;
|
||||
if (paddingRequired <= 0)
|
||||
{
|
||||
// The value is as large or larger than the required amount of padding,
|
||||
// so just write the value.
|
||||
AppendFormatted(value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write the value along with the appropriate padding.
|
||||
EnsureCapacityForAdditionalChars(value.Length + paddingRequired);
|
||||
if (leftAlign)
|
||||
{
|
||||
value.CopyTo(_chars.Slice(_pos));
|
||||
_pos += value.Length;
|
||||
_chars.Slice(_pos, paddingRequired).Fill(' ');
|
||||
_pos += paddingRequired;
|
||||
}
|
||||
else
|
||||
{
|
||||
_chars.Slice(_pos, paddingRequired).Fill(' ');
|
||||
_pos += paddingRequired;
|
||||
value.CopyTo(_chars.Slice(_pos));
|
||||
_pos += value.Length;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region AppendFormatted string
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public void AppendFormatted(string? value)
|
||||
{
|
||||
// Fast-path for no custom formatter and a non-null string that fits in the current destination buffer.
|
||||
if (!_hasCustomFormatter &&
|
||||
value is not null &&
|
||||
value.AsSpan().TryCopyTo(_chars.Slice(_pos)))
|
||||
{
|
||||
_pos += value.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
AppendFormattedSlow(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <remarks>
|
||||
/// Slow path to handle a custom formatter, potentially null value,
|
||||
/// or a string that doesn't fit in the current buffer.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void AppendFormattedSlow(string? value)
|
||||
{
|
||||
if (_hasCustomFormatter)
|
||||
{
|
||||
AppendCustomFormatter(value, format: null);
|
||||
}
|
||||
else if (value is not null)
|
||||
{
|
||||
EnsureCapacityForAdditionalChars(value.Length);
|
||||
value.AsSpan().CopyTo(_chars.Slice(_pos));
|
||||
_pos += value.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="alignment">Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
|
||||
/// <param name="format">The format string.</param>
|
||||
public void AppendFormatted(string? value, int alignment = 0, string? format = null) =>
|
||||
// Format is meaningless for strings and doesn't make sense for someone to specify. We have the overload
|
||||
// simply to disambiguate between ROS<char> and object, just in case someone does specify a format, as
|
||||
// string is implicitly convertible to both. Just delegate to the T-based implementation.
|
||||
AppendFormatted<string?>(value, alignment, format);
|
||||
#endregion
|
||||
|
||||
#region AppendFormatted object
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="alignment">Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
|
||||
/// <param name="format">The format string.</param>
|
||||
public void AppendFormatted(object? value, int alignment = 0, string? format = null) =>
|
||||
// This overload is expected to be used rarely, only if either a) something strongly typed as object is
|
||||
// formatted with both an alignment and a format, or b) the compiler is unable to target type to T. It
|
||||
// exists purely to help make cases from (b) compile. Just delegate to the T-based implementation.
|
||||
AppendFormatted<object?>(value, alignment, format);
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
/// <summary>Gets whether the provider provides a custom formatter.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] // only used in a few hot path call sites
|
||||
internal static bool HasCustomFormatter(IFormatProvider provider)
|
||||
{
|
||||
Debug.Assert(provider is not null);
|
||||
Debug.Assert(provider is not CultureInfo || provider.GetFormat(typeof(ICustomFormatter)) is null, "Expected CultureInfo to not provide a custom formatter");
|
||||
return
|
||||
provider!.GetType() != typeof(CultureInfo) && // optimization to avoid GetFormat in the majority case
|
||||
provider.GetFormat(typeof(ICustomFormatter)) != null;
|
||||
}
|
||||
|
||||
/// <summary>Formats the value using the custom formatter from the provider.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="format">The format string.</param>
|
||||
/// <typeparam name="T">The type of the value to write.</typeparam>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void AppendCustomFormatter<T>(T value, string? format)
|
||||
{
|
||||
// This case is very rare, but we need to handle it prior to the other checks in case
|
||||
// a provider was used that supplied an ICustomFormatter which wanted to intercept the particular value.
|
||||
// We do the cast here rather than in the ctor, even though this could be executed multiple times per
|
||||
// formatting, to make the cast pay for play.
|
||||
Debug.Assert(_hasCustomFormatter);
|
||||
Debug.Assert(_provider != null);
|
||||
|
||||
ICustomFormatter? formatter = (ICustomFormatter?)_provider!.GetFormat(typeof(ICustomFormatter));
|
||||
Debug.Assert(formatter != null, "An incorrectly written provider said it implemented ICustomFormatter, and then didn't");
|
||||
|
||||
if (formatter is not null && formatter.Format(format, value, _provider) is string customFormatted)
|
||||
{
|
||||
AppendLiteral(customFormatted);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles adding any padding required for aligning a formatted value in an interpolation expression.</summary>
|
||||
/// <param name="startingPos">The position at which the written value started.</param>
|
||||
/// <param name="alignment">Non-zero minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
|
||||
private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment)
|
||||
{
|
||||
Debug.Assert(startingPos >= 0 && startingPos <= _pos);
|
||||
Debug.Assert(alignment != 0);
|
||||
|
||||
int charsWritten = _pos - startingPos;
|
||||
|
||||
bool leftAlign = false;
|
||||
if (alignment < 0)
|
||||
{
|
||||
leftAlign = true;
|
||||
alignment = -alignment;
|
||||
}
|
||||
|
||||
int paddingNeeded = alignment - charsWritten;
|
||||
if (paddingNeeded > 0)
|
||||
{
|
||||
EnsureCapacityForAdditionalChars(paddingNeeded);
|
||||
|
||||
if (leftAlign)
|
||||
{
|
||||
_chars.Slice(_pos, paddingNeeded).Fill(' ');
|
||||
}
|
||||
else
|
||||
{
|
||||
_chars.Slice(startingPos, charsWritten).CopyTo(_chars.Slice(startingPos + paddingNeeded));
|
||||
_chars.Slice(startingPos, paddingNeeded).Fill(' ');
|
||||
}
|
||||
|
||||
_pos += paddingNeeded;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ensures <see cref="_chars"/> has the capacity to store <paramref name="additionalChars"/> beyond <see cref="_pos"/>.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void EnsureCapacityForAdditionalChars(int additionalChars)
|
||||
{
|
||||
if (_chars.Length - _pos < additionalChars)
|
||||
{
|
||||
Grow(additionalChars);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fallback for fast path in <see cref="AppendLiteral(string)"/> when there's not enough space in the destination.</summary>
|
||||
/// <param name="value">The string to write.</param>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void GrowThenCopyString(string value)
|
||||
{
|
||||
Grow(value.Length);
|
||||
value.AsSpan().CopyTo(_chars.Slice(_pos));
|
||||
_pos += value.Length;
|
||||
}
|
||||
|
||||
/// <summary>Fallback for <see cref="AppendFormatted(ReadOnlySpan{char})"/> for when not enough space exists in the current buffer.</summary>
|
||||
/// <param name="value">The span to write.</param>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void GrowThenCopySpan(scoped ReadOnlySpan<char> value)
|
||||
{
|
||||
Grow(value.Length);
|
||||
value.CopyTo(_chars.Slice(_pos));
|
||||
_pos += value.Length;
|
||||
}
|
||||
|
||||
/// <summary>Grows <see cref="_chars"/> to have the capacity to store at least <paramref name="additionalChars"/> beyond <see cref="_pos"/>.</summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible
|
||||
private void Grow(int additionalChars)
|
||||
{
|
||||
// This method is called when the remaining space (_chars.Length - _pos) is
|
||||
// insufficient to store a specific number of additional characters. Thus, we
|
||||
// need to grow to at least that new total. GrowCore will handle growing by more
|
||||
// than that if possible.
|
||||
Debug.Assert(additionalChars > _chars.Length - _pos);
|
||||
GrowCore((uint)_pos + (uint)additionalChars);
|
||||
}
|
||||
|
||||
/// <summary>Grows the size of <see cref="_chars"/>.</summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible
|
||||
private void Grow()
|
||||
{
|
||||
// This method is called when the remaining space in _chars isn't sufficient to continue
|
||||
// the operation. Thus, we need at least one character beyond _chars.Length. GrowCore
|
||||
// will handle growing by more than that if possible.
|
||||
GrowCore((uint)_chars.Length + 1);
|
||||
}
|
||||
|
||||
/// <summary>Grow the size of <see cref="_chars"/> to at least the specified <paramref name="requiredMinCapacity"/>.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] // but reuse this grow logic directly in both of the above grow routines
|
||||
private void GrowCore(uint requiredMinCapacity)
|
||||
{
|
||||
// We want the max of how much space we actually required and doubling our capacity (without going beyond the max allowed length). We
|
||||
// also want to avoid asking for small arrays, to reduce the number of times we need to grow, and since we're working with unsigned
|
||||
// ints that could technically overflow if someone tried to, for example, append a huge string to a huge string, we also clamp to int.MaxValue.
|
||||
// Even if the array creation fails in such a case, we may later fail in ToStringAndClear.
|
||||
|
||||
const int StringMaxLength = 0x3FFFFFDF;
|
||||
uint newCapacity = Math.Max(requiredMinCapacity, Math.Min((uint)_chars.Length * 2, StringMaxLength));
|
||||
int arraySize = (int)Clamp(newCapacity, MinimumArrayPoolLength, int.MaxValue);
|
||||
|
||||
char[] newArray = ArrayPool<char>.Shared.Rent(arraySize);
|
||||
_chars.Slice(0, _pos).CopyTo(newArray);
|
||||
|
||||
char[]? toReturn = _arrayToReturnToPool;
|
||||
_chars = _arrayToReturnToPool = newArray;
|
||||
|
||||
if (toReturn is not null)
|
||||
{
|
||||
ArrayPool<char>.Shared.Return(toReturn);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint Clamp(uint value, uint min, uint max)
|
||||
{
|
||||
Debug.Assert(min <= max);
|
||||
|
||||
if (value < min)
|
||||
{
|
||||
return min;
|
||||
}
|
||||
else if (value > max)
|
||||
{
|
||||
return max;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ using ModelContextProtocol.Protocol.Types;
|
|||
using ModelContextProtocol.Shared;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ public static class McpClientExtensions
|
|||
parameters: null,
|
||||
McpJsonUtilities.JsonContext.Default.Object!,
|
||||
McpJsonUtilities.JsonContext.Default.Object,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -92,7 +92,7 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async Task<IList<McpClientTool>> ListToolsAsync(
|
||||
public static async ValueTask<IList<McpClientTool>> ListToolsAsync(
|
||||
this IMcpClient client,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
|
|
@ -205,7 +205,7 @@ public static class McpClientExtensions
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async Task<IList<McpClientPrompt>> ListPromptsAsync(
|
||||
public static async ValueTask<IList<McpClientPrompt>> ListPromptsAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
|
@ -311,7 +311,7 @@ public static class McpClientExtensions
|
|||
/// </remarks>
|
||||
/// <exception cref="McpException">Thrown when the prompt does not exist, when required arguments are missing, or when the server encounters an error processing the prompt.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static Task<GetPromptResult> GetPromptAsync(
|
||||
public static ValueTask<GetPromptResult> GetPromptAsync(
|
||||
this IMcpClient client,
|
||||
string name,
|
||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||
|
|
@ -349,12 +349,12 @@ public static class McpClientExtensions
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async Task<IList<ResourceTemplate>> ListResourceTemplatesAsync(
|
||||
public static async ValueTask<IList<McpClientResourceTemplate>> ListResourceTemplatesAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
List<ResourceTemplate>? templates = null;
|
||||
List<McpClientResourceTemplate>? resourceTemplates = null;
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
|
|
@ -366,20 +366,17 @@ public static class McpClientExtensions
|
|||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (templates is null)
|
||||
resourceTemplates ??= new List<McpClientResourceTemplate>(templateResults.ResourceTemplates.Count);
|
||||
foreach (var template in templateResults.ResourceTemplates)
|
||||
{
|
||||
templates = templateResults.ResourceTemplates;
|
||||
}
|
||||
else
|
||||
{
|
||||
templates.AddRange(templateResults.ResourceTemplates);
|
||||
resourceTemplates.Add(new McpClientResourceTemplate(client, template));
|
||||
}
|
||||
|
||||
cursor = templateResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
|
||||
return templates;
|
||||
return resourceTemplates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -395,7 +392,7 @@ public static class McpClientExtensions
|
|||
/// with cursors if the server responds with templates split across multiple responses.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Every iteration through the returned <see cref="IAsyncEnumerable{ResourceTemplate}"/>
|
||||
/// Every iteration through the returned <see cref="IAsyncEnumerable{McpClientResourceTemplate}"/>
|
||||
/// will result in re-querying the server and yielding the sequence of available resource templates.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
|
|
@ -409,7 +406,7 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async IAsyncEnumerable<ResourceTemplate> EnumerateResourceTemplatesAsync(
|
||||
public static async IAsyncEnumerable<McpClientResourceTemplate> EnumerateResourceTemplatesAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
|
@ -424,9 +421,9 @@ public static class McpClientExtensions
|
|||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var template in templateResults.ResourceTemplates)
|
||||
foreach (var templateResult in templateResults.ResourceTemplates)
|
||||
{
|
||||
yield return template;
|
||||
yield return new McpClientResourceTemplate(client, templateResult);
|
||||
}
|
||||
|
||||
cursor = templateResults.NextCursor;
|
||||
|
|
@ -463,12 +460,12 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async Task<IList<Resource>> ListResourcesAsync(
|
||||
public static async ValueTask<IList<McpClientResource>> ListResourcesAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
List<Resource>? resources = null;
|
||||
List<McpClientResource>? resources = null;
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
|
|
@ -480,13 +477,10 @@ public static class McpClientExtensions
|
|||
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (resources is null)
|
||||
resources ??= new List<McpClientResource>(resourceResults.Resources.Count);
|
||||
foreach (var resource in resourceResults.Resources)
|
||||
{
|
||||
resources = resourceResults.Resources;
|
||||
}
|
||||
else
|
||||
{
|
||||
resources.AddRange(resourceResults.Resources);
|
||||
resources.Add(new McpClientResource(client, resource));
|
||||
}
|
||||
|
||||
cursor = resourceResults.NextCursor;
|
||||
|
|
@ -509,7 +503,7 @@ public static class McpClientExtensions
|
|||
/// with cursors if the server responds with resources split across multiple responses.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Every iteration through the returned <see cref="IAsyncEnumerable{Resource}"/>
|
||||
/// Every iteration through the returned <see cref="IAsyncEnumerable{McpClientResource}"/>
|
||||
/// will result in re-querying the server and yielding the sequence of available resources.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
|
|
@ -523,7 +517,7 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async IAsyncEnumerable<Resource> EnumerateResourcesAsync(
|
||||
public static async IAsyncEnumerable<McpClientResource> EnumerateResourcesAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
|
@ -540,7 +534,7 @@ public static class McpClientExtensions
|
|||
|
||||
foreach (var resource in resourceResults.Resources)
|
||||
{
|
||||
yield return resource;
|
||||
yield return new McpClientResource(client, resource);
|
||||
}
|
||||
|
||||
cursor = resourceResults.NextCursor;
|
||||
|
|
@ -557,7 +551,7 @@ public static class McpClientExtensions
|
|||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="uri"/> is empty or composed entirely of whitespace.</exception>
|
||||
public static Task<ReadResourceResult> ReadResourceAsync(
|
||||
public static ValueTask<ReadResourceResult> ReadResourceAsync(
|
||||
this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
|
@ -579,7 +573,7 @@ public static class McpClientExtensions
|
|||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
|
||||
public static Task<ReadResourceResult> ReadResourceAsync(
|
||||
public static ValueTask<ReadResourceResult> ReadResourceAsync(
|
||||
this IMcpClient client, Uri uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
|
@ -588,6 +582,31 @@ public static class McpClientExtensions
|
|||
return ReadResourceAsync(client, uri.ToString(), cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a resource from the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client instance used to communicate with the MCP server.</param>
|
||||
/// <param name="uriTemplate">The uri template of the resource.</param>
|
||||
/// <param name="arguments">Arguments to use to format <paramref name="uriTemplate"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uriTemplate"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="uriTemplate"/> is empty or composed entirely of whitespace.</exception>
|
||||
public static ValueTask<ReadResourceResult> ReadResourceAsync(
|
||||
this IMcpClient client, string uriTemplate, IReadOnlyDictionary<string, object?> arguments, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(uriTemplate);
|
||||
Throw.IfNull(arguments);
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.ResourcesRead,
|
||||
new() { Uri = UriTemplate.FormatUri(uriTemplate, arguments) },
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests completion suggestions for a prompt argument or resource reference.
|
||||
/// </summary>
|
||||
|
|
@ -617,7 +636,7 @@ public static class McpClientExtensions
|
|||
/// <exception cref="ArgumentNullException"><paramref name="argumentName"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="argumentName"/> is empty or composed entirely of whitespace.</exception>
|
||||
/// <exception cref="McpException">The server returned an error response.</exception>
|
||||
public static Task<CompleteResult> CompleteAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default)
|
||||
public static ValueTask<CompleteResult> CompleteAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNull(reference);
|
||||
|
|
@ -675,7 +694,7 @@ public static class McpClientExtensions
|
|||
new() { Uri = uri },
|
||||
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -744,7 +763,7 @@ public static class McpClientExtensions
|
|||
new() { Uri = uri },
|
||||
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -813,7 +832,7 @@ public static class McpClientExtensions
|
|||
/// });
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static Task<CallToolResponse> CallToolAsync(
|
||||
public static ValueTask<CallToolResponse> CallToolAsync(
|
||||
this IMcpClient client,
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||
|
|
@ -842,7 +861,7 @@ public static class McpClientExtensions
|
|||
McpJsonUtilities.JsonContext.Default.CallToolResponse,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
static async Task<CallToolResponse> SendRequestWithProgressAsync(
|
||||
static async ValueTask<CallToolResponse> SendRequestWithProgressAsync(
|
||||
IMcpClient client,
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, object?>? arguments,
|
||||
|
|
@ -1061,7 +1080,7 @@ public static class McpClientExtensions
|
|||
new() { Level = level },
|
||||
McpJsonUtilities.JsonContext.Default.SetLevelRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
64
src/ModelContextProtocol/Client/McpClientResource.cs
Normal file
64
src/ModelContextProtocol/Client/McpClientResource.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using ModelContextProtocol.Protocol.Types;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a named resource that can be retrieved from an MCP server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This class provides a client-side wrapper around a resource defined on an MCP server. It allows
|
||||
/// retrieving the resource's content by sending a request to the server with the resource's URI.
|
||||
/// Instances of this class are typically obtained by calling <see cref="McpClientExtensions.ListResourcesAsync"/>
|
||||
/// or <see cref="McpClientExtensions.EnumerateResourcesAsync"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class McpClientResource
|
||||
{
|
||||
private readonly IMcpClient _client;
|
||||
|
||||
internal McpClientResource(IMcpClient client, Resource resource)
|
||||
{
|
||||
_client = client;
|
||||
ProtocolResource = resource;
|
||||
}
|
||||
|
||||
/// <summary>Gets the underlying protocol <see cref="Resource"/> type for this instance.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property provides direct access to the underlying protocol representation of the resource,
|
||||
/// which can be useful for advanced scenarios or when implementing custom MCP client extensions.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For most common use cases, you can use the more convenient <see cref="Name"/> and
|
||||
/// <see cref="Description"/> properties instead of accessing the <see cref="ProtocolResource"/> directly.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public Resource ProtocolResource { get; }
|
||||
|
||||
/// <summary>Gets the URI of the resource.</summary>
|
||||
public string Uri => ProtocolResource.Uri;
|
||||
|
||||
/// <summary>Gets the name of the resource.</summary>
|
||||
public string Name => ProtocolResource.Name;
|
||||
|
||||
/// <summary>Gets a description of the resource.</summary>
|
||||
public string? Description => ProtocolResource.Description;
|
||||
|
||||
/// <summary>Gets a media (MIME) type of the resource.</summary>
|
||||
public string? MimeType => ProtocolResource.MimeType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets this resource's content by sending a request to the server.
|
||||
/// </summary>
|
||||
/// <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}"/> containing the resource's result with content and messages.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is a convenience method that internally calls <see cref="McpClientExtensions.ReadResourceAsync(IMcpClient, string, CancellationToken)"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask<ReadResourceResult> ReadAsync(
|
||||
CancellationToken cancellationToken = default) =>
|
||||
_client.ReadResourceAsync(Uri, cancellationToken);
|
||||
}
|
||||
64
src/ModelContextProtocol/Client/McpClientResourceTemplate.cs
Normal file
64
src/ModelContextProtocol/Client/McpClientResourceTemplate.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using ModelContextProtocol.Protocol.Types;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a named resource template that can be retrieved from an MCP server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This class provides a client-side wrapper around a resource template defined on an MCP server. It allows
|
||||
/// retrieving the resource template's content by sending a request to the server with the resource's URI.
|
||||
/// Instances of this class are typically obtained by calling <see cref="McpClientExtensions.ListResourceTemplatesAsync"/>
|
||||
/// or <see cref="McpClientExtensions.EnumerateResourceTemplatesAsync"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class McpClientResourceTemplate
|
||||
{
|
||||
private readonly IMcpClient _client;
|
||||
|
||||
internal McpClientResourceTemplate(IMcpClient client, ResourceTemplate resourceTemplate)
|
||||
{
|
||||
_client = client;
|
||||
ProtocolResourceTemplate = resourceTemplate;
|
||||
}
|
||||
|
||||
/// <summary>Gets the underlying protocol <see cref="ResourceTemplate"/> type for this instance.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property provides direct access to the underlying protocol representation of the resource template,
|
||||
/// which can be useful for advanced scenarios or when implementing custom MCP client extensions.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For most common use cases, you can use the more convenient <see cref="UriTemplate"/> and
|
||||
/// <see cref="Description"/> properties instead of accessing the <see cref="ProtocolResourceTemplate"/> directly.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ResourceTemplate ProtocolResourceTemplate { get; }
|
||||
|
||||
/// <summary>Gets the URI template of the resource template.</summary>
|
||||
public string UriTemplate => ProtocolResourceTemplate.UriTemplate;
|
||||
|
||||
/// <summary>Gets the name of the resource template.</summary>
|
||||
public string Name => ProtocolResourceTemplate.Name;
|
||||
|
||||
/// <summary>Gets a description of the resource template.</summary>
|
||||
public string? Description => ProtocolResourceTemplate.Description;
|
||||
|
||||
/// <summary>Gets a media (MIME) type of the resource template.</summary>
|
||||
public string? MimeType => ProtocolResourceTemplate.MimeType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets this resource template's content by formatting a URI from the template and supplied arguments
|
||||
/// and sending a request to the server.
|
||||
/// </summary>
|
||||
/// <param name="arguments">A dictionary of arguments to pass to the tool. Each key represents a parameter name,
|
||||
/// and its associated value represents the argument value.
|
||||
/// </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}"/> containing the resource template's result with content and messages.</returns>
|
||||
public ValueTask<ReadResourceResult> ReadAsync(
|
||||
IReadOnlyDictionary<string, object?> arguments,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
_client.ReadResourceAsync(UriTemplate, arguments, cancellationToken);
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ public sealed class McpClientTool : AIFunction
|
|||
/// });
|
||||
/// </code>
|
||||
/// </example>
|
||||
public Task<CallToolResponse> CallAsync(
|
||||
public ValueTask<CallToolResponse> CallAsync(
|
||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||
IProgress<ProgressNotificationValue>? progress = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
|
|
|
|||
|
|
@ -297,6 +297,140 @@ public static partial class McpServerBuilderExtensions
|
|||
}
|
||||
#endregion
|
||||
|
||||
#region WithResources
|
||||
private const string WithResourcesRequiresUnreferencedCodeMessage =
|
||||
$"The non-generic {nameof(WithResources)} and {nameof(WithResourcesFromAssembly)} methods require dynamic lookup of member metadata" +
|
||||
$"and may not work in Native AOT. Use the generic {nameof(WithResources)} method instead.";
|
||||
|
||||
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
|
||||
/// <typeparam name="TResourceType">The resource type.</typeparam>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// This method discovers all instance and static methods (public and non-public) on the specified <typeparamref name="TResourceType"/>
|
||||
/// type, where the members are attributed as <see cref="McpServerResourceAttribute"/>, and adds an <see cref="McpServerResource"/>
|
||||
/// instance for each. For instance members, an instance will be constructed for each invocation of the resource.
|
||||
/// </remarks>
|
||||
public static IMcpServerBuilder WithResources<[DynamicallyAccessedMembers(
|
||||
DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors)] TResourceType>(
|
||||
this IMcpServerBuilder builder)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
foreach (var resourceTemplateMethod in typeof(TResourceType).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
|
||||
{
|
||||
if (resourceTemplateMethod.GetCustomAttribute<McpServerResourceAttribute>() is not null)
|
||||
{
|
||||
builder.Services.AddSingleton((Func<IServiceProvider, McpServerResource>)(resourceTemplateMethod.IsStatic ?
|
||||
services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services }) :
|
||||
services => McpServerResource.Create(resourceTemplateMethod, typeof(TResourceType), new() { Services = services })));
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="resourcetemplates">The <see cref="McpServerResource"/> instances to add to the server.</param>
|
||||
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="resourcetemplates"/> is <see langword="null"/>.</exception>
|
||||
public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IEnumerable<McpServerResource> resourcetemplates)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(resourcetemplates);
|
||||
|
||||
foreach (var resourceTemplate in resourcetemplates)
|
||||
{
|
||||
if (resourceTemplate is not null)
|
||||
{
|
||||
builder.Services.AddSingleton(resourceTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="resourceTemplateTypes">Types with marked methods to add as resources to the server.</param>
|
||||
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="resourceTemplateTypes"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// This method discovers all instance and static methods (public and non-public) on the specified <paramref name="resourceTemplateTypes"/>
|
||||
/// types, where the methods are attributed as <see cref="McpServerResourceAttribute"/>, and adds an <see cref="McpServerResource"/>
|
||||
/// instance for each. For instance methods, an instance will be constructed for each invocation of the resource.
|
||||
/// </remarks>
|
||||
[RequiresUnreferencedCode(WithResourcesRequiresUnreferencedCodeMessage)]
|
||||
public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IEnumerable<Type> resourceTemplateTypes)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(resourceTemplateTypes);
|
||||
|
||||
foreach (var resourceTemplateType in resourceTemplateTypes)
|
||||
{
|
||||
if (resourceTemplateType is not null)
|
||||
{
|
||||
foreach (var resourceTemplateMethod in resourceTemplateType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
|
||||
{
|
||||
if (resourceTemplateMethod.GetCustomAttribute<McpServerResourceAttribute>() is not null)
|
||||
{
|
||||
builder.Services.AddSingleton((Func<IServiceProvider, McpServerResource>)(resourceTemplateMethod.IsStatic ?
|
||||
services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services }) :
|
||||
services => McpServerResource.Create(resourceTemplateMethod, resourceTemplateType, new() { Services = services })));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds types marked with the <see cref="McpServerResourceTypeAttribute"/> attribute from the given assembly as resources to the server.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="resourceAssembly">The assembly to load the types from. If <see langword="null"/>, the calling assembly will be used.</param>
|
||||
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method scans the specified assembly (or the calling assembly if none is provided) for classes
|
||||
/// marked with the <see cref="McpServerResourceTypeAttribute"/>. It then discovers all members within those
|
||||
/// classes that are marked with the <see cref="McpServerResourceAttribute"/> and registers them as <see cref="McpServerResource"/>s
|
||||
/// in the <paramref name="builder"/>'s <see cref="IServiceCollection"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The method automatically handles both static and instance members. For instance members, a new instance
|
||||
/// of the containing class will be constructed for each invocation of the resource.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Resource templates registered through this method can be discovered by clients using the <c>list_resourcetemplates</c> request
|
||||
/// and invoked using the <c>read_resource</c> request.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Note that this method performs reflection at runtime and may not work in Native AOT scenarios. For
|
||||
/// Native AOT compatibility, consider using the generic <see cref="WithResources{TResourceType}"/> method instead.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RequiresUnreferencedCode(WithResourcesRequiresUnreferencedCodeMessage)]
|
||||
public static IMcpServerBuilder WithResourcesFromAssembly(this IMcpServerBuilder builder, Assembly? resourceAssembly = null)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
resourceAssembly ??= Assembly.GetCallingAssembly();
|
||||
|
||||
return builder.WithResources(
|
||||
from t in resourceAssembly.GetTypes()
|
||||
where t.GetCustomAttribute<McpServerResourceTypeAttribute>() is not null
|
||||
select t);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Handlers
|
||||
/// <summary>
|
||||
/// Configures a handler for listing resource templates available from the Model Context Protocol server.
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public static class McpEndpointExtensions
|
|||
/// <param name="serializerOptions">The options governing request serialization.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the deserialized result.</returns>
|
||||
public static Task<TResult> SendRequestAsync<TParameters, TResult>(
|
||||
public static ValueTask<TResult> SendRequestAsync<TParameters, TResult>(
|
||||
this IMcpEndpoint endpoint,
|
||||
string method,
|
||||
TParameters parameters,
|
||||
|
|
@ -66,7 +66,7 @@ public static class McpEndpointExtensions
|
|||
/// <param name="requestId">The request id for the request.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the deserialized result.</returns>
|
||||
internal static async Task<TResult> SendRequestAsync<TParameters, TResult>(
|
||||
internal static async ValueTask<TResult> SendRequestAsync<TParameters, TResult>(
|
||||
this IMcpEndpoint endpoint,
|
||||
string method,
|
||||
TParameters parameters,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Protocol.Messages;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Transport;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.ServerSentEvents;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Types;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Types;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Types;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
|
|
|||
|
|
@ -16,5 +16,5 @@ public class ReadResourceRequestParams : RequestParams
|
|||
/// The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it.
|
||||
/// </summary>
|
||||
[JsonPropertyName("uri")]
|
||||
public string? Uri { get; init; }
|
||||
public required string Uri { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ public abstract class ResourceContents
|
|||
[JsonPropertyName("mimeType")]
|
||||
public string? MimeType { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Provides a <see cref="JsonConverter"/> for <see cref="ResourceContents"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -67,4 +67,27 @@ public record ResourceTemplate
|
|||
/// </remarks>
|
||||
[JsonPropertyName("annotations")]
|
||||
public Annotations? Annotations { get; init; }
|
||||
|
||||
/// <summary>Gets whether <see cref="UriTemplate"/> contains any template expressions.</summary>
|
||||
[JsonIgnore]
|
||||
public bool IsTemplated => UriTemplate.Contains('{');
|
||||
|
||||
/// <summary>Converts the <see cref="ResourceTemplate"/> into a <see cref="Resource"/>.</summary>
|
||||
/// <returns>A <see cref="Resource"/> if <see cref="IsTemplated"/> is <see langword="false"/>; otherwise, <see langword="null"/>.</returns>
|
||||
public Resource? AsResource()
|
||||
{
|
||||
if (IsTemplated)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
Uri = UriTemplate,
|
||||
Name = Name,
|
||||
Description = Description,
|
||||
MimeType = MimeType,
|
||||
Annotations = Annotations,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -87,4 +87,22 @@ public class ResourcesCapability
|
|||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<UnsubscribeRequestParams>, CancellationToken, ValueTask<EmptyResult>>? UnsubscribeFromResourcesHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of resources served by the server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Resources specified via <see cref="ResourceCollection"/> augment the <see cref="ListResourcesHandler"/>, <see cref="ListResourceTemplatesHandler"/>
|
||||
/// and <see cref="ReadResourceHandler"/> handlers, if provided. Resources with template expressions in their URI templates are considered resource templates
|
||||
/// and are listed via ListResourceTemplate, whereas resources without template parameters are considered static resources and are listed with ListResources.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// ReadResource requests will first check the <see cref="ResourceCollection"/> for the exact resource being requested. If no match is found, they'll proceed to
|
||||
/// try to match the resource against each resource template in <see cref="ResourceCollection"/>. If no match is still found, the request will fall back to
|
||||
/// any handler registered for <see cref="ReadResourceHandler"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public McpServerPrimitiveCollection<McpServerResource>? ResourceCollection { get; set; }
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
|
@ -27,7 +27,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerPrompt Create(
|
||||
MethodInfo method,
|
||||
|
|
@ -44,7 +44,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerPrompt Create(
|
||||
MethodInfo method,
|
||||
|
|
@ -89,6 +89,27 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
};
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
static RequestContext<GetPromptRequestParams>? GetRequestContext(AIFunctionArguments args)
|
||||
|
|
@ -138,13 +159,18 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
return new AIFunctionMcpServerPrompt(function, prompt);
|
||||
}
|
||||
|
||||
private static McpServerPromptCreateOptions? DeriveOptions(MethodInfo method, McpServerPromptCreateOptions? options)
|
||||
private static McpServerPromptCreateOptions DeriveOptions(MethodInfo method, McpServerPromptCreateOptions? options)
|
||||
{
|
||||
McpServerPromptCreateOptions newOptions = options?.Clone() ?? new();
|
||||
|
||||
if (method.GetCustomAttribute<McpServerPromptAttribute>() is { } attr)
|
||||
if (method.GetCustomAttribute<McpServerPromptAttribute>() is { } promptAttr)
|
||||
{
|
||||
newOptions.Name ??= attr.Name;
|
||||
newOptions.Name ??= promptAttr.Name;
|
||||
}
|
||||
|
||||
if (method.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)
|
||||
{
|
||||
newOptions.Description ??= descAttr.Description;
|
||||
}
|
||||
|
||||
return newOptions;
|
||||
|
|
@ -167,18 +193,6 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
public override Prompt ProtocolPrompt { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// This implementation invokes the underlying <see cref="AIFunction"/> with the request arguments, and processes
|
||||
/// the result to create a standardized <see cref="GetPromptResult"/>. The method supports various return types from
|
||||
/// the underlying function:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Direct <see cref="GetPromptResult"/> instances are returned as-is</description></item>
|
||||
/// <item><description>String values are converted to a single user message</description></item>
|
||||
/// <item><description>Single <see cref="PromptMessage"/> objects are wrapped in a result</description></item>
|
||||
/// <item><description>Collections of <see cref="PromptMessage"/> objects are combined in a result</description></item>
|
||||
/// <item><description><see cref="ChatMessage"/> objects are converted to prompt messages</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public override async ValueTask<GetPromptResult> GetAsync(
|
||||
RequestContext<GetPromptRequestParams> request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue