Mcp Access — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Mcp Access (MCP Server) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<!-- mcp-name: io.github.unmateria/msaccess-database -->
Give any AI assistant full control over Microsoft Access databases.
Create forms, write VBA, design tables, manage controls, run queries, build relationships, and edit every corner of an .accdb — all through natural language. 67 tools that turn Access into something you can talk to.
No Access expertise required. Just describe what you want.
"Create a form called Invoices with a ListBox, two date filters, and a search button"
"Add a VBA click handler that filters the recordsource by date range"
"Create a table called audit_log with timestamp, user, and action fields"
"List all controls inside the Payment tab and change the combo's row source"The AI handles the COM automation, design view, VBA modules, binary sections, cache invalidation, and all the ugly parts. You get the result.
access_lint_form flags objectively broken layouts (white-on-white text, overlaps, truncation, off-canvas controls). A checker, not a designer — see the note belowWorks with Claude Code, Cursor, Windsurf, Continue, or any MCP-compatible client.
pip install mcp pywin32File → Options → Trust Center → Trust Center Settings → Macro Settings → check Trust access to the VBA project object model
Or run the included PowerShell script:
.\enable_vba_trust.ps1Global (available in all projects):
claude mcp add access -- python C:\path\to\access_mcp_server.pyProject-only (creates .mcp.json in current directory):
claude mcp add --scope project access -- python C:\path\to\access_mcp_server.pyAdd to your MCP config file (.mcp.json, mcp.json, or client-specific settings):
{
"mcpServers": {
"access": {
"type": "stdio",
"command": "python",
"args": ["C:\\path\\to\\access_mcp_server.py"]
}
}
}Compatible with any MCP-compliant client (Cursor, Windsurf, Continue, etc.).
| Tool | Description |
|---|---|
access_create_database | Create a new empty .accdb database file |
access_close | Close the COM session and release the .accdb file |
| Tool | Description |
|---|---|
access_list_objects | List objects by type (table, module, form, report, query, macro, all). System tables filtered |
access_get_code | Export an object's full definition as text |
access_set_code | Import modified text back (creates or overwrites) |
access_export_structure | Generate a Markdown index of all modules, forms, reports, queries |
access_delete_object | Delete a module, form, report, query, or macro. Requires confirm=true |
access_create_form | Create a new form without triggering the "Save As" MsgBox that blocks COM. Optional has_header for header/footer section, record_source (bind to table/query), default_view (0=Single, 1=Continuous, 2=Datasheet, ...) |
| Tool | Description |
|---|---|
access_execute_sql | Run SQL via DAO — SELECT returns rows as JSON (limit default 500). DELETE/DROP/ALTER require confirm_destructive=true |
access_execute_batch | Execute multiple SQL statements in one call. Supports mixed SELECT/INSERT/UPDATE/DELETE with per-statement results, stop_on_error, and confirm_destructive |
access_table_info | Show table structure via DAO (fields, types, sizes, required, linked status) |
access_search_queries | Search text in the SQL of ALL queries at once (find which queries reference a table, field, or keyword) |
access_create_table | Create a table via DAO with full type, default, description and primary key support in one call. More robust than CREATE TABLE SQL |
access_alter_table | Modify table structure via DAO: add field, delete field (requires confirm=true), rename field |
| Tool | Description |
|---|---|
access_vbe_get_lines | Read a line range from a VBA module without exporting the whole file |
access_vbe_get_proc | Get a procedure's code and position by name |
access_vbe_module_info | List all procedures with their line numbers |
access_vbe_replace_lines | Replace/insert/delete lines in a VBA module directly via VBE |
access_vbe_find | Search text in ONE specific module. To search all modules at once, use access_vbe_search_all |
access_vbe_search_all | Search text across ALL modules/forms/reports in the database at once |
access_vbe_replace_proc | Replace a full procedure by name (auto-calculates line bounds). Strips misplaced Option lines, runs structural health check |
access_vbe_patch_proc | Surgical find/replace within a procedure. Whitespace-tolerant fallback matching + contextual error messages when patches fail |
access_vbe_append | Append code at the end of a module. Auto-strips Option Explicit/Option Compare to prevent misplacement |
| Tool | Description |
|---|---|
access_list_controls | List all controls of a form/report with key properties. Controls inside Pages/OptionGroups include a parent field |
access_get_control | Get the full definition block of a specific control (finds controls inside Pages/OptionGroups) |
access_create_control | Create a new control via COM in design view. Supports class_name for ActiveX (type 119) ProgID initialization. Use type 128 (acWebBrowser) for native WebBrowser |
access_delete_control | Delete a control via COM |
access_set_control_props | Modify control properties via COM in design view |
access_set_multiple_controls | Modify properties of multiple controls in a single design-view session |
access_lint_form | Deterministic check for objectively-broken layout: contrast (WCAG), overlap, out-of-bounds, truncation, sibling inconsistency, zero-size/invisible. Returns verdict PASS/REVIEW/FAIL. Also runs automatically on every control edit |
### ⚠️ A note on access_lint_form — manage your expectations>
This is NOT a designer and there is zero super-design here. Don't expect it to make a form look good, suggest a nice palette, or have any taste — it has none and never will.
>
It is a dumb, deterministic verifier of the obvious, easy-to-check stuff: is the text the same colour as its background? do two controls physically overlap? does a caption not fit its box? is something off the edge of the form, or zero pixels tall? That's it. Plain math — WCAG contrast ratios and rectangle intersection — with a pile of false-positive guards so it doesn't cry wolf.
>
Think seatbelt, not stylist: it won't make the car pretty, it just stops you shipping a form with white text on a white background without noticing. It runs automatically on every control edit so those obvious mistakes surface on their own. If you were hoping for a UI-design AI, this isn't it (honest PRs to make it smarter are very welcome 😄).
| Tool | Description |
|---|---|
access_export_text | Export form/report/module as text via SaveAsText. Does NOT open Design view. UTF-16 LE output |
access_import_text | Import form/report/module from text via LoadFromText. Replaces if exists. Auto-splits CodeBehindForm VBA |
| Tool | Description |
|---|---|
access_get_db_property | Read a DB property (CurrentDb.Properties) or Access option (GetOption) |
access_set_db_property | Set a DB property or Access option — creates the property if it doesn't exist |
access_get_form_property | Read form or report properties (RecordSource, Caption, DefaultView, etc.). object_type required (form or report). Omit property_names for all |
access_set_form_property | Set form/report properties (RecordSource, Caption, DefaultView, HasModule, etc.) via COM in Design view |
| Tool | Description |
|---|---|
access_list_linked_tables | List linked tables with source table, connection string, ODBC flag. name='X' returns one table; names_only=true is a light listing (no connect strings — use it when hundreds of links overflow the result); mask_password=true masks PWD= |
access_relink_table | Change connection string and refresh link — auto-saves credentials (dbAttachSavePWD) when UID/PWD detected. relink_all=true updates all tables with the same original connection. refresh=true re-reads the schema using the table's own connect string (no new_connect, password never dumped) |
| Tool | Description |
|---|---|
access_list_relationships | List table relationships with field mappings and cascade flags |
access_create_relationship | Create a relationship between two tables (supports cascade update/delete) |
access_delete_relationship | Delete a relationship by name |
| Tool | Description |
|---|---|
access_list_references | List VBA project references with GUID, path, broken/built-in status |
access_manage_reference | Add (by GUID or file path) or remove a VBA reference — guards against removing built-in refs |
| Tool | Description |
|---|---|
access_compact_repair | Compact & repair the database — closes, compacts to temp, swaps atomically, reopens |
access_decompile_compact | Remove orphaned VBA p-code via /decompile, recompile, then compact. Typical reduction: 60-70% on heavily-edited front-end databases. Use when a data-free .accdb exceeds 30-40 MB |
| Tool | Description |
|---|---|
access_manage_query | Create, modify, delete, rename, or read SQL of a QueryDef. Delete requires confirm=true |
| Tool | Description |
|---|---|
access_list_indexes | List indexes of a table with fields, primary, unique, foreign flags |
access_manage_index | Create or delete an index. Create requires fields list with optional sort order |
| Tool | Description |
|---|---|
access_compile_vba | Compile and save all VBA modules. Optional timeout to auto-dismiss error MsgBox |
| Tool | Description |
|---|---|
access_run_macro | Execute an Access macro by name |
access_run_vba | Execute a VBA Sub/Function. Standard modules via Application.Run, form modules via Forms.FormName.Method syntax (COM). Optional timeout auto-dismisses MsgBox/InputBox |
access_eval_vba | Evaluate a VBA expression via Application.Eval. Domain functions, VBA built-ins, open form properties, standard module functions. Auto-fallback via temp module for class instances and other expressions Eval cannot resolve |
| Tool | Description |
|---|---|
access_output_report | Export a report to PDF, XLSX, RTF, or TXT via DoCmd.OutputTo |
| Tool | Description |
|---|---|
access_transfer_data | Import/export data between Access and Excel (.xlsx) or CSV. Supports range (Excel) and spec_name (CSV) |
| Tool | Description |
|---|---|
access_get_field_properties | Read all properties of a table field (DefaultValue, ValidationRule, Description, Format, etc.) |
access_set_field_property | Set a field property — creates the property if it doesn't exist |
| Tool | Description |
|---|---|
access_list_startup_options | List 14 common startup options (AppTitle, StartupForm, AllowBypassKey, etc.) with current values |
| Tool | Description |
|---|---|
access_screenshot | Capture the Access window as PNG. Optionally opens a form/report first. Returns path, dimensions (original + image), and metadata. Configurable max_width (default 1920), wait_ms (pumps Windows messages — Timer events fire, ActiveX initializes), and open_timeout_sec (default 30 — sends ESC to cancel if Form_Load hangs on a slow query) |
access_ui_click | Click at image coordinates on the Access window. Coordinates are relative to a previous screenshot (image_width required for scaling). Supports left, double, and right click |
access_ui_type | Type text or send keyboard shortcuts. text for normal characters (WM_CHAR), key for special keys (enter, tab, escape, f1-f12, arrows, etc.), modifiers for combos (ctrl, shift, alt) |
| Tool | Description |
|---|---|
access_find_usages | Search a name across VBA code, query SQL, and control properties (ControlSource, RecordSource, RowSource, SourceObject, DefaultValue, ValidationRule, LinkChildFields, LinkMasterFields) in one call |
| Tool | Description |
|---|---|
access_tips | On-demand tips and gotchas. Topics: eval, controls, gotchas, sql, vbe, compile, design. Zero tokens until called |
1. access_list_objects → find the module or form name
2. access_vbe_module_info → get procedure list and line numbers
3. access_vbe_get_proc → read the specific procedure
4. access_vbe_replace_lines → apply targeted line-level changes
5. access_close → release the file when done1. access_get_code → export to text
2. (edit the text)
3. access_set_code → reimport — binary sections are restored automatically1. access_create_form(db, "myForm", has_header=true) → creates empty form
2. access_create_control(db, "form", "myForm", "CommandButton", {Name: "btn1", ...})
3. access_vbe_append(db, "form", "myForm", code) → add VBA event handlers
4. access_set_form_property(db, "form", "myForm", {HasModule: true, OnCurrent: "[Event Procedure]"})1. access_screenshot(db, "form", "myForm") → capture form as PNG
2. (LLM reads the image and identifies UI elements)
3. access_ui_click(db, x=850, y=120, image_width=1920) → click a button
4. access_ui_type(db, text="search term") → type in a field
5. access_ui_type(db, key="enter") → press Enter
6. access_screenshot(db) → verify the resultVisible = True) so VBE COM access works correctly..accdb closes the previous one._com_executor) with CoInitialize(). This keeps COM in one STA thread while the asyncio event loop stays free for stdio I/O, preventing -32602 errors from message corruption.access_get_code strips binary sections (PrtMip, PrtDevMode, etc.) from form/report exports — access_set_code restores them automatically before importing.acCustomControl): access_create_control now accepts a class_name parameter with the ProgID (e.g. Shell.Explorer.2) to initialize the OLE control. For WebBrowser specifically, use type 128 (acWebBrowser) which creates a native control without OLE complexity. Setting ctrl.Class from COM may not work for all ActiveX controls — manual insertion from the ribbon remains the most reliable method.Forms.FormName.Method syntax (direct COM access, form must be open). Also supports timeout parameter — if exceeded, auto-dismisses MsgBox/InputBox dialogs. For more flexible form interaction, use access_eval_vba.Form_Timer): Now fire during access_screenshot when wait_ms > 0 — the wait loop pumps Windows messages via pythoncom.PumpWaitingMessages(). Other tools still block the message pump.& as & due to MCP transport escaping. Fixed in v0.7.3 with explicit html.unescape() decoding.-32602 Invalid request parameters errorsThe MCP Python SDK (v1.26.0) has a catch-all except Exception in mcp/shared/session.py that swallows real errors and returns a generic -32602 code with no detail. A local patch is applied to this machine that includes the actual exception and traceback in the error response. If you upgrade the mcp package, re-apply the patch — see CLAUDE.md for details.
Bugfix reported by @TvanStiphout-Home (Tom van Stiphout) — thank you Tom, once again, for the laser-precise diagnosis and repro steps. We owe you a beer (or ten).
issue #33).access_vbe_replace_lines, access_vbe_replace_proc, access_vbe_patch_proc and access_vbe_append all call DoCmd.Save to persist VBE changes to the .accdb. When the target module or form was open in the VBE, Access popped a modal "Save isn't available now" error dialog and waited for a click — one dialog per write call, completely blocking the UI. The except Exception: pass swallowed the COM error so edits landed fine, but the watchdog that covers compile/eval paths was absent on the write path so nothing dismissed the dialog. Fix: the four DoCmd.Save calls are now wrapped in a new _save_vbe_module helper that spins up a daemon watchdog thread (0.3 s grace period, same pattern as _call_with_dialog_watchdog in maintenance.py) to dismiss the dialog automatically. No behaviour change: the save remains best-effort and the edit always lands regardless.
Usability fixes from a real editing session against a database with many ODBC-linked tables. No behaviour change for existing callers (still 67 tools).
hundreds of linked tables the tool used to dump every full connect string, blowing past the per-result token cap and forcing a grep. New optional args: name='X' (return just that table, exact + case-insensitive), names_only=true (light listing, no connect strings), and mask_password=true (mask PWD= in the returned connect strings). Defaults preserve the previous output.
its own current connect string (no new_connect needed, password never dumped) — the common "I altered the table on the server, refresh the link" case. relink_all=true refreshes every table sharing the connect string.
used to attach the whole-form lint (pre-existing warnings on unrelated controls drowned out the change). access_create_control, access_set_control_props and access_set_multiple_controls now scope the lint.violations list to the controls they touched (the error/warning/info counts stay whole-form); pass full_lint=true for the unfiltered list.
access_relink_table description now notes that LoginTimeout=8is injected into new_connect, so the returned connect string differs from the one you sent.
Bugfix reported by @jbchea (#32) — thanks!
work added a second "design" entry to the _TIPS dict, silently shadowing the original one. Python keeps only the last assignment, so access_tips('design') returned only the new design-direction guidance and the earlier tip (Design view ↔ VBE close-ordering + SaveAsText per-object-type encoding) was unreachable dead code. The original tip now lives under its own key, access_tips('design_vbe'), so both are reachable again. No behaviour change beyond the tips topic (still 67 tools).
Real design taste for access_build_form — three curated design directions replace the ad-hoc themes (still 67 tools).
theme=despacho|panel|archivo) — each acoherent bundle of a typeface with character, a modular type scale, a dominant+accent palette with WCAG-verified contrast, a spacing density and an accent header band. despacho (serif on warm paper, teal), panel (semibold sans, white card on a cool canvas, slate), archivo (serif, warm editorial, spacious, clay). Each builds lint-clean.
type_scale, SPACE, DENSITY, DIRECTIONS) and adesign guide at access_tips('design') — including the honest ceiling (native Access has no gradients, shadows, rounded corners, blur or animation).
info-only rules — generic_font (Arial/Roboto/Inter/…) anda header-title check folded into hierarchy. Never change the verdict.
_set_section resolved sections via theindexed Form.Section(i), which pywin32 can't late-bind, so it failed silently — the canvas was never painted and the header kept Access' themed default, bleeding past the accent rectangle. Sections are now resolved by their named properties and the band fills the full width.
Better-looking forms by construction — the layout arithmetic moves from the LLM into the MCP (no skill, no hooks). One new tool (67 total).
(title, ordered fields, action buttons, single/two-column) and it computes every coordinate from a canonical 60-twip grid, applies a closed WCAG-safe palette, assigns a tab order and sizes the form + header/footer. The model never picks a coordinate.
mcp_access/design_defaults.py) — single source oftruth for grid, sizes, spacing, fonts and the BGR palette. Documented for hand-placement in access_tips('layout').
access_create_control /access_set_control_props — rounds Left/Top/Width/Height to the 60-twip grid.
info-only layout-quality rules — grid_alignment,spacing_consistency, edge_margin, hierarchy. They enrich the full access_lint_form report without changing the verdict or the embedded mutation lint.
Follow-ups to the attached-mode dialog hangs reported by @CaptainStormfield (#31).
access_eval_vba gains an optional timeout parameter — samedialog-watchdog treatment as access_run_vba, covering both Application.Eval and the temp-module fallback.
_mcp_eval_wrapper temp modules (left behind when a modalblocked their cleanup) no longer wedge the session — they are swept before the next eval fallback.
access_delete_object saves dirty VBA modules best-effort beforedeleting, preventing the "save changes to the design of module X?" prompt.
named in the tool result, instead of being dismissed silently.
Wedged-session detection — thanks to @CaptainStormfield (#30) — plus a usability bughunt round.
wedges the COM session permanently. The open is validated (CurrentDb()), the session resets itself, and every tool call health-checks that the db is still open (auto-reconnect if not). Based on PR #30 by @CaptainStormfield.
instances — a modal raised by one of our blocked COM calls (e.g. a VBA project that fails to load) is dismissed after a 5 s grace, instead of hanging the tool call until a human clicks. Idle dialogs of the interactive user are never touched.
access_vbe_search_all / access_find_usages /access_find_definition report objects_skipped + errors instead of a false "0 matches" when modules are inaccessible.
access_list_references survives broken references (per-propertyfallback to null, is_broken flag, broken_count).
start_line (access_vbe_replace_lines),VBA-only code on a non-existent form (access_set_code), and empty modules (access_vbe_get_lines); batch replace warns on destructive no-op deletes; access_execute_batch gained a limit parameter.
VBE procedure-editing fixes from field reports.
access_vbe_replace_proc no longer deletes the blank separator lineabove a procedure — it preserves leading blank line(s) on replace (a pure new_code='' delete still removes the proc and its leading blank).
spurious "Option … expected in first 5 lines" warning. The check now flags an Option statement only when real code precedes it.
access_vbe_replace_lines accepts new_lines (a list of lines) as analias for new_code, and now warns when a replace deletes lines but inserts nothing — so a misnamed argument can no longer cause a silent destructive delete.
access_vbe_get_proc / access_vbe_module_info docs clarify start_line(includes the blank/comment lines above the proc) vs body_line (the declaration line).
DX fixes for form-building workflows, found while assembling a non-trivial form (modes + ListBox cesta + 23 controls + 589 LOC VBA) end-to-end in a single MCP session. Three of the four bugs were silent traps that wasted ~5–15 min each the first time you hit them.
Fixed:
ac_set_code(form|report) failed with errors while importing on afreshly-created form.** Pure VBA (Option Compare Database + Private Sub btCerrar_Click ...) is not a valid form text export — but LoadFromText was called unconditionally, and restore_binary_sections only works against a baseline that exists on disk. New forms created via ac_create_form have no such baseline, so the import bailed and rolled back. v0.7.38 detects VBA-only input (_looks_like_vba_only) and routes it through _inject_vba_after_import: open in Design view → activate HasModule → write the VBA through VBE. The form layout is preserved, no LoadFromText is attempted. A complete form export (with Version = / Begin Form) still takes the original path.
error pointing at Trust access to the VBA project object model in the Trust Center — the actual cause was HasModule=False. _force_vbe_init now flips HasModule to True when opening the form/report in Design view during the retry, so access_vbe_module_info / _get_lines / _search_all on a form just made by ac_create_form no longer needs a manual set_form_property({"HasModule": true}) round-trip. The fallback error message is also rewritten so it tells you about HasModule before blaming Trust Center, when the object_type is form/report.
'CreateControl.Parent' can not be set. Special keys (section, parent, column_name, left, top, width, height) are now popped from props case-insensitively, so dropping a control into a TabControl Page works whether you pass {"parent": "tabGestion"} or {"Parent": "tabGestion"}. Same goes for Left/Top`/etc.
ac_create_control lost properties Access only exposes via theProperties collection.** Some props raise on setattr(ctrl, "X", val) but succeed via ctrl.Properties("X").Value = val (e.g. ScrollBars on certain control types). The prop loop now retries via the Properties collection before recording an entry in property_errors. Props that genuinely don't exist on the target control (e.g. the UserForm-only MultiLine on an Access TextBox — Access uses EnterKeyBehavior instead) still fail loudly.
Added:
the historical {"Name": "..."} inside props. Without this you had to discover that the new control got an auto-name like Command1 / Label2 and rename it via a second set_control_props call. props["Name"] still wins if both are provided — old fixtures don't change.
Why this release: full form construction in a single MCP session (create_form → create_control ×N → set_form_property → vbe_append ×N → compile_vba) is now a clean path. The previous failure modes all looked like "your code is wrong" or "your Trust Center is wrong" — they were neither.
Critical hotfix for v0.7.35 / v0.7.36 — strict MCP clients (Claude Code in particular) refused every tool with Invalid input: expected "object" on tools.0.inputSchema.type … through tools.64.inputSchema.type, leaving the server visibly connected but with 0 tools available. If you upgraded to v0.7.35 or v0.7.36 and your client started reporting "tools fetch failed" for all 65 tools, this release fixes it.
Fixed:
inputSchema.type === "object" (literal string), and clients that validate the protocol envelope (Claude Code, anything backed by Zod with strict literals) reject the array form on every single tool. Other clients that only check tool input at call time (e.g. some IDE integrations) silently accepted it, which is why the regression went unnoticed in v0.7.35/v0.7.36 CI. Fix: _fixup_schema now takes is_root and skips type-widening at the top level; nested property widening for integer / boolean / number / array / object is preserved, so the v0.7.35 fix for string-typed batch arguments still works.Why this matters: anyone on Claude Code (or any strict MCP client) who upgraded to v0.7.35 or v0.7.36 saw mcp-access turn into a black box — the server connected fine, but no tools were exposed and no error message in the client made the root cause obvious. The breakage was 100% on the server side and affected 100% of tools.
Five new capabilities — three new tools, one docs-only upgrade for macros, one transparent refactor for Office-version autodetect. 62 → 65 tools. All changes additive; no existing schema or signature changed.
Added:
MSys*, ~*) and linked tables (querying remote SQL servers per text column rarely ends well). Per-table and total caps; optional tables whitelist; match_case. Returns matches grouped by table with an _excerpt around each hit and _matched_fields per row.form / report / module / class_module / query / macro to a new name. Uses SaveAsText → LoadFromText with binary sections (PrtMip / PrtDevMode / NameMap / GUID) preserved by reading the raw export (bypasses strip_binary_sections). VBA code-behind rides along via the existing ac_set_code injection. Refuses to overwrite unless overwrite=true.get / set / auto_renumber the TabIndex of controls on a form/report. set does a two-phase reassignment (park at high indices, then assign 0..N-1) so it doesn't trip Access's per-section uniqueness constraint mid-loop. Skips controls that don't support TabIndex (Label, Line, Rectangle, Image, PageBreak, Page). Optional section filter.Changed:
access_get_code / access_set_code (UTF-16 encoding) plus access_list_objects, access_run_macro, access_delete_object. Tool descriptions now name macros explicitly and access_tips('macros') documents the read → edit → write workflow.16.0 / Office16 strings used by _Session._suppress_recovery_dialog, _Session._decompile and ac_decompile_compact now come from a one-shot registry probe (_Session._detect_office_install). Detection walks Software\Microsoft\Office\<ver>\Access\InstallRoot\Path under HKLM, HKLM\\WOW6432Node and HKCU (per-user C2R), picks the highest matching version with a working MSACCESS.EXE, falls back to App Paths\MSACCESS.EXE\(Default), and finally to the previous hardcoded paths. Schema of access_decompile_compact is unchanged; behaviour on machines with a normal Office install is identical to v0.7.35.Preventive bug sweep across the codebase. No reported regressions; the issues below were caught by a full-package review while everything in production was working.
Critical:
taskkill fallback added in the previous diff captured the Access PID by calling app.hWndAccessApp() inside quit(), but quit() runs from the atexit main thread while the COM proxy was created on the dedicated COM worker. Cross-thread STA calls return RPC_E_WRONG_THREAD silently, the result was pid=None, and the taskkill fallback was effectively a no-op exactly when it was needed (Access hung on Quit). PID is now captured at the end of _launch() — same thread that created the proxy — and stored as cls._pid.array / object, so batch tools (execute_batch, create_table, manage_index, set_multiple_controls, vbe_patch_proc, vbe_replace_lines, find_definition, run_vba, etc.) failed for those clients. Now both schemas and the coercer accept JSON-encoded strings for arrays and objects, plus number widening and additional truthy literals (on, y, si, sí).End after Begin Form, but a subform has its own nested Begin Form ... End — the injection landed inside the subform and corrupted the result on ac_set_code. Now tracks full block depth (same approach as the v0.7.34 control parser fix) and injects only at the outermost End.errors="replace", which substituted ? for any character the codepage couldn't represent (emoji, asian characters, ✓ in a comment). Switched to errors="strict" and the resulting UnicodeEncodeError now carries a snippet of the offending substring with an actionable hint.time.sleep(2). Now the parameter controls the watchdog grace window (default 2, clamped to 1-30).Medium:
get_lines, get_proc, module_info, find, search_all) now close the form/report Design view before touching the CodeModule — same protection write tools already had. Skipping this could surface as Catastrophic failure (-2147418113) when the same object was open in design mode.ac_vbe_replace_lines no longer calls cm.DeleteLines(line, 0) (which raises in VBE) when count clamps to zero at end of module. Error message now lists separate upper bounds for replace/delete vs pure insert (was: misleading single range).ac_vbe_patch_proc normalizes find_text / replace_text line endings to CRLF before the exact-match check — callers commonly send LF and the exact match always fell through to the ws-normalized fallback. Also warns when find_text matches more than once (only the first occurrence is replaced via .replace(..., 1))._proc_kind raises a descriptive error when a procedure name resolves to multiple kinds (a class with both Property Get Foo and Property Let Foo — normal VBA). Previously the first kind found was silently picked and write operations acted on it regardless of which the caller meant.set_db_property / set_field_property infer dbDouble (7), dbDate (8), dbSingle (6), dbMemo (12) when creating properties from float / datetime / long-string values. Previously these fell to dbText (10) and were stored as string._eval_via_temp_module pre-binds temp_name so the cleanup finally can log a sensible name if proj.VBComponents.Add(1) succeeded but the subsequent comp.Name access failed (was: UnboundLocalError in the cleanup masking the real failure).ac_create_relationship validates that the named tables exist and that each local / foreign field exists on its respective table before Append. Replaces a cryptic DAO error ("Item not found in this collection") with a message that names the missing item._check_module_health / ac_vbe_module_info regexes recognize Public Static Sub | Function | Property (was: at most one modifier — VBA accepts both scope and Static).decompile_compact resets _Session._pid and _attached after killing the spawned Access, keeping the new quit() fallback consistent.Hardening:
read_tmp tries utf-8 before cp1252 — cp1252 is single-byte and almost never raises, so genuine UTF-8 files were being mis-decoded as mojibake._invoke_app_run validates len(args) <= 30 explicitly instead of producing a confusing InvokeTypes failure via a negative-multiplier padding ([Missing] * -1 is just [])._split_code_behind matches CodeBehindForm / CodeBehindReport only at start of a line, so a property value that happens to contain the literal can't trick the splitter.SELECT … INTO (make-table query) is flagged as destructive in execute_sql / execute_batch. Dead _SQL_LINE_COMMENT / _SQL_BLOCK_COMMENT regexes removed (the destructive guard already used _sql_effective_prefix).ac_create_database requires the path to end in .accdb or .mdb.relink_table UID/PWD detection uses a parameter-boundary regex ((?:^|;)\s*(UID|PWD)\s*=) instead of a substring check that would false-positive on values like APP=My UID Manager.ac_table_info escapes ] in table names by doubling.compact_repair cleans up the orphaned _compact_tmp.accdb on the rolled-back swap path.server.py / dispatcher.py guards against non-string code to avoid TypeError inside the error handler.tools.py module docstring updated 58 → 62 tools.`access_list_controls` silently lost controls inside `Page` / `OptionGroup`: when any earlier control in the same Page had a multi-line property block (GUID = Begin … End, NameMap = Begin … End, ConditionalFormat = Begin … End, etc.) the depth counter inside the control's body matched plain Begin <Type> but not Property = Begin. The property's closing End was decremented without ever being incremented — the enclosing control closed prematurely, and every control that came after it inside the Page was never enumerated. The form-level loop in _parse_controls already handled this; the per-control loop now mirrors it (r"^\w+\s*=\s*Begin\s*$").
Visible symptom: access_list_controls reported a TabControl Page as a 15-line empty stub even though the Page actually contained dozens of controls. Fixed in mcp_access/controls.py:_parse_controls. Tool count unchanged (62).
`access_create_form` silently dropped `record_source` and `default_view`:
record_source="myTable" or default_view=2 got back a form that was bound to nothing and rendered in Single view. The arguments were not in the input schema, not in the dispatcher, and not in ac_create_form's signature, so the MCP transport accepted them and discarded them without warning. Symptom downstream: every bound TextBox on the form (or on a continuous subform built on top of it) showed #Name?, because the form had no recordset to resolve ControlSource against.ac_create_form(db_path, form_name, has_header=False, record_source=None, default_view=None) now applies RecordSource and DefaultView on the live CreateForm() object before DoCmd.Save. Schema in tools.py and the dispatcher.py branch updated to forward the new arguments. Both fields are optional and back-compat (existing access_create_form calls without them keep working unchanged). Result dict now echoes back the applied values when set.access_create_form("subf_lines", record_source="my_table", default_view=1) so the new form is immediately a continuous subform bound to the table. Without this fix, you had to follow up with access_set_form_property to bind it manually — and the missing RecordSource was easy to miss until the parent form rendered and showed #Name? everywhere.Tool count unchanged (62).
Fix `_vbe_code_cache` returning stale text after external edits — thanks to @TvanStiphout-Home for reporting in issue #26:
access_vbe_get_proc (and other VBE read tools) could return a cached snapshot of a procedure that no longer matched what was in the VBE module. The cache was only invalidated when the MCP itself wrote to the module — manual edits in the VBE (including Ctrl+Z), add-ins, or any change made outside the MCP left the cache stale. Worse, the WRITE tools (access_vbe_replace_proc / patch_proc / replace_lines / append) also read through the same cache before writing, so they could overwrite the wrong baseline and corrupt code._cm_all_code() no longer caches; it reads directly from COM via cm.Lines(1, total) on every call. The _vbe_code_cache dictionary and all its .pop / .clear invalidation sites have been removed (core.py, vbe.py, code.py, controls.py, compile.py, maintenance.py, relations.py, plus stale imports in database.py and helpers.py). The _Session._cm_cache (CodeModule COM proxies) is kept — proxies are live, not snapshots, so they don't suffer the same problem and they save 2 COM calls per VBE tool. Tom's case (Claude wrote a buggy version with replace_proc, Tom reverted manually in VBE, the next get_proc still served the buggy cached version) now reads fresh from COM and matches the real module state.`access_relink_table` no longer hangs the COM session on a bad ODBC connect string:
_ensure_login_timeout() injects LoginTimeout=8 into the connect string when missing, so DAO bails out instead of opening a dialog. Before touching DAO at all, _odbc_preflight() test-opens the connect string via ADODB; if that fails, ac_relink_table raises a clean RuntimeError with the underlying ODBC error. _detect_named_instance() adds a hint for the common case of a SERVER=host\instance named instance whose UDP 1434 (SQL Browser) is firewalled — the suggested fix is to switch to explicit TCP SERVER=host,port.Tool count unchanged (62).
Bug fix — thanks to @CaptainStormfield (#27):
len(decoded.splitlines()) treats a trailing newline as a terminator, so it does not count the extra blank line that VBE's InsertLines does add when the input ends in \r\n. In access_vbe_replace_lines batch mode this triggered spurious WARNING: Expected N lines after edit, but module has N+k health-check warnings — one count off per operation whose new_code had a trailing newline (intentional blank-line separators between procedures). Fix: replace the splitlines() estimate with an exact measurement — read cm.CountOfLines after InsertLines and subtract the pre-insert total. Same off-by-N pattern was also present in ac_vbe_replace_proc and ac_vbe_append; both corrected the same way for consistency..mcp.json and .claude/ so local Claude Code / MCP config doesn't leak into commits.Tool count unchanged (62).
Audit pass — bugs and UX rough edges found during a full-package review:
DoCmd.Save after writing, unlike access_vbe_replace_proc / access_vbe_replace_lines / access_vbe_append. On forms/reports this meant the object's dirty flag was never raised, so the edits could be discarded on close. Fix: added the same app.DoCmd.Save(obj_type_code, object_name) block that the other three writers use.access_execute_sql's confirm_destructive pattern.-- note\nDELETE FROM t and /* prefix */ DROP TABLE t passed through the startswith("DELETE"|"DROP"|...) check. New _sql_effective_prefix() walks past leading -- line comments and /* ... */ block comments before prefix-matching, in both ac_execute_sql and ac_execute_batch.DoCmd.OutputTo silently overwrites, which can lose an earlier export if the same output_path is reused.WM_CHAR with ord(ch), which routes through the window's ANSI code page and produces mojibake on English locales. Characters ≤127 still use WM_CHAR so keyboard shortcuts work the same."" instead of omitting the argument when a procedure-scope filter is not wanted. Now treated as "search the whole module"._attached=True is set. Zombie marshalled references from a dying process now fall through to DispatchEx cleanly.code.py and controls.py, now lives in helpers.split_code_behind with backwards-compatible re-exports at both old names."Error en %s" log message in dispatcher translated to English; removed an unused DB_SEE_CHANGES import in database.py.Tool count unchanged (62). Documented false positives from the review (type-name case mismatch in ac_table_info, silent-typo in ac_set_control_props) were verified against the code and turned out to be non-issues.
Polish of `access_find_definition` (v0.7.27 follow-up):
Public Const MAX_ROWS = 100 ' default page size returned value = "100 ' default page size" instead of "100". The naive split("'", 1)[0] stripper also broke on string literals with apostrophes like Public Const Name = "O'Brien", truncating the value at the inner '. Fix: new _strip_trailing_vba_comment() helper that respects "..." string literals (same state machine as _split_top_level_commas), applied at all value-extraction sites.Public Default Property Get Item(...) — valid VBA on class modules with a default member — was invisible to the tool. _FD_PROC_RE only allowed Static. Fix: widened to (?:(?:Static|Default)\s+)?.Public Const FOO As Long = _\n &H1000 returned value = "_" (only the first physical line was parsed). Public Declare PtrSafe Function SendMessage _\n Lib "user32" … had the same problem. Fix: new _join_continuations() helper folds trailing-_ continuations into single logical statements before matching. The reported line still points at the first physical line of the statement.["module", "form", "report"]. Passing scan_types=["module"] skips form/report code-behind and is ≈7× faster on cold scans, because form/report scanning triggers a Design-view open/close round-trip per object when the VBComponent cache is empty. Useful when you know the target is a public declaration in a standard module — as Tom pointed out in his original request: "All public enums are defined in modGlobal".subkind is now only emitted when it carries information beyond kind — i.e. on property (Get/Let/Set) and declare (Sub vs Function). For sub and function it was redundant with kind and has been removed.New tool — thanks to @TvanStiphout-Home:
access_find_usages. Scans every standard module, form code-behind and report code-behind for DECLARATIONS of a given name and returns where each one lives (object, line, full declaration, scope, and for constants the literal value). Detects Const (including multi-const lines like Const A = 1, B = 2), Enum + enum members, Type + type fields, Sub, Function, Property Get/Let/Set, Declare (Win32 API), and module-level Dim/Public/Private/Global variables. Locals inside a Sub/Function/Property are correctly skipped — they are not definitions in the "go to" sense. Case-insensitive by default (VBA is), with an optional kinds whitelist to narrow results (e.g. ["const","enum_member"] when resolving a symbol used as a numeric literal like dbAccess). Previously the agent had to spawn throw-away VBA helpers with MsgBox just to discover the value of a named constant — this tool makes that unnecessary.Bug fix — thanks to @TvanStiphout-Home (#25):
GetActiveObject attach fix applied to _Session._launch(), but the auto-decompile path in _Session._decompile() (triggered on first compile per session) and ac_decompile_compact() still unconditionally called cls._app.Quit(1) and re-launched — killing the user's attached Access and spawning a fresh one via DispatchEx. Fix: new _Session._attached flag tracks whether we attached (GetActiveObject) or launched (DispatchEx). When attached, /decompile now releases the file lock via CloseCurrentDatabase() only, never calls Quit(), and reuses the same COM reference after the subprocess finishes. atexit handler also skips Quit() when attached so MCP shutdown doesn't kill the user's Access. Defence-in-depth PID-diff cleanup (_list_msaccess_pids() before vs. after the subprocess) kills any forked msaccess.exe children that escape taskkill /T. Refuses to decompile a path when the user has a different DB open, instead of silently closing their unsaved work.Bug fix — thanks to @TvanStiphout-Home (#24):
win32com.client.DispatchEx("Access.Application") unconditionally created a new COM instance instead of attaching to the running one. Fix: _launch() now tries win32com.client.GetActiveObject("Access.Application") first, syncs cls._db_open from the attached instance's CurrentDb().Name so connect() can skip an unnecessary _switch() when the target DB is already open, and falls back to DispatchEx only when no running instance exists. The DispatchEx fallback is still required after /decompile kills (stale ROT entries) — GetActiveObject will fail cleanly in that path since the process was taskkill'd.Enhancement — thanks to @AccessWizard (#23):
msoAutomationSecurityForceDisable before OpenCurrentDatabase() and restores to msoAutomationSecurityLow (1) in the finally block. Does not replace the Shift key bypass (Access ignores AutomationSecurity for AutoExec macro objects), but provides an extra safety layer for VBA auto-run code in edge cases where the Shift key doesn't register (remote desktop sessions, key events eaten by other processes).Bug fixes:
inserted = len(new_code.splitlines()) used the pre-unescape string; now uses len(decoded.splitlines()) so the count matches what VBE actually received.ac_vbe_patch_proc called cm.ProcCountLines() without protection after patching — if the patch corrupted the proc structure, this threw an unhandled COM error. Now wrapped in try/except.SetValueEx call failed, CloseKey was never reached. Now uses try/finally to guarantee key closure..bak original and the compacted temp file.comp.Name on a potentially dead COM object. Now uses the pre-captured temp_name variable.Option Compare/Option Explicit, so code with these directives at line 6+ got duplicates. Now searches the entire code.ac_set_code now uses locale.getpreferredencoding() (the system ANSI codepage) instead of hardcoded cp1252, so non-Western Windows systems (Greek, Cyrillic, etc.) work correctly.Bug fixes — thanks to @CaptainStormfield:
_proc_kind() only tried kind=0 (vbext_pk_Proc) and kind=3 (vbext_pk_Get), completely missing kind=1 (vbext_pk_Let) and kind=2 (vbext_pk_Set). Any Let-only or Set-only property (e.g. Property Let ItemPrefix) would fail with "Sub or Function not defined". Fix: new _ALL_PROC_KINDS = (0, 1, 2, 3) tuple — _proc_kind(), _proc_of_line(), and all callers now iterate all four VBE proc kinds. The old constant _VBEXT_PK_PROPERTY = 3 was misleadingly named (3 is specifically vbext_pk_Get, not a generic "property" kind) and has been replaced with explicit _VBEXT_PK_LET = 1, _VBEXT_PK_SET = 2, _VBEXT_PK_GET = 3.seen set deduplicated by procedure name alone, so when Property Get Foo was encountered first, Property Let Foo with the same name was skipped entirely. Fix: deduplicate by (name.lower(), keyword.lower()) so Get, Let, and Set variants of the same property are listed as separate entries. Each entry now includes a "keyword" field (e.g. "Property Get", "Property Let"). A _KEYWORD_TO_KIND mapping lets module_info pass the correct VBE kind directly to _proc_bounds() instead of relying on the blind iteration in _proc_kind().ProcStartLine fails for a specific kind (Access quirk with certain Let-only or Set-only properties), the old fallback emitted an entry with no body_line or count. Fix: scans forward in the source text from the declaration line to the matching End Property/End Sub/End Function keyword to derive an accurate count.taskkill /F /T kills the /decompile subprocess, Access doesn't run cleanup code and can leave a stale entry in the Windows Running Object Table (ROT). The subsequent Dispatch("Access.Application") in _Session._launch() latched onto this dead ROT entry, yielding a zombie COM object that passed the _app.Visible health check but failed on any database operation. Fix: replaced win32com.client.Dispatch with win32com.client.DispatchEx — always creates a fresh instance, bypassing the ROT entirely. Added a 1-second sleep after taskkill in both _Session._decompile() and ac_decompile_compact() as belt-and-suspenders to allow Windows time to evict the dead entry.Bug fixes — thanks to @CaptainStormfield and @unmateria (wizard-during-compact report), and @TvanStiphout-Home (class module request):
maintenance.py had app2.RunCommand(137) # acCmdCompileAllModules = 137 — but 137 is `acCmdNewObjectReport`, not acCmdCompileAllModules (the correct value is 125 for compile-only or 126 for compile-and-save). Every ac_decompile_compact invocation was silently opening the Report Wizard and blocking the COM thread indefinitely until a human clicked Cancel. The "intermittent" symptoms in the original report were actually 100% reproducible — the wizard was always there, just sometimes hidden behind other windows. Fix: changed RunCommand(137) → RunCommand(126) (acCmdCompileAndSaveAllModules).CompactRepair nor the /decompile subprocess had dialog protection — only OpenCurrentDatabase did. Fix: new _call_with_dialog_watchdog(app, label, callable_fn) generic helper wraps any blocking COM call with a polling daemon thread that dismisses any Access-owned dialog every 0.5s via _dismiss_access_dialogs. _compact_with_watchdog is a thin wrapper around it. The RunCommand(126) call in ac_decompile_compact is also wrapped in this helper. _Session._decompile() and ac_decompile_compact replace their fixed time.sleep(3) + sleep(5) sequence with a polling loop that calls _dismiss_dialogs_by_pid(proc.pid) on the standalone MSACCESS subprocess.VK_RETURN) to dismiss blocking dialogs — dangerous on wizards, as Enter clicks "Next >" and advances the wizard, creating stray Report1/Form1 objects. Rewritten as a polling loop that delegates to _dismiss_access_dialogs with the new Cancel-first button priority.set of target button labels with undefined iteration order, so it could click any of End/OK/Cancel depending on set hash ordering. Now uses an explicit priority tuple ("cancel", "cancelar", "end", "finalizar", "ok", "aceptar") — Cancel first so wizards cancel cleanly, End second to preserve existing ac_run_vba behaviour on VBA runtime-error dialogs (which have no Cancel button)._dismiss_dialogs_by_pid now matches windows where class == '#32770' OR the title contains "wizard" / "asistente" (case-insensitive). Catches non-standard wizard windows that don't use the #32770 class.New feature:
Attribute VB_* lines at the top of the text. Previously, object_type="module" always created a standard module. Tested on production (Access 2016): Application.LoadFromText(acModule=5) distinguishes class from standard modules by the presence of Attribute VB_GlobalNameSpace, Attribute VB_Creatable, Attribute VB_PredeclaredId, Attribute VB_Exposed at the top of the file — NOT by a VERSION 1.0 CLASS header (that header is for VBComponent.Export/Import, a different mechanism; passing it to LoadFromText makes Access interpret the header lines as literal VBA code and creates a corrupt Type=1 standard module). New _ensure_class_module_header(code, name) strips any BOM, strips any legacy VERSION 1.0 CLASS / BEGIN / END / Attribute VB_Name block the user may have pasted from a VBComponent.Export file, detects existing Attribute VB_GlobalNameSpace (round-trip safe — feeding access_get_code output back does not duplicate), and prepends the 4 attribute lines with CRLF endings. class_module re-uses acModule=5 under the hood — no changes needed in access_get_code or access_delete_object. Verified on production DB round-trip: create → read → re-import → overwrite → delete, all with VBComponent.Type == 2.Bug fixes — thanks to @CaptainStormfield (PR #17):
get_proc, module_info, replace_proc, patch_proc, find) hardcoded kind=0 (vbext_pk_Proc). Property procedures require kind=3 (vbext_pk_Property). New helpers _proc_kind(), _proc_bounds(), _proc_of_line() try kind=0 first and fall back to kind=3app.VBE.VBProjects(1) could return acwzmain (wizard library) instead of the user's databas~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.