MCP Server
Expose your registered AI tools, prompts, and resources to external MCP clients over HTTP.
Quick Start
builder.Services
.AddCoreAIServices()
.AddCoreAIOrchestration()
.AddCoreAIMcpServer()
.AddCoreAIFtpMcpResources()
.AddCoreAISftpMcpResources();
Or, using the builder pattern:
builder.Services.AddCrestAppsCore(crestApps => crestApps
.AddAISuite(ai => ai
.AddOpenAI()
.AddMcpServer(mcpServer => mcpServer
.AddYesSqlStores()
.AddFtpResources()
.AddSftpResources()
)
)
.AddYesSqlDataStore(configuration => configuration
.UseSqLite("Data Source=app.db;Cache=Shared")
)
);
AddCoreAIMcpServer() registers the shared prompt and resource services. FTP and SFTP resource handlers now live in the optional CrestApps.Core.AI.Mcp.Ftp and CrestApps.Core.AI.Mcp.Sftp packages, so hosts opt into those transport dependencies explicitly.
Registering MCP Server Stores
The MCP server requires stores for McpPrompt and McpResource catalogs. Register them on the MCP server builder:
Entity Framework Core (via builder):
.AddMcpServer(mcpServer => mcpServer
.AddEntityCoreStores()
)
YesSql (via builder):
.AddMcpServer(mcpServer => mcpServer
.AddYesSqlStores()
)
Or use the IServiceCollection extensions directly: AddCoreAIMcpServerStoresYesSql() / AddCoreAIMcpServerStoresEntityCore().
When the same host also acts as an MCP client, use AddCoreAIMcpServices() or AddCoreAIMcpClient(...) once for the shared runtime pieces, then layer AddCoreAIMcpServer() on top for the prompt and resource services. The MCP client stores (McpConnection catalog) and MCP server stores (McpPrompt, McpResource catalogs) are registered independently.
Problem & Solution
External AI clients — IDE assistants, chat agents, orchestration frameworks — need a standardized way to discover and call your application's tools, read your prompts, and access your resources. The Model Context Protocol (MCP) provides that standard.
The MCP server framework:
- Exposes your registered AI tools so external clients can invoke them
- Serves prompts from the catalog and from code-registered
McpServerPromptinstances - Serves resources (files, data, templates) via pluggable resource type handlers
- Supports OpenID Connect, API key, or no-auth for development
- Uses the official ModelContextProtocol C# SDK for HTTP transport
Registered Services
AddCoreAIMcpServer() registers these services:
| Service | Implementation | Lifetime | Purpose |
|---|---|---|---|
IMcpServerPromptService | DefaultMcpServerPromptService | Scoped | Lists and retrieves server prompts |
IMcpServerResourceService | DefaultMcpServerResourceService | Scoped | Lists, templates, and reads server resources |
Optional transport resource packages
| Extension | Package | Purpose |
|---|---|---|
AddCoreAIFtpMcpResources() | CrestApps.Core.AI.Mcp.Ftp | Registers the FTP/FTPS MCP resource type handler |
AddCoreAISftpMcpResources() | CrestApps.Core.AI.Mcp.Sftp | Registers the SFTP MCP resource type handler |
Server Endpoint Setup
Use the ModelContextProtocol SDK to map the SSE endpoint and wire in your tool, prompt, and resource handlers:
app.MapMcpSse(mcpBuilder =>
{
mcpBuilder.WithHttpTransport(options =>
{
options.ServerInfo = new() { Name = "My MCP Server", Version = "1.0" };
});
mcpBuilder.WithListToolsHandler(async (context, ct) =>
{
// Return your registered AI tools
});
mcpBuilder.WithCallToolHandler(async (context, ct) =>
{
// Invoke a tool by name, delegate to your tool registry
});
mcpBuilder.WithListPromptsHandler(async (context, ct) =>
{
var service = context.Services.GetRequiredService<IMcpServerPromptService>();
return new ListPromptsResult { Prompts = await service.ListAsync() };
});
mcpBuilder.WithGetPromptHandler(async (context, ct) =>
{
var service = context.Services.GetRequiredService<IMcpServerPromptService>();
return await service.GetAsync(context, ct);
});
mcpBuilder.WithListResourcesHandler(async (context, ct) =>
{
var service = context.Services.GetRequiredService<IMcpServerResourceService>();
return new ListResourcesResult { Resources = await service.ListAsync() };
});
mcpBuilder.WithListResourceTemplatesHandler(async (context, ct) =>
{
var service = context.Services.GetRequiredService<IMcpServerResourceService>();
return new ListResourceTemplatesResult
{
ResourceTemplates = await service.ListTemplatesAsync()
};
});
mcpBuilder.WithReadResourceHandler(async (context, ct) =>
{
var service = context.Services.GetRequiredService<IMcpServerResourceService>();
return await service.ReadAsync(context, ct);
});
});
See the MVC Example for a complete working server setup.
Prompt Serving
IMcpServerPromptService
Serves prompts from two sources:
- Catalog prompts —
McpPromptentries stored in the named catalog (INamedCatalog<McpPrompt>) - SDK prompts —
McpServerPromptinstances registered directly in DI
public interface IMcpServerPromptService
{
Task<IList<Prompt>> ListAsync();
Task<GetPromptResult> GetAsync(
RequestContext<GetPromptRequestParams> request,
CancellationToken cancellationToken = default);
}
The DefaultMcpServerPromptService merges both sources, with catalog prompts taking precedence when names collide.
McpPrompt Model
public sealed class McpPrompt : CatalogItem, INameAwareModel
{
public string Name { get; set; }
public DateTime CreatedUtc { get; set; }
public string Author { get; set; }
public string OwnerId { get; set; }
public Prompt Prompt { get; set; } // MCP SDK Prompt with arguments
}
Resource Serving
IMcpServerResourceService
Serves resources and resource templates from the catalog and code-registered McpServerResource instances:
public interface IMcpServerResourceService
{
Task<IList<Resource>> ListAsync();
Task<IList<ResourceTemplate>> ListTemplatesAsync();
Task<ReadResourceResult> ReadAsync(
RequestContext<ReadResourceRequestParams> request,
CancellationToken cancellationToken = default);
}
How resource reading works:
- The service checks if any registered
McpServerResourcematches the URI - If not, it parses the URI to extract the resource
ItemId(from the path segment after://) - Looks up the
McpResourcecatalog entry and resolves theIMcpResourceTypeHandlerby the entry'sSource(type key) - For template URIs, extracts variables using
McpResourceUri.TryMatch()and passes them to the handler
McpResource Model
public sealed class McpResource : SourceCatalogEntry, IDisplayTextAwareModel
{
public string DisplayText { get; set; }
public DateTime CreatedUtc { get; set; }
public string Author { get; set; }
public string OwnerId { get; set; }
public Resource Resource { get; set; } // MCP SDK Resource (URI, name, description, MIME type)
}
URI Templates
Resources can use URI templates with {variable} placeholders. The McpResourceUri utility handles matching and variable extraction:
ftp://my-resource/{path}
recipe-step-schema://my-resource/{stepName}
When a client requests ftp://my-resource/reports/2024/sales.csv, the framework matches it against the template and extracts { "path": "reports/2024/sales.csv" }. The last variable in a template can match multi-segment paths.
Resource Type Handlers
Optional transport resource handlers:
| Type | Handler | Description |
|---|---|---|
ftp | FtpResourceTypeHandler | Registered by AddCoreAIFtpMcpResources() from CrestApps.Core.AI.Mcp.Ftp |
sftp | SftpResourceTypeHandler | Registered by AddCoreAISftpMcpResources() from CrestApps.Core.AI.Mcp.Sftp |
Registering Custom Resource Types
builder.Services.AddMcpResourceType<BlobStorageResourceHandler>("blob", entry =>
{
entry.DisplayName = new LocalizedString("Blob Storage", "Azure Blob Storage");
entry.Description = new LocalizedString("Blob Desc", "Reads content from Azure Blob Storage.");
entry.SupportedVariables =
[
new McpResourceVariable("path")
{
Description = new LocalizedString("Blob Path", "The blob path within the container.")
},
];
});
For a full guide on implementing custom resource type handlers, see Resource Types.
Authentication
McpServerOptions
Configure server authentication via McpServerOptions:
services.Configure<McpServerOptions>(options =>
{
options.AuthenticationType = McpServerAuthenticationType.OpenId;
options.RequireAccessPermission = true;
});
McpServerAuthenticationType
| Type | Description |
|---|---|
OpenId | (Default) Uses OpenID Connect authentication via the "Api" scheme. Most secure option for production. |
ApiKey | Uses a predefined API key provided via the Authorization header. Configure the key in McpServerOptions.ApiKey. |
None | Disables authentication. Only for local development. |
Permission Control
When using OpenId authentication, the RequireAccessPermission property (default: true) controls whether the AccessMcpServer permission is checked. When set to false, any authenticated user can access the MCP server.
Example — API key authentication:
services.Configure<McpServerOptions>(options =>
{
options.AuthenticationType = McpServerAuthenticationType.ApiKey;
options.ApiKey = builder.Configuration["Mcp:ServerApiKey"];
});
Tool Exposure
When your application acts as an MCP server, tool exposure is opt-in. Nothing is listed or callable by default: the server only exposes the tools and configured tool instances that you explicitly allow through the McpServerOptions site settings.
services.Configure<McpServerOptions>(options =>
{
// Expose specific tools and tool instances by name.
options.Tools = ["crestapps-docs", "weather"];
// Or expose every tool and tool instance.
// When set to true, the Tools allow-list above is ignored.
options.ExposeAllTools = true;
});
| Property | Effect |
|---|---|
Tools | An allow-list of tool and tool instance names to expose. Matching is case-insensitive. |
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.
Both code-registered tools (from AddCoreAITool<T>(), see Custom Tools) and stored tool instances (from any registered tool instance source, such as the documentation search sources) participate in the same allow-list.
Exposing a documentation knowledge base
A common reason to run an MCP server is to answer questions from product or framework documentation that lives on a public site (such as a Docusaurus or MkDocs site). Instead of indexing that content into a vector store, the built-in documentation search tool instance sources let an operator declare a documentation site as a tool instance and scan it on demand.
These sources are ordinary tool instance sources registered on the tool instances builder, so they are usable with or without the MCP server. Each configured instance binds one site and surfaces as its own callable function, so a host can offer "search the CrestApps docs" and "search the Orchard Core docs" as two distinct tools. Instances are persisted and managed through the standard tool instance store and UI, and are exposed to MCP clients through the allow-list above — nothing is exposed until you opt in.
Registering the sources
builder.Services.AddCrestAppsCore(crestApps => crestApps
.AddAISuite(ai => ai
.AddOpenAI()
.AddToolInstances(toolInstances => toolInstances
.AddDocumentationSearchSources()
.AddYesSqlStores()
)
.AddMcpServer(mcpServer => mcpServer
.AddYesSqlStores()
)
)
.AddYesSqlDataStore(configuration => configuration
.UseSqLite("Data Source=app.db;Cache=Shared")
)
);
AddDocumentationSearchSources() registers all three built-in sources. To register only the ones you need, call the individual AddSitemapDocumentationSource(), AddSearchIndexDocumentationSource(), and AddAlgoliaDocumentationSource() methods instead. Once the sources are registered, operators create configured instances (each bound to one site) through the tool instances UI or store, and each instance becomes a callable function the AI model can invoke.
Search strategies
A documentation site can be indexed in different ways depending on what the generator publishes. Each strategy is its own source with its own settings model, so you pick the one that matches the site.
| Source | Registration | Best for | How it works |
|---|---|---|---|
| Sitemap crawl | AddSitemapDocumentationSource() | Any site that publishes sitemap.xml (Docusaurus, MkDocs, and most static sites). | Crawls pages, strips HTML, and ranks locally with keyword scoring. |
| Search index | AddSearchIndexDocumentationSource() | MkDocs Material and other sites that publish a fetchable search_index.json. | Downloads the prebuilt index once and ranks its entries locally. |
| Algolia DocSearch | AddAlgoliaDocumentationSource() | Docusaurus sites (and others) wired to hosted Algolia DocSearch. | Forwards the query to Algolia, which performs the ranking. |
The registered source names are defined by DocumentationToolConstants (sitemap-documentation, search-index-documentation, algolia-documentation), and all three carry the Knowledgebase category.
Each strategy binds a settings model:
SitemapDocumentationToolSettings—BaseUrl(site root, for examplehttps://core.crestapps.com), optionalSitemapUrl(defaults to{BaseUrl}/sitemap.xml), optionalMaxResults, and optionalMaxPages.SearchIndexDocumentationToolSettings—BaseUrl(used to resolve relative locations and the default index URL), optionalIndexUrl(defaults to{BaseUrl}/search/search_index.json), and optionalMaxResults. This targets the MkDocs Materialsearch_index.jsonschema ({ "docs": [ { "location", "title", "text" } ] }).AlgoliaDocumentationToolSettings—ApplicationId,ApiKey(Algolia search-only key, never a write key),IndexName, and optionalMaxResults.
Example: a public Docusaurus site
A public Docusaurus site that requires no authentication — such as core.crestapps.com — only needs the sitemap crawl source. Docusaurus publishes a standard sitemap.xml at the site root, so the crawler discovers {BaseUrl}/sitemap.xml automatically.
- Register the sitemap source (or all sources) as shown above.
- Create a tool instance from the Documentation search (sitemap) source with a Name (for example
crestapps-docs, the name you expose to MCP clients), a clear Description, and a Base URL ofhttps://core.crestapps.com. - Expose the instance through the MCP server by adding its name to the allow-list:
services.Configure<McpServerOptions>(options =>
{
options.Tools = ["crestapps-docs"];
});
Because the site is public, no headers, API keys, or credentials are involved — the crawler issues plain anonymous GET requests. The first search crawls the site and caches the corpus; later searches reuse the cache.
Corpus caching
The runtime documentation source (the crawled corpus or downloaded index) is built lazily and cached by a singleton IDocumentationSourceMaterializer, keyed by the instance identifier. The cache is rebuilt only when the instance changes, so an edit to a site's settings is picked up on the next search while an unchanged instance reuses its corpus across calls.
Adding a new documentation source
To support a documentation site that none of the built-in strategies cover, implement a new tool instance source:
- Create a settings model for the user-provided configuration.
- Implement
IAIToolInstanceSource.CreateTool(AIToolInstance)to read the settings and return aDocumentationSearchToolFunction(or your ownAIFunction) bound to a concreteIDocumentationSource. - Register the source on the tool instances builder with
AddSource<TSource>(name, configure).
Operators then create instances of your new source exactly like the built-in ones, and expose them through the same MCP allow-list.
Server Metadata
IMcpServerMetadataProvider
Implementations provide server metadata for the MCP handshake:
public interface IMcpServerMetadataCacheProvider
{
Task<McpServerCapabilities> GetCapabilitiesAsync(McpConnection connection);
Task InvalidateAsync(string connectionId);
}
McpServerMetadata
public sealed class McpServerMetadata
{
public bool UseLocalServer { get; set; }
}
File Provider Integration
IMcpFileProviderResolver
Allows resource handlers to resolve IFileProvider instances by name, enabling resources served from media libraries, web roots, or custom file stores:
public interface IMcpFileProviderResolver
{
IFileProvider Resolve(string providerName);
}
Resource Export
IMcpResourceHandler
Hook into resource export to strip sensitive data:
public interface IMcpResourceHandler
{
void Exporting(ExportingMcpResourceContext context);
}
public sealed class ExportingMcpResourceContext
{
public readonly McpResource Resource;
public readonly JsonObject ExportData; // Modify to remove credentials
}
Metadata Prompt Generation
IMcpMetadataPromptGenerator
Generates structured system prompts describing MCP server capabilities so the AI model can reason about when to invoke them:
public interface IMcpMetadataPromptGenerator
{
string Generate(IReadOnlyList<McpServerCapabilities> capabilities);
}
The generated prompt is injected into the model context, giving the AI awareness of available MCP capabilities without manually listing each tool in the system prompt.
Configuration
McpOptions
McpOptions holds the registry of resource types. Resource types are added via AddMcpResourceType<T>():
services.Configure<McpOptions>(options =>
{
// Inspect registered resource types
foreach (var (type, entry) in options.ResourceTypes)
{
Console.WriteLine($"{type}: {entry.DisplayName}");
}
});
Each McpResourceTypeEntry provides:
| Property | Description |
|---|---|
Type | Unique type identifier string |
DisplayName | Localized display name |
Description | Localized description |
SupportedVariables | Array of McpResourceVariable describing URI template variables |
- Configuring server authentication (OpenID, API key, or none)
- Managing prompts and resources through the admin dashboard
- Registering and configuring resource types
- Viewing server capabilities and health status