Releases: hallboys/MCP4Acumatica
Releases · hallboys/MCP4Acumatica
Release list
25R2-0.52.0
Added
- GI alignment diagnostics + a
/docs/admin/gi-alignmentconsole page.resolveFieldsrefuses a GI's curated column annotations wholesale when the design-row → OData-property mapping isn't provably unique — the right call, since a shifted mapping attributes every description to the wrong column and nothing downstream can detect it. But the refusal was invisible:describe_inquirysimply returned bare names while the descriptions sat correctly in Acumatica.resolveFieldsnow reports its own verdict through an optional sink intoGiRegistry.alignment(GiAlignmentDiagnostic[]— status plus activeRows/properties/captioned/described/hoisted per GI), and the new admin page renders it worst-first with a per-status remedy. The statuses separate causes that need different fixes:alignmentAmbiguous(caption the ambiguous columns),feedMissingActiveFlag/feedRowsExceedProperties(re-importMCPGIFields.xml, clear the GI cache),noMetadata,noFieldRows. The page reads only the cached registry from KV — no Acumatica request, no borrowed user token, nothing in Acumatica's audit trail. The diagnostic lives onGiRegistry, never onGiRegistryEntry, so nothing model-facing changed; registries built before this version simply report no alignment data and the page says how to force a rebuild. Tool count unchanged at 51.
Fixed
expectedTypeFamily()classified any field merely starting with "is"/"has" as boolean, refusing whole GIs. The prefix test ran against the lowercased field name, so/^(is|has)[a-z]/matchedissueDate("is" + "sueDate"). That made it contradict its ownEdm.DateTimeOffsetproperty, score as an impossible pairing, and refuse the entire GI's annotation. Found onFS-Licensesafter the operator had captioned every hoisted key exactly as prescribed — and because the conflict sat on an uncaptioned row, no caption edit could ever have fixed it. Now tested as/^(is|has)[A-Z]/against the original camelCase, where real DAC booleans (isActive,hasChildren) have the uppercase boundary andissueDate/issuedBy/hashValuedo not; an all-lowercase field yields no constraint rather than a wrong one, which is the safe direction for a check whose false positives silently drop annotations. Two regression tests, including an end-to-end case built from the realFS-Licensesshape.
Notes
- Re-measured the refusals for the first time since 0.48.2: 118 of 121 gated GIs align, 3 refuse, and the refusals hide ZERO curated descriptions. Every GI that has column descriptions aligns. The long-quoted "13 refuse" figure predated the 0.49.1 bitmask-DP fix (which existed to stop false refusals) and had never been re-measured. Also disproved: caption density does not predict refusal —
IN-StockItemaligns at 7 % captioned andGL-Journal Transactionsat 0 %, because the declared-type constraint resolves them. - Caption-pinning validated live, and it is a declaration rather than a guess. Acumatica derives a property name from the column's display name, so captioning a row causes its property name and the mapping is then correct by construction.
HPL-PHYINVDATADBandHPL-AcuStockIssuesboth flipped toaligned, with values confirming the pairings. Two cautions learned in the process: a typo in a caption becomes the property name (DcoumentType_2,LastMoidifiedare live examples — correcting one renames the property and breaks consumers, so fix before any description references it), and collision suffixes must be captioned literally (ReferenceNbr_2). - Known gap recorded, not fixed: a GI whose name contains a URL path separator is registered but unusable. A GI named with
/resolves in$metadata(the server matches vianormalizeName, which strips punctuation) so the registry builds a full entry — whilerun_inquiry404s andlist_generic_inquiriesnever returns it. It had always been undiscoverable despite being taggedExposedToMCP; the operator un-exposed it.align_columns.mjschecks for this and the server does not. Same file also models a dropped-row stateresolveFieldslacks — that drift is now recorded in CLAUDE.md. - Dead end, so it isn't retried: do not derive the hoist count by sweeping candidate values. On one GI the declared
hoist=2refuses while bothH=0andH=1align, so a sweep produces multiple candidate answers and must refuse anyway. Relatedly, Acumatica sometimes hoists columns that are not declared keys, a shape the hoist-prefix model cannot express at all.
25R2-0.51.1
Fixed
FORM_ID_DEF_REnow accepts escaped parentheses, so Form IDs resolve on a DITA-converted docs corpus. Acumatica published the official documentation as Markdown atgithub.com/Acumatica/Acumatica-AI-Resources(branch per release,2026R1first) on 2026-08-26. That set is converted directly from DITA rather than from PDF, and escapes literal parens in prose —Form ID: \(AP301000\)— which the previous pattern could not match. The failure was silent: zero Form IDs and no error, i.e. the whole Form-ID lookup path foracumatica_get_doc_sectionwould quietly return nothing. Covered by a new regression test. Note this is correct converter behavior, not an Acumatica defect; the assumption of unescaped parens was ours.
Changed
- Documentation sources: the public GitHub repo is now documented alongside the Beacon Portal.
docs/upgrading-acumatica.md§3b,docs/documentation-tools.md, thebuild-docs-index.mjsheader and its no-files error message, and the CLAUDE.md docs-tools section previously named Beacon as the only source. Both are now described with their tradeoffs (GitHub: DITA-converted, cleaner tables, Form IDs recoverable from filenames, 2026 R1+ only; Beacon: PDF-converted, the only source for earlier releases), plus two warnings that apply either way — the documentation is licensed content and public readability is not redistribution rights (the GitHub repo excludesDocumentation/from its GPLv3 and marks it all rights reserved), and an index must match the release the instance runs or it describes fields that aren't there. - Recorded that the rest of that repo is
GPL-3.0-onlyand therefore cannot be copied into this Apache-2.0 project.
Notes
- GI column-description alignment: the "resolve names from the display name" avenue is closed. Acumatica documents that OData property names are generated from the field's display name (Preparation of an Inquiry for Exposure → "Supporting the OData Specification"), which suggests replacing the positional aligner with direct name resolution. Tested and disproven:
GIResult.fieldNameis a virtual (unbound) field — Acumatica reportsFilter on '{0}' is not allowed because it is a virtual field— so it is never projected into the GI's SQL and is NULL over OData (null in 1 000 of 1 000 active exposed result columns across 60 GIs). Because a virtual field is uniformly null, no per-GI investigation can change this and the GIs that currently refuse alignment cannot be rescued this way;resolveFieldsandalign_columns.mjsstay positional. Recorded in CLAUDE.md so the avenue isn't retried. Also confirmed documented, and now cited where relevant: keys always appear in the EntityType even when absent from the Results Grid; the_WithParametersFunctionImport is the supported way to bind GI parameters (validating the existing parameterized-GI refusal); and formula-calculated columns cannot be sorted or filtered (validating the 0.49.0 calculated-column pre-flight). Column order, the_Ncollision-suffix rule, andCaption's effect on property names remain undocumented. - Known work before the GitHub corpus can actually be used: an ingestion adapter. Verified against real
2026R1files,build-docs-index.mjsdegrades silently on a topic-per-file DITA corpus in two ways — heading attribute blocks ({#id .class}, present on 78/78 sampled headings) leak into the breadcrumbs that search matches, and the form heading shifting from##to#stops Form IDs propagating to a form's tabs, so a Form ID lookup returns the intro and no field tables. Tracked as a TODO for the 26R1 move; detail and fixes indocs/upgrading-acumatica.md§3b.
25R2-0.51.0
Added
- Documentation-knowledge tools (
acumatica_search_docs,acumatica_get_doc_section) — 51 tools total. Search + retrieval over the official Acumatica documentation (49 module/developer guides, the Form/Report Reference with every screen keyed by Form ID, and the release notes) for the instance's release. Motivation:help.acumatica.comintermittently bot-blocks browsers and AI clients ("refused for this browser"), so "the model can web-search the docs" — the original reason documentation lookups were scoped out — proved unreliable in practice. Same architecture as the schema-knowledge tools: an OSS ingestion script (scripts/build-docs-index.mjs) that the operator runs against the Markdown documentation set downloaded with their own portal login, producing a private index in theINDEX_STORER2 bucket (licensed content is never committed or redistributed); the tools register conditionally when the index exists. Design points: the memoized catalog holds only heading breadcrumbs + Form IDs (~3.5 MB — search matches section headings, not body text; Acumatica's headings are descriptive enough, and a Vectorize upgrade path stays open behind the same functions); section text lives in ~700 KBdocs-chunks/*R2 parts fetched on demand through a small bounded cache, so the ~40 MB corpus is never held in worker memory;acumatica_get_doc_sectionaccepts a Form ID directly (e.g.AP301000→ the screen's purpose, toolbar commands, tabs/fields) and returnsprev/nextneighbors for browsing; docs output bypasses field redaction (vendor documentation legitimately discusses fields named SSN etc. — new opt-out oncallTool, valid only for tools that cannot return tenant records).upload-indexes.mjsuploads chunk parts before the catalog so a live worker never resolves a new catalog against missing parts. New docs page at/docs/documentation-tools; upgrade step added to the upgrading guide (the index describes one release). 19 new unit tests (ingestion cleanup/chunking/Form-ID scoping + search/resolution); the ingestion script exports its pure functions and only runsmainwhen executed directly. npm run build-docs-indexscript; removed the stalebuild-gi-indexscript entry that pointed at a file that doesn't exist.
25R2-0.50.3
Added
- Releases are now created by CI. Pushing a version tag triggers
.github/workflows/release-on-tag.yml, a thin caller of the org-wide reusable workflow (hallboys/.github), which creates the GitHub Release with that version's CHANGELOG section as notes (auto-generated notes as fallback), idempotently. Previously tags were pushed without Release objects, leaving the repo's Releases page stale (stuck at 0.47.0 while main was at 0.50.2 — twelve releases were backfilled by hand). The close-session checklist now verifies the CI-created release instead of prescribing a manualgh release create.
25R2-0.50.2
Changed
acumatica-gi-descriptionsskill: four hard-won lessons added to the reference docs. (1) The silent-no-op 200: aResultGridPUT issued right after a GI design save can return 200 while persisting nothing — read-back is the only truth, and the fix is re-GETting the record for fresh detail ids. (2) Caption-pinning as a self-verifying procedure: pin an ambiguous hoist by captioning the key column with the property name OData already reports, gated on a before/after$metadatadiff (byte-identical = correct no-op; changed = wrong pick, revert). (3) Blocked saves breed numbered GI copies that inherit exposure flags and pollute the AI menu — sweep, back up, and delete them after any blocked-save episode. (4) A new "Re-verify when anything changes" workflow step: access grants, join changes (INNER→LEFT rewrites a grain), and column edits silently invalidate existing descriptions; audit and correct only what observation contradicts. Also replaced one residual real vendor code in an example with a neutral placeholder.
25R2-0.50.1
Changed
- Documentation hygiene: tenant-specific example identifiers removed. Real inquiry names and warehouse codes from the reference deployment that had accumulated in docs, changelog entries, code comments, and test fixtures were replaced with neutral placeholders (e.g.
MAIN01, "a production forecast GI"). No functional change — comments, docs, and test-fixture strings only; all 154 tests unchanged and passing.
25R2-0.50.0
Changed
- Upgraded the
agentsSDK from 0.0.98 to 0.21.0, which carries the streamable-http transport rewrite that fixes concurrent-request response correlation. Under 0.0.98 two concurrent tool calls in one session could have their responses crossed — arun_inquiryfor one GI returning another GI's rows, with the other call'stopN— which is the silent-wrong-data failure class, indistinguishable from a legitimate result. 0.21.0 scopes request→stream mappings per connection, prefers the originating connection, and raises an explicit-32603when two connections claim the same request id rather than silently crossing payloads.@modelcontextprotocol/sdk(1.30.0) andzod(4.x) were transitive dependencies of agents 0.0.98 that this codebase imports directly; they are now required peers ofagentsand are pinned as explicit direct dependencies.@cloudflare/workers-typesmoves to v5 (required bypartyserver0.5.x and currentwrangler).
- The audit-log flush is scheduled through the Agent
schedule()API instead of a raw DO alarm. Since 0.21.0 the baseAgentclass owns the DO's single alarm slot — its scheduler re-arms the alarm from its own schedule table and callsdeleteAlarm()when nothing is due — so the previousctx.storage.setAlarm()+alarm()override would have been silently cancelled (log batches dropped with no error) and would have shadowed the SDK's dispatcher. The buffer now armsthis.schedule(seconds, "flushLogsScheduled"). Eviction-survival semantics are unchanged: the buffer is still mirrored toctx.storageon every append and hydrated before flushing. paramsShape()/writerParamsShape()now returnRecord<string, z.ZodType<string | undefined>>rather thanZodTypeAny. Under zod 4 a bareZodTypeAnyshape infersunknownhandler args, which no longer matches the sharedrunGetter/runWritersignatures.propsis re-narrowed to non-optional viadeclare props: AuthProps— 0.21.0 types itprops?: Props, but every/mcprequest passes OAuthProvider bearer validation (which injects the props) before the DO runs a tool.declareemits no field, so the base class keeps ownership of the value.
Notes
- One-time session reset on deploy. 0.21.0 gates sessions on an
initializeRequeststorage key that 0.0.98 never wrote, so every live MCP session receivesSession not found(-32001) on its first request after this deploy. Spec-compliant clients re-initialize transparently; worst case is one retried call per session. - Verified before shipping:
tsc --noEmitclean, 154/154 unit tests pass,wrangler devboots (/docs200, unauthenticated/mcp401s correctly),wrangler deploy --dry-runbundles, and an in-memory MCP client↔server harness confirms every schema shape this server uses (string/optional/default, and thez.coerce.number().int().min().max().default()topNchain) still registers, serializes to clean draft-07 JSON Schema intools/list, coercestopN: "15"→15, and applies defaults — i.e. the SDK-1.30/zod-4 combination does not reintroduce the tool-discovery serialization failure documented in CLAUDE.md. McpAgentis now marked deprecated / feature-frozen upstream in favour ofcreateMcpHandler. That migration was assessed this session and deferred — it is simultaneously an MCP SDK v2 move and a drop to a stateless, Durable-Object-free handler, and the cost sits in the DO-resident audit-log buffer rather than the tools. Full cost breakdown and recommended sequencing are in CLAUDE.md → "Deferred —createMcpHandlermigration".
25R2-0.49.2
Fixed
acumatica_clear_cachewithtarget=ginow also clears the per-GI inferred sample caches (gi_schema:*). Thegibulk target clearedgi_list,gi_metadata, and the GI registry, but not the per-GI schema/sample caches written byacumatica_describe_inquiry(cache:gi_schema:{InquiryName}, 1 h TTL). Observed live 2026-08-18: after a GI's design changed (columns deactivated), atarget=giclear leftdescribe_inquiryreturning the fresh curated field list alongside a stalesampleRowthat still contained the removed columns — internally inconsistent output. The bulk-target key matching is extracted tomatchesClearTarget()(src/tools/clear-cache-match.ts, import-free leaf, unit-tested); the clear-everything path was unaffected (it already sweeps everycache:*key by prefix). Tool description and docs updated to state thattarget=gicovers the per-GI schemas.
25R2-0.49.1
Fixed
- The hoist-assignment step now finds the globally optimal row→property assignment (bitmask DP) with a uniqueness check, replacing greedy best-pair-first. Greedy's local ambiguity test refused whenever the chosen row scored equally on another property — even when a different captioned row's hard constraint forced the choice. Production case:
SubCrewMaterial'sDocumentType/DocumentType_2collision family, where a row captionedDocumentTypeties at 100 on both properties, but the sibling captionedDocumentType_2can only take_2, forcing the first onto the bare name — greedy refused this no matter what the operator captioned. A genuinely tied optimum (two equal-total assignments) still refuses. Verified: zero mapping or status changes across the 112-GI production feed; the change only accepts previously-refused constraint-forced cases. Ported toalign_columns.mjs(kept in sync) and unit-tested (147 tests). - Documented (and rejected) exact-caption preference. Ranking an exact caption→property match above a suffix-stripped one looks like it should pin
Xwhen bothXandX_2exist — but collision suffixes are POSITIONAL (the earlier grid row takes the bare name), so a captioned row sitting after an uncaptioned same-name row correctly ownsX_2. Verified counterexample on a production forecast GI, where exact-preference stoleCostCodefrom the storedcostCodeIDrow. The scoring keeps exact and stripped matches equal on purpose; the comment incolumnScore()records why.
25R2-0.49.0
Added
- Filters on calculated GI columns are refused before reaching Acumatica. A
$filterthat references a calculated column (an=…expression in the GI design) makes Acumatica return HTTP 200 with an empty body — not an error, not an empty list. This was the single largest remaining error class: production logs attribute 279 empty-body occurrences (~14% of tool errors) almost entirely toacumatica_run_inquiry, and the cause was reproduced live three times (a filter on a stored column works; addingand <ExpressionColumn> ne 0fails with the empty body).- The GI registry now flags expression columns (
GiFieldMeta.expression), set during the positional alignment from the design row'sFieldstarting with=. Flags are attached only where a design row was actually aligned to the property — a GI whose alignment was refused carries none, because a mis-placed flag would refuse filters on a perfectly filterable stored column. acumatica_run_inquirypre-flightsfilterExpressionwithfilterReferencedColumns()(src/lib/odata-v4-errors.ts— string-literal-safe, case-sensitive identifier match, unit-tested) and returns abuildCalculatedColumnRefusal()envelope without contacting Acumatica: names the offending columns, lists the storedfilterableFieldsto rewrite against, and states the query never executed so a refusal can't be reported as "no records matched".acumatica_describe_inquirymarks such fieldscalculated: trueand warns in itsnotethat they cannot be filtered; both tools' descriptions carry the same guidance.- Uncurated GIs (gate inactive, or a refused alignment) still hit the raw empty body;
parseAcumaticaJson()'s anomaly report (0.44.0) remains the backstop there.
- The GI registry now flags expression columns (
Changed
align_columns.mjsbrought back in sync with the server aligner. The skill script (skills/acumatica-gi-descriptions/scripts/align_columns.mjs) now carries the 0.48.2 rejection rules — declared-type constraint (expectedTypeFamily/typeConflicts), the weak shared-token tiebreak, DP optimal-solution counting, hoist-assignment ambiguity refusal, and the final pair sweep — so it no longer reports success on alignments the server rejects. A tied optimum stops the dropped-column search rather than escalating it (a higher drop count that happens to align is a free parameter rationalising a wrong mapping) and reportsalignment_ambiguouswith the caption-pinning remedy. Verified against the production feed: reproduces the hand-checked hoists onAP-Bills and Adjustments({1,2,6,33}) andSO-Invoice({1,2,6,22},Amount ← curyOrigDocAmt), and refuses the same under-determined GIs the server does.