Extensions
An extension is a single JavaScript or TypeScript file that contributes at runtime to one or more ferridriver hosts:
- MCP server (
ferridriver mcp) — registers tools viadefineTool(...). - BDD test runner (
ferridriver bdd) — registers Cucumber step definitions, hooks, and parameter types viaGiven/When/Then/Before/After/defineParameterType/setWorldConstructor/setDefaultTimeout. - Ad-hoc scripts (
ferridriver run, MCPrun_script) — same VM, same globals.
The same file can serve all three. Branch on the ferridriver.host
global to decide which contributions apply where.
Mental model
Registration functions (defineTool, Given, Before, …) are
native Rust functions, not JS shims. Calling them at the top level
of your module pushes an entry into a Rust-owned registry. Hosts read
back the kinds they care about and invoke your handler natively — the
MCP tool path and the BDD step path use the same dispatch mechanism.
Implication: all contribution happens as a side effect of the
module's top-level code running once. There is no activate() /
onLoad() hook — ES module top-level is your load hook.
Detecting the host
ferridriver.host is a string set once per session: "mcp", "bdd",
or "script". Gate your registrations so one file does not pollute the
wrong host:
Registering for the wrong host is harmless (the host ignores kinds it does not consume), but it wastes work and muddies intent.
defineTool
Two equivalent forms:
Fields
exposeAsTool
false(default): the tool is callable from other extension / script code asawait plugins["name"](args), but not advertised in the MCP server'stools/list. Use for shared helpers.true: additionally promoted to a first-class MCP tool.name,description, andinputSchemabecome the tool contract. The tool call and theplugins[...]binding route through the same handler.
Handler context
The handler receives one object:
Return any JSON-serialisable value; it becomes the tool result.
When the manifest declares inputSchema, the caller's args are
validated against it (full JSON Schema, via the jsonschema crate)
before the handler runs; a non-conforming call is rejected as a
tool error and the handler is never entered.
Discovery and configuration
Extensions are configured in ferridriver.toml:
ferridriver bdd bundles discovered step files and the configured
extensions into one module, so an extension's Given / When /
Then are available to tests exactly like a step file's.
Both discovery paths (MCP loader and BDD runner) share one
accepted-extension set and one recursive walk — a .tsx / .cts
extension is visible identically to both hosts.
Runtime guarantees
inputSchemais enforced. Calls whose arguments do not match the declared schema are rejected before your handler runs. A schema that is itself invalid JSON Schema is reported, not silently ignored.- Tool names are unique and non-empty. A duplicate or blank
namefails that extension at load time. A name that collides with a built-in or another loaded tool is not exposed. Namespace your names (vendor.area.action). - Tool failures are reported as errors. When your handler throws,
the caller gets an error result (not a "success" containing an
error string), with the message first and full detail after. (Plain
run_scriptis different: it always succeeds and you inspect itsstatusfield.) timeoutMsis honoured for every caller — whether the tool is invoked as a promoted MCP tool or by another extension. Without it, only the session-wide script timeout applies.- Discovery is recursive and uniform. A configured directory is
scanned recursively;
.js .cjs .mjs .jsx .ts .cts .mts .tsxare all accepted, the same way for the MCP server and the test runner. - You can inspect what loaded. The built-in
ferridriver_extensionsMCP tool lists every loaded extension file, its tools, descriptions, whether each is exposed, its timeout, and its declared capabilities.
What is intentionally not provided
activate()/onLoad()hook. Module top-level is the load hook; ES module evaluation runs your registrations.- Plugin dependency / ordering. The loader sorts files deterministically by path; cross-file load ordering is not configurable.
- Cross-plugin shared state channel. Share helpers via
importstatements (rolldown will resolve and bundle them); there is no global registry. - Middleware / hook pipeline (Rollup-style ordered hooks). Not shipped — no consumer today justifies the abstraction. The capability boundary is the natural insertion point if one ever does.
See Capabilities for allow.commands and
allow.net. See BDD JS / TS API for Given
/ When / Then reference.