Every tool handler is wrapped in a small wrap helper (src/tools.ts) that catches thrown errors and returns a structured MCP error result instead of letting the rejection bubble up:
{
content: [{ type: "text", text: "<JSON payload>" }],
isError: true
}
MCP clients render isError: true results gracefully, typically as an inline error block an assistant, agent, host workflow, and user can inspect, instead of aborting the whole turn the way a raw rejection might. The text inside is a JSON-encoded payload the caller can parse and route on.
| Field | Always present? | Notes |
|---|---|---|
error |
yes | "http" | "timeout" | "client" | "unknown" — the error kind. |
tool |
yes | The MCP tool name that produced the error, e.g. "list_modules". |
message |
yes | One-line human-readable summary. |
status |
http only | The HTTP status code returned by seed4j. |
endpoint |
http + timeout | The absolute URL (and method, for timeouts) that was hit. |
bodyExcerpt |
http only | The first 500 chars of the seed4j response body; longer bodies get a … (N more chars) suffix. |
timeoutMs |
timeout only | The configured timeout that fired. |
hint |
when actionable | A short next-step suggestion (e.g. "check the tool inputs", "increase SEED4J_TIMEOUT_MS"). |
http — seed4j responded with a non-2xx status. Hint differs by status family.
{
"error": "http",
"tool": "apply_module",
"status": 400,
"endpoint": "http://localhost:1339/api/modules/maven-java/apply-patch",
"message": "seed4j responded with HTTP 400",
"bodyExcerpt": "missing mandatory property: packageName",
"hint": "check the tool inputs — module slug, properties, or project folder may be wrong"
}{
"error": "http",
"tool": "list_modules",
"status": 503,
"endpoint": "http://localhost:1339/api/modules",
"message": "seed4j responded with HTTP 503",
"bodyExcerpt": "starting up...",
"hint": "seed4j returned a server error; check the seed4j server logs"
}timeout — the per-request timer fired.
{
"error": "timeout",
"tool": "get_module_details",
"endpoint": "GET http://localhost:1339/api/modules/maven-java",
"timeoutMs": 30000,
"message": "request timed out after 30000ms",
"hint": "increase SEED4J_TIMEOUT_MS or verify seed4j is reachable at SEED4J_BASE_URL"
}client — a plain Error thrown before any HTTP call (e.g. unknown preset name, empty step list).
{
"error": "client",
"tool": "get_preset_details",
"message": "Preset not found: Foo"
}unknown — a non-Error value was thrown. The original value is stringified into message.
The transport layer still throws typed errors; the wrapping happens at the tool boundary. These types are exported from src/client.ts and remain useful for tests and for any direct programmatic use.
HttpError { status, body, url, message: `HTTP ${status} for ${url}: ${body}` }
Thrown when seed4j responds with a non-2xx status.
TimeoutError { url, method, timeoutMs, message: `seed4j request timed out after ${timeoutMs}ms: ${method} ${url}` }
Thrown when the AbortController armed for timeoutMs (default 30 s, override via SEED4J_TIMEOUT_MS) fires.
Some client methods throw plain Errors before any HTTP call. After wrapping these surface as error: "client":
| Method | Throws when |
|---|---|
| mutation tools | projectFolder is empty, relative, or a filesystem root. |
getPresetDetails |
preset name is blank or no preset matches. |
applyModules |
the step list is empty. |
applyPreset |
the preset resolves to zero modules. |
getModuleDependencies |
the slug is not found in the landscape. |
validateProperties |
(does not throw — returns { valid: false, errors }). |
create_project creates the target folder locally before applying seed4j's init module. If init fails and the folder was newly created and still empty, the MCP server removes it. If the folder already existed, or seed4j wrote any partial files before failing, the folder is left in place so the user can inspect and recover manually.
Read-only GETs (/api/modules, /api/modules/{slug}, /api/presets, /api/modules-landscape, /api/projects?path=…) are automatically retried inside Seed4jClient when the failure looks transient:
- Retryable:
TimeoutError, HTTP 5xx (HttpErrorwithstatus >= 500), and other thrown errors (e.g. network errors fromfetchitself). - Not retryable: HTTP 4xx (
HttpErrorwithstatus < 500) — these are deterministic (auth, bad slug, malformed query). - Not retried at all: POSTs to
apply-patch. Re-running a half-applied module could leave the project in an inconsistent state — that decision belongs to the calling assistant, agent, or host workflow, not the transport layer.
Backoff is capped exponential: min(retryBaseDelayMs * 2^attempt, retryMaxDelayMs). Defaults are retries = 2 (so up to 3 attempts, override via SEED4J_RETRIES), retryBaseDelayMs = 200, retryMaxDelayMs = 2_000. When all attempts fail, the last error propagates unchanged — and the tool wrapper then turns it into the structured payload above.
These two tools intentionally aggregate per-step failures into a successful response (the failure and remaining fields described in tools.md), so they typically don't trigger the structured-error wrapper. wrap only kicks in for them when something blows up before the apply loop starts (e.g. a 503 on the preset lookup, an empty step list).