Archicad et MCP Tapir Archicad
Voici une présentation complète de MCP Tapir Archicad — un serveur MCP qui expose les commandes de l'add-on Tapir (ENZYME-APD) ainsi que l'
Etape 3 : scripts python / golang
Voici une sélection des scripts les plus utiles pour un architecte, organisés par usage métier réel.
Base de connexion commune
Tous les scripts Python ci-dessous utilisent cette base de connexion via Tapir :enzyme-apd.github+1
python# requirements: pip install tapir-archicad-mcp archicad tapir-py openpyxl anthropic ACConnection
from archicad importimport json, requests# Connexion à l'instance Archicad active (port par défaut 19723)commands
conn = ACConnection.connect()
acc = conn.act = conn.typesdef tapir(command: str, params: dict = {}):
"""Appel générique aux commandes Tapir"""
return acc.ExecuteAddOnCommand(
act.AddOnCommandId('TapirCommand', command),
params )
1 — Générateur de Cahier des Charges (CCTP/DPGF)
Combine l'extraction BIM depuis Archicad avec Claude pour produire un document de marché structuré.graphisoft+1
pythonimport anthropicdef extract_elements_summary() -> dict:
"""Extrait les quantités par type d'élément depuis Archicad"""
summary = {}
# Récupère tous les éléments
all_elements = acc.GetAllElements()
# Récupère les zones (pièces) pour le programme
zones_raw = tapir("GetZoneDetails", {
"elements": [{"elementId": e.elementId} for e in all_elements]
})
zones = []
for z in zones_raw.get("zoneDetailsList", []):
zones.append({
"name": z.get("name"),
"number": z.get("number"),
"area": round(z.get("area", 0), 2),
"floor": z.get("floorName")
})
# Récupère les propriétés de finition pour les éléments
prop_ids = acc.GetAllPropertyIds("UserDefined")
summary["zones"] = zones summary["total_area"] = sum(z["area"] for z in zones) summary
summary["element_count"] = len(all_elements)
returndef generate_cctp(lot: str = "Gros Œuvre") -> str:
"""Génère un CCTP structuré depuis les données BIM via Claude"""
bim_data = extract_elements_summary()
client = anthropic.Anthropic()
prompt = f"""
Tu es un économiste de la construction expert en marchés publics français.
À partir des données BIM suivantes extraites d'Archicad, génère un CCTP structuré pour le lot "{lot}" conforme au CCTG.
Données BIM du projet :- Surface totale : {bim_data['total_area']} m²- Nombre d'espaces : {len(bim_data['zones'])}
- Détail des zones :{json.dumps(bim_data['zones'], indent=2, ensure_ascii=False)}
Structure attendue :
1. Généralités & documents de référence (normes DTU)
2. Matériaux et produits (avec spécifications techniques)
3. Mode d'exécution par poste
4. Contrôles et essais
5. Clauses de garantie"""
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}]
)
cctp_text = response.content[0].text
# Sauvegarde en fichier
with open(f"CCTP_{lot.replace(' ', '_')}.md", "w", encoding="utf-8") as f:
f.write(cctp_text)
print(f"✅ CCTP généré : CCTP_{lot}.md") cctp_text
return# Usage
cctp = generate_cctp("Cloisons et doublages")
2 — Analyse d'Appel d'Offres vs. Documentation BIM
Ce script lit un PDF d'AO, extrait les exigences techniques et les confronte aux propriétés du modèle Archicad :community.graphisoft+1
pythonimport anthropicimport pathlibdef extract_ao_requirements(pdf_path: str) -> list[dict]:
"""Extrait les exigences techniques d'un AO via Claude (lecture PDF)"""
client = anthropic.Anthropic()
pdf_data = pathlib.Path(pdf_path).read_bytes()
response = client.messages.create("""Extrais toutes les exigences techniques de ce document d'appel d'offres.
model="claude-opus-4-5",
max_tokens=2048,
messages=[{
"role": "user",
"content": [
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": __import__('base64').b64encode(pdf_data).decode()
}
},
{
"type": "text",
"text":
Pour chaque exigence, retourne un JSON structuré :
[{"poste": "...", "exigence": "...", "valeur_requise": "...", "unite": "..."}]Retourne UNIQUEMENT le JSON, sans texte autour."""
}
]
}]
)
return json.loads(response.content[0].text)
def get_bim_properties_for_element_type(element_type: str) -> list[dict]:
"""Récupère les propriétés BIM d'un type d'élément depuis Archicad"""
# Récupère tous les éléments
elements = acc.GetAllElements()
# Filtre par type via les propriétés
prop_ids = acc.GetAllPropertyIds("UserDefined")
results = []value
for el in elements[:50]: # Échantillon représentatif
try:
vals = acc.GetPropertyValuesOfElements(
[el.elementId], prop_ids[:10]
)
if vals:
results.append({
"guid": str(el.elementId.guid),
"properties": {
str(v.propertyId.guid): v.propertyValue. for v in vals[0].propertyValues if hasattr(v.propertyValue, 'value') results
}
})
except:
pass
returndef analyze_ao_vs_bim(pdf_path: str) -> str:
"""Confronte un AO avec les données du modèle BIM et génère un rapport"""
client = anthropic.Anthropic()
print("📄 Extraction des exigences de l'AO...")
ao_requirements = extract_ao_requirements(pdf_path)
print("🏗️ Extraction des données BIM depuis Archicad...")
bim_data = extract_elements_summary()
bim_props = get_bim_properties_for_element_type("Wall")
print("🤖 Analyse comparative par Claude...")f"""
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=3000,
messages=[{
"role": "user",
"content":
Tu es un expert BIM et économiste de la construction.
Compare les exigences de l'appel d'offres avec les données du modèle BIM.
EXIGENCES DE L'AO :{json.dumps(ao_requirements, indent=2, ensure_ascii=False)}
DONNÉES BIM DU PROJET ARCHICAD :- Zones et surfaces : {json.dumps(bim_data['zones'][:10], indent=2, ensure_ascii=False)}
- Propriétés relevées : {json.dumps(bim_props[:5], indent=2, ensure_ascii=False)}
Pour chaque exigence :
1. ✅ CONFORME si le BIM satisfait l'exigence (avec justification)
2. ⚠️ ÉCART si une divergence est détectée (avec chiffres)
3. ❓ DONNÉES MANQUANTES si l'info n'est pas dans le BIM
Conclus avec les actions correctives prioritaires."""
}]
)
rapport = response.content[0].text with open("Rapport_AO_vs_BIM.md", "w", encoding="utf-8") as f: rapport
f.write(rapport)
print("✅ Rapport généré : Rapport_AO_vs_BIM.md")
return# Usage
rapport = analyze_ao_vs_bim("AppelOffres_Lot3_Facades.pdf")
3 — Export Nomenclature vers Excel (avec screenshots)
Directement inspiré du script communautaire — extrait issues, propriétés et zones vers un fichier Excel exploitable :[community.graphisoft]
pythonimport openpyxlfrom openpyxl.utils import get_column_letterdef export_zones_to_excel(output_path: str = "nomenclature_zones.xlsx"):active
"""Exporte toutes les zones avec leurs propriétés dans un Excel structuré"""
wb = openpyxl.Workbook()
ws = wb. ws.title = "Zones"
# En-têtes
headers = ["N°", "Nom de la zone", "Étage", "Surface (m²)", "Hauteur (m)", "Volume (m³)", "Finitions", "Statut"]
for col, h in enumerate(headers, 1):
ws.cell(row=1, column=col, value=h).font = openpyxl.styles.Font(bold=True)
# Données depuis Archicad
all_elements = acc.GetAllElements()
zones_raw = tapir("GetZoneDetails", {
"elements": [{"elementId": e.elementId} for e in all_elements]
})
# Propriétés custom (ex: finition de sol)
prop_ids = acc.GetAllPropertyIds("UserDefined")
finition_prop = next(
(p for p in prop_ids if "finition" in str(p).lower()), None
)
for row_idx, zone in enumerate(zones_raw.get("zoneDetailsList", []), 2):
ws.cell(row=row_idx, column=1, value=zone.get("number", ""))
ws.cell(row=row_idx, column=2, value=zone.get("name", ""))
ws.cell(row=row_idx, column=3, value=zone.get("floorName", ""))
ws.cell(row=row_idx, column=4, value=round(zone.get("area", 0), 2))
ws.cell(row=row_idx, column=5, value=round(zone.get("height", 0), 2))
ws.cell(row=row_idx, column=6, value=round(zone.get("volume", 0), 2))
# Ajustement largeur colonnes
for col in range(1, len(headers) + 1):
ws.column_dimensions[get_column_letter(col)].width = 20
wb.save(output_path)
print(f"✅ Excel exporté : {output_path} ({row_idx - 1} zones)")
export_zones_to_excel()
4 — Script Golang : Synchronisation Archicad ↔ Suivi de chantier
Pour ton stack Go — un client HTTP qui interroge Archicad et pousse les données vers une API de suivi :[archicadapi.graphisoft]
gopackage mainimport (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
const archicadPort = 19723
type TapirRequest struct {
Command string `json:"command"`
Parameters any `json:"parameters"`
}
type ZoneDetail struct {
Name string `json:"name"`
Number string `json:"number"`
Area float64 `json:"area"`
FloorName string `json:"floorName"`
}
type SuiviChantierRecord struct {
ZoneName string `json:"zone_name"`
Area float64 `json:"area"`
Statut string `json:"statut"`
UpdatedAt time.Time `json:"updated_at"`
}
// CallArchicad envoie une commande JSON à l'instance Archicad locale
func CallArchicad(command string, params any) (map[string]any, error) {
payload := TapirRequest{
Command: fmt.Sprintf("TapirCommand.%s", command),
Parameters: params,
}
body, _ := json.Marshal(payload)
url := fmt.Sprintf("http://localhost:%d", archicadPort)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("archicad unreachable on port %d: %w", archicadPort, err)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)any
var result map[string] json.Unmarshal(raw, &result) err
return result, nil
}
// GetAllZones récupère toutes les zones avec leurs surfaces
func GetAllZones() ([]ZoneDetail, error) {
// Étape 1 : récupère tous les éléments
elements, err := CallArchicad("GetAllElements", map[string]any{})
if err != nil {
return nil, }
elemList, _ := elements["elements"].([]any)
// Étape 2 : récupère les détails des zones
elems := make([]map[string]any, 0, len(elemList))
for _, e := range elemList {
if em, ok := e.(map[string]any); ok {
elems = append(elems, em)
}
}
details, err := CallArchicad("GetZoneDetails", map[string]any{"elements": elems}) err
if err != nil {
return nil, }
var zones []ZoneDetail zoneList, _ := details["zoneDetailsList"].([]any) area
for _, z := range zoneList {
if zm, ok := z.(map[string]any); ok {
zone := ZoneDetail{
Name: fmt.Sprintf("%v", zm["name"]),
Number: fmt.Sprintf("%v", zm["number"]),
FloorName: fmt.Sprintf("%v", zm["floorName"]),
}
if area, ok := zm["area"].(float64); ok {
zone.Area = }
zones = append(zones, zone)
}
}
return zones, nil
}
// SyncToSuiviChantier pousse les données Archicad vers une API de suivi
func SyncToSuiviChantier(apiEndpoint string) error {
zones, err := GetAllZones()
if err != nil {
return fmt.Errorf("failed to get zones: %w", err)
}
records := make([]SuiviChantierRecord, 0, len(zones))
for _, z := range zones {
records = append(records, SuiviChantierRecord{
ZoneName: z.Name,
Area: z.Area,
Statut: "en_cours", // à enrichir depuis propriété BIM
UpdatedAt: time.Now(),
})
}
payload, _ := json.Marshal(records)
resp, err := http.Post(apiEndpoint, "application/json", bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("sync failed: %w", err)
}
defer resp.Body.Close()
fmt.Printf("✅ Synced %d zones to %s (HTTP %d)\n", len(records), apiEndpoint, resp.StatusCode)
return nil
}
func main() {
// Test de connexion
info, err := CallArchicad("GetProjectInfo", map[string]any{})
if err != nil {
fmt.Printf("❌ Archicad non accessible : %v\n", err)
return
}
fmt.Printf("🏗️ Projet : %v\n", info)
// Sync vers API de suivi chantier
SyncToSuiviChantier("https://mon-suivi-chantier.app/api/sync")
}
5 — Script de numérotation automatique des éléments
Inspiré d'un script partagé par la communauté Graphisoft — utilise la position spatiale pour numéroter automatiquement les pièces ou portes selon un parcours logique :[community.graphisoft]
pythonimport mathdef auto_number_zones_by_floor():
"""Numérote les zones par étage, de gauche à droite, de bas en haut"""
all_elements = acc.GetAllElements()
zones_raw = tapir("GetZoneDetails", {
"elements": [{"elementId": e.elementId} for e in all_elements]
})
zones = zones_raw.get("zoneDetailsList", [])
# Groupe par étage
floors = {}
for z in zones:
floor = z.get("floorIndex", 0)
if floor not in floors:
floors[floor] = []
floors[floor].append(z)
updates = []
global_counter = 1
for floor_idx in sorted(floors.keys()):
floor_zones = floors[floor_idx]
# Tri géographique : Y décroissant (nord→sud), puis X croissant (ouest→est)
sorted_zones = sorted(
floor_zones,
key=lambda z: (-z.get("pos", {}).get("y", 0), z.get("pos", {}).get("x", 0))
)
for zone in sorted_zones: new_number
floor_prefix = str(floor_idx).zfill(2)
new_number = f"{floor_prefix}-{str(global_counter).zfill(3)}"
updates.append({
"elementId": zone["elementId"],
"newNumber": })
print(f" {zone.get('name')} → {new_number}")
global_counter += 1
# Applique les numéros via SetPropertyValuesOfElements updates
print(f"\n✅ {len(updates)} zones numérotées automatiquement")
returnauto_number_zones_by_floor()
Architecture recommandée pour un pipeline complet
Pour un usage production dans ton contexte (MFO + consulting BIM), la chaîne optimale est :github+1
textPDF AO / Brief client
↓
Claude API (extraction structurée)
↓
Script Python / Go
├── CallArchicad() → GetZoneDetails / GetAllElements / GetPropertyValues
├── Analyse de conformité (Claude)
└── Export → Excel / Markdown / API REST
↓
Rapport livrable automatique
La bibliothèque multiconn_archicad est particulièrement adaptée si tu dois piloter plusieurs instances simultanément (ex: fichier archi + fichier structure) dans un workflow batch. Elle gère nativement la sérialisation des connexions, la gestion d'erreurs typées et la bascule entre instances via un simple conn.primary = Port(19725).[github]