From 30de9ba00e650b18d303a0169ffd8f7fbdeed1bc Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 11 Aug 2026 01:47:41 +0300 Subject: [PATCH] Expose only selectable tools over MCP server The MCP list and call handlers exposed every non-hidden tool, including system tools that agents auto-include based on context. They now gate on AIToolDefinitionEntry.IsSelectable() so system and hidden tools are never listed or callable over MCP, even when ExposeAllTools is true. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs/changelog/1.1.0.md | 7 +++- src/CrestApps.Core.Docs/docs/mcp/server.md | 4 ++- .../McpServerBuilderExtensions.cs | 12 ++++--- .../Mcp/McpServerBuilderExtensionsTests.cs | 35 ++++++++++++++++--- 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/CrestApps.Core.Docs/docs/changelog/1.1.0.md b/src/CrestApps.Core.Docs/docs/changelog/1.1.0.md index d7a7ff23..5414eafe 100644 --- a/src/CrestApps.Core.Docs/docs/changelog/1.1.0.md +++ b/src/CrestApps.Core.Docs/docs/changelog/1.1.0.md @@ -34,7 +34,9 @@ page will be updated as changes land after 1.0.0. - reworks tool exposure into an opt-in allow-list driven by the `McpServerOptions` site settings. Nothing is exposed by default: an MCP server lists and invokes only the tools and configured [tool instances](../core/tool-instances.md) named in `McpServerOptions.Tools`, or every tool - and instance when `McpServerOptions.ExposeAllTools` is `true`. The allow-list is enforced by both the + and instance when `McpServerOptions.ExposeAllTools` is `true`. Only selectable tools participate: system + tools (marked `IsSystemTool`) and hidden tools are never exposed, even when `ExposeAllTools` is `true`. + The allow-list is enforced by both the list and call handlers, so a tool that is not exposed can neither be discovered nor invoked. Because `McpServerOptions` is backed by site settings, operators choose which tools to expose from the admin settings UI without redeploying. The MVC and Blazor sample hosts add an "Exposed tools" editor to their @@ -65,6 +67,9 @@ page will be updated as changes land after 1.0.0. ## Fixes +- stops the MCP server from exposing system tools. `WithCrestAppsHandlers()` now lists and invokes only + selectable tools, so tools marked `IsSystemTool` (which agents auto-include based on context) are never + discoverable or callable over MCP, even when `McpServerOptions.ExposeAllTools` is `true` - makes the MCP tool allow-list respond to site-settings changes at runtime. The list and call handlers now read `McpServerOptions` through `IOptionsMonitor` instead of the cached `IOptions`, so exposing or removing a tool from the admin settings page takes effect without restarting the host diff --git a/src/CrestApps.Core.Docs/docs/mcp/server.md b/src/CrestApps.Core.Docs/docs/mcp/server.md index 24947781..2b17d2f3 100644 --- a/src/CrestApps.Core.Docs/docs/mcp/server.md +++ b/src/CrestApps.Core.Docs/docs/mcp/server.md @@ -316,7 +316,9 @@ services.Configure(options => | Property | Effect | |----------|--------| | `Tools` | An allow-list of tool and tool instance names to expose. Matching is case-insensitive. | -| `ExposeAllTools` | When `true`, every tool and tool instance is exposed and the allow-list is ignored. | +| `ExposeAllTools` | When `true`, every **selectable** tool and tool instance is exposed and the allow-list is ignored. | + +Only selectable tools are ever exposed over MCP. System tools (those marked `IsSystemTool`, which agents auto-include based on context) and hidden tools are never listed or callable — not even when `ExposeAllTools` is `true`. Because `McpServerOptions` is backed by site settings, an operator can choose which tools to expose from the admin **Settings → MCP server** page without redeploying. The allow-list is enforced by **both** the list and call handlers, so a tool that is not exposed can neither be discovered nor invoked. diff --git a/src/Primitives/CrestApps.Core.AI.Mcp/McpServerBuilderExtensions.cs b/src/Primitives/CrestApps.Core.AI.Mcp/McpServerBuilderExtensions.cs index 2b60a27f..8120949f 100644 --- a/src/Primitives/CrestApps.Core.AI.Mcp/McpServerBuilderExtensions.cs +++ b/src/Primitives/CrestApps.Core.AI.Mcp/McpServerBuilderExtensions.cs @@ -22,8 +22,10 @@ public static class McpServerBuilderExtensions /// This wires the CrestApps tool registry (), /// , and /// into the MCP protocol so both Orchard Core and standalone MVC hosts share the same handler logic. - /// Which tools and tool instances are actually listed and callable is controlled by the - /// site settings allow-list. + /// Only selectable tools (those that are neither system tools nor hidden) are ever exposed, so + /// system tools that the orchestrator auto-includes are never listed or callable over MCP. + /// Which of those selectable tools and tool instances are actually listed and callable is further + /// controlled by the site settings allow-list. /// /// The builder. public static IMcpServerBuilder WithCrestAppsHandlers(this IMcpServerBuilder builder) @@ -43,7 +45,7 @@ public static IMcpServerBuilder WithCrestAppsHandlers(this IMcpServerBuilder bui foreach (var (name, definition) in toolDefinitions.Tools) { - if (definition.Hidden || !IsAllowed(exposeAll, allowList, name, definition.Name)) + if (!definition.IsSelectable() || !IsAllowed(exposeAll, allowList, name, definition.Name)) { continue; } @@ -248,7 +250,7 @@ private static AIFunction ResolveAllowedCodeTool( ILogger logger) { if (toolDefinitions.Tools.TryGetValue(protocolName, out var direct) && - !direct.Hidden && + direct.IsSelectable() && IsAllowed(exposeAll, allowList, protocolName, direct.Name) && TryCreateFunction(services, protocolName, logger) is { } directFunction && string.Equals(directFunction.Name, protocolName, StringComparison.Ordinal)) @@ -258,7 +260,7 @@ private static AIFunction ResolveAllowedCodeTool( foreach (var (name, definition) in toolDefinitions.Tools) { - if (definition.Hidden || !IsAllowed(exposeAll, allowList, name, definition.Name)) + if (!definition.IsSelectable() || !IsAllowed(exposeAll, allowList, name, definition.Name)) { continue; } diff --git a/tests/CrestApps.Core.Tests/Core/Mcp/McpServerBuilderExtensionsTests.cs b/tests/CrestApps.Core.Tests/Core/Mcp/McpServerBuilderExtensionsTests.cs index cbedafe1..6e773074 100644 --- a/tests/CrestApps.Core.Tests/Core/Mcp/McpServerBuilderExtensionsTests.cs +++ b/tests/CrestApps.Core.Tests/Core/Mcp/McpServerBuilderExtensionsTests.cs @@ -39,7 +39,8 @@ public async Task ListToolsHandler_DefaultDeny_ReturnsEmpty() } /// - /// Verifies that enabling ExposeAllTools lists every non-hidden tool while hidden tools are omitted. + /// Verifies that enabling ExposeAllTools lists every selectable tool while hidden and system + /// tools are omitted, because system tools are auto-included by agents and must not be exposed over MCP. /// [Fact] public async Task ListToolsHandler_ExposeAllTools_ReturnsVisibleToolsAndOmitsHidden() @@ -48,6 +49,7 @@ public async Task ListToolsHandler_ExposeAllTools_ReturnsVisibleToolsAndOmitsHid AddLocalTool(services, "search-key", new TestAIFunction("search")); AddLocalTool(services, "hidden-key", new TestAIFunction("hidden"), hidden: true); + AddLocalTool(services, "system-key", new TestAIFunction("system"), isSystemTool: true); AddLocalTool(services, "create-key", new TestAIFunction("create")); using var serviceProvider = services.BuildServiceProvider(); @@ -348,6 +350,26 @@ await InvokeCallToolHandlerAsync( TestContext.Current.CancellationToken)); } + /// + /// Verifies that a system tool cannot be invoked through the call handler even when + /// ExposeAllTools is enabled, because system tools are never exposed over MCP. + /// + [Fact] + public async Task CallToolHandler_ExposeAll_RejectsSystemTool() + { + var services = CreateServices(configureOptions: options => options.ExposeAllTools = true); + + AddLocalTool(services, "system", new TestAIFunction("system"), isSystemTool: true); + + using var serviceProvider = services.BuildServiceProvider(); + + await Assert.ThrowsAsync(async () => + await InvokeCallToolHandlerAsync( + serviceProvider, + "system", + TestContext.Current.CancellationToken)); + } + /// /// Creates the MCP service collection and registers the CrestApps handlers. /// @@ -404,13 +426,15 @@ private static void AddToolInstanceSource(IServiceCollection services, string so /// The keyed registration name. /// The local AI function. /// Whether the tool is hidden. + /// Whether the tool is a system tool. private static void AddLocalTool( IServiceCollection services, string registrationName, AIFunction tool, - bool hidden = false) + bool hidden = false, + bool isSystemTool = false) { - AddLocalToolDefinition(services, registrationName, hidden); + AddLocalToolDefinition(services, registrationName, hidden, isSystemTool); services.AddKeyedSingleton(registrationName, tool); } @@ -420,10 +444,12 @@ private static void AddLocalTool( /// The service collection. /// The keyed registration name. /// Whether the tool is hidden. + /// Whether the tool is a system tool. private static void AddLocalToolDefinition( IServiceCollection services, string registrationName, - bool hidden = false) + bool hidden = false, + bool isSystemTool = false) { services.Configure(options => { @@ -432,6 +458,7 @@ private static void AddLocalToolDefinition( new AIToolDefinitionEntry(typeof(TestAIFunction)) { Hidden = hidden, + IsSystemTool = isSystemTool, }); }); }