diff --git a/src/api/Elastic.Documentation.Mcp.Remote/McpFeatureModule.cs b/src/api/Elastic.Documentation.Mcp.Remote/McpFeatureModule.cs index 6ec251af5..7acb6d082 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/McpFeatureModule.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/McpFeatureModule.cs @@ -21,7 +21,7 @@ namespace Elastic.Documentation.Mcp.Remote; /// 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}). +/// Lines for the tool guidance section. Use {tool:name} or {tool:name_{resource}} for tool names (e.g. {tool:search_{resource}}). /// 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( @@ -47,8 +47,8 @@ internal static class McpFeatureModules ], 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." + "Prefer {tool:search_{resource}} over a general web search when looking up Elastic documentation content.", + "Use {tool:find_related_{resource}} when exploring what documentation exists around a topic." ], ToolType: typeof(SearchTools), RegisterServices: services => _ = services.AddSearchServices() @@ -60,7 +60,7 @@ internal static class McpFeatureModules WhenToUse: [], ToolGuidance: [ - "Use {tool:get_document_by_url} to retrieve a specific page when the user provides or you already know the URL." + "Use {tool:get_{scope}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() @@ -75,7 +75,7 @@ internal static class McpFeatureModules ], ToolGuidance: [ - "Use {tool:check_coherence} or {tool:find_inconsistencies} when reviewing or auditing documentation quality." + "Use {tool:check_{resource}_coherence} or {tool:find_{resource}_inconsistencies} when reviewing or auditing documentation quality." ], ToolType: typeof(CoherenceTools), RegisterServices: _ => { } diff --git a/src/api/Elastic.Documentation.Mcp.Remote/McpServerProfile.cs b/src/api/Elastic.Documentation.Mcp.Remote/McpServerProfile.cs index 2c61dfef9..0af0baa82 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/McpServerProfile.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/McpServerProfile.cs @@ -13,14 +13,16 @@ namespace Elastic.Documentation.Mcp.Remote; /// 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_"). +/// Resource noun for tool names (e.g. "docs", "internal_docs"). Replaces {resource} in tool name templates. +/// Scope prefix for tool names (e.g. "" for public, "internal_" for internal). Replaces {scope} in tool name templates. /// 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 ResourceNoun, + string ScopePrefix, string DocsDescription, string Introduction, string[] ExtraTriggers, @@ -28,7 +30,8 @@ public sealed record McpServerProfile( { public static McpServerProfile Public { get; } = new( "public", - "public_docs_", + "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."], @@ -37,7 +40,8 @@ public sealed record McpServerProfile( public static McpServerProfile Internal { get; } = new( "internal", - "internal_docs_", + "internal_docs", + "internal_", "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."], @@ -87,7 +91,7 @@ public string ComposeServerInstructions() .ToList(); var toolGuidance = Modules .SelectMany(m => m.ToolGuidance) - .Select(line => ReplaceToolPlaceholders(line, ToolNamePrefix)) + .Select(line => ReplaceToolPlaceholders(line, ResourceNoun, ScopePrefix)) .ToList(); var whenToUseBlock = whenToUse.Count > 0 @@ -107,19 +111,33 @@ public string ComposeServerInstructions() """; } - private static string ReplaceToolPlaceholders(string line, string prefix) + private static string ReplaceToolPlaceholders(string line, string resourceNoun, string scopePrefix) { 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) + var templateStart = start + 6; + var depth = 1; + var end = templateStart; + while (end < line.Length && depth > 0) + { + if (line[end] == '{') + depth++; + else if (line[end] == '}') + depth--; + end++; + } + if (depth != 0) break; + end--; + var template = line[templateStart..end]; + var resolved = template + .Replace("{resource}", resourceNoun, StringComparison.Ordinal) + .Replace("{scope}", scopePrefix, StringComparison.Ordinal); _ = sb.Append(line, pos, start - pos); - _ = sb.Append(prefix); - _ = sb.Append(line, start + 6, end - start - 6); + _ = sb.Append(resolved); pos = end + 1; } _ = sb.Append(line, pos, line.Length - pos); diff --git a/src/api/Elastic.Documentation.Mcp.Remote/McpToolRegistration.cs b/src/api/Elastic.Documentation.Mcp.Remote/McpToolRegistration.cs index f5010a8d6..0047cb7fe 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/McpToolRegistration.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/McpToolRegistration.cs @@ -2,24 +2,27 @@ // 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.ComponentModel; using System.Reflection; -using System.Runtime.CompilerServices; +using Elastic.Documentation.Assembler.Mcp; using ModelContextProtocol.Server; namespace Elastic.Documentation.Mcp.Remote; /// -/// Creates MCP tools with profile-based name prefixes. +/// Creates MCP tools with profile-based names and descriptions. /// public static class McpToolRegistration { /// - /// Creates prefixed tools for all enabled modules in the profile. + /// Creates 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 resourceNoun = profile.ResourceNoun; + var scopePrefix = profile.ScopePrefix; + var docsDescription = profile.DocsDescription; var tools = new List(); foreach (var module in profile.Modules) @@ -33,10 +36,20 @@ public static IEnumerable CreatePrefixedTools(McpServerProfile pr foreach (var method in methods) { - var snakeName = ToSnakeCase(method.Name); - var prefixedName = prefix + snakeName; + var nameAttr = method.GetCustomAttribute() + ?? throw new InvalidOperationException($"Method {method.DeclaringType?.Name}.{method.Name} must have [McpToolName] attribute."); + var toolName = nameAttr.Template + .Replace("{resource}", resourceNoun, StringComparison.Ordinal) + .Replace("{scope}", scopePrefix, StringComparison.Ordinal); - var options = new McpServerToolCreateOptions { Name = prefixedName }; + var descAttr = method.GetCustomAttribute(); + var description = descAttr?.Description?.Replace("{docs}", docsDescription, StringComparison.Ordinal); + + var options = new McpServerToolCreateOptions + { + Name = toolName, + Description = description + }; var tool = McpServerTool.Create( method, @@ -49,19 +62,4 @@ public static IEnumerable CreatePrefixedTools(McpServerProfile pr 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/Tools/CoherenceTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs index c84e47fe8..21418a8ca 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs @@ -5,6 +5,7 @@ using System.ComponentModel; using System.Text.Json; using Elastic.Documentation.Api.Core.Search; +using Elastic.Documentation.Assembler.Mcp; using Elastic.Documentation.Mcp.Remote.Responses; using Microsoft.Extensions.Logging; using ModelContextProtocol.Server; @@ -20,8 +21,8 @@ public class CoherenceTools(IFullSearchGateway fullSearchGateway, ILogger /// Checks documentation coherence for a given topic. /// - [McpServerTool, Description( - "Checks how coherently a topic is covered across all Elastic documentation. " + + [McpServerTool, McpToolName("check_{resource}_coherence"), Description( + "Checks how coherently a topic is covered across all {docs}. " + "Use when reviewing documentation quality, auditing coverage of a feature or concept, " + "or checking whether a topic is documented consistently across products and sections.")] public async Task CheckCoherence( @@ -92,8 +93,8 @@ public async Task CheckCoherence( /// /// Finds potential inconsistencies in documentation for a given topic. /// - [McpServerTool, Description( - "Finds potential inconsistencies across Elastic documentation pages covering the same topic. " + + [McpServerTool, McpToolName("find_{resource}_inconsistencies"), Description( + "Finds potential inconsistencies across {docs} pages covering the same topic. " + "Use when auditing docs quality, verifying that instructions don't contradict each other, " + "or checking for overlapping content within a product area.")] public async Task FindInconsistencies( diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Tools/DocumentTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/DocumentTools.cs index 52ae69ce3..332a8ad1e 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/DocumentTools.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/DocumentTools.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Text.Json; +using Elastic.Documentation.Assembler.Mcp; using Elastic.Documentation.Mcp.Remote.Gateways; using Elastic.Documentation.Mcp.Remote.Responses; using Microsoft.Extensions.Logging; @@ -20,9 +21,9 @@ public class DocumentTools(IDocumentGateway documentGateway, ILogger /// Gets a document by its URL. /// - [McpServerTool, Description( - "Retrieves a specific Elastic documentation page by its URL. " + - "Use when the user provides an elastic.co/docs URL, references a known page, " + + [McpServerTool, McpToolName("get_{scope}document_by_url"), Description( + "Retrieves a specific {docs} page by its URL. " + + "Use when the user provides a documentation URL, references a known page, " + "or you need the full content and metadata of a specific doc. " + "Returns title, AI summaries, headings, navigation context, and optionally the full body.")] public async Task GetDocumentByUrl( @@ -89,8 +90,8 @@ public async Task GetDocumentByUrl( /// /// Analyzes the structure of a document. /// - [McpServerTool, Description( - "Analyzes the structure of an Elastic documentation page. " + + [McpServerTool, McpToolName("analyze_{scope}document_structure"), Description( + "Analyzes the structure of a {docs} page. " + "Use when evaluating page quality, checking heading hierarchy, or assessing AI enrichment status. " + "Returns heading count, link count, parent pages, and whether AI summaries are present.")] public async Task AnalyzeDocumentStructure( diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs index f87e32c4d..162ef11e7 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs @@ -5,6 +5,7 @@ using System.ComponentModel; using System.Text.Json; using Elastic.Documentation.Api.Core.Search; +using Elastic.Documentation.Assembler.Mcp; using Elastic.Documentation.Mcp.Remote.Responses; using Microsoft.Extensions.Logging; using ModelContextProtocol.Server; @@ -20,8 +21,8 @@ public class SearchTools(IFullSearchGateway fullSearchGateway, ILogger /// Performs semantic search across all Elastic documentation. /// - [McpServerTool, Description( - "Searches all published Elastic documentation by meaning. " + + [McpServerTool, McpToolName("search_{resource}"), Description( + "Searches all published {docs} by meaning. " + "Use when the user asks about Elastic product features, needs to find existing docs pages, " + "verify published content, or research what documentation exists on a topic. " + "Returns relevant documents with AI summaries, relevance scores, and navigation context.")] @@ -84,8 +85,8 @@ public async Task SemanticSearch( /// /// Finds documents related to a given topic or document URL. /// - [McpServerTool, Description( - "Finds Elastic documentation pages related to a given topic. " + + [McpServerTool, McpToolName("find_related_{resource}"), Description( + "Finds {docs} pages related to a given topic. " + "Use when exploring what documentation exists around a subject, building context for writing, " + "or discovering related content the user should be aware of.")] public async Task FindRelatedDocs( diff --git a/src/services/Elastic.Documentation.Assembler/Mcp/ContentTypeTools.cs b/src/services/Elastic.Documentation.Assembler/Mcp/ContentTypeTools.cs index 41749c77d..e443a4a26 100644 --- a/src/services/Elastic.Documentation.Assembler/Mcp/ContentTypeTools.cs +++ b/src/services/Elastic.Documentation.Assembler/Mcp/ContentTypeTools.cs @@ -50,7 +50,7 @@ public partial class ContentTypeTools(ContentTypeProvider provider) /// /// Lists all available Elastic Docs content types. /// - [McpServerTool, Description( + [McpServerTool, McpToolName("list_content_types"), Description( "Lists all Elastic documentation content types (overview, how-to, tutorial, troubleshooting, changelog) " + "with descriptions and guidance on when to use each. " + "Use when deciding what type of page to create or when the user asks about Elastic docs structure.")] @@ -71,7 +71,7 @@ public string ListContentTypes() /// /// Generates a template for a specific content type. /// - [McpServerTool, Description( + [McpServerTool, McpToolName("generate_template"), Description( "Generates a ready-to-use Elastic documentation template for a specific content type. " + "Use when the user wants to create a new documentation page, needs a starting point with correct " + "frontmatter and structure, or asks for a template. Returns Markdown (or YAML for changelogs).")] @@ -109,7 +109,7 @@ public string GenerateTemplate( /// /// Gets authoring and evaluation guidelines for a content type. /// - [McpServerTool, Description( + [McpServerTool, McpToolName("get_content_type_guidelines"), Description( "Returns detailed authoring and evaluation guidelines for a specific Elastic documentation content type. " + "Use when writing new content, reviewing existing pages against standards, or when the user asks about " + "Elastic docs best practices. Includes required elements, recommended sections, and anti-patterns.")] diff --git a/src/services/Elastic.Documentation.Assembler/Mcp/LinkTools.cs b/src/services/Elastic.Documentation.Assembler/Mcp/LinkTools.cs index e11259795..8477bb755 100644 --- a/src/services/Elastic.Documentation.Assembler/Mcp/LinkTools.cs +++ b/src/services/Elastic.Documentation.Assembler/Mcp/LinkTools.cs @@ -16,7 +16,7 @@ public class LinkTools(ILinkUtilService linkUtilService) /// /// Resolves a cross-link URI to its target URL. /// - [McpServerTool, Description( + [McpServerTool, McpToolName("resolve_cross_link"), Description( "Resolves an Elastic docs cross-link URI (e.g. 'docs-content://get-started/intro.md') to its published URL. " + "Use when the user references a cross-link, needs to verify a link target, or wants to know what anchors are available on a page.")] public async Task ResolveCrossLink( @@ -52,7 +52,7 @@ public async Task ResolveCrossLink( /// /// Lists all available repositories in the link index. /// - [McpServerTool, Description( + [McpServerTool, McpToolName("list_repositories"), Description( "Lists all Elastic documentation source repositories in the cross-link index. " + "Use when the user needs to know which repositories publish documentation or wants to explore the docs ecosystem.")] public async Task ListRepositories(CancellationToken cancellationToken = default) @@ -88,7 +88,7 @@ public async Task ListRepositories(CancellationToken cancellationToken = /// /// Gets all links published by a repository. /// - [McpServerTool, Description( + [McpServerTool, McpToolName("get_repository_links"), Description( "Gets all pages and anchors published by a specific Elastic documentation repository. " + "Use when exploring what a repository publishes, building a cross-link, or looking up available anchor targets.")] public async Task GetRepositoryLinks( @@ -132,7 +132,7 @@ public async Task GetRepositoryLinks( /// /// Finds all cross-links from one repository to another. /// - [McpServerTool, Description( + [McpServerTool, McpToolName("find_cross_links"), Description( "Finds cross-links between Elastic documentation repositories. " + "Use when analyzing inter-repository dependencies, checking what links into or out of a repository, " + "or auditing cross-link usage. Can filter by source or target repository.")] @@ -172,7 +172,7 @@ public async Task FindCrossLinks( /// /// Validates cross-links and finds broken ones. /// - [McpServerTool, Description( + [McpServerTool, McpToolName("validate_cross_links"), Description( "Validates cross-links targeting an Elastic documentation repository and reports broken ones. " + "Use when checking link health, preparing a release, or diagnosing broken cross-references.")] public async Task ValidateCrossLinks( diff --git a/src/services/Elastic.Documentation.Assembler/Mcp/McpToolNameAttribute.cs b/src/services/Elastic.Documentation.Assembler/Mcp/McpToolNameAttribute.cs new file mode 100644 index 000000000..adf6107f3 --- /dev/null +++ b/src/services/Elastic.Documentation.Assembler/Mcp/McpToolNameAttribute.cs @@ -0,0 +1,15 @@ +// 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 + +namespace Elastic.Documentation.Assembler.Mcp; + +/// +/// Specifies the tool name template for an MCP tool method. Use {resource} as a placeholder +/// replaced with the profile's ResourceNoun (e.g. "docs", "internal_docs"). +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class McpToolNameAttribute(string template) : Attribute +{ + public string Template { get; } = template; +} diff --git a/tests/Mcp.Remote.Tests/McpServerInstructionTests.cs b/tests/Mcp.Remote.Tests/McpServerInstructionTests.cs index ade22ce40..5d52c3afb 100644 --- a/tests/Mcp.Remote.Tests/McpServerInstructionTests.cs +++ b/tests/Mcp.Remote.Tests/McpServerInstructionTests.cs @@ -19,12 +19,12 @@ public void PublicProfile_ContainsAllModuleGuidance() 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"); + instructions.Should().Contain("Prefer search_docs over a general web search"); + instructions.Should().Contain("Use get_document_by_url to retrieve a specific page"); + instructions.Should().Contain("Use find_related_docs when exploring what documentation exists"); + instructions.Should().Contain("Use check_docs_coherence or find_docs_inconsistencies when reviewing or auditing"); + instructions.Should().Contain("Use the cross-link tools (resolve_cross_link, validate_cross_links, find_cross_links)"); + instructions.Should().Contain("Use list_content_types, get_content_type_guidelines, and generate_template when creating new pages"); } [Fact] @@ -34,9 +34,9 @@ public void InternalProfile_ContainsSearchAndDocumentGuidanceOnly() 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().Contain("Prefer search_internal_docs over a general web search"); + instructions.Should().Contain("Use get_internal_document_by_url to retrieve a specific page"); + instructions.Should().Contain("Use find_related_internal_docs when exploring what documentation exists"); instructions.Should().NotContain("check_coherence"); instructions.Should().NotContain("find_inconsistencies"); instructions.Should().NotContain("resolve_cross_link"); @@ -115,12 +115,12 @@ public void PublicProfile_ComposesExactInstructions() - - 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. + - Prefer search_docs over a general web search when looking up Elastic documentation content. + - Use find_related_docs when exploring what documentation exists around a topic. + - Use get_document_by_url to retrieve a specific page when the user provides or you already know the URL. + - Use check_docs_coherence or find_docs_inconsistencies when reviewing or auditing documentation quality. + - Use the cross-link tools (resolve_cross_link, validate_cross_links, find_cross_links) when working with links between documentation source repositories. + - Use list_content_types, get_content_type_guidelines, and generate_template when creating new pages. """; @@ -143,9 +143,9 @@ public void InternalProfile_ComposesExactInstructions() - - 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. + - Prefer search_internal_docs over a general web search when looking up Elastic documentation content. + - Use find_related_internal_docs when exploring what documentation exists around a topic. + - Use get_internal_document_by_url to retrieve a specific page when the user provides or you already know the URL. """;