Neuigkeiten:

Wenn ihr euch für eine gute Antwort bedanken möchtet, im entsprechenden Posting einfach den Knopf "sag Danke" drücken!

Mobiles Hauptmenü

Claude und Access: MCP-Server getestet ...

Begonnen von Josef P., Oktober 13, 2026, 10:29:08

⏪ vorheriges - nächstes ⏩

Josef P.

Hallo!

Wie jedes Jahr gab es Vorträge auf der AEK, die zum Ausprobieren anregten.
Und da ich immer etwas länger mit der Bahn fahren darf, war auch die Zeit dafür verfügbar, MCP-Server besser kennen zu lernen. :)

Test-Szenario
Claude code soll ein Formular zum Anzeigen und Filtern von Kunden erstellen und dafür meine wiederverwendbaren Codemodule der Access CodeLib nutzen.

Ziel
Ich wollte wissen, wie der erzeugte Code aussehen wird. (Bin ein Freund von Clean Code.)

Vorarbeit
Tabelle und Bearbeitungsformular in Test-Datenbank bereitgestellt (auch von Claude Code erstellt, aber ohne spezielle MCP-Server und Skills. (Anhang: Sie dürfen in diesem Board keine Dateianhänge sehen.)
MCP-Server eingerichtet (https://github.com/unmateria/MCP-Access und https://github.com/joyfullservice/msaccess-vcs-mcp)
Eigenen MCP-Server und Skill für den Zugriff auf meine Code-Bibliothek erstellt (auch mit Claude).

Arbeit mit Claude Code
Einstieg mit einer (abgsichtlich) sehr unpräzisen Anweisung:
ZitatErstelle eine Formular zum Anzeigen in Listenform und Filtern der Kunden
[...]
Claude:
ZitatIch habe mir die Ausgangslage angesehen:

  - tKunden: KundeID, Kundennummer, Firma, Anrede, Titel, Vorname, Nachname,
    Strasse, PLZ, Ort, Land, Telefon, Mobil, Email, UID, Bemerkung, Aktiv,
    ErstelltAm, GeaendertAm
  - fKunde: Einzelformular zum Bearbeiten, mit Speichern/Neu/Löschen/Schließen
  - Es gibt noch keine Standardmodule, also auch keine ACLib-Module, und nur die
    Referenz auf DAO.

  Plan: fKundenListe

  Layout (als Endlosformular, Datenquelle tKunden, nur lesend:
  AllowEdits/Additions/Deletions = Nein)
  - Formularkopf, Filterbereich:
    - txtFilterSuche: Suche über Kundennummer, Firma, Nachname, Vorname
      (Teiltreffer)
    - txtFilterPLZ und txtFilterOrt
    - cboFilterLand: Werteliste aus den vorhandenen Ländern
    - cboFilterAktiv: Alle / Aktiv / Inaktiv (Standard: Aktiv)
    - cmdFilterAnwenden, cmdFilterZuruecksetzen, lblAnzahl (Anzahl der Treffer)
  - Spaltenköpfe im Kopf, Detailbereich: Kundennummer, Firma, Nachname, Vorname,
    PLZ, Ort, Land, Telefon, Email, Aktiv
  - Buttons: cmdNeu öffnet fKunde mit einem neuen Datensatz, cmdOeffnen und ein
    Doppelklick auf eine Zeile öffnen fKunde mit dem gewählten Kunden (KundeID =
    ...)
  - Wenn fKunde geschlossen wird, aktualisiere ich die Liste (Requery) und
    behalte dabei die aktuelle Position.

  Umsetzung
  1. Formular und Steuerelemente mit dem MCP-Server access anlegen, dann
    access_close und vcs_export_object
  2. Code in forms/fKundenListe.cls schreiben, dann vcs_import_object,
    vcs_compile_vba, Log prüfen und vcs_export_object
  3. Ein Screenshot zur Kontrolle
[...]
... ich trenne lieber die Datenanzeige und die Steuerung in Hauptformular und Unterformular auf =>
Zitatjetzt hätte ich noch gerne diesen Aufbau: ein ungebundens Hauptformular mit 
  den Filter-Feldern im Formularkopf und im Detailbereich ein Endlosformular   
  mit den angzeigten Datensätzen. Beim Starten des Hauptformulars sollen keine 
  Datensätze angezeigt (und natürlich auch nicht geladen) werden 
Claude:
ZitatGut, dann baue ich das Ganze als Hauptformular mit Unterformular auf.

  Aufbau

  Hauptformular fKundenSuche (ungebunden)
  - Kopfbereich: die Filterfelder txtFilterSuche, txtFilterPLZ, txtFilterOrt,
    cboFilterLand und cboFilterAktiv, die Buttons cmdFilterZuruecksetzen, cmdNeu
    und cmdOeffnen sowie lblAnzahl für die Trefferanzahl.
  - Detailbereich: das Unterformular-Steuerelement sfrKundenListe, ohne
    Verknüpfungsfelder.
  - Code-Behind: hält WithEvents FilterManager As FilterControlManager und
    registriert in Form_Load die Filter-Steuerelemente. Im Event
    FilterStringChanged setzt es die Datenquelle des Unterformulars neu.

  Unterformular fKundenListe (Endlosformular)
  - Spaltenköpfe im Formularkopf, darunter die Zeilen mit Kundennummer, Firma,
    Nachname, Vorname, PLZ, Ort, Land, Telefon, Email und Aktiv.
  - Nur lesend: AllowEdits, AllowAdditions und AllowDeletions stehen auf Nein.
  - Ein Doppelklick auf eine Zeile öffnet fKunde mit diesem Kunden.

  Keine Datensätze beim Start
  - Die Datenquelle des Unterformulars ist im Entwurf SELECT ... FROM tKunden
    WHERE False. Die Datenbank-Engine liefert dafür keine Zeilen, es wird also
    nichts geladen. Die gebundenen Felder zeigen trotzdem kein #Name?, was bei
    einer leeren Datenquelle passieren würde.
  - Erst wenn ein Filter gesetzt ist, setzt das Hauptformular die Datenquelle
    auf SELECT ... FROM tKunden WHERE <Filter> ORDER BY .... Den Filterstring
    erzeugt die ACLib, er ist also gegen SQL-Injection abgesichert.
  - Ich setze die Datenquelle direkt statt Filter/FilterOn. Sonst würde das
    Unterformular zuerst alle Datensätze laden und erst danach filtern.

  Ablauf
  [...]
Where mehr mitlesen will, ich hänge den vollständigen Text an. (Anhang: Sie dürfen in diesem Board keine Dateianhänge sehen.)

Ergebnis
Sie dürfen in diesem Board keine Dateianhänge sehen.
Code im Formular (gekürzt):
Option Compare Text
Option Explicit

Private Const MSG_TITLE As String = "Kundenliste"
Private Const CUSTOMER_FORM_NAME As String = "fKunde"

Private Const ITEMS_SELECT_SQL As String = _
    "SELECT KundeID, Kundennummer, Firma, Nachname, Vorname, PLZ, Ort, Land, Telefon, Email, Aktiv FROM tKunden"
Private Const ITEMS_ORDER_BY_SQL As String = " ORDER BY Firma, Nachname, Vorname"
Private Const NO_RECORDS_CRITERIA As String = "1=0"

' Bound values of cboFilterAktiv (value list "-1;Aktiv;0;Inaktiv;1;Alle")
Private Const STATUS_ACTIVE As Long = -1
Private Const STATUS_ALL As Long = 1

Private WithEvents FilterManager As FilterControlManager
Private WithEvents ItemsForm As Form_fKundenListe_SF_Items

Private ActiveCriteria As String

'---------------------------------------------------------------------------------------
' Form events
'---------------------------------------------------------------------------------------

Private Sub Form_Load()
    InitItemsForm
    InitFilterManager
End Sub

[...]

Private Sub FilterManager_FilterStringChanged(ByVal NewFilterString As String)
    ApplyItemsCriteria GetItemsCriteria(NewFilterString)
End Sub

Private Sub ItemsForm_ItemSelected(ByVal KundeID As Long)
    OpenCustomer KundeID
End Sub

'---------------------------------------------------------------------------------------
' Initialization
'---------------------------------------------------------------------------------------

Private Sub InitItemsForm()
    Set ItemsForm = Me.sfrItems.Form
    ApplyItemsCriteria NO_RECORDS_CRITERIA
End Sub

Private Sub InitFilterManager()
    Set FilterManager = New FilterControlManager
    Set FilterManager.SqlTool = SqlTools.DAO
    AddFilterControls FilterManager.FilterControls
    FilterManager.AutoFilterOn = True
End Sub

Private Sub AddFilterControls(ByVal FilterControls As FilterControlCollection)
    AddSearchTextFilter FilterControls.AddGroup(SQL_Or)

    With FilterControls
        .Add "PLZ", SQL_Text, SQL_Like + SQL_Add_WildCardSuffix, Me.txtFilterPLZ
        .Add "Ort", SQL_Text, SQL_Like + SQL_Add_WildCardSuffix, Me.txtFilterOrt
        .Add "Land", SQL_Text, SQL_Equal, Me.cboFilterLand
        ' Null (cleared combobox) is ignored as well, otherwise it would filter "Aktiv Is Null"
        .Add "Aktiv", SQL_Numeric, SQL_Equal, Me.cboFilterAktiv, , Array(STATUS_ALL, Null)
    End With
End Sub

' txtFilterSuche matches a part of any of these fields.
Private Sub AddSearchTextFilter(ByVal SearchGroup As FilterControlCollection)
    Dim FieldName As Variant

    For Each FieldName In Array("Kundennummer", "Firma", "Nachname", "Vorname")
        SearchGroup.Add CStr(FieldName), SQL_Text, _
                        SQL_Like + SQL_Add_WildCardPrefix + SQL_Add_WildCardSuffix, Me.txtFilterSuche
    Next
End Sub

'---------------------------------------------------------------------------------------
' Filter
'---------------------------------------------------------------------------------------

Private Sub ResetFilter()
    FilterManager.RemoveFilter
    Me.cboFilterAktiv.Value = STATUS_ACTIVE
End Sub

[...]

'---------------------------------------------------------------------------------------
' Customer form
'---------------------------------------------------------------------------------------

Private Sub OpenSelectedCustomer()
    Dim KundeID As Variant

    KundeID = ItemsForm.SelectedKundeID
    If IsNull(KundeID) Then
        MsgBox "Bitte zuerst einen Kunden auswählen.", vbInformation, MSG_TITLE
        Exit Sub
    End If

    OpenCustomer KundeID
End Sub
[...]

Anm.: Das Ergebnis (ClaudeTest.accdb) ist absichtlich nicht nachbearbeitet. Es fehlt z. B. das Verankern des Unterformulars u.ä.
Das wäre mit ein paar weiteren Anweisungen oder manuellem Eingriff schnell behoben, aber ich wollte nur das Ergebnis sehen, das als erstes in die Anwendung eingebaut wurde - also ohne Nachbesserungen. Die Nachbesserungen machte ich nur im Plan (Chat mit Claude).

Ich behaupte: Test bestanden. :)

--
Josef


PhilS

Zitat von: Josef P. am Oktober 13, 2026, 10:29:08und dafür meine wiederverwendbaren Codemodule der Access CodeLib nutzen.
Hast du Claude vorher oder auf anderem Weg weitergehende Informationen zur Access CodeLib gegeben?

Oder ist Claude der Grund dass jetzt auf https://access-codelib.net/ ein "508 - Resource Limit Is Reached"-Error angezeigt wird? ;-)
Neue Videoserie: Windows API in VBA

Klassische CommandBars visuell bearbeiten: Access DevTools CommandBar Editor

Josef P.

:)  :P  ;D

Claude greift auf GitHub zu. ;)
Dafür hab ich mit Claude einen Katalog und Rezepte eingerichtet, damit nicht jedes Codemodule durchgelesen werden muss.

Die Information darüber läuft über einen Skill.

LG
Josef

Josef P.

#3
Update:

Mittlerweile testet ich auch Codex und GitHub CoPilot. (Beide testete ich über deren VS Code Integration.)

Codex
Log: Sie dürfen in diesem Board keine Dateianhänge sehen.
Ergebnis: Sie dürfen in diesem Board keine Dateianhänge sehen.
Ich musste an einer Stelle nacharbeiten: die Filter-Ausdrücke wurden nicht für DAO eingestellt. Es wurde % statt * wurde für like verwendet, weil der SQL-Dialekt nicht eingestellt wurde. (Claude erkannte das selbst.)
Anm.: Abweichende Namen von den anderen Tests hab ich verursacht, da ich vergaß, das im Chat vorzugeben. (gelernt: so etwas muss in die AGENTS.md rein)

CoPilot
Log: Sie dürfen in diesem Board keine Dateianhänge sehen.
Ergebnis: Sie dürfen in diesem Board keine Dateianhänge sehen.
Ich musste eine Formulareigenschaft (AllowEdit) nacharbeiten, die war auf False eingestellt.
Die Filter-Ausdrücke wurden für DAO eingestellt. CoPilot erkannte das wie Claude von selbst.


Zusammengefasst
Alle drei liefern brauchbare Ergebnisse.
Mit Claude war für mich das gestalten (gefühlt) übersichtlicher.

Der Code ist bei allen 3 ok.
Der Code von Claude gefällt mir etwas besser, da auch Konstanten verwendet wurden. Damit sieht man z. B. beeits oben im Codemodul die Select-Anweisung.
Das Filter-Framework setzten alle 3 ordentlich um Die fehlende Dialekt-Auswahl bei Codex sehe ich nicht so kritisch. So etwas lässt sich über die Projektkonfiguration (Agents.md im Projektordner) gut vermeiden.

Bezüglich Feature-Erkennung des Filter-Frameworks sind Claude und CoPilot etwas vor Codex: die ersten 2 erkannten die Möglichkeit, dass nach Eingabe eines Filterwertes in einem Steuerelement die Filterung ausgelöst werden kann. Codex erstellte dafür einen extra Button (nur nach Klick auf den Button wird gefiltert).
CoPilot fragte nach, welche Variante ich möchte - zur Auswahl kam "Button Click" oder "Direkt während Eingabe" .. hier gab ich indirekt den Hinweis für das Framework, da ich mit "Nach der Eingabe eines Filterwertes (AfterUpdate), nicht nach jedem Zeichen beim Tippen" antwortete.

Review
Hier ist das Review von Claude zur Arbeit von CoPilot:

Review: CoPilotTest.accdb
Vorher habe ich den Quelltext neu exportiert (Fast Save).

Umfang: 2 Tabellen (tKunden mit 10 Datensätzen, tLieferanten mit 8 Datensätzen), 4 Formulare, 8 ACLib-Module. Abfragen und Berichte gibt es keine.

🔴 Hoch
Ereignisprozeduren enthalten Logik (fKunde.cls, fLieferant.cls). Das widerspricht eurer Vorgabe.

Form_BeforeUpdate prüft selbst die Eingaben. Besser wäre z. B. Cancel = Not IsRecordValid().
cmdLoeschen_Click enthält den ganzen Löschablauf. Besser DeleteCurrentRecord bzw. DiscardNewRecord.
cmdNeu_Click und cmdSchliessen_Click sind grenzwertig, sollten aber ebenfalls nur eine sprechend benannte Prozedur aufrufen (GoToNewRecord, SaveAndClose).
fKundenliste.cls hält die Regel dagegen sauber ein.

🟠 Mittel
Die Kundenliste ist nicht sortiert. UpdateKundenliste setzt SELECT * FROM tKunden WHERE ... ohne ORDER BY, die Reihenfolge ist also zufällig. Empfehlung: ORDER BY Nachname, Firma oder ORDER BY Kundennummer.

SELECT * in der Liste. Die Liste zeigt nur 5 Felder, lädt aber alle, auch das Memofeld Bemerkung. Besser die Felder ausdrücklich angeben (KundeID, Kundennummer, Firma, Nachname, Ort, Aktiv).

🟡 Niedrig / Hinweise
Die Bedingung 1=0 steht an drei Stellen: in der RecordSource von fKundenlisteDatensaetze, in UpdateKundenliste und in ResetKundenFilter. Eine Konstante wie Private Const NO_RECORDS_CRITERIA As String = "1=0" würde das vereinfachen.

Funktionale Lücke: Aus der Liste kann man keinen Kunden öffnen. Ein Doppelklick, der fKunde mit KundeID öffnet, liegt nahe.

Nach dem Löschen springt Me.Requery in fKunde und fLieferant zum ersten Datensatz. Für den Benutzer wäre es angenehmer, auf dem nächsten Datensatz zu landen.

Einige ACLib-Module verwenden Option Compare Database statt Text. Das ist Fremdcode und sollte nicht angepasst werden. Ich würde es als bewusste Ausnahme stehen lassen.

Positiv:

Der Filter wird über ACLib FilterControlManager/SqlTools gebaut, es gibt kein selbst zusammengesetztes SQL.
Datumsformat ist ISO, Boolean-Werte sind korrekt formatiert.
Beim Start werden wirklich keine Datensätze geladen.
SaveRecord reicht Fehler sauber weiter, mit gesicherten Fehlerinfos und Err.Raise, ohne MsgBox.
ErstelltAm hat den Standardwert Now(), Aktiv den Standardwert True.
Die Referenzen sind minimal (DAO, stdole).