Skip to content

Commit 842c242

Browse files
committed
fix: Apply all corrections from verified archive
- client.py: Add table_id parameter to download_doc_xlsx - tools/export.py: Add table_id parameter and auto-selection - tools/schema.py: Use correct client methods (list_tables, list_columns, etc.) - tools/utils.py: Fix client method calls All tools verified working: ✅ create_table, modify_table ✅ create_column, modify_column (widgetOptions serialized) ✅ download_table_csv, download_document_excel ✅ create_reference_column (auto visibleCol) ✅ validate_schema, create_schema, export_schema
1 parent 6b1fdad commit 842c242

File tree

4 files changed

+58
-142
lines changed

4 files changed

+58
-142
lines changed

src/mcp_server_grist/client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -408,13 +408,15 @@ async def download_doc(self, doc_id: str, nohistory: bool = False, template: boo
408408
response.raise_for_status()
409409
return response.content
410410

411-
async def download_doc_xlsx(self, doc_id: str, header: str = "label") -> bytes:
411+
async def download_doc_xlsx(self, doc_id: str, table_id: Optional[str] = None, header: str = "label") -> bytes:
412412
"""Télécharge un document au format Excel."""
413413
params = {"header": header}
414+
if table_id:
415+
params["tableId"] = table_id
414416

415-
logger.debug(f"Downloading document {doc_id} as Excel")
417+
logger.debug(f"Downloading document {doc_id} as Excel with params: {params}")
416418
try:
417-
async with httpx.AsyncClient(timeout=120.0) as client: # Augmenter timeout
419+
async with httpx.AsyncClient(timeout=120.0) as client:
418420
response = await client.request(
419421
method="GET",
420422
url=f"{self.api_url.rstrip('/')}/docs/{doc_id}/download/xlsx",
@@ -423,17 +425,15 @@ async def download_doc_xlsx(self, doc_id: str, header: str = "label") -> bytes:
423425
)
424426

425427
logger.debug(f"Excel download response status: {response.status_code}")
426-
logger.debug(f"Excel download response headers: {dict(response.headers)}")
427428

428429
response.raise_for_status()
429430
return response.content
430431

431432
except httpx.TimeoutException as e:
432433
logger.error(f"Excel download timeout for doc {doc_id}: {e}")
433-
raise ValueError(f"Excel download timeout - document may be too large. Try download_document_sqlite as alternative.")
434+
raise ValueError(f"Excel download timeout - document may be too large.")
434435
except httpx.HTTPStatusError as e:
435436
logger.error(f"Excel download HTTP error for doc {doc_id}: {e}")
436-
logger.error(f"Response text: {e.response.text}")
437437
raise ValueError(f"Excel download failed: {e.response.status_code} - {e.response.text}")
438438
except Exception as e:
439439
logger.error(f"Excel download unexpected error for doc {doc_id}: {e}")

src/mcp_server_grist/tools/export.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ async def download_document_sqlite(
8686

8787
async def download_document_excel(
8888
doc_id: str,
89+
table_id: Optional[str] = None,
8990
header: str = "label",
9091
ctx: Context = None
9192
) -> Dict[str, Any]:
@@ -97,12 +98,13 @@ async def download_document_excel(
9798
9899
Args:
99100
doc_id: L'ID du document
101+
table_id: L'ID de la table à exporter (optionnel, première table par défaut)
100102
header: Format des en-têtes (label, id, ou none)
101103
102104
Returns:
103105
Dict avec statut, message et contenu encodé en base64
104106
"""
105-
logger.info(f"Tool called: download_document_excel with doc_id: {doc_id}")
107+
logger.info(f"Tool called: download_document_excel with doc_id: {doc_id}, table_id: {table_id}")
106108

107109
try:
108110
client = get_client(ctx)
@@ -118,16 +120,27 @@ async def download_document_excel(
118120
"message": "Format d'en-tête invalide. Doit être: label, id, ou none"
119121
}
120122

121-
content = await client.download_doc_xlsx(doc_id, header=header)
123+
# Si pas de table_id, récupérer la première table du document
124+
if not table_id:
125+
tables = await client.list_tables(doc_id)
126+
if not tables:
127+
return {
128+
"success": False,
129+
"message": "Aucune table trouvée dans le document"
130+
}
131+
table_id = tables[0].id
132+
logger.info(f"Auto-selected table: {table_id}")
133+
134+
content = await client.download_doc_xlsx(doc_id, table_id=table_id, header=header)
122135

123136
# Encoder le contenu binaire en base64
124137
encoded_content = base64.b64encode(content).decode('utf-8')
125138

126139
return {
127140
"success": True,
128-
"message": f"Document {doc_id} téléchargé avec succès au format Excel",
141+
"message": f"Table {table_id} du document {doc_id} téléchargée avec succès au format Excel",
129142
"content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
130-
"filename": f"{doc_id}.xlsx",
143+
"filename": f"{table_id}.xlsx",
131144
"content_base64": encoded_content,
132145
"size_bytes": len(content)
133146
}

src/mcp_server_grist/tools/schema.py

Lines changed: 29 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,6 @@ async def create_reference_column(
7575
3. Crée la colonne de référence
7676
4. Optionnellement crée la relation inverse
7777
78-
Prérequis:
79-
- list_tables: Pour connaître les tables disponibles
80-
- La table cible doit exister
81-
8278
Args:
8379
doc_id: L'ID du document
8480
table_id: L'ID de la table source
@@ -91,22 +87,6 @@ async def create_reference_column(
9187
9288
Returns:
9389
Dict avec statut, détails et éventuels warnings
94-
95-
Examples:
96-
# Référence simple
97-
create_reference_column(
98-
doc_id, "Taches", "responsable",
99-
target_table="Agents",
100-
visible_column="nom_complet"
101-
)
102-
103-
# Référence avec relation inverse
104-
create_reference_column(
105-
doc_id, "Taches", "projet",
106-
target_table="Projets",
107-
visible_column="nom",
108-
reverse_column="taches" # Crée Projets.taches
109-
)
11090
"""
11191
try:
11292
client = get_client(ctx)
@@ -139,12 +119,8 @@ async def create_reference_column(
139119
visible_col=col_ref
140120
)
141121

142-
response = await client.post(
143-
f"/docs/{doc_id}/tables/{table_id}/columns",
144-
json=payload
145-
)
146-
147-
columns_created = response.get("columns", [])
122+
# Utiliser la méthode create_columns du client
123+
columns_created = await client.create_columns(doc_id, table_id, payload)
148124

149125
# PHASE 4: Création de la relation inverse (optionnel)
150126
reverse_result = None
@@ -167,12 +143,8 @@ async def create_reference_column(
167143
visible_col=source_col_ref
168144
)
169145

170-
reverse_response = await client.post(
171-
f"/docs/{doc_id}/tables/{target_table}/columns",
172-
json=reverse_payload
173-
)
174-
175-
reverse_result = reverse_response.get("columns", [{}])[0]
146+
reverse_columns = await client.create_columns(doc_id, target_table, reverse_payload)
147+
reverse_result = reverse_columns[0] if reverse_columns else {"id": reverse_column}
176148

177149
except Exception as e:
178150
warnings.append(
@@ -226,23 +198,6 @@ async def validate_schema(
226198
227199
Returns:
228200
Dict avec statut de validation, erreurs et warnings
229-
230-
Example schema:
231-
{
232-
"tables": {
233-
"Agents": {
234-
"columns": {
235-
"nom": {"type": "Text"},
236-
"service": {"type": "Choice", "choices": ["A", "B"]}
237-
}
238-
},
239-
"Projets": {
240-
"columns": {
241-
"chef": {"type": "Ref", "target": "Agents", "visible": "nom"}
242-
}
243-
}
244-
}
245-
}
246201
"""
247202
try:
248203
errors = []
@@ -260,8 +215,10 @@ async def validate_schema(
260215
client = get_client(ctx)
261216
if not client:
262217
return build_error_response("Client Grist non configuré")
263-
response = await client.get(f"/docs/{doc_id}/tables")
264-
existing_tables = {t.get("id") for t in response.get("tables", [])}
218+
219+
# Utiliser list_tables du client
220+
tables_list = await client.list_tables(doc_id)
221+
existing_tables = {t.id for t in tables_list}
265222

266223
# Tables définies dans le schéma
267224
schema_tables = {t.id for t in schema_def.tables}
@@ -348,38 +305,6 @@ async def create_schema(
348305
349306
Returns:
350307
Dict avec rapport détaillé de création
351-
352-
Example schema:
353-
{
354-
"tables": {
355-
"Agents": {
356-
"columns": {
357-
"nom": {"type": "Text", "label": "Nom"},
358-
"email": {"type": "Text", "label": "Email"},
359-
"nom_complet": {
360-
"type": "Text",
361-
"formula": "$prenom + ' ' + $nom"
362-
}
363-
}
364-
},
365-
"Projets": {
366-
"columns": {
367-
"nom": {"type": "Text"},
368-
"chef": {
369-
"type": "Ref",
370-
"target": "Agents",
371-
"visible": "nom_complet",
372-
"label": "Chef de projet"
373-
}
374-
}
375-
}
376-
},
377-
"data": {
378-
"Agents": [
379-
{"nom": "Dupont", "prenom": "Jean"}
380-
]
381-
}
382-
}
383308
"""
384309
report = SchemaCreationReport()
385310

@@ -405,8 +330,8 @@ async def create_schema(
405330
schema_def = parse_schema_dict(schema)
406331

407332
# Récupérer les tables existantes
408-
response = await client.get(f"/docs/{doc_id}/tables")
409-
existing_tables = {t.get("id") for t in response.get("tables", [])}
333+
tables_list = await client.list_tables(doc_id)
334+
existing_tables = {t.id for t in tables_list}
410335

411336
# Mapping table_id schéma → table_id réel (au cas où Grist renomme)
412337
table_mapping = {}
@@ -444,9 +369,8 @@ async def create_schema(
444369
# Créer la table
445370
try:
446371
payload = prepare_table_payload(table.id, simple_columns)
447-
response = await client.post(f"/docs/{doc_id}/tables", json=payload)
372+
tables_created = await client.create_tables(doc_id, payload)
448373

449-
tables_created = response.get("tables", [])
450374
if tables_created:
451375
actual_id = tables_created[0].get("id", table.id)
452376
table_mapping[table.id] = actual_id
@@ -475,13 +399,11 @@ async def get_col_ref(target_table: str, column_name: str) -> Optional[int]:
475399

476400
if actual_table not in col_ref_cache:
477401
try:
478-
response = await client.get(
479-
f"/docs/{doc_id}/tables/{actual_table}/columns"
480-
)
402+
columns_list = await client.list_columns(doc_id, actual_table)
481403
col_ref_cache[actual_table] = {}
482-
for col in response.get("columns", []):
483-
col_id = col.get("id")
484-
col_fields = col.get("fields", {})
404+
for col in columns_list:
405+
col_id = col.id
406+
col_fields = col.fields if hasattr(col, 'fields') else {}
485407
col_label = col_fields.get("label", col_id)
486408
col_ref = col_fields.get("colRef")
487409
col_ref_cache[actual_table][col_id] = col_ref
@@ -518,10 +440,7 @@ async def get_col_ref(target_table: str, column_name: str) -> Optional[int]:
518440
visible_col=visible_col_ref
519441
)
520442

521-
response = await client.post(
522-
f"/docs/{doc_id}/tables/{actual_table_id}/columns",
523-
json=payload
524-
)
443+
await client.create_columns(doc_id, actual_table_id, payload)
525444

526445
report.references_created.append({
527446
"table": actual_table_id,
@@ -539,10 +458,7 @@ async def get_col_ref(target_table: str, column_name: str) -> Optional[int]:
539458
label=f"{table.id}s"
540459
)
541460

542-
await client.post(
543-
f"/docs/{doc_id}/tables/{target_table}/columns",
544-
json=reverse_payload
545-
)
461+
await client.create_columns(doc_id, target_table, reverse_payload)
546462

547463
report.references_created.append({
548464
"table": target_table,
@@ -568,13 +484,7 @@ async def get_col_ref(target_table: str, column_name: str) -> Optional[int]:
568484

569485
try:
570486
if records:
571-
payload = {"records": [{"fields": r} for r in records]}
572-
response = await client.post(
573-
f"/docs/{doc_id}/tables/{actual_table_id}/records",
574-
json=payload
575-
)
576-
577-
record_ids = response.get("records", [])
487+
record_ids = await client.add_records(doc_id, actual_table_id, records)
578488
report.records_inserted.append({
579489
"table": actual_table_id,
580490
"count": len(record_ids)
@@ -629,32 +539,28 @@ async def export_schema(
629539
if not client:
630540
return build_error_response("Client Grist non configuré")
631541

632-
# Récupérer les tables
633-
response = await client.get(f"/docs/{doc_id}/tables")
634-
tables = response.get("tables", [])
542+
# Récupérer les tables avec list_tables
543+
tables_list = await client.list_tables(doc_id)
635544

636545
schema = {"tables": {}}
637546
data = {} if include_data else None
638547

639-
for table in tables:
640-
table_id = table.get("id")
548+
for table in tables_list:
549+
table_id = table.id
641550
if not table_id or table_id.startswith("_"):
642551
continue # Ignorer les tables système
643552

644-
# Récupérer les colonnes
645-
col_response = await client.get(
646-
f"/docs/{doc_id}/tables/{table_id}/columns"
647-
)
648-
columns = col_response.get("columns", [])
553+
# Récupérer les colonnes avec list_columns
554+
columns_list = await client.list_columns(doc_id, table_id)
649555

650556
table_def = {"columns": {}}
651557

652-
for col in columns:
653-
col_id = col.get("id")
558+
for col in columns_list:
559+
col_id = col.id
654560
if not col_id or col_id == "id":
655561
continue
656562

657-
fields = col.get("fields", {})
563+
fields = col.fields if hasattr(col, 'fields') else {}
658564
col_type = fields.get("type", "Text")
659565

660566
col_def = {"type": col_type}
@@ -699,12 +605,8 @@ async def export_schema(
699605
# Récupérer les données si demandé
700606
if include_data:
701607
try:
702-
data_response = await client.get(
703-
f"/docs/{doc_id}/tables/{table_id}/records",
704-
params={"limit": 100}
705-
)
706-
records = data_response.get("records", [])
707-
data[table_id] = [r.get("fields", {}) for r in records]
608+
records_list = await client.list_records(doc_id, table_id, limit=100)
609+
data[table_id] = [r.fields if hasattr(r, 'fields') else {} for r in records_list]
708610
except Exception:
709611
pass
710612

src/mcp_server_grist/tools/utils.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,9 @@ async def resolve_visible_col(
209209
Tuple (colRef, error_message)
210210
"""
211211
try:
212-
response = await client.get(f"/docs/{doc_id}/tables/{table_id}/columns")
213-
columns = response.get("columns", [])
212+
# Utiliser la méthode list_columns du client
213+
columns_list = await client.list_columns(doc_id, table_id)
214+
columns = [{"id": c.id, "fields": c.fields} for c in columns_list]
214215

215216
for col in columns:
216217
col_id = col.get("id")
@@ -242,9 +243,9 @@ async def check_table_exists(
242243
Tuple (exists, list_of_tables)
243244
"""
244245
try:
245-
response = await client.get(f"/docs/{doc_id}/tables")
246-
tables = response.get("tables", [])
247-
table_ids = [t.get("id") for t in tables]
246+
# Utiliser la méthode list_tables du client
247+
tables_list = await client.list_tables(doc_id)
248+
table_ids = [t.id for t in tables_list]
248249
return table_id in table_ids, table_ids
249250
except Exception:
250251
return False, []

0 commit comments

Comments
 (0)