Error classification
LocalData MCP classifies every database error into a structured response that tells LLM agents what went wrong, whether it makes sense to retry, and what to do next. Instead of parsing raw driver messages, agents receive a uniform JSON object they can act on programmatically.
Error categories
Every classified error carries one of these categories:
Category |
Value |
Description |
Example |
|---|---|---|---|
|
|
Authentication or authorization failure |
Wrong password, missing privileges |
|
|
Missing or invalid database objects |
Non-existent table or column |
|
|
Malformed SQL |
Typo in a keyword, unbalanced parentheses |
|
|
Temporary failure that may resolve itself |
Lock timeout, deadlock, busy database |
|
|
System resource exhaustion |
Out of memory, disk full |
|
|
Data integrity violation |
Duplicate key, foreign key mismatch |
|
|
Unable to reach the database |
Network unreachable, server down |
|
|
General execution failure (fallback) |
Division by zero, type mismatch |
Retryability
The is_retryable flag signals whether repeating the same operation is likely
to succeed:
Retryable |
Categories |
Recommended action |
|---|---|---|
Yes |
|
Wait briefly and retry (with backoff) |
No |
All others |
Fix the query, schema, credentials, or data before retrying |
Agents should treat is_retryable: false as a hard stop for the current
request. Retrying a syntax error or constraint violation will always produce
the same failure.
Structured error response format
{
"error": true,
"error_type": "schema_error",
"is_retryable": false,
"message": "no such table: orders",
"suggestion": "Verify table/column names in the SQLite database.",
"database_error_code": null,
"database": "sales.db"
}
Field |
Type |
Description |
|---|---|---|
|
|
Always |
|
|
One of the category values listed above |
|
|
Whether the agent should retry |
|
|
Human-readable description of the error |
|
|
Actionable next step |
|
|
Backend-specific code (SQLSTATE, errno) when available |
|
|
Name of the database where the error occurred |
Database-specific mappings
Each supported backend has a dedicated mapper that uses backend-native signals (message patterns, SQLSTATE codes, error numbers, or exception class names) to classify errors. When no backend-specific mapper matches, a generic keyword-based fallback is used.
SQLite
SQLite errors are classified by scanning the exception message for known substrings:
Pattern |
Category |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
PostgreSQL
PostgreSQL errors are classified by their SQLSTATE code (pgcode):
SQLSTATE prefix |
Category |
Notes |
|---|---|---|
|
|
Exact match for syntax errors |
|
|
Undefined table, column, etc. |
|
|
Invalid authorization |
|
|
Connection exception (retryable) |
|
|
Integrity constraint violation |
|
|
Transaction rollback (retryable) |
|
|
Insufficient resources |
MySQL / MariaDB
MySQL errors are classified by their numeric error code (errno):
Error code |
Category |
Notes |
|---|---|---|
1044, 1045 |
|
Access denied |
1146, 1054 |
|
Unknown table or column |
1064 |
|
SQL syntax error |
1205 |
|
Lock wait timeout (retryable) |
1114 |
|
Table is full |
1062, 1452 |
|
Duplicate entry, FK violation |
2002, 2003, 2006 |
|
Connection failures (retryable) |
DuckDB
DuckDB errors are classified by the Python exception class name:
Exception class |
Category |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Oracle
Oracle errors are classified by their ORA-XXXXX error codes:
Error code |
Category |
Notes |
|---|---|---|
|
|
Invalid username/password |
|
|
Insufficient privileges |
|
|
User lacks CREATE SESSION privilege |
|
|
Table or view does not exist |
|
|
Invalid identifier |
|
|
Invalid SQL statement |
|
|
SQL command not properly ended |
|
|
Unable to extend table |
|
|
Unable to allocate shared memory |
|
|
Deadlock detected (retryable) |
|
|
Serialization failure (retryable) |
|
|
End-of-file on communication channel (retryable) |
|
|
Not connected to Oracle (retryable) |
|
|
No listener (retryable) |
|
|
TNS could not resolve connect identifier (retryable) |
MS SQL Server
MSSQL errors are classified by their numeric error code (Msg) and severity level:
Error code |
Category |
Notes |
|---|---|---|
18456 |
|
Login failed |
18452 |
|
Login from untrusted domain |
208 |
|
Invalid object name |
207 |
|
Invalid column name |
102, 156 |
|
Incorrect syntax |
1205 |
|
Transaction deadlocked (retryable) |
1222 |
|
Lock request timeout (retryable) |
547, 2627, 2601 |
|
Constraint / unique key violation |
1105 |
|
Could not allocate space |
9002 |
|
Transaction log full |
When the error code is not in the table, severity is used as a fallback:
Severity >= 20:
CONNECTION_ERROR(fatal error)Severity >= 17:
RESOURCE_ERROR
Integration guide
To add a mapper for a new database backend:
Create a class that inherits from
DatabaseErrorMapperand implementsmap_error(self, exception) -> StructuredErrorResponse.Register the mapper with
ErrorMapperRegistry:
from localdata_mcp.error_classification import (
DatabaseErrorMapper,
ErrorMapperRegistry,
StructuredErrorResponse,
)
from localdata_mcp.error_handler import ErrorCategory
class MyDBErrorMapper(DatabaseErrorMapper):
def map_error(self, exception: Exception) -> StructuredErrorResponse:
msg = str(exception).lower()
if "duplicate" in msg:
return StructuredErrorResponse(
error_type=ErrorCategory.CONSTRAINT_ERROR,
message=str(exception),
suggestion="Check for duplicate values.",
)
# Fall back to the generic mapper for unrecognised errors.
from localdata_mcp.error_classification import GenericDatabaseErrorMapper
return GenericDatabaseErrorMapper().map_error(exception)
ErrorMapperRegistry.register("mydb", MyDBErrorMapper())
All calls to
classify_error(exc, db_type="mydb")will now use the new mapper automatically.
Helper functions
Three convenience functions in localdata_mcp.error_classification cover the
most common agent needs:
classify_error(exception, db_type="generic")
Returns the full StructuredErrorResponse for an exception. Use this when the
agent needs all fields (category, retryability, suggestion, error code).
from localdata_mcp.error_classification import classify_error
resp = classify_error(exc, db_type="sqlite")
print(resp.error_type) # ErrorCategory.SYNTAX_ERROR
print(resp.suggestion) # "Review the SQL statement for syntax errors."
is_error_retryable(exception, db_type="generic")
Returns True if the error is transient and worth retrying. A shorthand for
classify_error(...).is_retryable.
from localdata_mcp.error_classification import is_error_retryable
if is_error_retryable(exc, db_type="postgresql"):
# back off and retry
...
get_error_suggestion(exception, db_type="generic")
Returns the actionable suggestion string. A shorthand for
classify_error(...).suggestion.
from localdata_mcp.error_classification import get_error_suggestion
hint = get_error_suggestion(exc, db_type="mysql")
All three functions use the same classification pipeline internally, so their results are always consistent for the same exception and database type.