feat: add structured and file-backed SQL result handling - #45
Open
Dao-you wants to merge 7 commits into
Open
Conversation
The existing output joins row values with commas and newlines, so any value containing a delimiter, a quote or a line break produces a result the caller cannot split back into rows reliably. NULL is also indistinguishable from the literal string "None". Add a serialization module offering three wire formats: - legacy: the current delimiter-joined output, still the default - json: compact columns/rows arrays; NULL stays null, Unicode and delimiters are preserved, and database-only types (Decimal, datetime, UUID, bytes) convert to stable strings - csv: RFC 4180 quoting via the standard library csv module Restrict the ruff test-file ignores to the lint rules the existing tests already trip, so the suite lints without rewriting unrelated tests.
A large result set returned inline can exhaust the caller''s context window before it can be examined. Add an opt-in store that writes a complete JSON result to disk and lets the caller retrieve it in bounded pieces. The store is disabled unless MSSQL_RESULT_OUTPUT_DIR names an absolute directory. Results over either inline threshold are written atomically via a temporary file and rename, and every retrieval path is bounded: - row pages capped by both row count and exact serialized byte size - byte-range chunks for results too large to page - a source-size guard so an oversized file is never parsed into memory - TTL expiry and a total-size quota, evicting oldest results first Callers never supply a path. They receive a server-generated 32-character hex ID, and every lookup is checked to resolve inside the configured directory with symlinks refused.
Wire the serialization formats and the result store into the MCP server, and correct three problems found while doing so. Result handling: - execute_sql accepts an optional result_format, defaulting to MSSQL_RESULT_FORMAT and then to legacy, so existing clients are unaffected - oversized results are stored and answered with metadata, a checksum and bounded preview rows instead of the full rowset - get_sql_result and read_sql_result_chunk are advertised only when storage is configured, and MSSQL_COMMAND may not shadow their names Fixes: - dispatch on cursor.description rather than by matching the query text, so CTEs, stored procedures and UPDATE ... OUTPUT are recognised as returning rows; every successful statement now commits, which previously did not happen for a write that also returned a rowset - database failures are raised as MCP errors instead of being returned as a successful result whose body happens to describe an error, with the configured password and password-like fragments redacted - connections and cursors close in a finally block, so a failure part way through no longer leaks them Tests reach the registered handlers through a small adapter. Previously they called the Server.call_tool decorator itself, which takes no arguments, so those assertions never ran against the real handlers.
Add export-sql-result, which validates a stored result as a complete canonical envelope and writes compact JSON to stdout, so a host-side process can consume a stored result without going through MCP. Non-standard JSON constants such as NaN are rejected rather than silently accepted. Add a benchmark that reports the UTF-8 size of the legacy output, the inline JSON output and a stored-result summary, so the context saving claimed for oversized results can be measured rather than asserted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The existing query output joins columns and values with commas and newlines without escaping them.
This becomes ambiguous when database values contain commas, quotes, line breaks, SQL definitions, or user-provided text. MCP clients and language models may no longer be able to reliably distinguish columns, rows, and original values.
Large result sets are also returned inline in a single MCP response, which can consume a significant portion of the model context before the result can be inspected or filtered.
Summary
This PR adds safer and more predictable SQL result handling:
New options
Environment variables
MSSQL_RESULT_FORMATlegacy,json, orcsvoutput.MSSQL_RESULT_OUTPUT_DIRMSSQL_RESULT_MAX_INLINE_ROWSMSSQL_RESULT_MAX_INLINE_BYTESMSSQL_RESULT_PREVIEW_ROWSMSSQL_RESULT_PREVIEW_BYTESMSSQL_RESULT_DEFAULT_PAGE_ROWSMSSQL_RESULT_MAX_PAGE_ROWSMSSQL_RESULT_MAX_PAGE_BYTESMSSQL_RESULT_MAX_PAGE_SOURCE_BYTESMSSQL_RESULT_MAX_CHUNK_BYTESMSSQL_RESULT_TTL_SECONDSMSSQL_RESULT_MAX_FILE_BYTESMSSQL_RESULT_MAX_STORE_BYTESMSSQL_RESULT_INCLUDE_PATHMCP tool arguments
result_formatexecute_sqlcall.store_resultoffset/limitget_sql_result.offset_bytes/max_bytesread_sql_result_chunk.Stored results return a server-generated
result_idby default. This mode works without shared filesystem access and allows MCP clients to retrieve bounded pages or chunks.When
MSSQL_RESULT_INCLUDE_PATH=true, the result also includesresult_path. This is intended for trusted local workflows where another host-side process can read the stored file directly.Export CLI
export-sql-resultexports a complete stored result byresult_idor validates and exports an existing canonical result file.Additional fixes
Result-returning statements are detected using cursor metadata rather than SQL text matching, improving support for CTEs, stored procedures, and statements such as
UPDATE ... OUTPUT.Database failures are also reported as MCP errors, with password-like values redacted, and database resources are closed reliably after failures.
Compatibility
The legacy result format remains the default.
JSON, CSV, and file-backed storage are opt-in. Existing configurations continue to behave as before unless the new settings or tool arguments are used.
Limitation
Query results are still fetched and serialized fully in server memory before the storage decision.
This change bounds MCP responses and prevents oversized payloads from entering the model context, but it does not provide streaming query processing.
Validation