Skip to content

Releases: hallboys/MCP4Acumatica

25R2-0.52.0

Choose a tag to compare

@github-actions github-actions released this 02 Sep 19:22

Added

  • GI alignment diagnostics + a /docs/admin/gi-alignment console page. resolveFields refuses 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_inquiry simply returned bare names while the descriptions sat correctly in Acumatica. resolveFields now reports its own verdict through an optional sink into GiRegistry.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-import MCPGIFields.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 on GiRegistry, never on GiRegistryEntry, 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]/ matched issueDate ("is" + "sueDate"). That made it contradict its own Edm.DateTimeOffset property, score as an impossible pairing, and refuse the entire GI's annotation. Found on FS-Licenses after 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 and issueDate/issuedBy/hashValue do 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 real FS-Licenses shape.

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 refusalIN-StockItem aligns at 7 % captioned and GL-Journal Transactions at 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-PHYINVDATADB and HPL-AcuStockIssues both flipped to aligned, with values confirming the pairings. Two cautions learned in the process: a typo in a caption becomes the property name (DcoumentType_2, LastMoidified are 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 via normalizeName, which strips punctuation) so the registry builds a full entry — while run_inquiry 404s and list_generic_inquiries never returns it. It had always been undiscoverable despite being tagged ExposedToMCP; the operator un-exposed it. align_columns.mjs checks for this and the server does not. Same file also models a dropped-row state resolveFields lacks — 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=2 refuses while both H=0 and H=1 align, 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

Choose a tag to compare

@github-actions github-actions released this 31 Aug 18:54

Fixed

  • FORM_ID_DEF_RE now accepts escaped parentheses, so Form IDs resolve on a DITA-converted docs corpus. Acumatica published the official documentation as Markdown at github.com/Acumatica/Acumatica-AI-Resources (branch per release, 2026R1 first) 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 for acumatica_get_doc_section would 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, the build-docs-index.mjs header 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 excludes Documentation/ 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-only and 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.fieldName is a virtual (unbound) field — Acumatica reports Filter 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; resolveFields and align_columns.mjs stay 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 _WithParameters FunctionImport 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 _N collision-suffix rule, and Caption's effect on property names remain undocumented.
  • Known work before the GitHub corpus can actually be used: an ingestion adapter. Verified against real 2026R1 files, build-docs-index.mjs degrades 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 in docs/upgrading-acumatica.md §3b.

25R2-0.51.0

Choose a tag to compare

@github-actions github-actions released this 31 Aug 11:49

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.com intermittently 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 the INDEX_STORE R2 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 KB docs-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_section accepts a Form ID directly (e.g. AP301000 → the screen's purpose, toolbar commands, tabs/fields) and returns prev/next neighbors for browsing; docs output bypasses field redaction (vendor documentation legitimately discusses fields named SSN etc. — new opt-out on callTool, valid only for tools that cannot return tenant records). upload-indexes.mjs uploads 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 runs main when executed directly.
  • npm run build-docs-index script; removed the stale build-gi-index script entry that pointed at a file that doesn't exist.

25R2-0.50.3

Choose a tag to compare

@github-actions github-actions released this 29 Aug 01:22

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 manual gh release create.

25R2-0.50.2

Choose a tag to compare

@saratcvemuri saratcvemuri released this 29 Aug 01:05

Changed

  • acumatica-gi-descriptions skill: four hard-won lessons added to the reference docs. (1) The silent-no-op 200: a ResultGrid PUT 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 $metadata diff (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

Choose a tag to compare

@saratcvemuri saratcvemuri released this 29 Aug 01:05

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

Choose a tag to compare

@saratcvemuri saratcvemuri released this 29 Aug 01:05

Changed

  • Upgraded the agents SDK 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 — a run_inquiry for one GI returning another GI's rows, with the other call's topN — 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 -32603 when two connections claim the same request id rather than silently crossing payloads.
    • @modelcontextprotocol/sdk (1.30.0) and zod (4.x) were transitive dependencies of agents 0.0.98 that this codebase imports directly; they are now required peers of agents and are pinned as explicit direct dependencies. @cloudflare/workers-types moves to v5 (required by partyserver 0.5.x and current wrangler).
  • The audit-log flush is scheduled through the Agent schedule() API instead of a raw DO alarm. Since 0.21.0 the base Agent class owns the DO's single alarm slot — its scheduler re-arms the alarm from its own schedule table and calls deleteAlarm() when nothing is due — so the previous ctx.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 arms this.schedule(seconds, "flushLogsScheduled"). Eviction-survival semantics are unchanged: the buffer is still mirrored to ctx.storage on every append and hydrated before flushing.
  • paramsShape() / writerParamsShape() now return Record<string, z.ZodType<string | undefined>> rather than ZodTypeAny. Under zod 4 a bare ZodTypeAny shape infers unknown handler args, which no longer matches the shared runGetter/runWriter signatures.
  • props is re-narrowed to non-optional via declare props: AuthProps — 0.21.0 types it props?: Props, but every /mcp request passes OAuthProvider bearer validation (which injects the props) before the DO runs a tool. declare emits 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 initializeRequest storage key that 0.0.98 never wrote, so every live MCP session receives Session 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 --noEmit clean, 154/154 unit tests pass, wrangler dev boots (/docs 200, unauthenticated /mcp 401s correctly), wrangler deploy --dry-run bundles, and an in-memory MCP client↔server harness confirms every schema shape this server uses (string/optional/default, and the z.coerce.number().int().min().max().default() topN chain) still registers, serializes to clean draft-07 JSON Schema in tools/list, coerces topN: "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.
  • McpAgent is now marked deprecated / feature-frozen upstream in favour of createMcpHandler. 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 — createMcpHandler migration".

25R2-0.49.2

Choose a tag to compare

@saratcvemuri saratcvemuri released this 29 Aug 01:05

Fixed

  • acumatica_clear_cache with target=gi now also clears the per-GI inferred sample caches (gi_schema:*). The gi bulk target cleared gi_list, gi_metadata, and the GI registry, but not the per-GI schema/sample caches written by acumatica_describe_inquiry (cache:gi_schema:{InquiryName}, 1 h TTL). Observed live 2026-08-18: after a GI's design changed (columns deactivated), a target=gi clear left describe_inquiry returning the fresh curated field list alongside a stale sampleRow that still contained the removed columns — internally inconsistent output. The bulk-target key matching is extracted to matchesClearTarget() (src/tools/clear-cache-match.ts, import-free leaf, unit-tested); the clear-everything path was unaffected (it already sweeps every cache:* key by prefix). Tool description and docs updated to state that target=gi covers the per-GI schemas.

25R2-0.49.1

Choose a tag to compare

@saratcvemuri saratcvemuri released this 29 Aug 01:05

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's DocumentType/DocumentType_2 collision family, where a row captioned DocumentType ties at 100 on both properties, but the sibling captioned DocumentType_2 can 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 to align_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 X when both X and X_2 exist — 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 owns X_2. Verified counterexample on a production forecast GI, where exact-preference stole CostCode from the stored costCodeID row. The scoring keeps exact and stripped matches equal on purpose; the comment in columnScore() records why.

25R2-0.49.0

Choose a tag to compare

@saratcvemuri saratcvemuri released this 29 Aug 01:05

Added

  • Filters on calculated GI columns are refused before reaching Acumatica. A $filter that 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 to acumatica_run_inquiry, and the cause was reproduced live three times (a filter on a stored column works; adding and <ExpressionColumn> ne 0 fails with the empty body).
    • The GI registry now flags expression columns (GiFieldMeta.expression), set during the positional alignment from the design row's Field starting 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_inquiry pre-flights filterExpression with filterReferencedColumns() (src/lib/odata-v4-errors.ts — string-literal-safe, case-sensitive identifier match, unit-tested) and returns a buildCalculatedColumnRefusal() envelope without contacting Acumatica: names the offending columns, lists the stored filterableFields to rewrite against, and states the query never executed so a refusal can't be reported as "no records matched".
    • acumatica_describe_inquiry marks such fields calculated: true and warns in its note that 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.

Changed

  • align_columns.mjs brought 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 reports alignment_ambiguous with the caption-pinning remedy. Verified against the production feed: reproduces the hand-checked hoists on AP-Bills and Adjustments ({1,2,6,33}) and SO-Invoice ({1,2,6,22}, Amount ← curyOrigDocAmt), and refuses the same under-determined GIs the server does.