Skip to content

Commit ab07843

Browse files
committed
Allow reserved expansion to include empty values
1 parent 29cab8f commit ab07843

File tree

2 files changed

+50
-7
lines changed

2 files changed

+50
-7
lines changed

src/ModelContextProtocol.Core/UriTemplate.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ public static Regex CreateParser(string uriTemplate)
8484

8585
switch (m.Groups["operator"].Value)
8686
{
87-
case "+": AppendExpression(ref pattern, paramNames, null, "[^?&#]+"); break;
88-
case "#": AppendExpression(ref pattern, paramNames, '#', "[^,]+"); break;
89-
case ".": AppendExpression(ref pattern, paramNames, '.', "[^./?#]+"); break;
90-
case "/": AppendExpression(ref pattern, paramNames, '/', "[^/?#]+"); break;
87+
case "+": AppendExpression(ref pattern, paramNames, null, "[^?&#]*"); break;
88+
case "#": AppendExpression(ref pattern, paramNames, '#', "[^,]*"); break;
89+
case ".": AppendExpression(ref pattern, paramNames, '.', "[^./?#]*"); break;
90+
case "/": AppendExpression(ref pattern, paramNames, '/', "[^/?#]*"); break;
9191
case ";": AppendPathParameterExpression(ref pattern, paramNames); break;
92-
default: AppendExpression(ref pattern, paramNames, null, "[^/?&#]+"); break;
92+
default: AppendExpression(ref pattern, paramNames, null, "[^/?&#]*"); break;
9393

9494
case "?": AppendQueryExpression(ref pattern, paramNames, '?'); break;
9595
case "&": AppendQueryExpression(ref pattern, paramNames, '&'); break;
@@ -135,7 +135,7 @@ static void AppendParameter(ref DefaultInterpolatedStringHandler pattern, string
135135
pattern.AppendFormatted(paramName);
136136
pattern.AppendFormatted("=(?<");
137137
pattern.AppendFormatted(paramName);
138-
pattern.AppendFormatted(">[^/?&]+))?");
138+
pattern.AppendFormatted(">[^/?&]*))?");
139139
}
140140
}
141141

@@ -210,7 +210,7 @@ static void AppendParameter(ref DefaultInterpolatedStringHandler pattern, string
210210
pattern.AppendLiteral(paramName);
211211
pattern.AppendLiteral("(?:=(?<");
212212
pattern.AppendLiteral(paramName);
213-
pattern.AppendLiteral(">[^;/?&]+))?)?");
213+
pattern.AppendLiteral(">[^;/?&]*))?)?");
214214
}
215215
}
216216
}

tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,49 @@ await AssertNoMatchAsync(
271271
uri: "wrongscheme://example.com/foo");
272272
}
273273

274+
/// <summary>
275+
/// RFC 6570 specifies that empty values should expand to empty strings.
276+
/// See https://datatracker.ietf.org/doc/html/rfc6570#page-22 test cases: O{+empty}X matches OX.
277+
/// </summary>
278+
[Fact]
279+
public async Task ReservedExpansion_MatchesEmptyValue()
280+
{
281+
// Per RFC 6570: O{+empty}X should match OX when empty is ""
282+
await AssertMatchAsync(
283+
uriTemplate: "test://O{+empty}X",
284+
method: (string empty) => $"empty:[{empty}]",
285+
uri: "test://OX",
286+
expectedResult: "empty:[]");
287+
}
288+
289+
/// <summary>
290+
/// RFC 6570 empty expansion test - reserved expansion at end of template.
291+
/// </summary>
292+
[Fact]
293+
public async Task ReservedExpansion_MatchesEmptyValueAtEnd()
294+
{
295+
// {+var} at the end should match empty string
296+
await AssertMatchAsync(
297+
uriTemplate: "test://prefix{+suffix}",
298+
method: (string suffix) => $"suffix:[{suffix}]",
299+
uri: "test://prefix",
300+
expectedResult: "suffix:[]");
301+
}
302+
303+
/// <summary>
304+
/// RFC 6570 empty expansion test - reserved expansion at start of template.
305+
/// </summary>
306+
[Fact]
307+
public async Task ReservedExpansion_MatchesEmptyValueAtStart()
308+
{
309+
// {+var} at the start should match empty string
310+
await AssertMatchAsync(
311+
uriTemplate: "test://{+prefix}suffix",
312+
method: (string prefix) => $"prefix:[{prefix}]",
313+
uri: "test://suffix",
314+
expectedResult: "prefix:[]");
315+
}
316+
274317
#endregion
275318

276319
#region Level 2: Fragment Expansion {#var}

0 commit comments

Comments
 (0)