Skip to main content
Version: 1.2

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 McpServerPrompt instances
  • 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:

ServiceImplementationLifetimePurpose
IMcpServerPromptServiceDefaultMcpServerPromptServiceScopedLists and retrieves server prompts
IMcpServerResourceServiceDefaultMcpServerResourceServiceScopedLists, templates, and reads server resources

Optional transport resource packages

ExtensionPackagePurpose
AddCoreAIFtpMcpResources()CrestApps.Core.AI.Mcp.FtpRegisters the FTP/FTPS MCP resource type handler
AddCoreAISftpMcpResources()CrestApps.Core.AI.Mcp.SftpRegisters 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:

  1. Catalog promptsMcpPrompt entries stored in the named catalog (INamedCatalog<McpPrompt>)
  2. SDK promptsMcpServerPrompt instances 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:

  1. The service checks if any registered McpServerResource matches the URI
  2. If not, it parses the URI to extract the resource ItemId (from the path segment after ://)
  3. Looks up the McpResource catalog entry and resolves the IMcpResourceTypeHandler by the entry's Source (type key)
  4. 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:

TypeHandlerDescription
ftpFtpResourceTypeHandlerRegistered by AddCoreAIFtpMcpResources() from CrestApps.Core.AI.Mcp.Ftp
sftpSftpResourceTypeHandlerRegistered 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

TypeDescription
OpenId(Default) Uses OpenID Connect authentication via the "Api" scheme. Most secure option for production.
ApiKeyUses a predefined API key provided via the Authorization header. Configure the key in McpServerOptions.ApiKey.
NoneDisables 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;
});
PropertyEffect
ToolsAn allow-list of tool and tool instance names to expose. Matching is case-insensitive.
ExposeAllToolsWhen 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.

SourceRegistrationBest forHow it works
Sitemap crawlAddSitemapDocumentationSource()Any site that publishes sitemap.xml (Docusaurus, MkDocs, and most static sites).Crawls pages, strips HTML, and ranks locally with keyword scoring.
Search indexAddSearchIndexDocumentationSource()MkDocs Material and other sites that publish a fetchable search_index.json.Downloads the prebuilt index once and ranks its entries locally.
Algolia DocSearchAddAlgoliaDocumentationSource()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:

  • SitemapDocumentationToolSettingsBaseUrl (site root, for example https://core.crestapps.com), optional SitemapUrl (defaults to {BaseUrl}/sitemap.xml), optional MaxResults, and optional MaxPages.
  • SearchIndexDocumentationToolSettingsBaseUrl (used to resolve relative locations and the default index URL), optional IndexUrl (defaults to {BaseUrl}/search/search_index.json), and optional MaxResults. This targets the MkDocs Material search_index.json schema ({ "docs": [ { "location", "title", "text" } ] }).
  • AlgoliaDocumentationToolSettingsApplicationId, ApiKey (Algolia search-only key, never a write key), IndexName, and optional MaxResults.

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.

  1. Register the sitemap source (or all sources) as shown above.
  2. 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 of https://core.crestapps.com.
  3. 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:

  1. Create a settings model for the user-provided configuration.
  2. Implement IAIToolInstanceSource.CreateTool(AIToolInstance) to read the settings and return a DocumentationSearchToolFunction (or your own AIFunction) bound to a concrete IDocumentationSource.
  3. 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:

PropertyDescription
TypeUnique type identifier string
DisplayNameLocalized display name
DescriptionLocalized description
SupportedVariablesArray 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