List tools from DI once (#115)

* List tools from DI once

- Follow best practice to not mutate options outside of IConfigureOptions
- Long-term, we should move the repeated work from SetToolsHandler into
  an IPostConfigureOptions service or IMcpServerConnectionFactory

* Add Can_Create_Multiple_Servers_From_Options_And_List_Registered_Tools

* Flip stdin and stdout in StreamClientTransport

* s/McpToolType/McpServerToolType

* Clarify stdin and stdout refers to server process in StreamClientTransport

* fixup
This commit is contained in:
Stephen Halter 2025-03-27 04:26:14 -07:00 • committed by GitHub
commit 4a53f6f6d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -15,6 +15,8 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
{
private readonly IServerTransport? _serverTransport;
private readonly string _serverDescription;
private readonly EventHandler? _toolsChangedDelegate;
private volatile bool _isInitializing;
/// <summary>
@ -32,17 +34,29 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
Throw.IfNull(options);
_serverTransport = transport as IServerTransport;
ServerInstructions = options.ServerInstructions;
ServerOptions = options;
Services = serviceProvider;
_serverDescription = $"{options.ServerInfo.Name} {options.ServerInfo.Version}";
_toolsChangedDelegate = delegate
{
_ = SendMessageAsync(new JsonRpcNotification()
{
Method = NotificationMethods.ToolListChangedNotification,
});
};
AddNotificationHandler("notifications/initialized", _ =>
{
if (ServerOptions.Capabilities?.Tools?.ToolCollection is { } tools)
{
tools.Changed += _toolsChangedDelegate;
}
IsInitialized = true;
return Task.CompletedTask;
});
SetToolsHandler(ref options);
SetToolsHandler(options);
SetInitializeHandler(options);
SetCompletionHandler(options);
@ -50,18 +64,15 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
SetPromptsHandler(options);
SetResourcesHandler(options);
SetSetLoggingLevelHandler(options);
ServerOptions = options;
}
public ServerCapabilities? ServerCapabilities { get; set; }
public ClientCapabilities? ClientCapabilities { get; set; }
/// <inheritdoc />
public Implementation? ClientInfo { get; set; }
/// <inheritdoc />
public string? ServerInstructions { get; set; }
/// <inheritdoc />
public McpServerOptions ServerOptions { get; }
@ -111,6 +122,15 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
}
}
protected override Task CleanupAsync()
{
if (ServerOptions.Capabilities?.Tools?.ToolCollection is { } tools)
{
tools.Changed -= _toolsChangedDelegate;
}
return base.CleanupAsync();
}
private void SetPingHandler()
{
SetRequestHandler<JsonNode, PingResult>("ping",
@ -127,9 +147,9 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
return Task.FromResult(new InitializeResult()
{
ProtocolVersion = options.ProtocolVersion,
Instructions = ServerInstructions,
Instructions = options.ServerInstructions,
ServerInfo = options.ServerInfo,
Capabilities = options.Capabilities ?? new ServerCapabilities(),
Capabilities = ServerCapabilities ?? new(),
});
});
}
@ -198,7 +218,7 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
SetRequestHandler<GetPromptRequestParams, GetPromptResult>("prompts/get", (request, ct) => getPromptHandler(new(this, request), ct));
}
private void SetToolsHandler(ref McpServerOptions options)
private void SetToolsHandler(McpServerOptions options)
{
ToolsCapability? toolsCapability = options.Capabilities?.Tools;
var listToolsHandler = toolsCapability?.ListToolsHandler;
@ -261,25 +281,25 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
return tool.InvokeAsync(request, cancellationToken);
};
toolsCapability ??= new();
toolsCapability.CallToolHandler = callToolHandler;
toolsCapability.ListToolsHandler = listToolsHandler;
toolsCapability.ToolCollection = tools;
toolsCapability.ListChanged = true;
options.Capabilities ??= new();
options.Capabilities.Tools = toolsCapability;
tools.Changed += delegate
ServerCapabilities = new()
{
_ = SendMessageAsync(new JsonRpcNotification()
Experimental = options.Capabilities?.Experimental,
Logging = options.Capabilities?.Logging,
Prompts = options.Capabilities?.Prompts,
Resources = options.Capabilities?.Resources,
Tools = new()
{
Method = NotificationMethods.ToolListChangedNotification,
});
ListToolsHandler = listToolsHandler,
CallToolHandler = callToolHandler,
ToolCollection = tools,
ListChanged = true,
}
};
}
else
{
ServerCapabilities = options.Capabilities;
if (toolsCapability is null)
{
// No tools, and no tools capability was declared, so nothing to do.