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”Dynamic context routes are still configured up front. Use the optional validation delegate to guard entering the scope, then bind the captured route value into each handler:
app.Context( "project {id:int}", project => { project.Map("show", static ([FromContext] int id, IProjectStore store) => store.Get(id) ?? throw new KeyNotFoundException($"Project {id} not found."));
project.Map("archive", static ([FromContext] int id, IProjectStore store) => { var project = store.Get(id) ?? throw new KeyNotFoundException($"Project {id} not found."); project.Archive(); store.Save(project); return Results.Success($"Archived '{project.Name}'."); }); }, validation: static (int id, IProjectStore store) => store.Get(id) is null ? $"Project {id} not found." : string.Empty);The validator runs when the context is matched. It can return bool, string, IReplResult, or null; a non-empty string is rendered as a validation failure. The command handlers remain regular mapped commands and can use [FromContext] to make context binding explicit.
Use the same validation delegate for access control checks that should block an entire dynamic scope.
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.