Skip to content

Commit f044564

Browse files
committed
Merge branch 'main' into jeffhandley/ci-build-packaging
2 parents 97358da + 4a53f6f commit f044564

File tree

11 files changed

+393
-96
lines changed

11 files changed

+393
-96
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ var response = await chatClient.GetResponseAsync(
8080

8181
Here is an example of how to create an MCP server and register all tools from the current application.
8282
It includes a simple echo tool as an example (this is included in the same file here for easy of copy and paste, but it needn't be in the same file...
83-
the employed overload of `WithTools` examines the current assembly for classes with the `McpToolType` attribute, and registers all methods with the
83+
the employed overload of `WithTools` examines the current assembly for classes with the `McpServerToolType` attribute, and registers all methods with the
8484
`McpTool` attribute as tools.)
8585

8686
```csharp

src/ModelContextProtocol/AIContentExtensions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using Microsoft.Extensions.AI;
22
using ModelContextProtocol.Protocol.Types;
33
using ModelContextProtocol.Utils;
4+
using ModelContextProtocol.Utils.Json;
45
using System.Runtime.InteropServices;
6+
using System.Text.Json;
57

68
namespace ModelContextProtocol;
79

@@ -101,4 +103,28 @@ internal static string GetBase64Data(this DataContent dataContent)
101103
Convert.ToBase64String(dataContent.Data.ToArray());
102104
#endif
103105
}
106+
107+
internal static Content ToContent(this AIContent content) =>
108+
content switch
109+
{
110+
TextContent textContent => new()
111+
{
112+
Text = textContent.Text,
113+
Type = "text",
114+
},
115+
DataContent dataContent => new()
116+
{
117+
Data = dataContent.GetBase64Data(),
118+
MimeType = dataContent.MediaType,
119+
Type =
120+
dataContent.HasTopLevelMediaType("image") ? "image" :
121+
dataContent.HasTopLevelMediaType("audio") ? "audio" :
122+
"resource",
123+
},
124+
_ => new()
125+
{
126+
Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))),
127+
Type = "text",
128+
}
129+
};
104130
}

src/ModelContextProtocol/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ var response = await chatClient.GetResponseAsync(
8585

8686
Here is an example of how to create an MCP server and register all tools from the current application.
8787
It includes a simple echo tool as an example (this is included in the same file here for easy of copy and paste, but it needn't be in the same file...
88-
the employed overload of `WithTools` examines the current assembly for classes with the `McpToolType` attribute, and registers all methods with the
88+
the employed overload of `WithTools` examines the current assembly for classes with the `McpServerToolType` attribute, and registers all methods with the
8989
`McpTool` attribute as tools.)
9090

9191
```csharp
@@ -101,7 +101,7 @@ builder.Services
101101
.WithTools();
102102
await builder.Build().RunAsync();
103103

104-
[McpToolType]
104+
[McpServerToolType]
105105
public static class EchoTool
106106
{
107107
[McpTool, Description("Echoes the message back to the client.")]

src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs

Lines changed: 38 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
2222
public static new AIFunctionMcpServerTool Create(
2323
Delegate method,
2424
string? name,
25-
string? description,
25+
string? description,
2626
IServiceProvider? services)
2727
{
2828
Throw.IfNull(method);
@@ -34,7 +34,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
3434
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
3535
/// </summary>
3636
public static new AIFunctionMcpServerTool Create(
37-
MethodInfo method,
37+
MethodInfo method,
3838
object? target,
3939
string? name,
4040
string? description,
@@ -195,57 +195,49 @@ public override async Task<CallToolResponse> InvokeAsync(
195195
};
196196
}
197197

198-
switch (result)
198+
return result switch
199199
{
200-
case null:
201-
return new()
202-
{
203-
Content = []
204-
};
205-
206-
case string text:
207-
return new()
208-
{
209-
Content = [new() { Text = text, Type = "text" }]
210-
};
211-
212-
case TextContent textContent:
213-
return new()
214-
{
215-
Content = [new() { Text = textContent.Text, Type = "text" }]
216-
};
217-
218-
case DataContent dataContent:
219-
return new()
220-
{
221-
Content = [new()
222-
{
223-
Data = dataContent.GetBase64Data(),
224-
MimeType = dataContent.MediaType,
225-
Type = dataContent.HasTopLevelMediaType("image") ? "image" : "resource",
226-
}]
227-
};
228-
229-
case string[] texts:
230-
return new()
231-
{
232-
Content = texts
233-
.Select(x => new Content() { Type = "text", Text = x ?? string.Empty })
234-
.ToList()
235-
};
200+
AIContent aiContent => new()
201+
{
202+
Content = [aiContent.ToContent()]
203+
},
204+
null => new()
205+
{
206+
Content = []
207+
},
208+
string text => new()
209+
{
210+
Content = [new() { Text = text, Type = "text" }]
211+
},
212+
Content content => new()
213+
{
214+
Content = [content]
215+
},
216+
IEnumerable<string> texts => new()
217+
{
218+
Content = [.. texts.Select(x => new Content() { Type = "text", Text = x ?? string.Empty })]
219+
},
220+
IEnumerable<AIContent> contentItems => new()
221+
{
222+
Content = [.. contentItems.Select(static item => item.ToContent())]
223+
},
224+
IEnumerable<Content> contents => new()
225+
{
226+
Content = [.. contents]
227+
},
228+
CallToolResponse callToolResponse => callToolResponse,
236229

237230
// TODO https://github.com/modelcontextprotocol/csharp-sdk/issues/69:
238231
// Add specialization for annotations.
239-
240-
default:
241-
return new()
242-
{
243-
Content = [new()
232+
_ => new()
233+
{
234+
Content = [new()
244235
{
245236
Text = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))),
246237
Type = "text"
247238
}]
248-
};
249-
}
239+
},
240+
};
250241
}
242+
251243
}

src/ModelContextProtocol/Server/McpServer.cs

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
1515
{
1616
private readonly IServerTransport? _serverTransport;
1717
private readonly string _serverDescription;
18+
private readonly EventHandler? _toolsChangedDelegate;
19+
1820
private volatile bool _isInitializing;
1921

2022
/// <summary>
@@ -32,36 +34,45 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory?
3234
Throw.IfNull(options);
3335

3436
_serverTransport = transport as IServerTransport;
35-
ServerInstructions = options.ServerInstructions;
37+
ServerOptions = options;
3638
Services = serviceProvider;
3739
_serverDescription = $"{options.ServerInfo.Name} {options.ServerInfo.Version}";
40+
_toolsChangedDelegate = delegate
41+
{
42+
_ = SendMessageAsync(new JsonRpcNotification()
43+
{
44+
Method = NotificationMethods.ToolListChangedNotification,
45+
});
46+
};
3847

3948
AddNotificationHandler("notifications/initialized", _ =>
4049
{
50+
if (ServerOptions.Capabilities?.Tools?.ToolCollection is { } tools)
51+
{
52+
tools.Changed += _toolsChangedDelegate;
53+
}
54+
4155
IsInitialized = true;
4256
return Task.CompletedTask;
4357
});
4458

45-
SetToolsHandler(ref options);
59+
SetToolsHandler(options);
4660

4761
SetInitializeHandler(options);
4862
SetCompletionHandler(options);
4963
SetPingHandler();
5064
SetPromptsHandler(options);
5165
SetResourcesHandler(options);
5266
SetSetLoggingLevelHandler(options);
53-
54-
ServerOptions = options;
5567
}
5668

69+
public ServerCapabilities? ServerCapabilities { get; set; }
70+
5771
public ClientCapabilities? ClientCapabilities { get; set; }
5872

5973
/// <inheritdoc />
6074
public Implementation? ClientInfo { get; set; }
6175

62-
/// <inheritdoc />
63-
public string? ServerInstructions { get; set; }
64-
6576
/// <inheritdoc />
6677
public McpServerOptions ServerOptions { get; }
6778

@@ -111,6 +122,15 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
111122
}
112123
}
113124

125+
protected override Task CleanupAsync()
126+
{
127+
if (ServerOptions.Capabilities?.Tools?.ToolCollection is { } tools)
128+
{
129+
tools.Changed -= _toolsChangedDelegate;
130+
}
131+
return base.CleanupAsync();
132+
}
133+
114134
private void SetPingHandler()
115135
{
116136
SetRequestHandler<JsonNode, PingResult>("ping",
@@ -127,9 +147,9 @@ private void SetInitializeHandler(McpServerOptions options)
127147
return Task.FromResult(new InitializeResult()
128148
{
129149
ProtocolVersion = options.ProtocolVersion,
130-
Instructions = ServerInstructions,
150+
Instructions = options.ServerInstructions,
131151
ServerInfo = options.ServerInfo,
132-
Capabilities = options.Capabilities ?? new ServerCapabilities(),
152+
Capabilities = ServerCapabilities ?? new(),
133153
});
134154
});
135155
}
@@ -198,7 +218,7 @@ private void SetPromptsHandler(McpServerOptions options)
198218
SetRequestHandler<GetPromptRequestParams, GetPromptResult>("prompts/get", (request, ct) => getPromptHandler(new(this, request), ct));
199219
}
200220

201-
private void SetToolsHandler(ref McpServerOptions options)
221+
private void SetToolsHandler(McpServerOptions options)
202222
{
203223
ToolsCapability? toolsCapability = options.Capabilities?.Tools;
204224
var listToolsHandler = toolsCapability?.ListToolsHandler;
@@ -261,25 +281,25 @@ private void SetToolsHandler(ref McpServerOptions options)
261281
return tool.InvokeAsync(request, cancellationToken);
262282
};
263283

264-
toolsCapability ??= new();
265-
toolsCapability.CallToolHandler = callToolHandler;
266-
toolsCapability.ListToolsHandler = listToolsHandler;
267-
toolsCapability.ToolCollection = tools;
268-
toolsCapability.ListChanged = true;
269-
270-
options.Capabilities ??= new();
271-
options.Capabilities.Tools = toolsCapability;
272-
273-
tools.Changed += delegate
284+
ServerCapabilities = new()
274285
{
275-
_ = SendMessageAsync(new JsonRpcNotification()
286+
Experimental = options.Capabilities?.Experimental,
287+
Logging = options.Capabilities?.Logging,
288+
Prompts = options.Capabilities?.Prompts,
289+
Resources = options.Capabilities?.Resources,
290+
Tools = new()
276291
{
277-
Method = NotificationMethods.ToolListChangedNotification,
278-
});
292+
ListToolsHandler = listToolsHandler,
293+
CallToolHandler = callToolHandler,
294+
ToolCollection = tools,
295+
ListChanged = true,
296+
}
279297
};
280298
}
281299
else
282300
{
301+
ServerCapabilities = options.Capabilities;
302+
283303
if (toolsCapability is null)
284304
{
285305
// No tools, and no tools capability was declared, so nothing to do.

src/ModelContextProtocol/Shared/McpJsonRpcEndpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ protected void SetRequestHandler<TRequest, TResponse>(string method, Func<TReque
359359
/// Cleans up the endpoint and releases resources.
360360
/// </summary>
361361
/// <returns></returns>
362-
protected async Task CleanupAsync()
362+
protected virtual async Task CleanupAsync()
363363
{
364364
if (_isDisposed)
365365
return;

tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ private async Task<IMcpClient> CreateMcpClientForServer()
3838
{
3939
await _server.StartAsync(TestContext.Current.CancellationToken);
4040

41-
var stdin = new StreamReader(_serverToClientPipe.Reader.AsStream());
42-
var stdout = new StreamWriter(_clientToServerPipe.Writer.AsStream());
41+
var serverStdinWriter = new StreamWriter(_clientToServerPipe.Writer.AsStream());
42+
var serverStdoutReader = new StreamReader(_serverToClientPipe.Reader.AsStream());
4343

4444
var serverConfig = new McpServerConfig()
4545
{
@@ -50,7 +50,7 @@ private async Task<IMcpClient> CreateMcpClientForServer()
5050

5151
return await McpClientFactory.CreateAsync(
5252
serverConfig,
53-
createTransportFunc: (_, _) => new StreamClientTransport(stdin, stdout),
53+
createTransportFunc: (_, _) => new StreamClientTransport(serverStdinWriter, serverStdoutReader),
5454
cancellationToken: TestContext.Current.CancellationToken);
5555
}
5656

0 commit comments

Comments
 (0)