Skip to content

Improve MCP TLS error reporting - #164

Open
piersonrazzi wants to merge 3 commits into
punitarani:mainfrom
piersonrazzi:codex/structured-mcp-tls-errors
Open

Improve MCP TLS error reporting#164
piersonrazzi wants to merge 3 commits into
punitarani:mainfrom
piersonrazzi:codex/structured-mcp-tls-errors

Conversation

@piersonrazzi

@piersonrazzi piersonrazzi commented May 16, 2026

Copy link
Copy Markdown

Summary

  • classify curl certificate verification failures as SearchCertificateError
  • allow users to configure a CA bundle via FLI_CA_BUNDLE, CURL_CA_BUNDLE, or REQUESTS_CA_BUNDLE
  • return stable MCP error_type values while preserving the legacy error and empty result fields
  • document CA bundle configuration for MCP users

Testing

  • uvx --system-certs ruff check .
  • uvx --system-certs ruff format --check .
  • UV_PROJECT_ENVIRONMENT=.venv-test uv --system-certs run pytest tests/search/test_client_errors.py tests/cli/test_errors.py tests/mcp/test_mcp_server_fixes.py -q

Greptile Summary

This PR improves TLS error handling for the MCP server by introducing SearchCertificateError, a new typed exception for certificate verification failures, and allowing users to configure a custom CA bundle via FLI_CA_BUNDLE, CURL_CA_BUNDLE, or REQUESTS_CA_BUNDLE. It also refactors MCP error responses to include a stable error_type field alongside the existing error and result fields.

  • SearchCertificateError is added as a subclass of SearchConnectionError and is classified before the parent class in every isinstance chain, so the more-specific type is always matched first.
  • _wrap_request_error uses getattr(curl_exc, \"CertificateVerifyError\", ()) as a forward-compatible guard, and _session() wires in the CA bundle when the env var is set.
  • _error_type / _error_response helpers centralize MCP error formatting, and the CLI, MCP server, and public __init__ exports are all updated consistently.

Confidence Score: 4/5

The change is safe to merge; all error classification paths are logically consistent and backed by new tests.

The exception hierarchy and isinstance ordering are correct throughout the CLI, MCP server, and _error_type helper. The two style concerns do not affect correctness in the normal path.

fli/search/client.py and fli/mcp/server.py are worth a second look for the CA bundle path handling and the error message prefix.

Important Files Changed

Filename Overview
fli/search/exceptions.py Adds SearchCertificateError as a subclass of SearchConnectionError; clean and correctly placed in the hierarchy.
fli/search/client.py Adds CA bundle env-var resolution and certificate error detection via getattr guard; minor concern that an invalid bundle path won't produce a SearchCertificateError at failure time.
fli/mcp/server.py Adds _error_type and _error_response helpers with correct isinstance ordering; the "Search failed: " prefix on typed SearchClientError messages is a minor style concern.
fli/cli/errors.py Adds SearchCertificateError handling in the correct order (before SearchConnectionError) in both _friendly_message and json_error_payload.
tests/search/test_client_errors.py Tests certificate error mapping and CA bundle attribute assignment; verifies the attribute is set on the session but does not make a live request to confirm curl_cffi actually uses it.
tests/mcp/test_mcp_server_fixes.py Adds two well-structured tests verifying error_type: "certificate_error" in MCP flight and date search responses.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[HTTP POST to Google Flights] --> B{Exception?}
    B -->|No| C[Return response]
    B -->|Yes| D[_wrap_request_error]
    D --> E{Already SearchClientError?}
    E -->|Yes| F[Return as-is]
    E -->|No| G{CertificateVerifyError?}
    G -->|Yes| H[SearchCertificateError\ncertificate_error]
    G -->|No| I{curl_exc.Timeout?}
    I -->|Yes| J[SearchTimeoutError\ntimeout]
    I -->|No| K{curl_exc.ConnectionError?}
    K -->|Yes| L[SearchConnectionError\nconnection_error]
    K -->|No| M{curl_exc.HTTPError?}
    M -->|Yes| N[SearchHTTPError\nhttp_error]
    M -->|No| O[SearchClientError\nsearch_error]
    H & J & L & N & O --> P[_error_response]
    P --> Q[success: false, error: msg, error_type: ...]
Loading

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
fli/mcp/server.py:532-533
The `except SearchClientError` block prefixes every typed error message with `"Search failed: "`, including `SearchCertificateError` whose message is already a complete, actionable sentence. The prefix is redundant and makes the `error` field slightly awkward when presented verbatim to MCP clients. Passing the exception message directly (like `ParseError` does) keeps it clean.

```suggestion
    except SearchClientError as e:
        return _error_response(e, "flights")
```

### Issue 2 of 3
fli/mcp/server.py:633-634
Same redundant prefix for the date-search path: `SearchCertificateError` (and other typed client errors) already carry self-contained messages, so the `"Search failed: "` wrapper adds noise without value.

```suggestion
    except SearchClientError as e:
        return _error_response(e, "dates")
```

### Issue 3 of 3
fli/search/client.py:78-84
**No validation that the CA bundle path is readable**

`_ca_bundle_from_env()` returns any non-empty string verbatim. If the user sets `FLI_CA_BUNDLE` to a path that does not exist or is not readable by curl, the failure happens at request time as a low-level curl error. That error won't match `CertificateVerifyError` in `_wrap_request_error`, so it falls through to the generic `SearchClientError` fallback with `error_type: "search_error"` — exactly the opaque behavior this PR set out to improve. A quick `os.path.isfile` check or a startup-time `ValueError` similar to the existing `FLI_TIMEOUT` validation would surface the misconfiguration clearly.

Reviews (1): Last reviewed commit: "Improve MCP TLS error reporting" | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

Comment thread fli/mcp/server.py Outdated
Comment on lines +532 to +533
except SearchClientError as e:
return _error_response(e, "flights", message=f"Search failed: {e}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The except SearchClientError block prefixes every typed error message with "Search failed: ", including SearchCertificateError whose message is already a complete, actionable sentence. The prefix is redundant and makes the error field slightly awkward when presented verbatim to MCP clients. Passing the exception message directly (like ParseError does) keeps it clean.

Suggested change
except SearchClientError as e:
return _error_response(e, "flights", message=f"Search failed: {e}")
except SearchClientError as e:
return _error_response(e, "flights")
Prompt To Fix With AI
This is a comment left during a code review.
Path: fli/mcp/server.py
Line: 532-533

Comment:
The `except SearchClientError` block prefixes every typed error message with `"Search failed: "`, including `SearchCertificateError` whose message is already a complete, actionable sentence. The prefix is redundant and makes the `error` field slightly awkward when presented verbatim to MCP clients. Passing the exception message directly (like `ParseError` does) keeps it clean.

```suggestion
    except SearchClientError as e:
        return _error_response(e, "flights")
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

Comment thread fli/mcp/server.py Outdated
Comment on lines +633 to +634
except SearchClientError as e:
return _error_response(e, "dates", message=f"Search failed: {e}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Same redundant prefix for the date-search path: SearchCertificateError (and other typed client errors) already carry self-contained messages, so the "Search failed: " wrapper adds noise without value.

Suggested change
except SearchClientError as e:
return _error_response(e, "dates", message=f"Search failed: {e}")
except SearchClientError as e:
return _error_response(e, "dates")
Prompt To Fix With AI
This is a comment left during a code review.
Path: fli/mcp/server.py
Line: 633-634

Comment:
Same redundant prefix for the date-search path: `SearchCertificateError` (and other typed client errors) already carry self-contained messages, so the `"Search failed: "` wrapper adds noise without value.

```suggestion
    except SearchClientError as e:
        return _error_response(e, "dates")
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Cursor Fix in Claude Code Fix in Codex

Comment thread fli/search/client.py
Comment on lines +78 to +84
def _ca_bundle_from_env() -> str | None:
"""Return the first configured CA bundle path from supported environment variables."""
for name in _CA_BUNDLE_ENV_VARS:
value = os.environ.get(name)
if value:
return value
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No validation that the CA bundle path is readable

_ca_bundle_from_env() returns any non-empty string verbatim. If the user sets FLI_CA_BUNDLE to a path that does not exist or is not readable by curl, the failure happens at request time as a low-level curl error. That error won't match CertificateVerifyError in _wrap_request_error, so it falls through to the generic SearchClientError fallback with error_type: "search_error" — exactly the opaque behavior this PR set out to improve. A quick os.path.isfile check or a startup-time ValueError similar to the existing FLI_TIMEOUT validation would surface the misconfiguration clearly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: fli/search/client.py
Line: 78-84

Comment:
**No validation that the CA bundle path is readable**

`_ca_bundle_from_env()` returns any non-empty string verbatim. If the user sets `FLI_CA_BUNDLE` to a path that does not exist or is not readable by curl, the failure happens at request time as a low-level curl error. That error won't match `CertificateVerifyError` in `_wrap_request_error`, so it falls through to the generic `SearchClientError` fallback with `error_type: "search_error"` — exactly the opaque behavior this PR set out to improve. A quick `os.path.isfile` check or a startup-time `ValueError` similar to the existing `FLI_TIMEOUT` validation would surface the misconfiguration clearly.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant