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 and retryAfter body field 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 API. Check the base URL before anything else — DevSpeak's SPA catch-all returns 200 with HTML for unmatched paths, so a typo'd path looks healthy to a status-code check.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 Settings → API Keys |
| 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
Two limiters apply:
| Scope | Limit | Applies to |
| -------------- | --------------------- | --------------------- |
| Per API key | 100 requests / minute | All /api/v1 traffic |
| Strict limiter | 25 requests / 5 min | Sensitive endpoints |
A 429 carries the wait time in two places — a Retry-After header and a retryAfter field in the JSON body, both in seconds:
`json
{
"success": false,
"error": "Too many requests",
"retryAfter": 47
}
`
Use that value. Do not guess, and do not retry immediately:
`javascript
async function callWithBackoff(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
const retryAfter = error.retryAfter;
if (retryAfter === undefined || attempt === maxAttempts) throw error;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
}
}
}
`
Only 429 and 5xx are worth retrying. Retrying a 400 or 403 burns quota to receive the identical error.Timeouts
The MCP client times out at 60 seconds, and the server is set to give up slightly sooner. That ordering is deliberate: you almost always get a useful error message back rather than the client aborting blind.
Reaching the wall 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 routes to a managed provider chain — Anthropic Claude Haiku as primary, with an ordered fallback through Mistral, Cerebras, DeepSeek, and NVIDIA. If the primary fails, the next provider with a configured key takes over. 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
`
Use any model id shown in the Model control in the translation workspace — that list is the set of accepted values, and it is what the server validates against.
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 rather than falling back, so a client cannot redirect managed spend to an arbitrary model.
When to pin: reproducibility. If you are comparing outputs or debugging a prompt, an unpinned request may land on a different provider between runs. Otherwise leave it unset and let the chain do its job.
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 and macOS surfaces. 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
400 naming a parameter I did not send
optimize_text uses a strict schema. Passing format, tone, or customInstructions is rejected rather than ignored, so you learn the parameter never applied.
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.
Output quality changed between identical runs
A different model served the request because the first was unavailable. Pin with managedModel when you need reproducibility.
400 on a model name that looks valid
The model is not on the server-side allowlist. Allowlisted models are the ones the model picker offers on the web surface.
Summary
You've learned how to: