Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/api/Elastic.Documentation.Mcp.Remote/McpFeatureModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace Elastic.Documentation.Mcp.Remote;
/// <param name="Name">Module identifier.</param>
/// <param name="Capability">Capability verb for the preamble (e.g. "search", "retrieve"). Null if the module does not add a capability.</param>
/// <param name="WhenToUse">Bullet points for the "Use the server when the user:" section. Use {docs} for the profile's DocsDescription.</param>
/// <param name="ToolGuidance">Lines for the tool guidance section. Use {tool:snake_case_name} for tool names (e.g. {tool:semantic_search}).</param>
/// <param name="ToolGuidance">Lines for the tool guidance section. Use {tool:name} or {tool:name_{resource}} for tool names (e.g. {tool:search_{resource}}).</param>
/// <param name="ToolType">The tool class type (e.g. typeof(SearchTools)). Null if the module has no tools.</param>
/// <param name="RegisterServices">DI registrations the module's tools depend on.</param>
public sealed record McpFeatureModule(
Expand All @@ -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()
Expand All @@ -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<IDocumentGateway, DocumentGateway>()
Expand All @@ -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: _ => { }
Expand Down
38 changes: 28 additions & 10 deletions src/api/Elastic.Documentation.Mcp.Remote/McpServerProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,25 @@ namespace Elastic.Documentation.Mcp.Remote;
/// with the profile's DocsDescription at composition time.
/// </summary>
/// <param name="Name">Profile identifier (e.g. "public", "internal").</param>
/// <param name="ToolNamePrefix">Prefix for all tool names (e.g. "public_docs_", "internal_docs_").</param>
/// <param name="ResourceNoun">Resource noun for tool names (e.g. "docs", "internal_docs"). Replaces {resource} in tool name templates.</param>
/// <param name="ScopePrefix">Scope prefix for tool names (e.g. "" for public, "internal_" for internal). Replaces {scope} in tool name templates.</param>
/// <param name="DocsDescription">Short noun phrase describing this profile's docs (e.g. "Elastic product documentation"). Used to replace {docs} in trigger templates.</param>
/// <param name="Introduction">Introduction template with a {capabilities} placeholder replaced at composition time.</param>
/// <param name="ExtraTriggers">Profile-specific trigger bullets appended after module triggers.</param>
/// <param name="Modules">Enabled feature modules.</param>
public sealed record McpServerProfile(
string Name,
string ToolNamePrefix,
string ResourceNoun,
string ScopePrefix,
string DocsDescription,
string Introduction,
string[] ExtraTriggers,
McpFeatureModule[] Modules)
{
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."],
Expand All @@ -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."],
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
42 changes: 20 additions & 22 deletions src/api/Elastic.Documentation.Mcp.Remote/McpToolRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Creates MCP tools with profile-based name prefixes.
/// Creates MCP tools with profile-based names and descriptions.
/// </summary>
public static class McpToolRegistration
{
/// <summary>
/// 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.
/// </summary>
public static IEnumerable<McpServerTool> CreatePrefixedTools(McpServerProfile profile)
{
var prefix = profile.ToolNamePrefix;
var resourceNoun = profile.ResourceNoun;
var scopePrefix = profile.ScopePrefix;
var docsDescription = profile.DocsDescription;
var tools = new List<McpServerTool>();

foreach (var module in profile.Modules)
Expand All @@ -33,10 +36,20 @@ public static IEnumerable<McpServerTool> CreatePrefixedTools(McpServerProfile pr

foreach (var method in methods)
{
var snakeName = ToSnakeCase(method.Name);
var prefixedName = prefix + snakeName;
var nameAttr = method.GetCustomAttribute<McpToolNameAttribute>()
?? 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<DescriptionAttribute>();
var description = descAttr?.Description?.Replace("{docs}", docsDescription, StringComparison.Ordinal);

var options = new McpServerToolCreateOptions
{
Name = toolName,
Description = description
};

var tool = McpServerTool.Create(
method,
Expand All @@ -49,19 +62,4 @@ public static IEnumerable<McpServerTool> CreatePrefixedTools(McpServerProfile pr

return tools;
}

/// <summary>
/// Converts PascalCase to snake_case (e.g. SemanticSearch → semantic_search).
/// </summary>
[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)}"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,8 +21,8 @@ public class CoherenceTools(IFullSearchGateway fullSearchGateway, ILogger<Cohere
/// <summary>
/// Checks documentation coherence for a given topic.
/// </summary>
[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<string> CheckCoherence(
Expand Down Expand Up @@ -92,8 +93,8 @@ public async Task<string> CheckCoherence(
/// <summary>
/// Finds potential inconsistencies in documentation for a given topic.
/// </summary>
[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<string> FindInconsistencies(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,9 +21,9 @@ public class DocumentTools(IDocumentGateway documentGateway, ILogger<DocumentToo
/// <summary>
/// Gets a document by its URL.
/// </summary>
[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<string> GetDocumentByUrl(
Expand Down Expand Up @@ -89,8 +90,8 @@ public async Task<string> GetDocumentByUrl(
/// <summary>
/// Analyzes the structure of a document.
/// </summary>
[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<string> AnalyzeDocumentStructure(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,8 +21,8 @@ public class SearchTools(IFullSearchGateway fullSearchGateway, ILogger<SearchToo
/// <summary>
/// Performs semantic search across all Elastic documentation.
/// </summary>
[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.")]
Expand Down Expand Up @@ -84,8 +85,8 @@ public async Task<string> SemanticSearch(
/// <summary>
/// Finds documents related to a given topic or document URL.
/// </summary>
[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<string> FindRelatedDocs(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public partial class ContentTypeTools(ContentTypeProvider provider)
/// <summary>
/// Lists all available Elastic Docs content types.
/// </summary>
[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.")]
Expand All @@ -71,7 +71,7 @@ public string ListContentTypes()
/// <summary>
/// Generates a template for a specific content type.
/// </summary>
[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).")]
Expand Down Expand Up @@ -109,7 +109,7 @@ public string GenerateTemplate(
/// <summary>
/// Gets authoring and evaluation guidelines for a content type.
/// </summary>
[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.")]
Expand Down
10 changes: 5 additions & 5 deletions src/services/Elastic.Documentation.Assembler/Mcp/LinkTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class LinkTools(ILinkUtilService linkUtilService)
/// <summary>
/// Resolves a cross-link URI to its target URL.
/// </summary>
[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<string> ResolveCrossLink(
Expand Down Expand Up @@ -52,7 +52,7 @@ public async Task<string> ResolveCrossLink(
/// <summary>
/// Lists all available repositories in the link index.
/// </summary>
[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<string> ListRepositories(CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -88,7 +88,7 @@ public async Task<string> ListRepositories(CancellationToken cancellationToken =
/// <summary>
/// Gets all links published by a repository.
/// </summary>
[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<string> GetRepositoryLinks(
Expand Down Expand Up @@ -132,7 +132,7 @@ public async Task<string> GetRepositoryLinks(
/// <summary>
/// Finds all cross-links from one repository to another.
/// </summary>
[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.")]
Expand Down Expand Up @@ -172,7 +172,7 @@ public async Task<string> FindCrossLinks(
/// <summary>
/// Validates cross-links and finds broken ones.
/// </summary>
[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<string> ValidateCrossLinks(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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").
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class McpToolNameAttribute(string template) : Attribute
{
public string Template { get; } = template;
}
Loading
Loading