Advanced reference for DevSpeak MCP server error handling and generation control. Covers the API envelope with success, data, and error fields, the meaning of HTTP 400, 401, 403, 429, and 5xx responses, honoring the Retry-After header on rate limits, handling request timeouts on long generations, pinning a specific model with the managedModel parameter, and requesting structured XML output with responseFormat.
Handle MCP Errors, Rate Limits, and Model Selection
Every DevSpeak MCP tool eventually fails at something — a quota, a rate limit, a timeout. This tutorial covers reading those failures precisely and the two generation controls worth knowing: model pinning and XML output.
What You'll Learn
429managedModelresponseFormatPrerequisites
translate_text and refine_translationTime Estimate
~10 minutes
The Response Envelope
Every response uses one shape:
`` { "success": true, "data": { "output": "## Technical Specification\n\n..." }, "error": null }json
`
On failure:
`json
{
"success": false,
"data": null,
"error": "Input must be at least 10 characters"
}
`
The MCP server surfaces error as the tool error message. When your assistant reports a DevSpeak failure, that string came from the server — it is not the assistant's paraphrase.
One case produces a different shape entirely. If a gateway or SPA fallback answers with HTML instead of JSON, you get:
`text
Malformed response from https://.../api/v1/translate:
expected JSON, received text/html.
`
That means the request never reached the DevSpeak API — typically a wrong base URL, or a proxy or gateway answering in its place. DevSpeak itself answers an unmatched /api/* path with a 404 JSON error envelope, so check the base URL before anything else.Status Code Reference
| Status | Meaning | Retry? | Fix |
| ------ | ---------------------- | ----------------- | ------------------------------------------------- |
| 400 | Validation failed | No | Fix the parameter named in error |
| 401 | Key invalid or revoked | No | Regenerate in Account Settings → Keys & Providers |
| 403 | Tier lacks the feature | No | Check get_account_info, upgrade |
| 429 | Rate limited | Yes, after wait | Honor Retry-After |
| 5xx | Server-side failure | Yes, with backoff | Retry; many of these resolve on their own |
The distinction that matters: 401 means _who you are_ failed, 403 means _what you may do_ failed. Regenerating a key will not fix a 403.Rate Limits and Backoff
Requests are rate-limited per API key, and some sensitive endpoints have a stricter limit of their own. Exceeding either returns a 429.
A 429 carries the wait time in the Retry-After header, in seconds. The JSON body states the same wait in its message:
`json
{
"success": false,
"error": "Rate limit exceeded. Retry after <n> seconds."
}
`
Some 429 responses also echo the wait as a retryAfter field in the body. Do not depend on that field; read the header.
Through MCP you only see the error string (the tool error is built from it), so wait the number of seconds that message states. Direct REST callers should read the header. Do not guess, and do not retry immediately:
`javascript
async function callWithBackoff(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const response = await fn(); // fn returns a fetch Response
if (response.status !== 429 || attempt === maxAttempts) return response;
const waitSeconds = Number(response.headers.get("Retry-After"));
if (!(waitSeconds > 0)) return response; // no server value: surface the 429, do not guess
await new Promise((r) => setTimeout(r, waitSeconds * 1000));
}
}
`
Only 429 and 5xx are worth retrying. Retrying a 400 or 403 burns quota to receive the identical error.Timeouts
Long generations can time out. Through MCP, a slow generation surfaces as a client-side error — DevSpeak API request to <url> timed out after <n>s. — with no server detail, and the server may still be working on the request.
A timeout usually means one of:
The fix is to reduce work: shorten the input, lower the tone, or disable connectors you do not need.
Pinning a Managed Model
By default DevSpeak chooses a managed model for each request on its own. You do not manage this.
To pin a specific model, pass managedModel:
` { "input": "Design a webhook retry policy.", "audience": "Senior Dev", "context": "Backend", "format": "Technical Spec", "tone": 70, "managedModel": "<model-id>" }json
`
In the translation workspace, pick a model from the Model control; it shows display labels, not ids. Over MCP or REST, managedModel takes a model id, and the server accepts only ids on its current allowlist — anything else returns 400. The set changes as models are added or retired, so when you do not need a specific model, omit the field.
Two consequences worth knowing:
1. The model pins its provider. Selecting a model routes to whichever provider owns it.
2. It bypasses BYOK. An explicit managedModel overrides a stored bring-your-own-key credential. Selecting a managed model never silently routes to your custom endpoint.
The allowlist is enforced server-side. An unlisted model returns 400; the request is not quietly run on a different model.
When to pin: reproducibility. If you are comparing outputs or debugging a prompt, an unpinned request may be served by a different model between runs, because DevSpeak chooses per request. Otherwise leave it unset and let DevSpeak choose.
Requesting XML Output
responseFormat controls the output structure:
`json
{
"input": "Design a webhook retry policy.",
"audience": "Senior Dev",
"context": "Backend",
"format": "Technical Spec",
"tone": 70,
"responseFormat": "xml"
}
`
| Value | Output | Use when |
| ---------- | ----------------------------------- | ------------------- |
| markdown | Prose (default) | A human reads it |
| xml | Validated machine-readable document | A program parses it |
XML output is validated against a server-side schema, which makes it safe to parse programmatically. Markdown headings are a convention; the XML structure is a contract. Choose xml when a downstream script consumes the result — feeding it to a human wastes the guarantee and reads worse.
Note the scope: webSearch, responseFormat, and managedModel are supported on the MCP server and the web app. The CLI and the VS Code extension deliberately exclude these flags.Checkpoint
At this point you should be able to:
and 403 rather than a guess does to a stored BYOK credential and xml deliberatelyTroubleshooting
optimize_text rejects format, tone, or customInstructions
The optimize_text tool schema is strict, so these parameters fail validation before any request is sent, and you learn they never applied. Called directly over REST, /api/v1/optimize instead drops unsupported fields and logs them rather than returning a 400.
Repeated 429 despite backing off
The per-key limit is shared across every client using that key. A CI job and your editor on one key contend with each other. Issue separate keys.
On the FREE plan, translations through DevSpeak's own models also count toward an hourly limit. That 429 says so in error, and its Retry-After points to the top of the next hour, so a short backoff loop only collects more of them. Wait for the reset, connect your own provider key (which the limit does not apply to), or upgrade.
Output quality changed between identical runs
A different model served the request — without a pin, DevSpeak chooses the model per request. Pin with managedModel when you need reproducibility.
400 on a model name that looks valid
The model is not on the server-side allowlist — it may be a picker label rather than an id, or a model that has since been retired. Omit managedModel to let DevSpeak choose, or pick the model from the Model control in the translation workspace.Summary
You've learned how to: