Skip to content

Modules

Modules are the primary composition unit in Repl Toolkit. A module is a self-contained set of commands that can be registered at any point in the command graph — once, or under multiple contexts.

Any method or class that calls Map(...) and Context(...) on an IReplMap is a module. There’s no base class or interface to implement — it’s a convention, not a framework type.

// A simple module as a static method
static void RegisterCrudCommands<T>(IReplMap map, IStore<T> store) where T : IEntity
{
map.Map("list", () => store.All());
map.Map("show {id:int}", (int id) => store.Get(id));
map.Map("delete {id:int}", (int id) =>
{
store.Delete(id);
return Results.Success($"Deleted {id}.");
});
}

Register it anywhere in the graph:

app.Context("contacts", contacts => RegisterCrudCommands(contacts, contactStore));
app.Context("products", products => RegisterCrudCommands(products, productStore));

Both contacts and products now expose list, show, and delete — without copy-pasting routes.

MapModule — registering a class-based module

Section titled “MapModule — registering a class-based module”

MapModule is the primary registration mechanism for class-based modules. There are three forms.

The recommended form. Repl resolves the constructor from the DI container — every service registered in builder.Services is available to the module:

builder.Services.AddScoped<IAuditLog, AuditLog>();
// …later, when building the command graph:
app.MapModule<AuditModule>(); // DI resolves IAuditLog automatically

The module declares its dependencies via a primary constructor (or any constructor):

public class AuditModule(IAuditLog log) : IReplModule
{
public void Map(IReplMap map)
{
map.Map("audit list", static (IAuditLog l) => l.All());
map.Map("audit clear", static (IAuditLog l) =>
{
l.Clear();
return Results.Success("Audit log cleared.");
});
}
}

Pass a pre-constructed instance when you wire dependencies manually or the module has no DI dependencies:

app.MapModule(new AuditModule(myAuditLog));

This also works inside a Context(...) callback, where local variables are already available:

app.Context("admin", admin =>
{
admin.MapModule(new AuditModule(myAuditLog));
admin.MapModule(new MaintenanceModule());
});

Conditional — MapModule(module, predicate)

Section titled “Conditional — MapModule(module, predicate)”

Mount a module only when a runtime condition is met. The predicate is evaluated each time the command graph is traversed:

app.MapModule(
new AdminModule(),
ctx => ctx.Channel != ReplRuntimeChannel.Session
&& ctx.SessionState.TryGet<bool>("auth.admin", out var isAdmin)
&& isAdmin);

For predicates that need DI services, pass a delegate — Repl resolves the parameters from the container at evaluation time:

app.MapModule(
new AdminModule(),
(IAuthService auth, ReplRuntimeChannel channel) =>
channel == ReplRuntimeChannel.Interactive && auth.IsAdmin());

ModulePresenceContext provides:

  • Channel: Cli, Interactive, or Session
  • SessionState: current mutable session state
  • SessionInfo: current read-only session metadata

A context factory receives the captured route values and can do work before returning the command registrar. This is the canonical pattern for “enter scope, validate, then expose commands”:

app.Context("project {id:int}", (int id, IProjectStore store) =>
{
var project = store.Get(id)
?? throw new KeyNotFoundException($"Project {id} not found.");
return (IReplMap scoped) =>
{
scoped.Map("show", () => project);
scoped.Map("archive", () =>
{
project.Archive();
store.Save(project);
return Results.Success($"Archived '{project.Name}'.");
});
};
});

The outer factory runs once when the scope is entered. All commands inside the returned lambda close over the resolved project — they don’t re-fetch on every invocation.

This pattern is also where you enforce access control: throw or return Results.Error(...) from the factory to block entry.

Use .AutomationHidden() to hide a command from MCP and other programmatic surfaces while keeping it in the interactive REPL. For MCP-only commands, register them with .AsMcpAppResource() or .AsResource() — these are only surfaced on the MCP channel:

app.Map("debug reset", () => store.Clear())
.AutomationHidden(); // visible in REPL, hidden from MCP agents
app.Map("contacts dashboard", BuildDashboard)
.AsMcpAppResource(); // surfaced as an MCP ui:// resource

Modules can register other modules — compose freely:

public class ContactsModule : IReplModule
{
public void Map(IReplMap map)
{
map.Context("contacts", contacts =>
{
RegisterCrud(contacts);
RegisterAudit(contacts, "contact");
RegisterImportExport(contacts);
});
}
}

There’s no depth limit and no performance penalty — routes are compiled at startup.