Skip to content

Error Handling

Seed implements consistent error handling across all endpoints using standardized error formats based on JSON-RPC 2.0 for MCP endpoints and OAuth 2.0 RFC standards for OAuth endpoints.

Error Response Formats

JSON-RPC 2.0 Format

Used for MCP endpoints and general server errors.

Structure:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": <number>,
    "message": "<string>",
    "data": {
      "reason": "<string>",
      "details": "<string>",
      ...
    }
  },
  "id": <number|string|null>
}

Fields:

  • jsonrpc (string): Always "2.0"
  • error (object): Error details
    • code (number): JSON-RPC error code
    • message (string): Human-readable error message
    • data (object, optional): Additional error context
      • reason (string): Machine-readable error reason
      • details (string): Additional human-readable details
  • id (number|string|null): Request ID (null for errors without corresponding request)

Example:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Unauthorized",
    "data": {
      "reason": "invalid_token",
      "details": "JWT signature verification failed"
    }
  },
  "id": null
}

OAuth 2.0 Error Format

Used for OAuth endpoints per RFC 6749.

Structure:

json
{
  "error": "<string>",
  "error_description": "<string>"
}

Fields:

  • error (string): OAuth error code
  • error_description (string, optional): Human-readable error description

Example:

json
{
  "error": "invalid_client",
  "error_description": "Unknown or expired client_id"
}

DCR Error Format

Used for Dynamic Client Registration per RFC 7591.

Structure:

json
{
  "error": "<string>",
  "error_description": "<string>"
}

DCR-Specific Error Codes:

  • invalid_redirect_uri: Redirect URI validation failed
  • invalid_client_metadata: Client metadata validation failed
  • invalid_software_statement: Software statement invalid (not used)
  • server_error: Server-side processing error

Example:

json
{
  "error": "invalid_redirect_uri",
  "error_description": "redirect_uri must use https"
}

HTTP Status Codes

Seed uses standard HTTP status codes with consistent meanings:

StatusNameUsage
200OKSuccessful request
201CreatedResource created (DCR)
302FoundOAuth authorization redirect
400Bad RequestInvalid request parameters
401UnauthorizedAuthentication required or failed
403ForbiddenValid auth but insufficient permissions
404Not FoundResource not found (session, endpoint)
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-side processing error
502Bad GatewayUpstream service error

Authentication Errors

Missing Token

HTTP Status: 401 Unauthorized

Response:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Unauthorized",
    "data": {
      "reason": "missing_token",
      "details": "No Authorization header provided"
    }
  },
  "id": null
}

Headers:

http
WWW-Authenticate: Bearer resource_metadata="https://seed.example.com/.well-known/oauth-protected-resource"

Trigger: Request to protected endpoint without Authorization header

Invalid Format

HTTP Status: 401 Unauthorized

Response:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Unauthorized",
    "data": {
      "reason": "invalid_format",
      "details": "Authorization header must use Bearer scheme"
    }
  },
  "id": null
}

Trigger: Authorization header present but not in format Bearer <token>

Invalid Token

HTTP Status: 401 Unauthorized

Response:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Unauthorized",
    "data": {
      "reason": "invalid_token",
      "details": "JWT signature verification failed"
    }
  },
  "id": null
}

Trigger: JWT signature verification fails (invalid signature, malformed token)

Expired Token

HTTP Status: 401 Unauthorized

Response:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Unauthorized",
    "data": {
      "reason": "expired_token",
      "details": "Token expired at 2026-01-05T12:00:00Z"
    }
  },
  "id": null
}

Trigger: JWT exp claim is in the past

Invalid Issuer

HTTP Status: 401 Unauthorized

Response:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Unauthorized",
    "data": {
      "reason": "invalid_issuer",
      "details": "Token issuer does not match expected issuer"
    }
  },
  "id": null
}

Trigger: JWT iss claim doesn't match OIDC_ISSUER configuration

Invalid Audience

HTTP Status: 401 Unauthorized

Response:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Unauthorized",
    "data": {
      "reason": "invalid_audience",
      "details": "Token audience does not match expected audience"
    }
  },
  "id": null
}

Trigger: JWT aud claim doesn't match OIDC_AUDIENCE configuration

Missing Claim

HTTP Status: 401 Unauthorized

Response:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Unauthorized",
    "data": {
      "reason": "missing_claim",
      "details": "Required claim 'sub' not found in token"
    }
  },
  "id": null
}

Trigger: JWT missing required sub claim

MCP Session Errors

Invalid or Expired Session

HTTP Status: 404 Not Found

Response:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32000,
    "message": "Invalid or expired session",
    "data": {
      "reason": "session_not_found"
    }
  },
  "id": null
}

Trigger: mcp-session-id header references non-existent or expired session

Cause:

  • Session never existed
  • Session expired (24-hour TTL exceeded)
  • Session was explicitly terminated via DELETE /mcp

Missing Session ID

HTTP Status: 400 Bad Request

Response:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32000,
    "message": "Bad Request",
    "data": {
      "reason": "missing_session_id",
      "details": "mcp-session-id header required for non-initialize requests"
    }
  },
  "id": null
}

Trigger: Request to /mcp without mcp-session-id header (except initialize method)

Rate Limiting Errors

Rate Limit Exceeded

HTTP Status: 429 Too Many Requests

Response:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32000,
    "message": "Too Many Requests",
    "data": {
      "reason": "rate_limit_exceeded",
      "details": "Rate limit exceeded. Please try again later.",
      "retryAfter": 45
    }
  },
  "id": null
}

Headers:

http
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1704643200
Retry-After: 45

Trigger: Per-IP rate limit exceeded

Rate Limits:

  • MCP endpoints: 100 requests/minute per IP
  • DCR endpoint: 10 registrations/hour per IP

Global Rate Limit Exceeded

HTTP Status: 429 Too Many Requests

Response:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32000,
    "message": "Too Many Requests",
    "data": {
      "reason": "global_rate_limit_exceeded",
      "details": "Global rate limit exceeded. Please try again later.",
      "retryAfter": 45
    }
  },
  "id": null
}

Trigger: Global rate limit exceeded (all IPs combined)

Global Limits:

  • MCP endpoints: 10,000 requests/minute globally
  • DCR endpoint: 1,000 registrations/hour globally

OAuth Errors

Authorization Errors

Invalid Client (authorization endpoint):

HTTP Status: 400 Bad Request

Response:

json
{
  "error": "invalid_client",
  "error_description": "Unknown or expired client_id"
}

Trigger: DCR client not found in Redis or expired

Invalid Redirect URI (authorization endpoint):

HTTP Status: 400 Bad Request

Response:

json
{
  "error": "invalid_redirect_uri",
  "error_description": "redirect_uri does not match any registered URI"
}

Trigger: redirect_uri not in client's registered URIs

Server Error (authorization endpoint):

HTTP Status: 500 Internal Server Error

Response:

json
{
  "error": "server_error",
  "error_description": "Authorization endpoint not configured"
}

Trigger: OAUTH_AUTHORIZATION_URL not configured

Token Errors

Invalid Request (token endpoint):

HTTP Status: 400 Bad Request

Response:

json
{
  "error": "invalid_request",
  "error_description": "Missing required parameter: code"
}

Trigger: Required parameter missing from token request

Possible Variations:

  • "Missing required parameter: code"
  • "Missing required parameter: redirect_uri"
  • "Missing required parameter: client_id"
  • "Missing required parameter: code_verifier (PKCE required)"
  • "Missing required parameter: refresh_token"
  • "Missing required parameter: grant_type"

Unsupported Grant Type (token endpoint):

HTTP Status: 400 Bad Request

Response:

json
{
  "error": "unsupported_grant_type",
  "error_description": "Unsupported grant type: implicit"
}

Trigger: grant_type not in ["authorization_code", "refresh_token"]

Invalid Grant (token endpoint):

HTTP Status: 400 Bad Request

Response:

json
{
  "error": "invalid_grant",
  "error_description": "redirect_uri does not match registered URI"
}

Trigger: redirect_uri in token request doesn't match authorization request

Invalid Client (token endpoint):

HTTP Status: 401 Unauthorized

Response:

json
{
  "error": "invalid_client",
  "error_description": "Unknown or expired client_id"
}

Trigger: DCR client not found in Redis or expired (during token exchange)

Server Error (token endpoint):

HTTP Status: 500 Internal Server Error

Response:

json
{
  "error": "server_error",
  "error_description": "Token endpoint not configured"
}

Trigger: OAUTH_TOKEN_URL not configured

Bad Gateway (token endpoint):

HTTP Status: 502 Bad Gateway

Response:

json
{
  "error": "server_error",
  "error_description": "Failed to contact authorization server: <error>"
}

Trigger: Network error contacting upstream IdP

DCR Errors

Invalid Redirect URI (DCR):

HTTP Status: 400 Bad Request

Response:

json
{
  "error": "invalid_redirect_uri",
  "error_description": "redirect_uri must use https"
}

Trigger: Redirect URI validation failure

Possible Variations:

  • "redirect_uri must use https (got http:)"
  • "redirect_uri must not contain a fragment"
  • "redirect_uri is not a valid URL"

Invalid Client Metadata (DCR):

HTTP Status: 400 Bad Request

Response:

json
{
  "error": "invalid_client_metadata",
  "error_description": "redirect_uris exceeds maximum of 10"
}

Trigger: Client metadata validation failure

Possible Variations:

  • "Missing required parameter: redirect_uris"
  • "redirect_uris must be an array"
  • "redirect_uris must contain at least one URI"
  • "redirect_uris exceeds maximum of 10"
  • "redirect_uris must contain only strings"
  • "grant_types must be an array"
  • "Invalid grant_type: implicit. Supported: authorization_code, refresh_token"
  • "response_types must be an array"
  • "Invalid response_type: token. Supported: code"
  • "Invalid token_endpoint_auth_method: client_secret_jwt. Supported: none, client_secret_post, client_secret_basic"
  • "client_name must be a string"
  • "software_id must be a string"
  • "software_version must be a string"

Server Error (DCR):

HTTP Status: 500 Internal Server Error

Response:

json
{
  "error": "server_error",
  "error_description": "Failed to register client"
}

Trigger: Redis error or other server-side processing failure during registration

Security Errors

Origin Validation Failed

HTTP Status: 403 Forbidden

Response:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32000,
    "message": "Forbidden",
    "data": {
      "reason": "invalid_origin",
      "details": "Origin not allowed by server policy"
    }
  },
  "id": null
}

Trigger: Origin header present but not in allowed origins list

Cause: DNS rebinding attack prevention

See Also: Security

Error Code Reference

JSON-RPC Error Codes

CodeCategoryUsage
-32001AuthenticationAuth failures (401)
-32000ServerGeneral server errors (400, 403, 404, 429, 500)

Note: JSON-RPC standard reserves codes -32768 to -32000 for predefined errors. Seed uses custom codes in this range.

OAuth Error Codes

Per RFC 6749 Section 5.2:

Error CodeHTTP StatusDescription
invalid_request400Missing or invalid request parameter
invalid_client401Client authentication failed
invalid_grant400Authorization grant invalid/expired
unauthorized_client400Client not authorized for grant type
unsupported_grant_type400Grant type not supported
invalid_scope400Requested scope invalid/unknown
server_error500Server encountered error

DCR Error Codes

Per RFC 7591:

Error CodeHTTP StatusDescription
invalid_redirect_uri400Redirect URI validation failed
invalid_client_metadata400Client metadata validation failed
invalid_software_statement400Software statement invalid
server_error500Server encountered error

Error Context

Additional context provided in error.data for debugging:

Authentication Context

Fields:

  • reason (string): Machine-readable error code
  • details (string): Human-readable explanation

Example:

json
{
  "reason": "expired_token",
  "details": "Token expired at 2026-01-05T12:00:00Z"
}

Rate Limiting Context

Fields:

  • reason (string): rate_limit_exceeded or global_rate_limit_exceeded
  • details (string): Human-readable message
  • retryAfter (number): Seconds until retry allowed

Example:

json
{
  "reason": "rate_limit_exceeded",
  "details": "Rate limit exceeded. Please try again later.",
  "retryAfter": 45
}

Session Context

Fields:

  • reason (string): session_not_found or missing_session_id

Example:

json
{
  "reason": "session_not_found"
}

Origin Validation Context

Fields:

  • reason (string): invalid_origin
  • details (string): Policy violation message

Example:

json
{
  "reason": "invalid_origin",
  "details": "Origin not allowed by server policy"
}

Client Error Handling Recommendations

Retry Strategy

Authentication Errors (401):

  • ❌ Do NOT retry with same token
  • ✅ Refresh token if available
  • ✅ Re-authenticate if no refresh token
  • ❌ Do NOT retry more than once

Rate Limit Errors (429):

  • ✅ Respect Retry-After header
  • ✅ Implement exponential backoff if no header
  • ✅ Use X-RateLimit-Reset for next window
  • ❌ Do NOT retry immediately

Session Errors (404):

  • ✅ Re-initialize session with initialize request
  • ❌ Do NOT retry with same session ID

Server Errors (500, 502):

  • ✅ Retry with exponential backoff
  • ✅ Maximum 3 retries
  • ✅ Log for debugging

Client Errors (400, 403):

  • ❌ Do NOT retry
  • ✅ Fix request parameters
  • ✅ Log for debugging

Error Parsing

JSON-RPC Errors:

typescript
interface JsonRpcError {
  jsonrpc: "2.0";
  error: {
    code: number;
    message: string;
    data?: {
      reason?: string;
      details?: string;
      retryAfter?: number;
    };
  };
  id: number | string | null;
}

function isJsonRpcError(response: unknown): response is JsonRpcError {
  return (
    typeof response === "object" &&
    response !== null &&
    "error" in response &&
    "jsonrpc" in response
  );
}

OAuth Errors:

typescript
interface OAuthError {
  error: string;
  error_description?: string;
}

function isOAuthError(response: unknown): response is OAuthError {
  return (
    typeof response === "object" &&
    response !== null &&
    "error" in response &&
    typeof response.error === "string"
  );
}

Logging Best Practices

Client-Side Logging:

typescript
// Log full error for debugging
console.error("API error", {
  status: response.status,
  error: errorBody,
  requestId: response.headers.get("x-request-id"),
  timestamp: new Date().toISOString(),
});

// Show user-friendly message
if (errorBody.error.code === -32001) {
  showError("Authentication failed. Please log in again.");
} else if (response.status === 429) {
  const retryAfter = errorBody.error.data?.retryAfter ?? 60;
  showError(`Rate limit exceeded. Try again in ${retryAfter} seconds.`);
}

Implementation Details

Error Response Helper (MCP/Auth)

File: src/middleware/auth.ts

typescript
function sendAuthError(
  res: Response,
  reason: string,
  details: string
): void {
  res.status(401);
  res.set("WWW-Authenticate", `Bearer resource_metadata="${config.baseUrl}/.well-known/oauth-protected-resource"`);
  res.json({
    jsonrpc: "2.0",
    error: {
      code: -32001,
      message: "Unauthorized",
      data: {
        reason,
        details,
      },
    },
    id: null,
  });
}

Error Response Helper (OAuth)

File: src/routes/oauth-token.ts

typescript
function sendOAuthError(
  res: Response,
  error: string,
  description: string,
  status = 400
): void {
  res.status(status).json({
    error,
    error_description: description,
  });
}

Error Response Helper (DCR)

File: src/routes/oauth-register.ts

typescript
function sendRegistrationError(
  res: Response,
  error: RegistrationErrorCode,
  description: string,
  status = 400
): void {
  res.status(status).json({
    error,
    error_description: description,
  });
}

Testing Error Responses

Example Test Cases

Authentication Error:

typescript
it("should return 401 for missing token", async () => {
  const response = await request(app)
    .post("/mcp")
    .send({ jsonrpc: "2.0", method: "initialize", id: 1 });

  expect(response.status).toBe(401);
  expect(response.body.error.code).toBe(-32001);
  expect(response.body.error.data.reason).toBe("missing_token");
});

Rate Limit Error:

typescript
it("should return 429 after rate limit exceeded", async () => {
  // Make 101 requests
  for (let i = 0; i < 101; i++) {
    await request(app).post("/mcp").set("Authorization", `Bearer ${token}`);
  }

  const response = await request(app)
    .post("/mcp")
    .set("Authorization", `Bearer ${token}`);

  expect(response.status).toBe(429);
  expect(response.body.error.data.reason).toBe("rate_limit_exceeded");
  expect(response.headers["retry-after"]).toBeDefined();
});

OAuth Error:

typescript
it("should return invalid_client for unknown client", async () => {
  const response = await request(app)
    .post("/oauth/token")
    .send({
      grant_type: "authorization_code",
      code: "abc123",
      client_id: "seed-nonexistent",
      redirect_uri: "https://app.example.com/callback",
      code_verifier: "verifier",
    });

  expect(response.status).toBe(401);
  expect(response.body.error).toBe("invalid_client");
});

Released under the MIT License.