Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/tool-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ Record numbering begins at 1 for the header; it counts parsed CSV records, not
physical lines inside quoted values. Entirely blank lines are skipped by the parser.
Empty input returns empty lists and zero rows. Malformed quoting returns `valid=False`
and an error message. Extra cells are flagged but not counted as named columns.
Header cells that are empty or whitespace-only are reported by zero-based position in
`empty_header_indices`; a blank name is a warning, not an invalid document, and the
original names are kept in `columns` unchanged.

## hash_text(text, algorithm="sha256")

Expand Down
12 changes: 9 additions & 3 deletions src/mcp_tools_lab/tools/csv_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@ def inspect_csv(text: str) -> dict:
"""Report rows, empty cells, and ragged records (record 1 is the header).

Missing counts include absent trailing cells and whitespace-only cells.
Entirely blank records are skipped by Python's CSV reader.
Entirely blank records are skipped by Python's CSV reader. Header cells that
are empty or whitespace-only are reported by zero-based position in
``empty_header_indices``; the original names are still kept in ``columns``,
and a blank name is a warning, not an invalid document.
"""
try:
rows = list(csv.reader(io.StringIO(text, newline=""), strict=True))
except csv.Error as error:
return {"valid": False, "error": str(error)}
if not rows:
return {"valid": True, "columns": [], "row_count": 0,
"missing_values": [], "inconsistent_rows": []}
"missing_values": [], "inconsistent_rows": [],
"empty_header_indices": []}
header, data = rows[0], rows[1:]
empty_header_indices = [index for index, name in enumerate(header) if not name.strip()]
missing = [0] * len(header)
inconsistent = []
for record, row in enumerate(data, start=2):
Expand All @@ -30,4 +35,5 @@ def inspect_csv(text: str) -> dict:
"missing_values": [
{"column": name, "index": index, "count": missing[index]}
for index, name in enumerate(header)
], "inconsistent_rows": inconsistent}
], "inconsistent_rows": inconsistent,
"empty_header_indices": empty_header_indices}
36 changes: 36 additions & 0 deletions tests/test_csv_tool_empty_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from mcp_tools_lab.tools.csv_tool import inspect_csv


def test_empty_header_indices_reports_positions():
result = inspect_csv("name,,age\nAda,20,x\nBob,30,y")
assert result["empty_header_indices"] == [1]


def test_whitespace_only_header_reports_position():
result = inspect_csv("name, ,age\nAda,20,x")
assert result["empty_header_indices"] == [1]


def test_empty_header_indices_lists_every_blank_column():
result = inspect_csv("a,,c,,e\n1,2,3,4,5")
assert result["empty_header_indices"] == [1, 3]


def test_header_names_are_still_columns():
result = inspect_csv("name,,age\nAda,20,x")
assert result["columns"] == ["name", "", "age"]


def test_empty_input_has_no_empty_header_indices():
assert inspect_csv("")["empty_header_indices"] == []


def test_blank_header_is_still_valid():
assert inspect_csv("name,,age\nAda,20,x")["valid"] is True


def test_malformed_csv_does_not_report_empty_header_indices():
# A document that will not parse has no header positions to name.
result = inspect_csv('name\n"unfinished')
assert result["valid"] is False
assert "empty_header_indices" not in result