-
Notifications
You must be signed in to change notification settings - Fork 35
MCP: add configurable server profiles (public, internal) #2813
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
37252a9
Add configurable MCP server profiles (public, internal)
reakaleek 6f00346
Update McpServerInstructionTests.cs
theletterf fdec96f
MCP: add profile-based tool name prefixes and context-aware triggers
reakaleek ec21e33
Fix code quality and AOT trim analysis errors
reakaleek d4a8f4e
Fix native AOT trim analysis errors and update container base image
reakaleek 7f4e341
Fix IL2069: annotate both constructor param and property for AOT trim
reakaleek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
src/api/Elastic.Documentation.Mcp.Remote/McpFeatureModule.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
|
||
| /// <summary> | ||
| /// A feature module with DI services and instruction template fragments. | ||
| /// WhenToUse bullets should be generic and context-free; the branding introduction frames their meaning. | ||
| /// </summary> | ||
| /// <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="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( | ||
| string Name, | ||
| string? Capability, | ||
| string[] WhenToUse, | ||
| string[] ToolGuidance, | ||
| [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] | ||
| [property: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] | ||
| Type? ToolType, | ||
| Action<IServiceCollection> 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<IDocumentGateway, DocumentGateway>() | ||
| ); | ||
|
|
||
| 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<ILinkIndexReader>(_ => Aws3LinkIndexReader.CreateAnonymous()); | ||
| _ = services.AddSingleton<LinksIndexCrossLinkFetcher>(); | ||
| _ = services.AddSingleton<ILinkUtilService, LinkUtilService>(); | ||
| } | ||
| ); | ||
|
|
||
| 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<ContentTypeProvider>() | ||
| ); | ||
| } |
145 changes: 145 additions & 0 deletions
145
src/api/Elastic.Documentation.Mcp.Remote/McpServerProfile.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
|
||
| /// <summary> | ||
| /// 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. | ||
| /// </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="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 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] | ||
| ); | ||
|
|
||
| /// <summary> | ||
| /// Resolves a profile by name. Throws if the name is unknown. | ||
| /// </summary> | ||
| 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)) | ||
| }; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Registers all DI services from enabled modules, including tool types for resolution at invocation time. | ||
| /// </summary> | ||
| public void RegisterAllServices(IServiceCollection services) | ||
| { | ||
| foreach (var module in Modules) | ||
| { | ||
| module.RegisterServices(services); | ||
| if (module.ToolType is not null) | ||
| _ = services.AddScoped(module.ToolType); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Composes server instructions from the profile introduction and enabled module fragments. | ||
| /// </summary> | ||
| 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<tool_guidance>\n" + string.Join("\n", toolGuidance.Select(l => $"- {l}")) + "\n</tool_guidance>" | ||
| : ""; | ||
|
|
||
| return $""" | ||
| {introduction} | ||
|
|
||
| <triggers> | ||
| Use the server when the user:{whenToUseBlock} | ||
| </triggers> | ||
| {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] | ||
| }; | ||
| } | ||
| } |
67 changes: 67 additions & 0 deletions
67
src/api/Elastic.Documentation.Mcp.Remote/McpToolRegistration.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
|
||
| /// <summary> | ||
| /// Creates MCP tools with profile-based name prefixes. | ||
| /// </summary> | ||
| public static class McpToolRegistration | ||
| { | ||
| /// <summary> | ||
| /// 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. | ||
| /// </summary> | ||
| public static IEnumerable<McpServerTool> CreatePrefixedTools(McpServerProfile profile) | ||
| { | ||
| var prefix = profile.ToolNamePrefix; | ||
| var tools = new List<McpServerTool>(); | ||
|
|
||
| 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<McpServerToolAttribute>() != 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; | ||
| } | ||
|
|
||
| /// <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)}")); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.