Skip to content

Commit 3602c66

Browse files
authored
Merge branch 'main' into localden/experimental
2 parents 8c6e1dc + 4043538 commit 3602c66

12 files changed

Lines changed: 150 additions & 92 deletions

File tree

.github/workflows/release.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ The following process is used when publishing new releases to NuGet.org:
1515
- Click the 'Target' dropdown button
1616
- Choose the 'Recent Commits' tab
1717
- Select the commit to use for the release, ensuring it's one from above where CI is known to be green
18-
- Click the 'Previous tag' dropdown button
19-
- Choose the previous release to use for generating release notes
18+
- The 'Previous tag' dropdown can remain on 'Auto' unless the previous release deviated from this process
2019
- Click the 'Generate release notes button'
2120
- This will add release notes into the Release description
2221
- The generated release notes include what has changed and the list of new contributors
@@ -26,7 +25,7 @@ The following process is used when publishing new releases to NuGet.org:
2625
- Augment the Release description as desired
2726
- This content is presented used on GitHub and is not persisted into any artifacts
2827
- Check the 'Set as a pre-release' button under the release description if appropriate
29-
- Click 'Public release'
28+
- Click 'Publish release'
3029

3130
3. **Monitor the Release workflow**
3231
- After publishing the release, a workflow will begin for producing the release's build artifacts and publishing the NuGet package to NuGet.org

Directory.Packages.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
44
<System10Version>10.0.0-preview.3.25171.5</System10Version>
5-
<MicrosoftExtensionsAIVersion>9.4.3-preview.1.25230.7</MicrosoftExtensionsAIVersion>
5+
<MicrosoftExtensionsAIVersion>9.4.4-preview.1.25259.16</MicrosoftExtensionsAIVersion>
66
</PropertyGroup>
77
<!-- Product dependencies netstandard -->
88
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
@@ -31,7 +31,7 @@
3131
</ItemGroup>
3232
<!-- Product dependencies shared -->
3333
<ItemGroup>
34-
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="$(MicrosoftExtensionsAIVersion)" />
34+
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.4.4-preview.1.25259.16" />
3535
<PackageVersion Include="Microsoft.Extensions.AI" Version="$(MicrosoftExtensionsAIVersion)" />
3636
<PackageVersion Include="System.Net.ServerSentEvents" Version="$(System10Version)" />
3737
</ItemGroup>

samples/EverythingServer/Program.cs

Lines changed: 2 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using EverythingServer;
22
using EverythingServer.Prompts;
3+
using EverythingServer.Resources;
34
using EverythingServer.Tools;
45
using Microsoft.Extensions.AI;
56
using Microsoft.Extensions.DependencyInjection;
@@ -14,8 +15,6 @@
1415
using OpenTelemetry.Resources;
1516
using OpenTelemetry.Trace;
1617

17-
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
18-
1918
var builder = Host.CreateApplicationBuilder(args);
2019
builder.Logging.AddConsole(consoleLogOptions =>
2120
{
@@ -38,82 +37,7 @@
3837
.WithTools<TinyImageTool>()
3938
.WithPrompts<ComplexPromptType>()
4039
.WithPrompts<SimplePromptType>()
41-
.WithListResourcesHandler(async (ctx, ct) =>
42-
{
43-
return new ListResourcesResult
44-
{
45-
Resources =
46-
[
47-
new ModelContextProtocol.Protocol.Types.Resource { Name = "Direct Text Resource", Description = "A direct text resource", MimeType = "text/plain", Uri = "test://direct/text/resource" },
48-
]
49-
};
50-
})
51-
.WithListResourceTemplatesHandler(async (ctx, ct) =>
52-
{
53-
return new ListResourceTemplatesResult
54-
{
55-
ResourceTemplates =
56-
[
57-
new ResourceTemplate { Name = "Template Resource", Description = "A template resource with a numeric ID", UriTemplate = "test://template/resource/{id}" }
58-
]
59-
};
60-
})
61-
.WithReadResourceHandler(async (ctx, ct) =>
62-
{
63-
var uri = ctx.Params?.Uri;
64-
65-
if (uri == "test://direct/text/resource")
66-
{
67-
return new ReadResourceResult
68-
{
69-
Contents = [new TextResourceContents
70-
{
71-
Text = "This is a direct resource",
72-
MimeType = "text/plain",
73-
Uri = uri,
74-
}]
75-
};
76-
}
77-
78-
if (uri is null || !uri.StartsWith("test://template/resource/"))
79-
{
80-
throw new NotSupportedException($"Unknown resource: {uri}");
81-
}
82-
83-
int index = int.Parse(uri["test://template/resource/".Length..]) - 1;
84-
85-
if (index < 0 || index >= ResourceGenerator.Resources.Count)
86-
{
87-
throw new NotSupportedException($"Unknown resource: {uri}");
88-
}
89-
90-
var resource = ResourceGenerator.Resources[index];
91-
92-
if (resource.MimeType == "text/plain")
93-
{
94-
return new ReadResourceResult
95-
{
96-
Contents = [new TextResourceContents
97-
{
98-
Text = resource.Description!,
99-
MimeType = resource.MimeType,
100-
Uri = resource.Uri,
101-
}]
102-
};
103-
}
104-
else
105-
{
106-
return new ReadResourceResult
107-
{
108-
Contents = [new BlobResourceContents
109-
{
110-
Blob = resource.Description!,
111-
MimeType = resource.MimeType,
112-
Uri = resource.Uri,
113-
}]
114-
};
115-
}
116-
})
40+
.WithResources<SimpleResourceType>()
11741
.WithSubscribeToResourcesHandler(async (ctx, ct) =>
11842
{
11943
var uri = ctx.Params?.Uri;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using ModelContextProtocol.Protocol.Types;
2+
using ModelContextProtocol.Server;
3+
using System.ComponentModel;
4+
5+
namespace EverythingServer.Resources;
6+
7+
[McpServerResourceType]
8+
public class SimpleResourceType
9+
{
10+
[McpServerResource(UriTemplate = "test://direct/text/resource", Name = "Direct Text Resource", MimeType = "text/plain")]
11+
[Description("A direct text resource")]
12+
public static string DirectTextResource() => "This is a direct resource";
13+
14+
[McpServerResource(UriTemplate = "test://template/resource/{id}", Name = "Template Resource")]
15+
[Description("A template resource with a numeric ID")]
16+
public static ResourceContents TemplateResource(RequestContext<ReadResourceRequestParams> requestContext, int id)
17+
{
18+
int index = id - 1;
19+
if ((uint)index >= ResourceGenerator.Resources.Count)
20+
{
21+
throw new NotSupportedException($"Unknown resource: {requestContext.Params?.Uri}");
22+
}
23+
24+
var resource = ResourceGenerator.Resources[index];
25+
return resource.MimeType == "text/plain" ?
26+
new TextResourceContents
27+
{
28+
Text = resource.Description!,
29+
MimeType = resource.MimeType,
30+
Uri = resource.Uri,
31+
} :
32+
new BlobResourceContents
33+
{
34+
Blob = resource.Description!,
35+
MimeType = resource.MimeType,
36+
Uri = resource.Uri,
37+
};
38+
}
39+
}

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<RepositoryUrl>https://github.com/modelcontextprotocol/csharp-sdk</RepositoryUrl>
77
<RepositoryType>git</RepositoryType>
88
<VersionPrefix>0.1.0</VersionPrefix>
9-
<VersionSuffix>preview.13</VersionSuffix>
9+
<VersionSuffix>preview.14</VersionSuffix>
1010
<Authors>ModelContextProtocolOfficial</Authors>
1111
<Copyright>© Anthropic and Contributors.</Copyright>
1212
<PackageTags>ModelContextProtocol;mcp;ai;llm</PackageTags>

src/ModelContextProtocol/ModelContextProtocol.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
<!-- Dependencies needed by all -->
3535
<ItemGroup>
3636
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
37-
<PackageReference Include="Microsoft.Extensions.AI" />
3837
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
3938
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
4039
<PackageReference Include="System.Net.ServerSentEvents" />

src/ModelContextProtocol/Server/AIFunctionMcpServerPrompt.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.AI;
2+
using Microsoft.Extensions.DependencyInjection;
23
using ModelContextProtocol.Protocol.Types;
34
using ModelContextProtocol.Utils;
45
using ModelContextProtocol.Utils.Json;
@@ -68,7 +69,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
6869
Description = options?.Description,
6970
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
7071
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
71-
Services = options?.Services,
72+
CreateInstance = AIFunctionMcpServerTool.GetCreateInstanceFunc(),
7273
ConfigureParameterBinding = pi =>
7374
{
7475
if (pi.ParameterType == typeof(RequestContext<GetPromptRequestParams>))
@@ -110,6 +111,32 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
110111
};
111112
}
112113

114+
if (options?.Services is { } services &&
115+
services.GetService<IServiceProviderIsService>() is { } ispis &&
116+
ispis.IsService(pi.ParameterType))
117+
{
118+
return new()
119+
{
120+
ExcludeFromSchema = true,
121+
BindParameter = (pi, args) =>
122+
GetRequestContext(args)?.Services?.GetService(pi.ParameterType) ??
123+
(pi.HasDefaultValue ? null :
124+
throw new ArgumentException("No service of the requested type was found.")),
125+
};
126+
}
127+
128+
if (pi.GetCustomAttribute<FromKeyedServicesAttribute>() is { } keyedAttr)
129+
{
130+
return new()
131+
{
132+
ExcludeFromSchema = true,
133+
BindParameter = (pi, args) =>
134+
(GetRequestContext(args)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
135+
(pi.HasDefaultValue ? null :
136+
throw new ArgumentException("No service of the requested type was found.")),
137+
};
138+
}
139+
113140
return default;
114141

115142
static RequestContext<GetPromptRequestParams>? GetRequestContext(AIFunctionArguments args)

src/ModelContextProtocol/Server/AIFunctionMcpServerResource.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
using Microsoft.Extensions.AI;
2+
using Microsoft.Extensions.DependencyInjection;
23
using ModelContextProtocol.Protocol.Types;
34
using ModelContextProtocol.Utils;
45
using ModelContextProtocol.Utils.Json;
56
using System.Collections.Concurrent;
67
using System.ComponentModel;
7-
using System.Diagnostics;
88
using System.Diagnostics.CodeAnalysis;
99
using System.Globalization;
1010
using System.Reflection;
@@ -76,7 +76,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
7676
Description = options?.Description,
7777
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
7878
SerializerOptions = McpJsonUtilities.DefaultOptions,
79-
Services = options?.Services,
79+
CreateInstance = AIFunctionMcpServerTool.GetCreateInstanceFunc(),
8080
ConfigureParameterBinding = pi =>
8181
{
8282
if (pi.ParameterType == typeof(RequestContext<ReadResourceRequestParams>))
@@ -118,6 +118,32 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
118118
};
119119
}
120120

121+
if (options?.Services is { } services &&
122+
services.GetService<IServiceProviderIsService>() is { } ispis &&
123+
ispis.IsService(pi.ParameterType))
124+
{
125+
return new()
126+
{
127+
ExcludeFromSchema = true,
128+
BindParameter = (pi, args) =>
129+
GetRequestContext(args)?.Services?.GetService(pi.ParameterType) ??
130+
(pi.HasDefaultValue ? null :
131+
throw new ArgumentException("No service of the requested type was found.")),
132+
};
133+
}
134+
135+
if (pi.GetCustomAttribute<FromKeyedServicesAttribute>() is { } keyedAttr)
136+
{
137+
return new()
138+
{
139+
ExcludeFromSchema = true,
140+
BindParameter = (pi, args) =>
141+
(GetRequestContext(args)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
142+
(pi.HasDefaultValue ? null :
143+
throw new ArgumentException("No service of the requested type was found.")),
144+
};
145+
}
146+
121147
// These parameters are the ones and only ones to include in the schema. The schema
122148
// won't be consumed by anyone other than this instance, which will use it to determine
123149
// which properties should show up in the URI template.

src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.AI;
2+
using Microsoft.Extensions.DependencyInjection;
23
using ModelContextProtocol.Protocol.Types;
34
using ModelContextProtocol.Utils;
45
using ModelContextProtocol.Utils.Json;
@@ -60,6 +61,14 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
6061
options);
6162
}
6263

64+
// TODO: Fix the need for this suppression.
65+
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111:ReflectionToDynamicallyAccessedMembers",
66+
Justification = "AIFunctionFactory ensures that the Type passed to AIFunctionFactoryOptions.CreateInstance has public constructors preserved")]
67+
internal static Func<Type, AIFunctionArguments, object> GetCreateInstanceFunc() =>
68+
static ([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] type, args) => args.Services is { } services ?
69+
ActivatorUtilities.CreateInstance(services, type) :
70+
Activator.CreateInstance(type)!;
71+
6372
private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
6473
MethodInfo method, McpServerToolCreateOptions? options) =>
6574
new()
@@ -68,7 +77,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
6877
Description = options?.Description,
6978
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
7079
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
71-
Services = options?.Services,
80+
CreateInstance = GetCreateInstanceFunc(),
7281
ConfigureParameterBinding = pi =>
7382
{
7483
if (pi.ParameterType == typeof(RequestContext<CallToolRequestParams>))
@@ -110,6 +119,32 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
110119
};
111120
}
112121

122+
if (options?.Services is { } services &&
123+
services.GetService<IServiceProviderIsService>() is { } ispis &&
124+
ispis.IsService(pi.ParameterType))
125+
{
126+
return new()
127+
{
128+
ExcludeFromSchema = true,
129+
BindParameter = (pi, args) =>
130+
GetRequestContext(args)?.Services?.GetService(pi.ParameterType) ??
131+
(pi.HasDefaultValue ? null :
132+
throw new ArgumentException("No service of the requested type was found.")),
133+
};
134+
}
135+
136+
if (pi.GetCustomAttribute<FromKeyedServicesAttribute>() is { } keyedAttr)
137+
{
138+
return new()
139+
{
140+
ExcludeFromSchema = true,
141+
BindParameter = (pi, args) =>
142+
(GetRequestContext(args)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
143+
(pi.HasDefaultValue ? null :
144+
throw new ArgumentException("No service of the requested type was found.")),
145+
};
146+
}
147+
113148
return default;
114149

115150
static RequestContext<CallToolRequestParams>? GetRequestContext(AIFunctionArguments args)

tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public async Task SupportsServiceFromDI()
6060
Assert.Contains("something", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []);
6161
Assert.DoesNotContain("actualMyService", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []);
6262

63-
await Assert.ThrowsAsync<ArgumentNullException>(async () => await prompt.GetAsync(
63+
await Assert.ThrowsAnyAsync<ArgumentException>(async () => await prompt.GetAsync(
6464
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
6565
TestContext.Current.CancellationToken));
6666

0 commit comments

Comments
 (0)