diff --git a/build/Targets.fs b/build/Targets.fs
index f01e86ae6..cf0a64a1f 100644
--- a/build/Targets.fs
+++ b/build/Targets.fs
@@ -110,7 +110,7 @@ let private publishContainers _ =
let createImage projectPath containerName =
let ci = Environment.environVarOrNone "GITHUB_ACTIONS"
let pr = prNumber()
- let baseImageTag = "9.0-noble-chiseled-aot"
+ let baseImageTag = "10.0-noble-chiseled"
let labels = imageTags()
let args =
["publish"; projectPath]
diff --git a/src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs b/src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs
index a8173f374..1781abe62 100644
--- a/src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs
+++ b/src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs
@@ -64,4 +64,11 @@ public interface IEnvironmentVariables
/// Reads MCP_ALLOWED_EMAIL_DOMAINS.
///
string McpAllowedEmailDomains => GetEnvironmentVariable("MCP_ALLOWED_EMAIL_DOMAINS") ?? "elastic.co";
+
+ ///
+ /// MCP server profile name (e.g. "public", "internal"). Defaults to "public".
+ /// Reads MCP_SERVER_PROFILE.
+ ///
+ string McpServerProfile => GetEnvironmentVariable("MCP_SERVER_PROFILE") ?? "public";
+
}
diff --git a/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs b/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs
index b12ce984b..b8e2d010d 100644
--- a/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs
+++ b/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs
@@ -44,4 +44,8 @@ public class SystemEnvironmentVariables : IEnvironmentVariables
///
public string McpAllowedEmailDomains => GetEnvironmentVariable("MCP_ALLOWED_EMAIL_DOMAINS") ?? "elastic.co";
+
+ ///
+ public string McpServerProfile => GetEnvironmentVariable("MCP_SERVER_PROFILE") ?? "public";
+
}
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/McpFeatureModule.cs b/src/api/Elastic.Documentation.Mcp.Remote/McpFeatureModule.cs
new file mode 100644
index 000000000..6ec251af5
--- /dev/null
+++ b/src/api/Elastic.Documentation.Mcp.Remote/McpFeatureModule.cs
@@ -0,0 +1,119 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.Diagnostics.CodeAnalysis;
+using Elastic.Documentation.Assembler.Links;
+using Elastic.Documentation.Assembler.Mcp;
+using Elastic.Documentation.LinkIndex;
+using Elastic.Documentation.Links.InboundLinks;
+using Elastic.Documentation.Mcp.Remote.Gateways;
+using Elastic.Documentation.Mcp.Remote.Tools;
+using Elastic.Documentation.Search;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elastic.Documentation.Mcp.Remote;
+
+///
+/// A feature module with DI services and instruction template fragments.
+/// WhenToUse bullets should be generic and context-free; the branding introduction frames their meaning.
+///
+/// Module identifier.
+/// Capability verb for the preamble (e.g. "search", "retrieve"). Null if the module does not add a capability.
+/// Bullet points for the "Use the server when the user:" section. Use {docs} for the profile's DocsDescription.
+/// Lines for the tool guidance section. Use {tool:snake_case_name} for tool names (e.g. {tool:semantic_search}).
+/// The tool class type (e.g. typeof(SearchTools)). Null if the module has no tools.
+/// DI registrations the module's tools depend on.
+public sealed record McpFeatureModule(
+ string Name,
+ string? Capability,
+ string[] WhenToUse,
+ string[] ToolGuidance,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)]
+ [property: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)]
+ Type? ToolType,
+ Action RegisterServices
+);
+
+internal static class McpFeatureModules
+{
+ public static readonly McpFeatureModule Search = new(
+ Name: "Search",
+ Capability: "search",
+ WhenToUse:
+ [
+ "Wants to find, read, or verify {docs} pages.",
+ "Needs to check whether a topic is already covered in {docs}."
+ ],
+ ToolGuidance:
+ [
+ "Prefer {tool:semantic_search} over a general web search when looking up Elastic documentation content.",
+ "Use {tool:find_related_docs} when exploring what documentation exists around a topic."
+ ],
+ ToolType: typeof(SearchTools),
+ RegisterServices: services => _ = services.AddSearchServices()
+ );
+
+ public static readonly McpFeatureModule Documents = new(
+ Name: "Documents",
+ Capability: "retrieve",
+ WhenToUse: [],
+ ToolGuidance:
+ [
+ "Use {tool:get_document_by_url} to retrieve a specific page when the user provides or you already know the URL."
+ ],
+ ToolType: typeof(DocumentTools),
+ RegisterServices: services => _ = services.AddScoped()
+ );
+
+ public static readonly McpFeatureModule Coherence = new(
+ Name: "Coherence",
+ Capability: "analyze",
+ WhenToUse:
+ [
+ "Asks about {docs} structure, coherence, or inconsistencies across pages."
+ ],
+ ToolGuidance:
+ [
+ "Use {tool:check_coherence} or {tool:find_inconsistencies} when reviewing or auditing documentation quality."
+ ],
+ ToolType: typeof(CoherenceTools),
+ RegisterServices: _ => { }
+ );
+
+ public static readonly McpFeatureModule Links = new(
+ Name: "Links",
+ Capability: null,
+ WhenToUse:
+ [
+ "Mentions cross-links between documentation repositories (e.g. 'docs-content://path/to/page.md')."
+ ],
+ ToolGuidance:
+ [
+ "Use the cross-link tools ({tool:resolve_cross_link}, {tool:validate_cross_links}, {tool:find_cross_links}) when working with links between documentation source repositories."
+ ],
+ ToolType: typeof(LinkTools),
+ RegisterServices: services =>
+ {
+ _ = services.AddSingleton(_ => Aws3LinkIndexReader.CreateAnonymous());
+ _ = services.AddSingleton();
+ _ = services.AddSingleton();
+ }
+ );
+
+ public static readonly McpFeatureModule ContentTypes = new(
+ Name: "ContentTypes",
+ Capability: "author",
+ WhenToUse:
+ [
+ "Is writing or editing {docs} and needs to find related content or check consistency.",
+ "Wants to generate {docs} templates following Elastic's content type guidelines."
+ ],
+ ToolGuidance:
+ [
+ "Use {tool:list_content_types}, {tool:get_content_type_guidelines}, and {tool:generate_template} when creating new pages."
+ ],
+ ToolType: typeof(ContentTypeTools),
+ RegisterServices: services => _ = services.AddSingleton()
+ );
+}
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/McpServerProfile.cs b/src/api/Elastic.Documentation.Mcp.Remote/McpServerProfile.cs
new file mode 100644
index 000000000..2c61dfef9
--- /dev/null
+++ b/src/api/Elastic.Documentation.Mcp.Remote/McpServerProfile.cs
@@ -0,0 +1,145 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.Text;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elastic.Documentation.Mcp.Remote;
+
+///
+/// MCP server profile that selects which feature modules are enabled and how the server
+/// introduces itself. Module WhenToUse bullets use {docs} placeholders that are replaced
+/// with the profile's DocsDescription at composition time.
+///
+/// Profile identifier (e.g. "public", "internal").
+/// Prefix for all tool names (e.g. "public_docs_", "internal_docs_").
+/// Short noun phrase describing this profile's docs (e.g. "Elastic product documentation"). Used to replace {docs} in trigger templates.
+/// Introduction template with a {capabilities} placeholder replaced at composition time.
+/// Profile-specific trigger bullets appended after module triggers.
+/// Enabled feature modules.
+public sealed record McpServerProfile(
+ string Name,
+ string ToolNamePrefix,
+ string DocsDescription,
+ string Introduction,
+ string[] ExtraTriggers,
+ McpFeatureModule[] Modules)
+{
+ public static McpServerProfile Public { get; } = new(
+ "public",
+ "public_docs_",
+ "Elastic documentation",
+ "Use this server to {capabilities} Elastic product documentation published at elastic.co/docs.",
+ ["References Elastic product names such as Elasticsearch, Kibana, Fleet, APM, Logstash, Beats, Elastic Security, Elastic Observability, or Elastic Cloud."],
+ [McpFeatureModules.Search, McpFeatureModules.Documents, McpFeatureModules.Coherence, McpFeatureModules.Links, McpFeatureModules.ContentTypes]
+ );
+
+ public static McpServerProfile Internal { get; } = new(
+ "internal",
+ "internal_docs_",
+ "Elastic internal documentation",
+ "Use this server to {capabilities} Elastic internal documentation: team processes, run books, architecture, and other internal knowledge.",
+ ["Asks about internal team processes, run books, architecture decisions, or operational knowledge."],
+ [McpFeatureModules.Search, McpFeatureModules.Documents]
+ );
+
+ ///
+ /// Resolves a profile by name. Throws if the name is unknown.
+ ///
+ public static McpServerProfile Resolve(string? name)
+ {
+ var key = string.IsNullOrWhiteSpace(name) ? "public" : name.Trim();
+ return key.ToLowerInvariant() switch
+ {
+ "public" => Public,
+ "internal" => Internal,
+ _ => throw new ArgumentException($"Unknown MCP server profile: '{name}'. Valid values: public, internal.", nameof(name))
+ };
+ }
+
+ ///
+ /// Registers all DI services from enabled modules, including tool types for resolution at invocation time.
+ ///
+ public void RegisterAllServices(IServiceCollection services)
+ {
+ foreach (var module in Modules)
+ {
+ module.RegisterServices(services);
+ if (module.ToolType is not null)
+ _ = services.AddScoped(module.ToolType);
+ }
+ }
+
+ ///
+ /// Composes server instructions from the profile introduction and enabled module fragments.
+ ///
+ public string ComposeServerInstructions()
+ {
+ var capabilities = DeriveCapabilities();
+ var introduction = Introduction.Replace("{capabilities}", capabilities, StringComparison.Ordinal);
+
+ var whenToUse = Modules
+ .SelectMany(m => m.WhenToUse)
+ .Distinct()
+ .Select(line => line.Replace("{docs}", DocsDescription, StringComparison.Ordinal))
+ .Concat(ExtraTriggers)
+ .ToList();
+ var toolGuidance = Modules
+ .SelectMany(m => m.ToolGuidance)
+ .Select(line => ReplaceToolPlaceholders(line, ToolNamePrefix))
+ .ToList();
+
+ var whenToUseBlock = whenToUse.Count > 0
+ ? "\n" + string.Join("\n", whenToUse.Select(b => $"- {b}"))
+ : "";
+ var toolGuidanceBlock = toolGuidance.Count > 0
+ ? "\n\n" + string.Join("\n", toolGuidance.Select(l => $"- {l}")) + "\n"
+ : "";
+
+ return $"""
+ {introduction}
+
+
+ Use the server when the user:{whenToUseBlock}
+
+ {toolGuidanceBlock}
+ """;
+ }
+
+ private static string ReplaceToolPlaceholders(string line, string prefix)
+ {
+ var sb = new StringBuilder(line.Length);
+ var pos = 0;
+ int start;
+ while ((start = line.IndexOf("{tool:", pos, StringComparison.Ordinal)) >= 0)
+ {
+ var end = line.IndexOf('}', start);
+ if (end < 0)
+ break;
+ _ = sb.Append(line, pos, start - pos);
+ _ = sb.Append(prefix);
+ _ = sb.Append(line, start + 6, end - start - 6);
+ pos = end + 1;
+ }
+ _ = sb.Append(line, pos, line.Length - pos);
+ return sb.ToString();
+ }
+
+ private string DeriveCapabilities()
+ {
+ var verbs = Modules
+ .Select(m => m.Capability)
+ .Where(c => !string.IsNullOrEmpty(c))
+ .Distinct()
+ .ToList();
+
+ return verbs.Count switch
+ {
+ 0 => "search and retrieve",
+ 1 => verbs[0]!,
+ 2 => $"{verbs[0]} and {verbs[1]}",
+ _ => string.Join(", ", verbs.Take(verbs.Count - 1)) + ", and " + verbs[^1]
+ };
+ }
+}
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/McpToolRegistration.cs b/src/api/Elastic.Documentation.Mcp.Remote/McpToolRegistration.cs
new file mode 100644
index 000000000..f5010a8d6
--- /dev/null
+++ b/src/api/Elastic.Documentation.Mcp.Remote/McpToolRegistration.cs
@@ -0,0 +1,67 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using ModelContextProtocol.Server;
+
+namespace Elastic.Documentation.Mcp.Remote;
+
+///
+/// Creates MCP tools with profile-based name prefixes.
+///
+public static class McpToolRegistration
+{
+ ///
+ /// Creates prefixed tools for all enabled modules in the profile.
+ /// Uses createTargetFunc so tool instances are resolved from the request's service provider at invocation time.
+ ///
+ public static IEnumerable CreatePrefixedTools(McpServerProfile profile)
+ {
+ var prefix = profile.ToolNamePrefix;
+ var tools = new List();
+
+ foreach (var module in profile.Modules)
+ {
+ if (module.ToolType is null)
+ continue;
+
+ var methods = module.ToolType
+ .GetMethods(BindingFlags.Public | BindingFlags.Instance)
+ .Where(m => m.GetCustomAttribute() != null);
+
+ foreach (var method in methods)
+ {
+ var snakeName = ToSnakeCase(method.Name);
+ var prefixedName = prefix + snakeName;
+
+ var options = new McpServerToolCreateOptions { Name = prefixedName };
+
+ var tool = McpServerTool.Create(
+ method,
+ ctx => (ctx.Services ?? throw new InvalidOperationException("RequestContext.Services is null")).GetRequiredService(module.ToolType),
+ options);
+
+ tools.Add(tool);
+ }
+ }
+
+ return tools;
+ }
+
+ ///
+ /// Converts PascalCase to snake_case (e.g. SemanticSearch → semantic_search).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string ToSnakeCase(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return value;
+
+ return string.Concat(value.Select((c, i) =>
+ i > 0 && char.IsUpper(c)
+ ? $"_{char.ToLowerInvariant(c)}"
+ : $"{char.ToLowerInvariant(c)}"));
+ }
+}
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Program.cs b/src/api/Elastic.Documentation.Mcp.Remote/Program.cs
index 8cbb8a306..fd0ca21ec 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Program.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Program.cs
@@ -3,14 +3,8 @@
// See the LICENSE file in the project root for more information
using Elastic.Documentation.Api.Infrastructure.OpenTelemetry;
-using Elastic.Documentation.Assembler.Links;
-using Elastic.Documentation.Assembler.Mcp;
using Elastic.Documentation.Configuration;
-using Elastic.Documentation.LinkIndex;
-using Elastic.Documentation.Links.InboundLinks;
using Elastic.Documentation.Mcp.Remote;
-using Elastic.Documentation.Mcp.Remote.Gateways;
-using Elastic.Documentation.Mcp.Remote.Tools;
using Elastic.Documentation.Search;
using Elastic.Documentation.ServiceDefaults;
using Microsoft.AspNetCore.Builder;
@@ -37,15 +31,10 @@
var environment = Environment.GetEnvironmentVariable("ENVIRONMENT");
Console.WriteLine($"Docs Environment: {environment}");
- _ = builder.Services.AddSearchServices();
+ var env = SystemEnvironmentVariables.Instance;
+ var profile = McpServerProfile.Resolve(env.McpServerProfile);
- _ = builder.Services.AddScoped();
-
- _ = builder.Services.AddSingleton(_ => Aws3LinkIndexReader.CreateAnonymous());
- _ = builder.Services.AddSingleton();
- _ = builder.Services.AddSingleton();
-
- _ = builder.Services.AddSingleton();
+ profile.RegisterAllServices(builder.Services);
// CreateSlimBuilder disables reflection-based JSON serialization.
// The MCP SDK's legacy SSE handler uses Results.BadRequest(string) which needs
@@ -59,39 +48,12 @@
// Cursor bug where it opens the SSE stream without the session header and receives 400.
// Stateless mode is appropriate here because all tools are pure request/response (no
// server-initiated push) and the server runs behind a load balancer without session affinity.
- _ = builder.Services
- .AddMcpServer(options =>
- {
- options.ServerInstructions = """
- The Elastic documentation server provides tools to search, retrieve, analyze, and author
- Elastic product documentation published at elastic.co/docs.
-
- Use the server when the user:
- - Asks about Elastic product documentation, features, configuration, or APIs.
- - Wants to find, read, or verify existing documentation pages.
- - Needs to check whether a topic is already documented or how it is covered.
- - Is writing or editing documentation and needs to find related content or check consistency.
- - Mentions cross-links between documentation repositories (e.g. 'docs-content://path/to/page.md').
- - Asks about documentation structure, coherence, or inconsistencies across pages.
- - Wants to generate documentation templates following Elastic's content type guidelines.
- - References elastic.co/docs URLs or Elastic product names such as Elasticsearch, Kibana,
- Fleet, APM, Logstash, Beats, Elastic Security, Elastic Observability, or Elastic Cloud.
-
- Prefer SemanticSearch over a general web search when looking up Elastic documentation content.
- Use GetDocumentByUrl to retrieve a specific page when the user provides or you already know the URL.
- Use FindRelatedDocs when exploring what documentation exists around a topic.
- Use CheckCoherence or FindInconsistencies when reviewing or auditing documentation quality.
- Use the cross-link tools (ResolveCrossLink, ValidateCrossLinks, FindCrossLinks) when working
- with links between documentation source repositories.
- Use ListContentTypes, GetContentTypeGuidelines, and GenerateTemplate when creating new pages.
- """;
- })
- .WithHttpTransport(o => o.Stateless = true)
- .WithTools()
- .WithTools()
- .WithTools()
- .WithTools()
- .WithTools();
+ var mcpBuilder = builder.Services
+ .AddMcpServer(options => options.ServerInstructions = profile.ComposeServerInstructions())
+ .WithHttpTransport(o => o.Stateless = true);
+
+ var prefixedTools = McpToolRegistration.CreatePrefixedTools(profile);
+ mcpBuilder = mcpBuilder.WithTools(prefixedTools);
var app = builder.Build();
diff --git a/tests/Mcp.Remote.Tests/Mcp.Remote.Tests.csproj b/tests/Mcp.Remote.Tests/Mcp.Remote.Tests.csproj
new file mode 100644
index 000000000..c82e36384
--- /dev/null
+++ b/tests/Mcp.Remote.Tests/Mcp.Remote.Tests.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net10.0
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Mcp.Remote.Tests/McpServerInstructionTests.cs b/tests/Mcp.Remote.Tests/McpServerInstructionTests.cs
new file mode 100644
index 000000000..ade22ce40
--- /dev/null
+++ b/tests/Mcp.Remote.Tests/McpServerInstructionTests.cs
@@ -0,0 +1,171 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using Elastic.Documentation.Mcp.Remote;
+using FluentAssertions;
+
+namespace Mcp.Remote.Tests;
+
+public class McpServerInstructionTests
+{
+ [Fact]
+ public void PublicProfile_ContainsAllModuleGuidance()
+ {
+ var instructions = McpServerProfile.Public.ComposeServerInstructions();
+
+ instructions.Should().Contain("Use this server to search, retrieve, analyze, and author");
+ instructions.Should().Contain("Elastic product documentation published at elastic.co/docs");
+ instructions.Should().Contain("");
+ instructions.Should().Contain("Use the server when the user:");
+ instructions.Should().Contain("");
+ instructions.Should().Contain("Prefer public_docs_semantic_search over a general web search");
+ instructions.Should().Contain("Use public_docs_get_document_by_url to retrieve a specific page");
+ instructions.Should().Contain("Use public_docs_find_related_docs when exploring what documentation exists");
+ instructions.Should().Contain("Use public_docs_check_coherence or public_docs_find_inconsistencies when reviewing or auditing");
+ instructions.Should().Contain("Use the cross-link tools (public_docs_resolve_cross_link, public_docs_validate_cross_links, public_docs_find_cross_links)");
+ instructions.Should().Contain("Use public_docs_list_content_types, public_docs_get_content_type_guidelines, and public_docs_generate_template when creating new pages");
+ }
+
+ [Fact]
+ public void InternalProfile_ContainsSearchAndDocumentGuidanceOnly()
+ {
+ var instructions = McpServerProfile.Internal.ComposeServerInstructions();
+
+ instructions.Should().Contain("Use this server to search and retrieve");
+ instructions.Should().Contain("Elastic internal documentation: team processes, run books, architecture");
+ instructions.Should().Contain("Prefer internal_docs_semantic_search over a general web search");
+ instructions.Should().Contain("Use internal_docs_get_document_by_url to retrieve a specific page");
+ instructions.Should().Contain("Use internal_docs_find_related_docs when exploring what documentation exists");
+ instructions.Should().NotContain("check_coherence");
+ instructions.Should().NotContain("find_inconsistencies");
+ instructions.Should().NotContain("resolve_cross_link");
+ instructions.Should().NotContain("list_content_types");
+ instructions.Should().NotContain("generate_template");
+ }
+
+ [Fact]
+ public void Triggers_AreProfileSpecific()
+ {
+ var publicInstructions = McpServerProfile.Public.ComposeServerInstructions();
+ var internalInstructions = McpServerProfile.Internal.ComposeServerInstructions();
+
+ publicInstructions.Should().Contain("Elastic documentation pages");
+ publicInstructions.Should().Contain("References Elastic product names");
+ publicInstructions.Should().NotContain("internal team processes");
+
+ internalInstructions.Should().Contain("Elastic internal documentation pages");
+ internalInstructions.Should().Contain("internal team processes");
+ internalInstructions.Should().NotContain("Elastic product names");
+ }
+
+ [Fact]
+ public void Resolve_WithPublic_ReturnsPublicProfile()
+ {
+ var profile = McpServerProfile.Resolve("public");
+
+ profile.Should().Be(McpServerProfile.Public);
+ profile.Name.Should().Be("public");
+ }
+
+ [Fact]
+ public void Resolve_WithInternal_ReturnsInternalProfile()
+ {
+ var profile = McpServerProfile.Resolve("internal");
+
+ profile.Should().Be(McpServerProfile.Internal);
+ profile.Name.Should().Be("internal");
+ }
+
+ [Fact]
+ public void Resolve_WithNullOrWhitespace_ReturnsPublicProfile()
+ {
+ McpServerProfile.Resolve(null).Should().Be(McpServerProfile.Public);
+ McpServerProfile.Resolve("").Should().Be(McpServerProfile.Public);
+ McpServerProfile.Resolve(" ").Should().Be(McpServerProfile.Public);
+ }
+
+ [Fact]
+ public void Resolve_WithUnknownProfile_Throws()
+ {
+ var act = () => McpServerProfile.Resolve("unknown");
+
+ act.Should().Throw()
+ .WithMessage("*Unknown MCP server profile*")
+ .WithParameterName("name");
+ }
+
+ [Fact]
+ public void PublicProfile_ComposesExactInstructions()
+ {
+ var instructions = McpServerProfile.Public.ComposeServerInstructions();
+
+ var expected = """
+ Use this server to search, retrieve, analyze, and author Elastic product documentation published at elastic.co/docs.
+
+
+ Use the server when the user:
+ - Wants to find, read, or verify Elastic documentation pages.
+ - Needs to check whether a topic is already covered in Elastic documentation.
+ - Asks about Elastic documentation structure, coherence, or inconsistencies across pages.
+ - Mentions cross-links between documentation repositories (e.g. 'docs-content://path/to/page.md').
+ - Is writing or editing Elastic documentation and needs to find related content or check consistency.
+ - Wants to generate Elastic documentation templates following Elastic's content type guidelines.
+ - References Elastic product names such as Elasticsearch, Kibana, Fleet, APM, Logstash, Beats, Elastic Security, Elastic Observability, or Elastic Cloud.
+
+
+
+ - Prefer public_docs_semantic_search over a general web search when looking up Elastic documentation content.
+ - Use public_docs_find_related_docs when exploring what documentation exists around a topic.
+ - Use public_docs_get_document_by_url to retrieve a specific page when the user provides or you already know the URL.
+ - Use public_docs_check_coherence or public_docs_find_inconsistencies when reviewing or auditing documentation quality.
+ - Use the cross-link tools (public_docs_resolve_cross_link, public_docs_validate_cross_links, public_docs_find_cross_links) when working with links between documentation source repositories.
+ - Use public_docs_list_content_types, public_docs_get_content_type_guidelines, and public_docs_generate_template when creating new pages.
+
+ """;
+
+ instructions.Should().Be(expected);
+ }
+
+ [Fact]
+ public void InternalProfile_ComposesExactInstructions()
+ {
+ var instructions = McpServerProfile.Internal.ComposeServerInstructions();
+
+ var expected = """
+ Use this server to search and retrieve Elastic internal documentation: team processes, run books, architecture, and other internal knowledge.
+
+
+ Use the server when the user:
+ - Wants to find, read, or verify Elastic internal documentation pages.
+ - Needs to check whether a topic is already covered in Elastic internal documentation.
+ - Asks about internal team processes, run books, architecture decisions, or operational knowledge.
+
+
+
+ - Prefer internal_docs_semantic_search over a general web search when looking up Elastic documentation content.
+ - Use internal_docs_find_related_docs when exploring what documentation exists around a topic.
+ - Use internal_docs_get_document_by_url to retrieve a specific page when the user provides or you already know the URL.
+
+ """;
+
+ instructions.Should().Be(expected);
+ }
+
+ private static List ExtractBullets(string instructions) =>
+ instructions
+ .Split('\n')
+ .Where(l => l.TrimStart().StartsWith("- ", StringComparison.Ordinal))
+ .Select(l => l.TrimStart()[2..])
+ .ToList();
+
+ private static List ExtractTriggersBullets(string instructions)
+ {
+ var start = instructions.IndexOf("", StringComparison.Ordinal);
+ var end = instructions.IndexOf("", StringComparison.Ordinal);
+ if (start < 0 || end < 0)
+ return [];
+ var section = instructions[start..end];
+ return ExtractBullets(section);
+ }
+}