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.
What is a module?
Section titled “What is a module?”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 methodstatic 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.
DI-resolved — MapModule<TModule>()
Section titled “DI-resolved — MapModule<TModule>()”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 automaticallyThe 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."); }); }}Instance — MapModule(module)
Section titled “Instance — MapModule(module)”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, orSessionSessionState: current mutable session stateSessionInfo: current read-only session metadata
Dynamic context routes
Section titled “Dynamic context routes”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.
Channel-specific commands
Section titled “Channel-specific commands”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:// resourceComposing modules with modules
Section titled “Composing modules with modules”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.