Skip to content

Commit 4da421c

Browse files
authored
Merge branch 'main' into copilot/normalize-calltoolresult-content
2 parents 6088d57 + 774a1db commit 4da421c

22 files changed

Lines changed: 1002 additions & 158 deletions

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262

6363
<!-- Testing dependencies -->
6464
<ItemGroup>
65-
<PackageVersion Include="Anthropic" Version="12.5.0" />
65+
<PackageVersion Include="Anthropic" Version="12.8.0" />
6666
<PackageVersion Include="coverlet.collector" Version="8.0.0">
6767
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
6868
<PrivateAssets>all</PrivateAssets>

src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs

Lines changed: 96 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -43,31 +43,39 @@ public void PostConfigure(string? name, McpServerOptions options)
4343

4444
private void ConfigureListToolsFilter(McpServerOptions options)
4545
{
46-
options.Filters.Request.ListToolsFilters.Add(next => async (context, cancellationToken) =>
46+
options.Filters.Request.ListToolsFilters.Add(next =>
4747
{
48-
context.Items[AuthorizationFilterInvokedKey] = true;
49-
50-
var result = await next(context, cancellationToken);
51-
await FilterAuthorizedItemsAsync(
52-
result.Tools, static tool => tool.McpServerTool,
53-
context.User, context.Services, context);
54-
return result;
48+
var toolCollection = options.ToolCollection;
49+
return async (context, cancellationToken) =>
50+
{
51+
context.Items[AuthorizationFilterInvokedKey] = true;
52+
53+
var result = await next(context, cancellationToken);
54+
await FilterAuthorizedItemsAsync(
55+
result.Tools, tool => toolCollection is not null && toolCollection.TryGetPrimitive(tool.Name, out var serverTool) ? serverTool : null,
56+
context.User, context.Services, context);
57+
return result;
58+
};
5559
});
5660
}
5761

5862
private static void CheckListToolsFilter(McpServerOptions options)
5963
{
60-
options.Filters.Request.ListToolsFilters.Add(next => async (context, cancellationToken) =>
64+
options.Filters.Request.ListToolsFilters.Add(next =>
6165
{
62-
var result = await next(context, cancellationToken);
63-
64-
if (HasAuthorizationMetadata(result.Tools.Select(static tool => tool.McpServerTool))
65-
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
66+
var toolCollection = options.ToolCollection;
67+
return async (context, cancellationToken) =>
6668
{
67-
throw new InvalidOperationException("Authorization filter was not invoked for tools/list operation, but authorization metadata was found on the tools. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
68-
}
69+
var result = await next(context, cancellationToken);
70+
71+
if (HasAuthorizationMetadata(result.Tools.Select(tool => toolCollection is not null && toolCollection.TryGetPrimitive(tool.Name, out var serverTool) ? serverTool : null))
72+
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
73+
{
74+
throw new InvalidOperationException("Authorization filter was not invoked for tools/list operation, but authorization metadata was found on the tools. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
75+
}
6976

70-
return result;
77+
return result;
78+
};
7179
});
7280
}
7381

@@ -103,61 +111,77 @@ private static void CheckCallToolFilter(McpServerOptions options)
103111

104112
private void ConfigureListResourcesFilter(McpServerOptions options)
105113
{
106-
options.Filters.Request.ListResourcesFilters.Add(next => async (context, cancellationToken) =>
114+
options.Filters.Request.ListResourcesFilters.Add(next =>
107115
{
108-
context.Items[AuthorizationFilterInvokedKey] = true;
109-
110-
var result = await next(context, cancellationToken);
111-
await FilterAuthorizedItemsAsync(
112-
result.Resources, static resource => resource.McpServerResource,
113-
context.User, context.Services, context);
114-
return result;
116+
var resourceCollection = options.ResourceCollection;
117+
return async (context, cancellationToken) =>
118+
{
119+
context.Items[AuthorizationFilterInvokedKey] = true;
120+
121+
var result = await next(context, cancellationToken);
122+
await FilterAuthorizedItemsAsync(
123+
result.Resources, resource => resourceCollection is not null && resourceCollection.TryGetPrimitive(resource.Uri, out var serverResource) ? serverResource : null,
124+
context.User, context.Services, context);
125+
return result;
126+
};
115127
});
116128
}
117129

118130
private static void CheckListResourcesFilter(McpServerOptions options)
119131
{
120-
options.Filters.Request.ListResourcesFilters.Add(next => async (context, cancellationToken) =>
132+
options.Filters.Request.ListResourcesFilters.Add(next =>
121133
{
122-
var result = await next(context, cancellationToken);
123-
124-
if (HasAuthorizationMetadata(result.Resources.Select(static resource => resource.McpServerResource))
125-
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
134+
var resourceCollection = options.ResourceCollection;
135+
return async (context, cancellationToken) =>
126136
{
127-
throw new InvalidOperationException("Authorization filter was not invoked for resources/list operation, but authorization metadata was found on the resources. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
128-
}
137+
var result = await next(context, cancellationToken);
138+
139+
if (HasAuthorizationMetadata(result.Resources.Select(resource => resourceCollection is not null && resourceCollection.TryGetPrimitive(resource.Uri, out var serverResource) ? serverResource : null))
140+
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
141+
{
142+
throw new InvalidOperationException("Authorization filter was not invoked for resources/list operation, but authorization metadata was found on the resources. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
143+
}
129144

130-
return result;
145+
return result;
146+
};
131147
});
132148
}
133149

134150
private void ConfigureListResourceTemplatesFilter(McpServerOptions options)
135151
{
136-
options.Filters.Request.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) =>
152+
options.Filters.Request.ListResourceTemplatesFilters.Add(next =>
137153
{
138-
context.Items[AuthorizationFilterInvokedKey] = true;
139-
140-
var result = await next(context, cancellationToken);
141-
await FilterAuthorizedItemsAsync(
142-
result.ResourceTemplates, static resourceTemplate => resourceTemplate.McpServerResource,
143-
context.User, context.Services, context);
144-
return result;
154+
var resourceCollection = options.ResourceCollection;
155+
return async (context, cancellationToken) =>
156+
{
157+
context.Items[AuthorizationFilterInvokedKey] = true;
158+
159+
var result = await next(context, cancellationToken);
160+
await FilterAuthorizedItemsAsync(
161+
result.ResourceTemplates, resourceTemplate => resourceCollection is not null && resourceCollection.TryGetPrimitive(resourceTemplate.UriTemplate, out var serverResource) ? serverResource : null,
162+
context.User, context.Services, context);
163+
return result;
164+
};
145165
});
146166
}
147167

148168
private static void CheckListResourceTemplatesFilter(McpServerOptions options)
149169
{
150-
options.Filters.Request.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) =>
170+
options.Filters.Request.ListResourceTemplatesFilters.Add(next =>
151171
{
152-
var result = await next(context, cancellationToken);
153-
154-
if (HasAuthorizationMetadata(result.ResourceTemplates.Select(static resourceTemplate => resourceTemplate.McpServerResource))
155-
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
172+
var resourceCollection = options.ResourceCollection;
173+
return async (context, cancellationToken) =>
156174
{
157-
throw new InvalidOperationException("Authorization filter was not invoked for resources/templates/list operation, but authorization metadata was found on the resource templates. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
158-
}
175+
var result = await next(context, cancellationToken);
176+
177+
if (HasAuthorizationMetadata(result.ResourceTemplates.Select(resourceTemplate => resourceCollection is not null && resourceCollection.TryGetPrimitive(resourceTemplate.UriTemplate, out var serverResource) ? serverResource : null))
178+
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
179+
{
180+
throw new InvalidOperationException("Authorization filter was not invoked for resources/templates/list operation, but authorization metadata was found on the resource templates. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
181+
}
159182

160-
return result;
183+
return result;
184+
};
161185
});
162186
}
163187

@@ -193,31 +217,39 @@ private static void CheckReadResourceFilter(McpServerOptions options)
193217

194218
private void ConfigureListPromptsFilter(McpServerOptions options)
195219
{
196-
options.Filters.Request.ListPromptsFilters.Add(next => async (context, cancellationToken) =>
220+
options.Filters.Request.ListPromptsFilters.Add(next =>
197221
{
198-
context.Items[AuthorizationFilterInvokedKey] = true;
199-
200-
var result = await next(context, cancellationToken);
201-
await FilterAuthorizedItemsAsync(
202-
result.Prompts, static prompt => prompt.McpServerPrompt,
203-
context.User, context.Services, context);
204-
return result;
222+
var promptCollection = options.PromptCollection;
223+
return async (context, cancellationToken) =>
224+
{
225+
context.Items[AuthorizationFilterInvokedKey] = true;
226+
227+
var result = await next(context, cancellationToken);
228+
await FilterAuthorizedItemsAsync(
229+
result.Prompts, prompt => promptCollection is not null && promptCollection.TryGetPrimitive(prompt.Name, out var serverPrompt) ? serverPrompt : null,
230+
context.User, context.Services, context);
231+
return result;
232+
};
205233
});
206234
}
207235

208236
private static void CheckListPromptsFilter(McpServerOptions options)
209237
{
210-
options.Filters.Request.ListPromptsFilters.Add(next => async (context, cancellationToken) =>
238+
options.Filters.Request.ListPromptsFilters.Add(next =>
211239
{
212-
var result = await next(context, cancellationToken);
213-
214-
if (HasAuthorizationMetadata(result.Prompts.Select(static prompt => prompt.McpServerPrompt))
215-
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
240+
var promptCollection = options.PromptCollection;
241+
return async (context, cancellationToken) =>
216242
{
217-
throw new InvalidOperationException("Authorization filter was not invoked for prompts/list operation, but authorization metadata was found on the prompts. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
218-
}
243+
var result = await next(context, cancellationToken);
244+
245+
if (HasAuthorizationMetadata(result.Prompts.Select(prompt => promptCollection is not null && promptCollection.TryGetPrimitive(prompt.Name, out var serverPrompt) ? serverPrompt : null))
246+
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
247+
{
248+
throw new InvalidOperationException("Authorization filter was not invoked for prompts/list operation, but authorization metadata was found on the prompts. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
249+
}
219250

220-
return result;
251+
return result;
252+
};
221253
});
222254
}
223255

src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,10 @@ await SendGetSseRequestWithRetriesAsync(
228228
SseStreamState state,
229229
CancellationToken cancellationToken)
230230
{
231-
int attempt = 0;
231+
// When LastEventId is null, the first attempt is the initial GET SSE connection (not a reconnection),
232+
// so we start at -1 to avoid counting it against MaxReconnectionAttempts.
233+
// When LastEventId is already set, all attempts are true reconnections, so we start at 0.
234+
int attempt = state.LastEventId is null ? -1 : 0;
232235

233236
// Delay before first attempt if we're reconnecting (have a Last-Event-ID)
234237
bool shouldDelay = state.LastEventId is not null;

src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Buffers;
22
using System.Buffers.Text;
33
using System.Diagnostics;
4+
using System.Diagnostics.CodeAnalysis;
45
using System.Runtime.InteropServices;
56
using System.Text.Json.Serialization;
67

@@ -28,7 +29,7 @@ namespace ModelContextProtocol.Protocol;
2829
public sealed class BlobResourceContents : ResourceContents
2930
{
3031
private ReadOnlyMemory<byte>? _decodedData;
31-
private ReadOnlyMemory<byte> _blob;
32+
private ReadOnlyMemory<byte>? _blob;
3233

3334
/// <summary>
3435
/// Creates an <see cref="BlobResourceContents"/> from raw data.
@@ -40,15 +41,20 @@ public sealed class BlobResourceContents : ResourceContents
4041
/// <exception cref="InvalidOperationException"></exception>
4142
public static BlobResourceContents FromBytes(ReadOnlyMemory<byte> bytes, string uri, string? mimeType = null)
4243
{
43-
ReadOnlyMemory<byte> blob = EncodingUtilities.EncodeToBase64Utf8(bytes);
44-
45-
return new()
46-
{
47-
_decodedData = bytes,
48-
Blob = blob,
49-
MimeType = mimeType,
50-
Uri = uri
51-
};
44+
return new(bytes, uri, mimeType);
45+
}
46+
47+
/// <summary>Initializes a new instance of the <see cref="BlobResourceContents"/> class.</summary>
48+
public BlobResourceContents()
49+
{
50+
}
51+
52+
[SetsRequiredMembers]
53+
private BlobResourceContents(ReadOnlyMemory<byte> decodedData, string uri, string? mimeType)
54+
{
55+
_decodedData = decodedData;
56+
Uri = uri;
57+
MimeType = mimeType;
5258
}
5359

5460
/// <summary>
@@ -60,7 +66,16 @@ public static BlobResourceContents FromBytes(ReadOnlyMemory<byte> bytes, string
6066
[JsonPropertyName("blob")]
6167
public required ReadOnlyMemory<byte> Blob
6268
{
63-
get => _blob;
69+
get
70+
{
71+
if (_blob is null)
72+
{
73+
Debug.Assert(_decodedData is not null);
74+
_blob = EncodingUtilities.EncodeToBase64Utf8(_decodedData!.Value);
75+
}
76+
77+
return _blob.Value;
78+
}
6479
set
6580
{
6681
_blob = value;

0 commit comments

Comments
 (0)