Skip to content

MCP Server

The Model Context Protocol (MCP) is the open standard through which AI agents discover and call tools. Repl.Mcp makes your entire command surface MCP-native with one line of code — then lets you fine-tune every aspect of how it’s exposed.

using Repl.Mcp;
app.UseMcpServer(); // exposes everything as MCP tools
return app.Run(args);

Start the server:

Terminal window
myapp mcp serve

Configure your agent (paths vary by client — see below):

{
"mcpServers": {
"myapp": { "command": "myapp", "args": ["mcp", "serve"] }
}
}

Agent config file locations:

AgentPath
Claude Desktop%APPDATA%\Claude\claude_desktop_config.json (Windows) / ~/.config/Claude/... (macOS)
Claude Code.claude/settings.json or ~/.claude.json
VS Code Copilot.vscode/mcp.json or ~/.mcp.json
Cursor.cursor/mcp.json or ~/.cursor/mcp.json
MCP InspectorUI-based

Every Map(...) call becomes an MCP tool automatically. The tool name is derived from the route (spaces replaced by _). The description comes from [Description] or .WithDescription(...).

Repl declarationMCP surfaceNotes
app.Map(...)ToolDefault — always a tool
.ReadOnly()Tool + auto-promoted ResourceURI derived from route
.AsResource()Resource onlyNot callable as tool; URI derived from route
.AsPrompt()PromptCallable as MCP prompt
.AsMcpAppResource()Tool + ui:// HTML resourceReturns HTML for capable clients
.AutomationHidden()ExcludedHuman-only command

Default: contact addcontact_add. Customize the separator:

app.UseMcpServer(o => o.ToolNamingSeparator = ToolNamingSeparator.Hyphen);
// contact add → contact-add

Annotations tell the agent how to call your tool — whether it should ask for confirmation, whether it’s safe to parallelize, whether it can be retried:

app.Map("contacts delete {id:int}", (int id, IContactStore store) =>
{
store.Delete(id);
return Results.Success("Contact deleted.");
})
.WithDescription("Permanently delete a contact.")
.Destructive(); // warn agent before calling; not safe to parallelize
AnnotationMeaning for the agent
.ReadOnly()No side effects — call freely, parallelize
.Destructive()Side effects — warn user, don’t parallelize
.Idempotent()Safe to retry on transient failure
.OpenWorld()Reaches external systems (email, HTTP, etc.)
.LongRunning()Enable call-now/poll-later pattern
.AutomationHidden()Exclude from MCP; visible in interactive REPL only

Expose read-only data as MCP resources — the agent can subscribe, list, and read without invoking a tool:

app.Map("contacts export", (IContactStore store) => store.All())
.AsResource()
.WithDescription("All contacts as a live JSON resource.");

The resource URI is derived from the route.

Use .ReadOnly() for the common case of “tool that’s also readable as a resource”:

app.Map("contacts {id:int} show", (int id, IContactStore store) => store.Get(id))
.ReadOnly()
.WithDescription("Get contact by ID.");
// Registered as both tool 'contacts_show' and a resource with a URI derived from the route

Prompt templates let the agent inject pre-built context into an LLM conversation:

app.Map("prompts summarize {id:int}", (int id, IContactStore store) =>
{
var contact = store.Get(id);
return new McpPromptResult
{
Messages =
[
new McpMessage(McpRole.User,
$"Summarize the following contact and suggest follow-up actions:\n\n{contact}")
]
};
})
.AsPrompt()
.WithDescription("Generate a summary prompt for a contact.");

MCP Apps are HTML surfaces rendered inside capable clients (e.g., Claude). The same Map(...) call serves launcher text for tool invocation and HTML for resources/read:

using System.Net; // WebUtility.HtmlEncode
app.Map("contacts dashboard", (IContactStore store) =>
{
var contacts = store.All().ToList();
return $$"""
<!DOCTYPE html>
<html>
<head><title>Contacts ({{contacts.Count}})</title></head>
<body>
<h1>Contacts</h1>
<ul>{{string.Join("", contacts.Select(c =>
$"<li><b>{WebUtility.HtmlEncode(c.Name)}</b> — {WebUtility.HtmlEncode(c.Email)}</li>"))}}</ul>
</body>
</html>
""";
})
.AsMcpAppResource()
.WithDescription("Open the contacts dashboard.")
.WithMcpAppDisplayMode(McpAppDisplayMode.Inline);

Configure Content Security Policy for apps that load external assets:

.WithMcpAppCsp(csp => csp.AddScriptSrc("https://cdn.myapp.io"))

Repl routes IReplInteractionChannel calls through MCP primitives when running as an MCP server. Configure the fallback when a client doesn’t support elicitation:

app.UseMcpServer(o =>
{
o.InteractivityMode = InteractivityMode.PrefillThenElicitation;
// PrefillThenFail — fail if no prefill provided (default)
// PrefillThenDefaults — use C# parameter defaults
// PrefillThenElicitation — structured form via agent
// PrefillThenSampling — LLM generates answer
});

Design for agents: prefer accepting all required data as route parameters or named options. Reserve IReplInteractionChannel prompts for the human-interactive experience — agents should pass everything upfront.

For direct programmatic access to sampling and elicitation (outside of the interactivity pipeline), see MCP — Advanced.


Tools can carry extended Markdown documentation for the agent, separate from the human-facing --help text:

app.Map("contacts add {name} {email:email}", handler)
.WithDescription("Add a new contact.")
.WithDetails("""
Creates a new contact record with the given name and email.
**Rules:**
- Email must be unique across all contacts.
- Name cannot exceed 200 characters.
- Returns the new contact's ID on success.
**Example:**
`contacts_add` with `name="Alice Dupont"`, `email="alice@example.com"`
""");

WithDetails(...) content appears in the MCP tool schema but not in --help output.


Hide individual commands from MCP:

app.Map("debug reset", handler)
.AutomationHidden(); // hidden from MCP, visible in interactive REPL

Filter at the server level:

app.UseMcpServer(o =>
{
o.CommandFilter = cmd => !cmd.Tags.Contains("internal");
});

Use module presence predicates for context-sensitive visibility:

app.MapModule(
new AdminModule(),
(IAuthService auth) => auth.IsAdmin());

To scope visibility to workspace roots (native MCP roots or soft-root fallbacks), see Client roots and soft roots.


app.UseMcpServer(o =>
{
o.ServerName = "MyApp";
o.ToolNamingSeparator = ToolNamingSeparator.Underscore;
o.InteractivityMode = InteractivityMode.PrefillThenElicitation;
o.ResourceFallbackToTools = true; // resources also callable as tools (compatibility fallback)
o.PromptFallbackToTools = true; // AsPrompt() tools also callable as tools
o.CommandFilter = cmd => true;
o.DynamicToolCompatibility = DynamicToolCompatibilityMode.Disabled; // → see MCP — Advanced for shim mode
});

Design for agents first. Commands that work well for agents work well for humans too. The reverse is not always true.

Use typed constraints. Route constraints generate typed JSON Schema properties — {id:int} becomes "type": "integer" in the tool schema. An agent that knows the type can fill parameters correctly without guessing.

One concern per tool. A command named contacts_sync_import_and_notify is hard for an agent to reason about. Prefer contacts_import and contacts_notify as separate tools.

Annotate everything destructive. Missing .Destructive() on a delete command may cause an agent to call it without warning. When in doubt, annotate conservatively.

Keep tool names short. Agents have context windows. contacts_list is better than contact_management_list_all_records. Prefer short, noun-verb names.

Test with MCP Inspector. Run myapp mcp serve and point MCP Inspector at it to verify tool schemas, annotations, and resources before connecting a real agent.


For advanced topics — programmatic sampling and elicitation from handler code, client roots and soft roots, custom transports, and HTTP hosting — see MCP — Advanced.