Skip to content

Commit 52d0463

Browse files
committed
fix(api): improve error handling for virtual tables
1 parent c9443d7 commit 52d0463

File tree

4 files changed

+376
-79
lines changed

4 files changed

+376
-79
lines changed

API.md

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -418,11 +418,11 @@ SELECT mcp_connect('http://localhost:8931/sse', NULL, 1);
418418

419419
## Error Handling
420420

421-
The sqlite-mcp extension has two types of error handling:
421+
The sqlite-mcp extension has consistent error handling across all interfaces:
422422

423423
1. **JSON Functions** (ending with `_json`): Return JSON with error information
424424
2. **Non-JSON Functions**: Return error strings directly (extracted from JSON)
425-
3. **Virtual Tables**: Return no rows on error, use scalar functions to check status
425+
3. **Virtual Tables**: Return SQL errors with extracted error messages (like non-JSON functions)
426426

427427
### mcp_connect()
428428

@@ -462,21 +462,26 @@ FROM (SELECT mcp_call_tool_json('test', '{}') as result);
462462

463463
### Virtual Tables: mcp_list_tools, mcp_call_tool, mcp_list_tools_respond, mcp_call_tool_respond
464464

465-
Virtual tables return no rows on error. To check for errors, use the corresponding JSON functions:
465+
Virtual tables return SQL errors with extracted error messages (similar to `mcp_connect()`):
466466

467467
```sql
468-
-- Check if server is connected before querying virtual table
469-
SELECT
470-
CASE
471-
WHEN json_extract(mcp_list_tools_json(), '$.error') IS NOT NULL
472-
THEN 'Error: Cannot query virtual table - ' || json_extract(mcp_list_tools_json(), '$.error')
473-
ELSE 'OK to query virtual table'
474-
END;
468+
-- Query virtual table - errors are automatically returned as SQL errors
469+
SELECT name, description FROM mcp_list_tools;
470+
-- If not connected, returns SQL error: "Not connected. Call mcp_connect() first"
471+
472+
-- Query call tool virtual table
473+
SELECT text FROM mcp_call_tool('nonexistent_tool', '{}');
474+
-- Returns SQL error: "Tool not found: nonexistent_tool"
475475

476-
-- Query virtual table only if no error
477-
SELECT name, description FROM mcp_list_tools WHERE name LIKE 'browser_%';
476+
-- Errors can be caught in application code using sqlite3_errmsg()
477+
-- Or handled in SQL with error handlers
478478
```
479479

480+
**Error Behavior:**
481+
- When an MCP error occurs (not connected, tool not found, invalid JSON, etc.), the virtual table returns `SQLITE_ERROR`
482+
- The error message is extracted from the JSON error response and set as the SQL error message
483+
- This provides immediate, clear feedback without needing to check JSON functions separately
484+
480485
### Common Error Messages
481486

482487
All functions may return these error types:
@@ -493,37 +498,34 @@ All functions may return these error types:
493498
1. **Always check mcp_connect() result**:
494499
```sql
495500
-- Good practice: Check connection first
496-
SELECT
497-
CASE
498-
WHEN mcp_connect('http://localhost:8931/mcp') IS NULL
501+
SELECT
502+
CASE
503+
WHEN mcp_connect('http://localhost:8931/mcp') IS NULL
499504
THEN 'Connected successfully'
500505
ELSE mcp_connect('http://localhost:8931/mcp')
501506
END;
502507
```
503508

504-
2. **Use JSON functions for error checking before virtual tables**:
509+
2. **Virtual tables automatically return errors**:
505510
```sql
506-
-- Check for errors before using virtual table
507-
WITH error_check AS (
508-
SELECT json_extract(mcp_list_tools_json(), '$.error') as error
509-
)
510-
SELECT
511-
CASE
512-
WHEN error_check.error IS NOT NULL
513-
THEN 'Error: ' || error_check.error
514-
ELSE 'Tools: ' || (SELECT GROUP_CONCAT(name) FROM mcp_list_tools)
515-
END
516-
FROM error_check;
511+
-- Virtual tables automatically return SQL errors - no need to pre-check
512+
SELECT name, description FROM mcp_list_tools;
513+
-- If not connected, query fails with error: "Not connected. Call mcp_connect() first"
514+
515+
-- Errors can be caught in application code:
516+
-- C: sqlite3_errmsg(db)
517+
-- Python: except sqlite3.Error as e
518+
-- Node.js: try/catch with better-sqlite3
517519
```
518520

519-
3. **Handle tool call errors**:
521+
3. **Handle JSON function errors manually**:
520522
```sql
521-
-- Safe tool calling with error handling
522-
SELECT
523-
CASE
523+
-- JSON functions still return JSON with error field
524+
SELECT
525+
CASE
524526
WHEN json_extract(result, '$.error') IS NOT NULL
525527
THEN 'Tool Error: ' || json_extract(result, '$.error')
526-
ELSE json_extract(result, '$.content[0].text')
528+
ELSE json_extract(result, '$.content[0].text')
527529
END as output
528530
FROM (
529531
SELECT mcp_call_tool_json('browser_navigate', '{"url": "https://example.com"}') as result

src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,39 @@ pub extern "C" fn mcp_free_string(s: *mut c_char) {
5151
}
5252
}
5353

54+
/// Extract error message from JSON error response
55+
/// Returns the error message string if found, or NULL if no error
56+
#[no_mangle]
57+
pub extern "C" fn mcp_extract_error_message(json_str: *const c_char) -> *mut c_char {
58+
if json_str.is_null() {
59+
return std::ptr::null_mut();
60+
}
61+
62+
let json_string = unsafe {
63+
match CStr::from_ptr(json_str).to_str() {
64+
Ok(s) => s,
65+
Err(_) => return std::ptr::null_mut(),
66+
}
67+
};
68+
69+
// Check if this is an error JSON
70+
match serde_json::from_str::<serde_json::Value>(json_string) {
71+
Ok(json) => {
72+
if let Some(error) = json.get("error").and_then(|e| e.as_str()) {
73+
// Found an error message, return it
74+
match CString::new(error) {
75+
Ok(c_str) => c_str.into_raw(),
76+
Err(_) => std::ptr::null_mut(),
77+
}
78+
} else {
79+
// No error field found
80+
std::ptr::null_mut()
81+
}
82+
}
83+
Err(_) => std::ptr::null_mut(),
84+
}
85+
}
86+
5487
/// Parse tools JSON and extract structured data for virtual table
5588
/// Returns number of tools found, or 0 on error
5689
#[no_mangle]

src/sqlite-mcp.c

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ extern void mcp_stream_free_result(StreamResult* result);
137137
extern char* mcp_list_tools_json(void*);
138138
extern char* mcp_call_tool_json(void*, const char*, const char*);
139139
extern void mcp_free_string(char*);
140+
extern char* mcp_extract_error_message(const char*);
140141

141142
// JSON parsing functions (using serde_json in Rust)
142143
extern size_t mcp_parse_tools_json(const char* json_str);
@@ -275,10 +276,21 @@ static int mcp_stream_next_impl(sqlite3_vtab_cursor *cur) {
275276
break;
276277

277278
case STREAM_TYPE_ERROR:
278-
// Stream error - stop iteration
279+
// Stream error - stop iteration and set error message
279280
DF("mcp_stream_next_impl: STREAM_TYPE_ERROR - %s", result->data ? result->data : "unknown error");
281+
if (result->data) {
282+
char *error_msg = mcp_extract_error_message(result->data);
283+
if (error_msg) {
284+
((mcp_stream_vtab*)pCur->base.pVtab)->base.zErrMsg = sqlite3_mprintf("%s", error_msg);
285+
mcp_free_string(error_msg);
286+
} else {
287+
// If not JSON error format, use the data directly
288+
((mcp_stream_vtab*)pCur->base.pVtab)->base.zErrMsg = sqlite3_mprintf("%s", result->data);
289+
}
290+
}
280291
pCur->eof = 1;
281-
break;
292+
mcp_stream_free_result(result);
293+
return SQLITE_ERROR;
282294

283295
case STREAM_TYPE_DONE:
284296
// Stream complete
@@ -543,6 +555,24 @@ static int mcp_call_tool_stream_next(sqlite3_vtab_cursor *cur){
543555
return SQLITE_OK;
544556
}
545557

558+
if (result->result_type == STREAM_TYPE_ERROR) {
559+
// Stream error - stop iteration and set error message
560+
DF("mcp_call_tool_stream_next: STREAM_TYPE_ERROR - %s", result->data ? result->data : "unknown error");
561+
if (result->data) {
562+
char *error_msg = mcp_extract_error_message(result->data);
563+
if (error_msg) {
564+
((mcp_call_tool_stream_vtab*)pCur->base.pVtab)->base.zErrMsg = sqlite3_mprintf("%s", error_msg);
565+
mcp_free_string(error_msg);
566+
} else {
567+
// If not JSON error format, use the data directly
568+
((mcp_call_tool_stream_vtab*)pCur->base.pVtab)->base.zErrMsg = sqlite3_mprintf("%s", result->data);
569+
}
570+
}
571+
pCur->eof = 1;
572+
mcp_stream_free_result(result);
573+
return SQLITE_ERROR;
574+
}
575+
546576
if (result->result_type == STREAM_TYPE_DONE) {
547577
pCur->eof = 1;
548578
mcp_stream_free_result(result);
@@ -763,12 +793,15 @@ static int mcp_tools_filter(
763793
}
764794

765795
DF("mcp_tools_filter: Got JSON result (%d bytes)", (int)strlen(result));
766-
// Check for errors - only check if it starts with error
767-
if (strncmp(result, "{\"error\"", 8) == 0) {
796+
// Check for errors and extract error message
797+
char *error_msg = mcp_extract_error_message(result);
798+
if (error_msg) {
768799
D("mcp_tools_filter: JSON contains error");
800+
pVtab->base.zErrMsg = sqlite3_mprintf("%s", error_msg);
801+
mcp_free_string(error_msg);
769802
mcp_free_string(result);
770803
pCur->eof = 1;
771-
return SQLITE_OK;
804+
return SQLITE_ERROR;
772805
}
773806

774807
// Create temporary table
@@ -1073,6 +1106,17 @@ static int mcp_results_filter(
10731106
// Parse JSON in the Rust layer
10741107
DF(" Tool result (%d bytes): %.200s%s", (int)strlen(pCur->json_result),
10751108
pCur->json_result, strlen(pCur->json_result) > 200 ? "..." : "");
1109+
1110+
// Check for errors first
1111+
char *error_msg = mcp_extract_error_message(pCur->json_result);
1112+
if (error_msg) {
1113+
D(" JSON contains error");
1114+
pVtab->base.zErrMsg = sqlite3_mprintf("%s", error_msg);
1115+
mcp_free_string(error_msg);
1116+
pCur->eof = 1;
1117+
return SQLITE_ERROR;
1118+
}
1119+
10761120
pCur->content_count = mcp_parse_call_result_json(pCur->json_result);
10771121
DF(" Parsed content count: %d", (int)pCur->content_count);
10781122

0 commit comments

Comments
 (0)