Skip to content

Commit 5e5bc29

Browse files
committed
Add compatibility with MCP tools registrations
We leverage the same API as MCP server builder by adding our own WithTools<TTools> for now. The notes agent sample is extended to showcase custom AIContextProvider (that injects the current notes as instructions) and an MCPTool method (save_notes) to mutate them as needed by the agent.
1 parent f979500 commit 5e5bc29

File tree

13 files changed

+234
-42
lines changed

13 files changed

+234
-42
lines changed

readme.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,15 @@ tools = ["get_date"]
264264
This enables a flexible and convenient mix of static and dynamic context for agents, all driven
265265
from configuration.
266266

267+
In addition to registering your own tools in DI, you can also use leverage the MCP C# SDK and reuse
268+
the same tool declarations:
269+
270+
```csharp
271+
builder.Services.AddMcpServer().WithTools<NotesTools>();
272+
273+
// 👇 Reuse same tool definitions in agents
274+
builder.AddAIAgents().WithTools<NotesTools>();
275+
```
267276

268277
<!-- #agents -->
269278

sample/Server/NotesTools.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.Agents.AI;
2+
using ModelContextProtocol.Server;
3+
4+
public class NotesContextProvider(NotesTools notes) : AIContextProvider
5+
{
6+
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
7+
=> ValueTask.FromResult(new AIContext
8+
{
9+
Instructions =
10+
$"""
11+
Your current state is:
12+
<notes>
13+
${notes.GetNotes()}
14+
</notes>
15+
"""
16+
});
17+
}
18+
19+
[McpServerToolType]
20+
public class NotesTools
21+
{
22+
string notes = "";
23+
24+
[McpServerTool]
25+
public string GetNotes() => notes;
26+
27+
[McpServerTool]
28+
public void SaveNotes(string notes) => this.notes = notes;
29+
}

sample/Server/Program.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
using System.Runtime.InteropServices;
1+
using System.Runtime.CompilerServices;
2+
using System.Runtime.InteropServices;
23
using System.Text;
34
using Devlooped.Extensions.AI;
5+
using Microsoft.Agents.AI;
46
using Microsoft.Agents.AI.Hosting;
57
using Microsoft.Agents.AI.Hosting.OpenAI;
68
using Microsoft.Extensions.AI;
9+
using ModelContextProtocol.Server;
710
using Spectre.Console;
811

912
var builder = WebApplication.CreateBuilder(args);
@@ -23,10 +26,15 @@
2326
// dummy ones for illustration
2427
builder.Services.AddKeyedSingleton("create_order", AIFunctionFactory.Create(() => "OK", "create_order"));
2528
builder.Services.AddKeyedSingleton("cancel_order", AIFunctionFactory.Create(() => "OK", "cancel_order"));
26-
builder.Services.AddKeyedSingleton("save_notes", AIFunctionFactory.Create((string notes) => true, "save_notes"));
29+
30+
builder.Services.AddKeyedSingleton<AIContextProvider, NotesContextProvider>("notes");
31+
32+
// 👇 seamless integration of MCP tools
33+
//builder.Services.AddMcpServer().WithTools<NotesTools>();
2734

2835
// 👇 implicitly calls AddChatClients
29-
builder.AddAIAgents();
36+
builder.AddAIAgents()
37+
.WithTools<NotesTools>();
3038

3139
var app = builder.Build();
3240

@@ -76,4 +84,4 @@
7684
});
7785
}
7886

79-
app.Run();
87+
app.Run();

sample/Server/Server.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
<PropertyGroup>
44
<TargetFramework>net10.0</TargetFramework>
5+
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
56
</PropertyGroup>
67

78
<ItemGroup>
89
<PackageReference Include="Microsoft.Agents.AI.Hosting.OpenAI" Version="1.0.0-alpha.251016.1" />
10+
<PackageReference Include="ModelContextProtocol" Version="0.4.0-preview.3" />
911
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
1012
<PackageReference Include="ThisAssembly.Project" Version="2.1.2" PrivateAssets="all" />
1113
<PackageReference Include="Tomlyn.Extensions.Configuration" Version="1.0.6" />

sample/Server/notes.agent.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,30 @@ id: ai.agents.notes
33
description: Provides free-form memory
44
client: grok
55
model: grok-4-fast
6-
use: ["tone"]
7-
tools: ["save_notes", "get_date"]
6+
use: ["tone", "notes"]
7+
tools: ["get_notes", "save_notes", "get_date"]
88
---
9-
You organize and keep notes for the user, using JSON-LD
9+
You organize and keep notes for the user.
10+
11+
You extract key points from the user's input and store them as notes in the agent
12+
state. You keep track of notes that reference external files too, adding corresponding
13+
notes relating to them if the user sends more information about them.
14+
15+
You use JSON-LD format to store notes, optimized for easy retrieval, filtering
16+
or reasoning later. This can involve nested structures, lists, tags, categories, timestamps,
17+
inferred relationships between nodes and entities, etc. As you collect more notes, you can
18+
go back and update, merge or reorganize existing notes to improve their structure, cohesion
19+
and usefulness for retrieval and querying.
20+
21+
When storing relative times (like "last week" or "next week"), always convert them to absolute
22+
dates or timestamps, so you can be precise when responding about them.
23+
24+
You are NOT a general-purpose assistant that can answer questions or perform tasks unrelated
25+
to note-taking and recalling notes. If the user asks you to do something outside of
26+
note-taking, you should politely decline and remind them of your purpose.
27+
28+
Never include technical details about the JSON format or the storage mechanism in your
29+
responses. Just focus on the content of the notes and how they can help the user.
30+
31+
When recalling information from notes, don't ask for follow-up questions or request
32+
any more information. Just provide the information.

src/Agents/Agents.csproj

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,29 @@
1717
<NoWarn>$(NoWarn);CS0436;SYSLIB1100;SYSLIB1101;MEAI001</NoWarn>
1818
</PropertyGroup>
1919

20-
<ItemGroup>
20+
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0' or '$(TargetFramework)' == 'net9.0'">
21+
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
2122
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
2223
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.10" />
23-
<PackageReference Include="NuGetizer" Version="1.4.5" PrivateAssets="all" />
24+
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
25+
</ItemGroup>
26+
27+
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
28+
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0-rc.*" />
29+
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-rc.*" />
30+
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0-rc.*" />
31+
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0-rc.*" />
32+
</ItemGroup>
33+
34+
<ItemGroup>
2435
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-preview.251016.1" />
2536
<PackageReference Include="Microsoft.Agents.AI.Hosting" Version="1.0.0-preview.251016.1" />
2637
<PackageReference Include="Microsoft.Agents.AI.AzureAI" Version="1.0.0-preview.251016.1" />
2738
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251016.1" />
28-
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
29-
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
39+
<PackageReference Include="ModelContextProtocol" Version="0.4.0-preview.3" />
3040
<PackageReference Include="PolySharp" Version="1.15.0" PrivateAssets="all" />
3141
<PackageReference Include="YamlDotNet" Version="16.3.0" />
42+
<PackageReference Include="NuGetizer" Version="1.4.5" PrivateAssets="all" />
3243
</ItemGroup>
3344

3445
<ItemGroup>

src/Agents/CompositeAIContextProvider.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ public override async ValueTask<AIContext> InvokingAsync(InvokingContext invokin
6262
if (staticContext is not null)
6363
return staticContext;
6464

65+
if (providers.Count == 1)
66+
return await providers[0].InvokingAsync(invoking, cancellationToken);
67+
6568
var context = new AIContext();
6669
var instructions = new List<string>();
6770
var messages = new List<ChatMessage>();

src/Agents/ConfigurableAIAgent.cs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
using System.ComponentModel;
1+
using System;
2+
using System.ComponentModel;
23
using System.Diagnostics;
4+
using System.Diagnostics.CodeAnalysis;
5+
using System.Linq.Expressions;
6+
using System.Reflection;
37
using System.Text.Json;
48
using Devlooped.Extensions.AI;
59
using Devlooped.Extensions.AI.Grok;
@@ -8,6 +12,7 @@
812
using Microsoft.Extensions.Configuration;
913
using Microsoft.Extensions.DependencyInjection;
1014
using Microsoft.Extensions.Logging;
15+
using ModelContextProtocol.Server;
1116

1217
namespace Devlooped.Agents.AI;
1318

@@ -134,21 +139,17 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
134139

135140
if (contextFactory is not null)
136141
{
137-
if (options.Use?.Count > 0)
138-
throw new InvalidOperationException($"Invalid simultaneous use of keyed service {nameof(AIContextProviderFactory)} and '{section}:use' in configuration.");
142+
if (options.Use?.Count > 0 || options.Tools?.Count > 0)
143+
throw new InvalidOperationException($"Invalid simultaneous use of keyed service {nameof(AIContextProviderFactory)} and '{section}:use/tools' in configuration.");
139144

140145
options.AIContextProviderFactory = contextFactory.CreateProvider;
141146
}
142-
else if (services.GetKeyedService<AIContextProvider>(name) is { } contextProvider)
143-
{
144-
if (options.Use?.Count > 0)
145-
throw new InvalidOperationException($"Invalid simultaneous use of keyed service {nameof(AIContextProvider)} and '{section}:use' in configuration.");
146-
147-
options.AIContextProviderFactory = _ => contextProvider;
148-
}
149-
else if (options.Use?.Count > 0 || options.Tools?.Count > 0)
147+
else
150148
{
151149
var contexts = new List<AIContextProvider>();
150+
if (services.GetKeyedService<AIContextProvider>(name) is { } contextProvider)
151+
contexts.Add(contextProvider);
152+
152153
foreach (var use in options.Use ?? [])
153154
{
154155
if (services.GetKeyedService<AIContext>(use) is { } staticContext)
@@ -196,7 +197,7 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
196197
{
197198
var tool = services.GetKeyedService<AITool>(toolName) ??
198199
services.GetKeyedService<AIFunction>(toolName) ??
199-
throw new InvalidOperationException($"Specified tool '{toolName}' for agent '{section}' is not registered as a keyed {nameof(AITool)} or {nameof(AIFunction)}.");
200+
throw new InvalidOperationException($"Specified tool '{toolName}' for agent '{section}' is not registered as a keyed {nameof(AITool)}, {nameof(AIFunction)} or MCP server tools.");
200201

201202
contexts.Add(new StaticAIContextProvider(new AIContext { Tools = [tool] }));
202203
}

src/Agents/ConfigurableAgentsExtensions.cs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System.ComponentModel;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Reflection;
4+
using System.Text.Json;
25
using Devlooped.Agents.AI;
36
using Devlooped.Extensions.AI;
47
using Microsoft.Agents.AI;
@@ -7,6 +10,7 @@
710
using Microsoft.Extensions.Configuration;
811
using Microsoft.Extensions.DependencyInjection.Extensions;
912
using Microsoft.Extensions.Hosting;
13+
using ModelContextProtocol.Server;
1014

1115
namespace Microsoft.Extensions.DependencyInjection;
1216

@@ -16,6 +20,52 @@ namespace Microsoft.Extensions.DependencyInjection;
1620
[EditorBrowsable(EditorBrowsableState.Never)]
1721
public static class ConfigurableAgentsExtensions
1822
{
23+
/// <summary>Adds <see cref="McpServerTool"/> instances to the service collection backing <paramref name="builder"/>.</summary>
24+
/// <typeparam name="TToolType">The tool type.</typeparam>
25+
/// <param name="builder">The builder instance.</param>
26+
/// <param name="serializerOptions">The serializer options governing tool parameter marshalling.</param>
27+
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
28+
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
29+
/// <remarks>
30+
/// This method discovers all instance and static methods (public and non-public) on the specified <typeparamref name="TToolType"/>
31+
/// type, where the methods are attributed as <see cref="McpServerToolAttribute"/>, and adds an <see cref="AIFunction"/>
32+
/// instance for each. For instance methods, an instance will be constructed for each invocation of the tool.
33+
/// </remarks>
34+
public static IAIAgentsBuilder WithTools<[DynamicallyAccessedMembers(
35+
DynamicallyAccessedMemberTypes.PublicMethods |
36+
DynamicallyAccessedMemberTypes.NonPublicMethods |
37+
DynamicallyAccessedMemberTypes.PublicConstructors)] TToolType>(
38+
this IAIAgentsBuilder builder,
39+
JsonSerializerOptions? serializerOptions = null,
40+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
41+
{
42+
Throw.IfNull(builder);
43+
44+
// Preserve existing registration if any, such as when using Devlooped.Extensions.DependencyInjection
45+
// via [Service] attribute or by convention.
46+
builder.Services.TryAdd(ServiceDescriptor.Describe(typeof(TToolType), typeof(TToolType), lifetime));
47+
48+
serializerOptions ??= ToolJsonOptions.Default;
49+
50+
foreach (var toolMethod in typeof(TToolType).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
51+
{
52+
if (toolMethod.GetCustomAttribute<McpServerToolAttribute>() is { } toolAttribute)
53+
{
54+
var function = toolMethod.IsStatic
55+
? AIFunctionFactory.Create(toolMethod, null, toolAttribute.Name ?? ToolJsonOptions.Default.PropertyNamingPolicy!.ConvertName(toolMethod.Name))
56+
: AIFunctionFactory.Create(toolMethod, args => args.Services?.GetRequiredService(typeof(TToolType)) ??
57+
throw new InvalidOperationException("Could not determine target instance for tool."),
58+
new AIFunctionFactoryOptions { Name = toolAttribute.Name ?? ToolJsonOptions.Default.PropertyNamingPolicy!.ConvertName(toolMethod.Name) });
59+
60+
builder.Services.TryAdd(ServiceDescriptor.DescribeKeyed(
61+
typeof(AIFunction), function.Name,
62+
(_, _) => function, lifetime));
63+
}
64+
}
65+
66+
return builder;
67+
}
68+
1969
/// <summary>
2070
/// Adds AI agents to the host application builder based on configuration.
2171
/// </summary>
@@ -24,8 +74,7 @@ public static class ConfigurableAgentsExtensions
2474
/// <param name="configureOptions">Optional action to configure options for each agent.</param>
2575
/// <param name="prefix">The configuration prefix for agents, defaults to "ai:agents".</param>
2676
/// <returns>The host application builder with AI agents added.</returns>
27-
public static TBuilder AddAIAgents<TBuilder>(this TBuilder builder, Action<string, AIAgentBuilder>? configurePipeline = default, Action<string, ChatClientAgentOptions>? configureOptions = default, string prefix = "ai:agents")
28-
where TBuilder : IHostApplicationBuilder
77+
public static IAIAgentsBuilder AddAIAgents(this IHostApplicationBuilder builder, Action<string, AIAgentBuilder>? configurePipeline = default, Action<string, ChatClientAgentOptions>? configureOptions = default, string prefix = "ai:agents")
2978
{
3079
builder.AddChatClients();
3180

@@ -61,7 +110,7 @@ public static TBuilder AddAIAgents<TBuilder>(this TBuilder builder, Action<strin
61110
=> sp.GetRequiredKeyedService<AIAgent>(name)));
62111
}
63112

64-
return builder;
113+
return new DefaultAIAgentsBuilder(builder);
65114
}
66115

67116
/// <summary>Gets an AI agent by name (case-insensitive) from the service provider.</summary>

src/Agents/IAIAgentsBuilder.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.Extensions.Configuration;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Diagnostics.Metrics;
4+
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Devlooped.Agents.AI;
8+
9+
/// <summary>Provides a mechanism to configure AI agents.</summary>
10+
public interface IAIAgentsBuilder : IHostApplicationBuilder
11+
{
12+
}
13+
14+
class DefaultAIAgentsBuilder(IHostApplicationBuilder builder) : IAIAgentsBuilder
15+
{
16+
public IDictionary<object, object> Properties => builder.Properties;
17+
18+
public IConfigurationManager Configuration => builder.Configuration;
19+
20+
public IHostEnvironment Environment => builder.Environment;
21+
22+
public ILoggingBuilder Logging => builder.Logging;
23+
24+
public IMetricsBuilder Metrics => builder.Metrics;
25+
26+
public IServiceCollection Services => builder.Services;
27+
28+
public void ConfigureContainer<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory, Action<TContainerBuilder>? configure = null) where TContainerBuilder : notnull => builder.ConfigureContainer(factory, configure);
29+
}

0 commit comments

Comments
 (0)