⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 51 additions & 10 deletions src/ModelContextProtocol.Core/UriTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,13 @@ public static Regex CreateParser(string uriTemplate)

switch (m.Groups["operator"].Value)
{
case "#": AppendExpression(ref pattern, paramNames, '#', "[^,]+"); break;
case "/": AppendExpression(ref pattern, paramNames, '/', "[^/?]+"); break;
default: AppendExpression(ref pattern, paramNames, null, "[^/?&]+"); break;

case "+": AppendExpression(ref pattern, paramNames, null, "[^?&#]*"); break;
case "#": AppendExpression(ref pattern, paramNames, '#', "[^,]*"); break;
case ".": AppendExpression(ref pattern, paramNames, '.', "[^./?#]*"); break;
case "/": AppendExpression(ref pattern, paramNames, '/', "[^/?#]*"); break;
case ";": AppendPathParameterExpression(ref pattern, paramNames); break;
default: AppendExpression(ref pattern, paramNames, null, "[^/?&#]*"); break;

case "?": AppendQueryExpression(ref pattern, paramNames, '?'); break;
case "&": AppendQueryExpression(ref pattern, paramNames, '&'); break;
}
Expand Down Expand Up @@ -132,7 +135,7 @@ static void AppendParameter(ref DefaultInterpolatedStringHandler pattern, string
pattern.AppendFormatted(paramName);
pattern.AppendFormatted("=(?<");
pattern.AppendFormatted(paramName);
pattern.AppendFormatted(">[^/?&]+))?");
pattern.AppendFormatted(">[^/?&]*))?");
}
}

Expand All @@ -143,9 +146,11 @@ static void AppendParameter(ref DefaultInterpolatedStringHandler pattern, string
// characters make up a parameter value. Then, for each name in `paramNames`, it optionally
// appends the escaped `prefix` (only on the first parameter, then switches to ','), and
// adds an optional named capture group `(?<paramName>valueChars)` to match and capture that value.
// Note: For "+" (reserved expansion) operator, prefix is null but valueChars allows "/" characters.
// Note: For "." (label expansion) operator, the separator is "." instead of ",".
static void AppendExpression(ref DefaultInterpolatedStringHandler pattern, List<string> paramNames, char? prefix, string valueChars)
{
Debug.Assert(prefix is '#' or '/' or null);
Debug.Assert(prefix is '#' or '/' or '.' or null);

if (paramNames.Count > 0)
{
Expand All @@ -157,9 +162,19 @@ static void AppendExpression(ref DefaultInterpolatedStringHandler pattern, List<
}

AppendParameter(ref pattern, paramNames[0], valueChars);

// For label expansion (.), the separator between values is also a dot
// For path segment expansion (/), the separator between values is also a slash
string separator = prefix switch
{
'.' => "\\.",
'/' => "\\/",
_ => "\\,"
};
for (int i = 1; i < paramNames.Count; i++)
{
pattern.AppendFormatted("\\,?");
pattern.AppendFormatted(separator);
pattern.AppendFormatted('?');
AppendParameter(ref pattern, paramNames[i], valueChars);
}

Expand All @@ -173,6 +188,32 @@ static void AppendParameter(ref DefaultInterpolatedStringHandler pattern, string
}
}
}

// Appends a regex fragment for path-style parameter expansion (;).
// Format: ;name=value or ;name (if value is empty), separated by semicolons.
// Each parameter is made optional and captured by a named group.
static void AppendPathParameterExpression(ref DefaultInterpolatedStringHandler pattern, List<string> paramNames)
{
if (paramNames.Count > 0)
{
AppendParameter(ref pattern, paramNames[0]);
for (int i = 1; i < paramNames.Count; i++)
{
AppendParameter(ref pattern, paramNames[i]);
}

static void AppendParameter(ref DefaultInterpolatedStringHandler pattern, string paramName)
{
// Match ;name or ;name=value
paramName = Regex.Escape(paramName);
pattern.AppendLiteral("(?:;");
pattern.AppendLiteral(paramName);
pattern.AppendLiteral("(?:=(?<");
pattern.AppendLiteral(paramName);
pattern.AppendLiteral(">[^;/?&]*))?)?");
}
}
}
}

/// <summary>
Expand Down Expand Up @@ -363,7 +404,7 @@ value as string ??
}
}

if (expansions.Count > 0 &&
if (expansions.Count > 0 &&
(modifierBehavior.PrefixEmptyExpansions || !expansions.All(string.IsNullOrEmpty)))
{
builder.AppendLiteral(modifierBehavior.Prefix);
Expand Down Expand Up @@ -460,13 +501,13 @@ static void AppendHex(ref DefaultInterpolatedStringHandler builder, char c)
/// Defines an equality comparer for Uri templates as follows:
/// 1. Non-templated Uris use regular System.Uri equality comparison (host name is case insensitive).
/// 2. Templated Uris use regular string equality.
///
///
/// We do this because non-templated resources are looked up directly from the resource dictionary
/// and we need to make sure equality is implemented correctly. Templated Uris are resolved in a
/// fallback step using linear traversal of the resource dictionary, so their equality is only
/// there to distinguish between different templates.
/// </summary>
public sealed class UriTemplateComparer : IEqualityComparer<string>
internal sealed class UriTemplateComparer : IEqualityComparer<string>
{
public static IEqualityComparer<string> Instance { get; } = new UriTemplateComparer();

Expand Down
51 changes: 35 additions & 16 deletions tests/ModelContextProtocol.Tests/ClientServerTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,55 @@ public abstract class ClientServerTestBase : LoggedTest, IAsyncDisposable
{
private readonly Pipe _clientToServerPipe = new();
private readonly Pipe _serverToClientPipe = new();
private readonly IMcpServerBuilder _builder;
private readonly CancellationTokenSource _cts;
private readonly Task _serverTask;
private readonly CancellationTokenSource _cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
private Task _serverTask = Task.CompletedTask;

public ClientServerTestBase(ITestOutputHelper testOutputHelper)
public ClientServerTestBase(ITestOutputHelper testOutputHelper, bool startServer = true)
: base(testOutputHelper)
{
ServiceCollection sc = new();
sc.AddLogging();
sc.AddSingleton(XunitLoggerProvider);
sc.AddSingleton<ILoggerProvider>(MockLoggerProvider);
_builder = sc
ServiceCollection.AddLogging();
ServiceCollection.AddSingleton(XunitLoggerProvider);
ServiceCollection.AddSingleton<ILoggerProvider>(MockLoggerProvider);
McpServerBuilder = ServiceCollection
.AddMcpServer()
.WithStreamServerTransport(_clientToServerPipe.Reader.AsStream(), _serverToClientPipe.Writer.AsStream());
ConfigureServices(sc, _builder);
ServiceProvider = sc.BuildServiceProvider(validateScopes: true);

_cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
Server = ServiceProvider.GetRequiredService<McpServer>();
_serverTask = Server.RunAsync(_cts.Token);
ConfigureServices(ServiceCollection, McpServerBuilder);

if (startServer)
{
StartServer();
}
}

protected McpServer Server { get; }
protected ServiceCollection ServiceCollection { get; } = [];

protected IMcpServerBuilder McpServerBuilder { get; }

protected McpServer Server
{
get => field ?? throw new InvalidOperationException("You must call StartServer first.");
private set => field = value;
}

protected IServiceProvider ServiceProvider { get; }
protected ServiceProvider ServiceProvider
{
get => field ?? throw new InvalidOperationException("You must call StartServer first.");
private set => field = value;
}

protected virtual void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
}

protected McpServer StartServer()
{
ServiceProvider = ServiceCollection.BuildServiceProvider(validateScopes: true);
Server = ServiceProvider.GetRequiredService<McpServer>();
_serverTask = Server.RunAsync(_cts.Token);
return Server;
}

public async ValueTask DisposeAsync()
{
await _cts.CancelAsync();
Expand Down
Loading
Loading