Skip to content

[MCP] Server Features in MCP configuration: API Tools, Dynamic Tool Mode, Data Query Tools (Preview)#8085

Open
onbuyuka wants to merge 22 commits into
mainfrom
private/onbuyuka/631012-al-query-mcp-config-ui
Open

[MCP] Server Features in MCP configuration: API Tools, Dynamic Tool Mode, Data Query Tools (Preview)#8085
onbuyuka wants to merge 22 commits into
mainfrom
private/onbuyuka/631012-al-query-mcp-config-ui

Conversation

@onbuyuka

@onbuyuka onbuyuka commented May 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a Server Features surface to the MCP configuration card so admins can opt configurations into the Data Query Tools (Preview) MCP runtime, alongside the existing Dynamic Tool Mode and API Tools controls. Each feature is a self-contained handler behind an interface, so the card is data-driven and extensible. Activation persists on the real MCP Configuration fields shipped in BC-Platform #44811; only the Data Query Tools server runtime lands separately (see Platform dependency).

Feature handlers (interface-based)

  • New interface "MCP Server Features"SetActive / IsActive / HasSettings / OpenSettings / Description / LoadSystemTools / TryGetParentFeature. enum "MCP Server Feature" implements it, binding each value to a handler codeunit; resolving ServerFeature := Rec.Feature replaces the old per-feature case blocks.
  • Both the MCP Server Feature List and MCP System Tool List pages are fully generic: each walks ServerFeature.Ordinals() + FromInteger(...) (the same pattern as MCP Config Warning Type) and dispatches every action — activation, settings, descriptions, and system-tool loading — through the handler. Adding a feature is a new enum value + handler codeunit — no page edits.
  • Each feature owns its description, activation, settings, system tools, and rules; SetActive delegates to codeunit "MCP Config Implementation", which reads/writes the real MCP Configuration fields (EnableDynamicToolMode, EnableApiTools, EnableAlQueryTools); IsActive reads them back.

Card UX

  • Server Features list-part with per-row Activate / Deactivate / Configure, mirroring page 7775 "Copilot AI Capabilities"Copilot Capabilities GA. Configure appears only for features that have settings — gated on a per-row Configurable flag set from the handler's HasSettings(). Dynamic Tool Mode renders indented as a sub-feature of API Tools (TryGetParentFeature + an Indentation column on the temp table).
    • API Tools — gates the Available APIs list-part (Visible = not IsDefault and APIToolsActive), where the admin curates which API pages / queries the client can reach. No settings.
    • Dynamic Tool Mode — its Configure dialog hosts Discover Read-Only Objects; activation flows to EnableDynamicToolMode, deactivation cascade-clears DiscoverReadOnlyObjects. Enabling it requires API Tools to be enabled firstimpl.EnableDynamicToolMode enforces the gate (APIToolsRequiredForDynamicErr).
    • Data Query Tools (Preview) — contributes the Data Query Tools system tools when active. No settings.
  • Available APIs (page 8352) — the curated API list. Its Select APIs action opens a single lookup listing both API pages and API queries (new temp table "MCP API Object Buffer" + page "MCP API Object Lookup", populated by impl.LookupAPIObjects); the per-row Object Id lookup shares the same path. The part was renamed 'Available Tools''API Tools''Available APIs' to disambiguate it from the API Tools feature row.
  • The single settings StandardDialog (page 8369 "MCP Server Feature Settings") is used only by Dynamic Tool Mode; its handler runs it via OpenSettings, writing on OK.
  • The System Tools list-part (right-rail FactBox) is rebuilt by iterating the active features and calling each handler's LoadSystemTools.

Configuration list (page 8350)

  • The Default field is read-only on the card; Set as Default / Clear Default actions (promoted) live on the configuration list instead.
  • Read-only API Tools / Data Query Tools status columns (bound to Rec.EnableApiTools / Rec.EnableAlQueryTools) replaced the previous Dynamic Tool Mode / Discover Read-Only Objects columns.

Public API

  • codeunit 8350 "MCP Config" exposes EnableAPITools and EnableDataQueryTools alongside the existing EnableDynamicToolMode / EnableDiscoverReadOnlyObjects. The two feature procedures read/write the MCP Configuration.EnableApiTools / .EnableAlQueryTools fields.

New objects

  • interface "MCP Server Features"
  • enum 8351 "MCP Server Feature" (implements "MCP Server Features"; API Tools, Dynamic Tool Mode, Data Query Tools)
  • table 8355 "MCP Server Feature" (temporary; carries Configurable + Indentation)
  • table 8357 "MCP API Object Buffer" (temporary; unifies API pages + queries for the Select APIs lookup)
  • page 8368 "MCP Server Feature List", page 8369 "MCP Server Feature Settings", page 8376 "MCP API Object Lookup"
  • handler codeunits 8369 "MCP API Tools Feature", 8368 "MCP Data Query Tools Feature", 8370 "MCP Dyn. Tool Mode Feature"

Deprecated

  • Pages 8353 "MCP API Config Tool Lookup" / 8367 "MCP Query Config Tool Lookup" are obsoleted (ObsoleteState = Pending, ObsoleteTag = '29.0', wrapped in #if not CLEAN29) — superseded by the unified MCP API Object Lookup (8376). They're public System Application objects, so they're deprecated rather than deleted (removing them breaks dependent extensions — AS0029). Their internal MCP Config Implementation lookup procedures (LookupAPIPageTools / LookupAPIQueryTools) and the test-library wrappers were removed (neither is public surface).

Permissions

  • MCP - Objects lists the new app tables (MCP Server Feature, MCP API Object Buffer).

Platform dependency

BC-Platform PR #44811 has merged, and this PR is productionized against it — no mocks remain (grep -rn "MOCK:|PLATFORM-PENDING" "src/System Application/App/MCP/" is empty).

  • Activation persists on MCP Configuration.EnableApiTools (field 8, InitValue = true) / .EnableAlQueryTools (field 9), read/written through four MCP Config Implementation procedures. An upgrade (MCP Upgrade.EnableApiToolsOnExistingConfigurations) turns API Tools on for existing configurations (InitValue only covers new ones).
  • Data Query Tools system tools come from "MCP Utilities".GetSystemToolsInDataQuery().

The platform uses the "AL Query Tools" field name; our UX stays "Data Query Tools" (it maps onto EnableAlQueryTools).

The Data Query Tools server runtime (compile + execute of client-submitted AL queries) is a separate, larger platform dependency — this PR only configures and advertises the tools. See Appendix E in docs/features/al-query-mcp-config-ui/design.md.

Test plan

  • MCP Config Test (codeunit 130130): activation tests asserting the real EnableApiTools / EnableAlQueryTools field state; a Server Features TestPage set on page 8351 "MCP Config Card" (the list shows all three features in order, Configure is enabled only on Dynamic Tool Mode, activating Dynamic Tool Mode flips its row Status and sets EnableDynamicToolMode, and the part is hidden on the default configuration; API Tools is enabled before Dynamic Tool Mode so the gate is satisfied); TestLookupAPIObjects exercising the unified API lookup; and Export/Import round-trip tests asserting the two new fields survive.
  • Manual walkthrough of the Server Features list / Configure / Activate-Deactivate cycle and the Select APIs lookup against a local BC instance.

Fixes AB#631012

🤖 Generated with Claude Code

onbuyuka and others added 3 commits May 6, 2026 16:37
…n card

UX-only prototype for the upcoming AL Query MCP server. Replaces the prior
AL Query Server fasttab with a generic Server Features list-part on the card
(Activate / Deactivate / Configure actions per row, mirroring page 7775
"Copilot AI Capabilities") and a per-feature StandardDialog for settings.
Tool Mode toggles (EnableDynamicToolMode + DiscoverReadOnlyObjects) stay on
the General fasttab — they're contract modifiers, not features.

System Tools sub-page now uses a push pattern (parent calls Reload(...))
instead of the prior pull/SubPageLink approach, fixing a render-collapsed bug
that affected newly-visible parts.

No real runtime, no platform fields, no public API, no Export/Import or
telemetry round-trip, no automated tests. See al-query-mock.md "Next steps to
production" for the full path to shipping.

New objects:
- enum 8351 "MCP Server Feature"
- table 8355 "MCP Server Feature" (temp)
- page 8368 "MCP Server Feature List"
- page 8369 "MCP Server Feature Settings"

Fixes AB#631012

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three empty-body procedures on codeunit 8350 "MCP Config" mirror the existing
EnableDynamicToolMode / EnableDiscoverReadOnlyObjects shapes for the prototype's
AL Query Server feature: EnableALQueryServer, SetALQueryMaxRowsPerQuery, and
SetALQueryTimeoutSeconds. Each has a // MOCK: comment in the implementation
spelling out the GetBySystemId → assign → Modify → LogConfigurationModified
flow to wire up once the platform adds the real fields.

MCPServerFeatureSettings.SaveChanges() is restored as an empty stub with a
commented-out routing-through-the-facade template, and OpenSettings re-gates
the call on Action::OK so the OK/Cancel contract is preserved.

Six minimal MCPConfigTest tests verify each facade procedure is callable; each
is // MOCK commented and just calls the API. Replace with real state assertions
once the platform-side fields exist.

Fixes AB#631012

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@onbuyuka onbuyuka changed the title [MCP] Prototype AL Query Server (Preview) feature in MCP configuration [MCP] AL Query Server (Preview) feature in MCP configuration May 11, 2026
@onbuyuka onbuyuka changed the title [MCP] AL Query Server (Preview) feature in MCP configuration [WIP][MCP] AL Query Server (Preview) feature in MCP configuration May 11, 2026
@github-actions github-actions Bot added this to the Version 29.0 milestone May 11, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AL Documentation Audit

Documentation gaps were detected in the following apps:

  • MCP-Test: 0% documentation coverage
  • MCP: 67% documentation coverage

To generate documentation, run /al-docs init or /al-docs update using GitHub Copilot CLI or Claude Code.
This review is for awareness to help keep documentation in sync with code changes. It is okay to dismiss this request.

onbuyuka and others added 3 commits May 12, 2026 23:39
Removes docs/features/al-query-mcp-config-ui/design.md and
src/System Application/App/MCP/docs/al-query-mock.md from the PR. The code
+ // MOCK: comments are the source of truth going forward.

Fixes AB#631012

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ature)

- Renamed "AL Query Server" -> "AL Query Tools" everywhere (enum value identifier
  and caption `'AL Query Tools (Preview)'`, facade procedure EnableALQueryServer
  -> EnableALQueryTools, test names, comments).

- Dynamic Tool Mode is back in the Server Features list as a feature row (enum
  ordinal 0, replacing the blank value introduced earlier). Removed
  EnableDynamicToolMode and DiscoverReadOnlyObjects fields from the card's
  General fasttab, along with the Tool Modes description block and its labels.
  Discover Additional Read-Only Objects is now exposed in the Configure dialog
  under Dynamic Tool Mode (loaded from / saved to Rec.DiscoverReadOnlyObjects
  on OK). The cascade "deactivating Dynamic Tool Mode clears
  DiscoverReadOnlyObjects" lives in the row's SetActive. LoadSystemTools tags
  the search/describe/invoke system tools with the Dynamic Tool Mode enum
  value so every System Tools row has a concrete feature label.

- MCPConfigToolList caption: 'Available Tools' -> 'API Tools'. All three CUD
  actions (SelectTools, AddToolsByAPIGroup, AddStandardAPITools) guard on
  `Enabled = not IsConfigActive`. New internal SetConfigActive(IsActive)
  procedure accepts a push from MCPConfigCard.RefreshSubPages. The redraw is
  triggered by CurrPage.Update() at the end of Active.OnValidate on the card,
  which cascades through UpdatePropagation = Both.

- System Tools list-part moved from area(Content) to area(FactBoxes).

- Restored docs/features/al-query-mcp-config-ui/design.md with D21-D24,
  iteration entries 10-13, and lessons 10-11 covering the above changes.

Fixes AB#631012

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@onbuyuka onbuyuka changed the title [WIP][MCP] AL Query Server (Preview) feature in MCP configuration [WIP][MCP] AL Query Tools (Preview) feature in MCP configuration May 29, 2026
…e APIs"

- enum 8351: API Tools (0), Dynamic Tool Mode (1), AL Query Tools (2)
- Server Features list seeds an API Tools row; activation is a page-local
  mock (APIToolsActiveLocal) exposed via IsAPIToolsActive()
- Dynamic Tool Mode pre-flights API Tools (APIToolsRequiredForDynamicErr)
- Card: "Available APIs" sub-part gated on `not IsDefault and APIToolsActive`
- page 8352 caption 'API Tools' -> 'Available APIs'; description labels updated
- design.md D25 / Appendix C #14 corrected to match the code (no AllowProdChanges
  relocation; API Tools Configure has no sub-settings yet)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@onbuyuka onbuyuka changed the title [WIP][MCP] AL Query Tools (Preview) feature in MCP configuration [WIP][MCP] Server Features in MCP configuration: API Tools, Dynamic Tool Mode, AL Query Tools (Preview) May 29, 2026
onbuyuka and others added 7 commits May 29, 2026 12:58
Introduce interface "MCP Feature Handler" implemented by enum
"MCP Server Feature"; each feature is a handler codeunit (MCP API Tools
Feature 8369, MCP AL Query Tools Feature 8368, MCP Dyn. Tool Mode Feature
8370). The list page resolves a handler and dispatches instead of a
per-feature case, and Reload walks the enum's Ordinals()/FromInteger()
so adding a feature needs no page edit.

- Description, activation, settings, and the Dynamic-Tool-Mode-requires-
  API-Tools pre-flight live in the handlers; the page holds no per-feature data
- temp table gains Configurable (= Handler.HasSettings()); Configure gates on it
- Dynamic Tool Mode handler is real; API Tools + AL Query are platform-pending
  stubs (IsActive returns false until the MCP Configuration booleans ship)
- drop AL Query Tools mock sub-settings (Max Rows / Query Timeout) and the
  SetALQueryMaxRowsPerQuery / SetALQueryTimeoutSeconds facade + impl + 4 tests
- extend MCP/app.json idRanges to 8365-8370 for the handler codeunits
- design.md D26 + D27

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tem tools

- rename interface "MCP Feature Handler" -> "MCP Server Features"
- handlers' SetActive delegate to MCP Config Implementation (EnableAPITools
  added to facade + impl, mock); the "API Tools required for Dynamic Tool Mode"
  gate moved into impl.EnableDynamicToolMode as a PLATFORM-PENDING commented
  check (inactive until the API Tools field is in symbols)
- add LoadSystemTools to the interface; Dyn. Tool Mode emits the real
  GetSystemToolsInDynamicMode() catalog, AL Query the mock tools, API Tools
  none; remove impl.LoadSystemTools + InsertSystemTool + the Include* flags
- MCP System Tool List.Reload(ConfigId) iterates the enum and dispatches to
  each active feature; card calls SystemToolList.Reload(Rec.SystemId)
- rename interface vars Handler -> ServerFeature

design.md D28-D29.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- API Tools facade smoke tests (EnableAPITools enable/disable)
- Server Features TestPage tests on MCP Config Card: lists all three features,
  Configure gated to Dynamic Tool Mode, Activate flips status + sets
  EnableDynamicToolMode, part hidden on the default configuration
- fix TestDefaultConfigurationPage: drop assertions on EnableDynamicToolMode /
  DiscoverReadOnlyObjects fields that D22 moved off the card (stale TestPage
  references that blocked the test codeunit from compiling)
- design.md Appendix C #19

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te/onbuyuka/631012-al-query-mcp-config-ui
Continues the Server Features work with activation persistence and UX polish.

- Persistence (mock): new internal table 8360 "MCP Feature Activation" holds the
  API Tools / AL Query Tools activation flags keyed by config SystemId, behind four
  MCP Config Implementation procedures (EnableAPITools/EnableALQueryTools and
  IsAPIToolsEnabled/IsALQueryToolsEnabled). These flags belong on the platform-owned
  MCP Configuration table, which the app can't extend; every mock site is tagged
  "// MOCK:" and Appendix E of the design doc lists the exact steps to remove them.
- Gate: EnableDynamicToolMode now requires API Tools to be enabled first
  (APIToolsRequiredForDynamicErr) - previously a platform-pending stub.
- Dynamic Tool Mode renders as an indented sub-feature of API Tools
  (interface TryGetParentFeature + MCP Server Feature.Indentation).
- UI polish: align the curated list with "Available APIs" terminology (action and
  field renames), drop the Server Feature column from the System Tools factbox, and
  promote Export/Import on the config list.
- Tests: enable API Tools before Dynamic Tool Mode so the new gate is satisfied.
- Docs: design doc D30-D32, Appendix C #20-22, Appendix D #12-13, and a new
  Appendix E "Productionizing - removing the mocks". D32 / Appendix D #13 record the
  card list-part "always collapsed" investigation (BC platform limitation, AL #6258).

AB#631012

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups on top of d71487c:

- Tag the MCPConfigCard.APIToolsActive page-local with "// MOCK:" so the
  productionizing grep surfaces it (it reads the mock activation table). The note
  is comment-only - the page-local code itself stays.
- Correct two stale "// MOCK:" comments in MCP Config Test that still called the
  EnableAPITools / EnableALQueryTools facade procedures "no-ops with no
  persistence" - they persist to the mock "MCP Feature Activation" table now.
- Shorten the Dynamic Tool Mode setting caption "Discover Additional Read-Only
  Objects" -> "Discover Read-Only Objects" (the longer one truncated in the
  dialog); the tooltip keeps the full meaning and it matches the config list
  page's default caption.
- Design doc Appendix E: note the card + facade mock comments are comment-only and
  "// MOCK:"-tagged, and drop the contradictory "not a mock" bullet for
  APIToolsActive.

AB#631012

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8356 is contiguous with table 8355 "MCP Server Feature" (8360 left a gap) and is
a free table ID within the app's 8350-8362 idRange. Every reference to the table
is by name, so there are no code-wiring changes - only the table declaration and
the design doc's 8360 mentions. PR description updated to match.

AB#631012

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@onbuyuka onbuyuka force-pushed the private/onbuyuka/631012-al-query-mcp-config-ui branch from 3a3217d to 49b399f Compare June 2, 2026 08:52
onbuyuka and others added 3 commits June 2, 2026 14:31
Implements the review feedback on the MCP configuration feature plus a
follow-up quality pass.

Feedback:
- Default field is read-only on the card; Set as Default / Clear Default
  actions (promoted) are back on the configuration list.
- Renamed AL Query Tools -> Data Query Tools (Preview) across the code: enum
  value, handler codeunit (MCP Data Query Tools Feature), facade/impl
  procedures, the mock activation field, and tests. The mock system-tool IDs
  compile_al_query / run_al_query are kept.
- Select APIs opens one lookup over both API pages and API queries (new temp
  table "MCP API Object Buffer" + page "MCP API Object Lookup", populated by
  MCPConfigImplementation.LookupAPIObjects). The per-row Object Id lookup
  shares the same path.
- Configuration list shows read-only API Tools / Data Query Tools status
  columns instead of Dynamic Tool Mode / Discover Read-Only Objects.
- Permissions: registered the recently-added tables and the new buffer in
  "MCP - Objects" and granted tabledata on the persistent MCP Feature
  Activation (R/IMD) - these were missing and broke the feature for non-SUPER
  users.

Quality pass:
- Removed the now-orphaned per-type lookups (LookupAPIPageTools /
  LookupAPIQueryTools, pages "MCP API Config Tool Lookup" / "MCP Query Config
  Tool Lookup", their test-library wrappers and two tests) in favour of the
  unified lookup; one TestLookupAPIObjects smoke test replaces them.
- "MCP API Object Buffer" declared TableType = Temporary (matching sibling
  buffers); temp-record variable naming aligned to the LinterCop convention
  (locals Temp-prefixed, parameters not).

Design doc updated throughout (D33-D34, Appendix C #23-24, Appendix E, the AL
Query -> Data Query naming note, lookup page renumber 8360 -> 8376).

AB#631012

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…44811

Stages the productionized versions of the mocked activation persistence + Data
Query Tools catalog next to each mock, so productionizing once BC-Platform PR
#44811 lands is "uncomment + delete the mock". #44811 adds
MCP Configuration.EnableApiTools (field 8, InitValue = true) + .EnableAlQueryTools
(field 9) and "MCP Utilities".GetSystemToolsInDataQuery().

- MCP Config Implementation: commented real versions of the four activation
  procedures (read/write the platform fields).
- MCP Data Query Tools Feature.LoadSystemTools: commented real version using
  GetSystemToolsInDataQuery().
- MCP Upgrade: commented EnableApiToolsOnExistingConfigurations (+ tag,
  registration, call) to set EnableApiTools := true on existing configurations
  (the field's InitValue only covers new ones).
- MCP Config Test: commented real-assertion versions of the four activation tests.
- MCP Config List: commented Rec.EnableApiTools / Rec.EnableAlQueryTools field
  bindings next to the function-sourced status columns; trimmed the redundant
  facade/card mock comments.

All pre-staged blocks are tagged // PLATFORM-PENDING (BC-Platform PR #44811).
Design doc Appendix E + D35 updated. UX keeps "Data Query Tools"; it maps onto
the platform's EnableAlQueryTools field.

AB#631012

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BC-Platform PR #44811 landed (MCP Configuration.EnableApiTools field 8
InitValue = true, .EnableAlQueryTools field 9, and
"MCP Utilities".GetSystemToolsInDataQuery()), so this removes every mock and
wires the feature to the real platform members.

- MCP Config Implementation: the four activation procedures read/write the real
  EnableApiTools / EnableAlQueryTools fields (writes via LogConfigurationModified).
  Reads fall back to Init() for a not-yet-saved config so a new config reflects
  the InitValue default.
- MCP Data Query Tools Feature.LoadSystemTools: consumes GetSystemToolsInDataQuery().
- MCP Upgrade: EnableApiToolsOnExistingConfigurations turns API Tools on for
  existing configs on upgrade (InitValue only covers new ones).
- MCP Config List: API Tools / Data Query Tools status columns bind to the real fields.
- Deleted mock table 8356 "MCP Feature Activation" + its permission-set entries.

New-config fix: the card's Name.OnValidate persists the config (CurrPage.Update
when IsNullGuid(SystemId)) the moment it's named, so its Server Features can be
configured without first closing the card.

Export/Import: now carries enableApiTools / enableAlQueryTools, with round-trip
tests asserting both the JSON keys and the imported field values.

MCP Config Test: the four activation tests assert the real field state.
Design doc updated (Appendix E flipped to "completed"; decision log D36-D38).

AB#631012

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@onbuyuka onbuyuka changed the title [WIP][MCP] Server Features in MCP configuration: API Tools, Dynamic Tool Mode, AL Query Tools (Preview) [MCP] Server Features in MCP configuration: API Tools, Dynamic Tool Mode, Data Query Tools (Preview) Jun 4, 2026
@onbuyuka onbuyuka marked this pull request as ready for review June 4, 2026 13:19
@onbuyuka onbuyuka requested review from a team as code owners June 4, 2026 13:19
if not Enable and IsDefaultConfiguration(MCPConfiguration) then
Error(DynamicToolModeCannotBeDisabledErr);

if Enable and not IsAPIToolsEnabled(ConfigId) then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

EnableDynamicToolMode error message lacks actionability

The new error label APIToolsRequiredForDynamicErr reads 'API Tools must be enabled before Dynamic Tool Mode can be enabled.' but doesn't tell the user where or how to enable API Tools. For admins unfamiliar with the new Server Features list, this may be confusing.

Recommendation:

  • Update the label to reference the Server Features list: e.g. 'API Tools must be enabled in the Server Features list before Dynamic Tool Mode can be activated.'
Suggested change
if Enable and not IsAPIToolsEnabled(ConfigId) then
APIToolsRequiredForDynamicErr: Label 'API Tools must be enabled in the Server Features list before Dynamic Tool Mode can be activated.';

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

begin
SystemTools := MCPUtilities.GetSystemToolsInDataQuery();
foreach ToolName in SystemTools.Keys() do
InsertTool(MCPSystemTool, CopyStr(ToolName, 1, MaxStrLen(MCPSystemTool."Tool Name")), CopyStr(SystemTools.Get(ToolName), 1, MaxStrLen(MCPSystemTool."Tool Description")));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

LoadSystemTools tool description truncation silently drops data

CopyStr(SystemTools.Get(ToolName), 1, MaxStrLen(MCPSystemTool."Tool Description")) silently truncates tool descriptions longer than the field length (250 chars). If the platform later returns longer descriptions, the truncated text will be stored without any warning, potentially making descriptions unintelligible.

Recommendation:

  • Assert or log a warning when the source string exceeds the target field length, or verify with the platform that descriptions will never exceed 250 characters and add a comment documenting that contract.
Suggested change
InsertTool(MCPSystemTool, CopyStr(ToolName, 1, MaxStrLen(MCPSystemTool."Tool Name")), CopyStr(SystemTools.Get(ToolName), 1, MaxStrLen(MCPSystemTool."Tool Description")));
local procedure InsertTool(var MCPSystemTool: Record "MCP System Tool"; ToolName: Text[100]; ToolDescription: Text[250])
begin
MCPSystemTool."Server Feature" := MCPSystemTool."Server Feature"::"Data Query Tools";
MCPSystemTool."Tool Name" := ToolName;
MCPSystemTool."Tool Description" := ToolDescription; // platform guarantees max 250 chars
MCPSystemTool.Insert();
end;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Name OnValidate fires CurrPage.Update on every keystroke for new records

The OnValidate trigger on the Name field calls CurrPage.Update() whenever IsNullGuid(Rec.SystemId) is true (i.e., for any unsaved new record). Since OnValidate fires after each field exit (or with auto-save on every keystroke in some clients), this causes repeated full page refreshes including sub-page reloads while the user is still typing the config name.

Recommendation:

  • Move the CurrPage.Update() call to OnNewRecord or use a flag to run it only once after the first meaningful field entry, rather than on every Name validation for new records.
trigger OnNewRecord(BelowxRec: Boolean)
begin
    // Trigger a refresh so sub-pages initialize correctly for the new record.
    CurrPage.Update();
end;

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

SystemToolList FactBox always reloads on cursor move

RefreshSubPages() is called from OnAfterGetCurrRecord, which fires every time the record changes on the card. This triggers ServerFeature.IsActive(ConfigSystemId) DB calls for every feature plus a full LoadSystemTools iteration for the SystemToolList, on every page open and every field save. On slower connections this creates visible latency.

Recommendation:

  • Defer Reload calls inside OnAfterGetRecord (once per record, not every cursor event) and call RefreshSubPages only on specific state changes (Active toggle, feature activation). Reserve OnAfterGetCurrRecord for lightweight UI-only refreshes.
trigger OnAfterGetRecord()
begin
    IsDefault := MCPConfigImplementation.IsDefaultConfiguration(Rec);
    RefreshSubPages();
end;

trigger OnAfterGetCurrRecord()
begin
    // Lightweight: only refresh action visibility, not full DB-reading sub-page reload
end;

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

{
action(SetAsDefault)
{
Caption = 'Set as Default';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

SetAsDefault / ClearDefault miss active-config guard

The new SetAsDefault and ClearDefault actions call SetAsDefaultConfiguration and ClearDefaultConfiguration directly without checking if the configuration is active first. SetAsDefaultConfiguration in the implementation presumably performs this check, but if a future refactor moves the guard, the list actions have no safety net.

Recommendation:

  • Add Enabled = Rec.Active to SetAsDefault (only active configs should be set as default, consistent with the tooltip text) and document the active requirement explicitly.
Suggested change
Caption = 'Set as Default';
action(SetAsDefault)
{
Caption = 'Set as Default';
ToolTip = 'Set this configuration as the default. Only active configurations can be set as the default.';
Image = Approve;
AccessByPermission = tabledata "MCP Configuration" = M;
Scope = Repeater;
Enabled = not Rec.Default and Rec.Active;
...

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

if not MCPConfigImplementation.CheckAPIToolExists(Rec.ID, TempSelectedObjects."Object ID", Rec."Object Type"::Page) then
MCPConfig.CreateAPITool(Rec.ID, TempSelectedObjects."Object ID");
TempSelectedObjects."Object Type"::Query:
if not MCPConfigImplementation.CheckAPIToolExists(Rec.ID, TempSelectedObjects."Object ID", Rec."Object Type"::Query) then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

AddAPIObjects deletes current rec unconditionally after lookup

At the end of AddAPIObjects(), if not IsNullGuid(Rec.SystemId) then Rec.Delete() deletes the current record when it has a SystemId. This is inherited from the old per-type lookup logic and assumes the 'lookup from an existing row' scenario deletes that row. If an admin clicks 'Select APIs' while positioned on a valid existing tool row (non-null SystemId), that row is silently deleted after the lookup completes.

Recommendation:

  • Remove or clarify the Rec.Delete() call. The delete was originally intended only for removing a blank new-row placeholder created by the OnLookup trigger. Use a flag or only delete when called from the OnLookup trigger path.
Suggested change
if not MCPConfigImplementation.CheckAPIToolExists(Rec.ID, TempSelectedObjects."Object ID", Rec."Object Type"::Query) then
local procedure AddAPIObjects(DeleteCurrentRow: Boolean)
var
TempSelectedObjects: Record "MCP API Object Buffer";
begin
if not MCPConfigImplementation.LookupAPIObjects(TempSelectedObjects) then
exit;
...
if DeleteCurrentRow and not IsNullGuid(Rec.SystemId) then
Rec.Delete();
CurrPage.Update();
end;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

area(Processing)
{
action(Activate)
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Activate visible for Dynamic Tool Mode when parent inactive

The Activate action is Visible = ActionsEnabled and (Rec.Status = Rec.Status::Inactive) with no check that the parent feature (API Tools) is active. A user clicking Activate on the Dynamic Tool Mode row when API Tools is inactive receives an error instead of a gracefully disabled button.

Recommendation:

  • Add a computed boolean variable ParentFeatureActive in Reload that is set per-row, or extend the MCP Server Feature table with a ParentActive field, and include it in the Visible/Enabled expression for the Activate action.
Suggested change
{
action(Activate)
{
...
Visible = ActionsEnabled and (Rec.Status = Rec.Status::Inactive);
Enabled = ActionsEnabled and (Rec.Status = Rec.Status::Inactive) and (Rec.Indentation = 0 or ParentFeatureActive);
...

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

internal procedure SaveChanges()
var
MCPConfigImplementation: Codeunit "MCP Config Implementation";
begin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

SaveChanges silently no-ops for unhandled features

SaveChanges() uses a case Feature of with only "Dynamic Tool Mode" handled and no else branch. Any feature that gains HasSettings() = true in a future iteration will open the settings dialog but silently discard changes when the user clicks OK.

Recommendation:

  • Add an else Error(...) branch, or an assertion, so an unhandled feature triggers a developer-visible error rather than a silent no-op.
Suggested change
begin
internal procedure SaveChanges()
var
MCPConfigImplementation: Codeunit "MCP Config Implementation";
begin
case Feature of
Feature::"Dynamic Tool Mode":
MCPConfigImplementation.EnableDiscoverReadOnlyObjects(ConfigSystemId, DiscoverReadOnlyObjectsLocal);
else
Error('Unhandled feature settings for: %1', Feature);
end;
end;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

ConfigId := CreateMCPConfig(false, false, true, false);
MCPConfig.EnableAPITools(ConfigId, false);
MCPConfiguration.GetBySystemId(ConfigId);
Assert.IsFalse(MCPConfiguration.EnableApiTools, 'API Tools should be disabled');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

TestDisableAPITools missing cascade-off verification for DiscoverReadOnly

TestDisableAPITools only asserts EnableApiTools = false but doesn't verify that EnableDynamicToolMode and DiscoverReadOnlyObjects also cascade off when API Tools is disabled on a config that had them enabled. This is tested in TestDisableAPIToolsDisablesDynamicToolMode but only for Dynamic Tool Mode; a separate test (or the same test) should also cover DiscoverReadOnlyObjects.

Recommendation:

  • Merge the cascade assertion into TestDisableAPITools, or add a dedicated test that starts with DynToolMode=true, DiscoverReadOnly=true, APITools=true and verifies all three cascade off.
Suggested change
Assert.IsFalse(MCPConfiguration.EnableApiTools, 'API Tools should be disabled');
[Test]
procedure TestDisableAPIToolsCascadesDiscoverReadOnly()
var
MCPConfiguration: Record "MCP Configuration";
ConfigId: Guid;
begin
ConfigId := CreateMCPConfig(false, true, true, true);
MCPConfig.EnableAPITools(ConfigId, true);
MCPConfig.EnableAPITools(ConfigId, false);
MCPConfiguration.GetBySystemId(ConfigId);
Assert.IsFalse(MCPConfiguration.EnableDynamicToolMode, 'DynToolMode should cascade off');
Assert.IsFalse(MCPConfiguration.DiscoverReadOnlyObjects, 'DiscoverReadOnly should cascade off');
end;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

exit('MS-612454-MCPSystemDefaultAsDefault-20260216');
end;

local procedure GetMCPEnableApiToolsUpgradeTag(): Text[250]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Upgrade} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Upgrade tag date mismatch with merge date

The upgrade tag value is 'MS-631012-MCPEnableApiTools-20260603' (June 3) but the PR description notes that the latest merge date is June 4 (2026-06-04). While functionally harmless, upgrade tags with dates earlier than the actual deployment can be confusing during incident triage.

Recommendation:

  • Update the tag date to match the actual merge/deployment date, or use a date-independent suffix (e.g., the sprint or iteration number) to avoid this discrepancy.
Suggested change
local procedure GetMCPEnableApiToolsUpgradeTag(): Text[250]
local procedure GetMCPEnableApiToolsUpgradeTag(): Text[250]
begin
exit('MS-631012-MCPEnableApiTools-20260604');
end;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

LogConfigurationModified(MCPConfiguration, xMCPConfiguration);
end;

internal procedure IsAPIToolsEnabled(ConfigId: Guid): Boolean

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Performance} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

IsAPIToolsEnabled returns table default for missing config

When GetBySystemId returns false (config not yet persisted or invalid GUID), the procedure calls MCPConfiguration.Init() and returns EnableApiTools from an uninitialised in-memory record. If the field's InitValue is false, passing any arbitrary or empty GUID silently returns false rather than signalling that the record does not exist, masking bugs in callers.

Recommendation:

  • Return false explicitly when the config is not found, or add a guard that raises an error for non-null GUIDs that don't resolve. Reserve the Init() path only for the documented new-record case and document that behaviour.
Suggested change
internal procedure IsAPIToolsEnabled(ConfigId: Guid): Boolean
internal procedure IsAPIToolsEnabled(ConfigId: Guid): Boolean
var
MCPConfiguration: Record "MCP Configuration";
begin
if IsNullGuid(ConfigId) then
exit(false); // new record not yet persisted
if not MCPConfiguration.GetBySystemId(ConfigId) then
exit(false);
exit(MCPConfiguration.EnableApiTools);
end;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

FeatureImplementation: Integer;
begin
ParentSystemId := ConfigSystemId;
ActionsEnabled := CanModify;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Performance} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Reload called with empty GUID on new card record

When MCPConfigCard.OnAfterGetCurrRecord fires during creation of a new configuration record, Rec.SystemId is an empty GUID. RefreshSubPages passes this to ServerFeatureList.Page.Reload(emptyGuid, ...), which then calls each feature's IsActive(emptyGuid). IsAPIToolsEnabled calls GetBySystemId(emptyGuid), fails, and falls through to Init()—returning whatever the field's table default is. This could misrepresent API Tools as active or inactive for unsaved records.

Recommendation:

  • Guard Reload against an empty GUID; skip the reload and show a neutral default state when the config has not yet been saved.
Suggested change
ActionsEnabled := CanModify;
internal procedure Reload(ConfigSystemId: Guid; CanModify: Boolean)
begin
if IsNullGuid(ConfigSystemId) then begin
Rec.Reset();
Rec.DeleteAll();
exit;
end;
// existing reload logic ...
end;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Security} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Export action missing AccessByPermission guard

The new ExportConfiguration action on MCPConfigCard has no AccessByPermission property, meaning any user who can open the card page can export the full MCP configuration JSON—including tool definitions, API versions, and all settings—to a local file. The sibling ImportConfiguration action on MCPConfigList correctly uses AccessByPermission = tabledata "MCP Configuration" = IM.

Recommendation:

  • Add AccessByPermission to the ExportConfiguration action on both MCPConfigCard and MCPConfigList consistent with the read intent.
action(ExportConfiguration)
{
    Caption = 'Export';
    ToolTip = 'Export the selected MCP configuration and its tools to a JSON file.';
    Image = Export;
    AccessByPermission = tabledata "MCP Configuration" = R;

    trigger OnAction()
    begin
        MCPConfigImplementation.ExportConfigurationToFile(Rec.SystemId, Rec.Name);
    end;
}

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Security} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

EnableAPITools allows modifying active configurations

EnableAPITools and EnableDataQueryTools do not check whether the target configuration is currently active before applying changes. The UI guards this with 'not IsDefault and not Rec.Active', but since these are internal procedures callable from other codeunits or test libraries, a programmatic caller can silently change feature flags on a live, active configuration mid-session.

Recommendation:

  • Add an active-configuration guard analogous to the one already present in EnableDynamicToolMode: if the configuration is active, raise an error or return early.
internal procedure EnableAPITools(ConfigId: Guid; Enable: Boolean)
var
    MCPConfiguration: Record "MCP Configuration";
    xMCPConfiguration: Record "MCP Configuration";
begin
    if not MCPConfiguration.GetBySystemId(ConfigId) then
        Error(ConfigurationNotFoundErr);
    if MCPConfiguration.Active then
        Error(CannotModifyActiveConfigurationErr);
    xMCPConfiguration := MCPConfiguration;
    MCPConfiguration.EnableApiTools := Enable;
    if not Enable then begin
        MCPConfiguration.EnableDynamicToolMode := false;
        MCPConfiguration.DiscoverReadOnlyObjects := false;
    end;
    MCPConfiguration.Modify();
    LogConfigurationModified(MCPConfiguration, xMCPConfiguration);
end;

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why


[ModalPageHandler]
procedure LookupAPIQueryToolsOKHandler(var MCPQueryConfigToolLookup: TestPage "MCP Query Config Tool Lookup")
procedure LookupAPIObjectsOKHandler(var MCPAPIObjectLookup: TestPage "MCP API Object Lookup")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

LookupAPIObjects test handler asserts no specific result

The replacement test handler LookupAPIObjectsOKHandler calls First() and OK() without verifying which object type or ID was actually selected. The test comment says '[THEN] A selection is returned', but the only assertion is on the Boolean return value of LookupAPIObjects(). The old tests (LookupAPIPageToolsOKHandler, LookupAPIQueryToolsOKHandler) explicitly navigated to known records and verified their IDs, providing much stronger coverage.

Recommendation:

  • Make the test handler navigate to a specific known API page or query record (e.g. GoToKey for a mock API object) and add an assertion that the returned buffer contains the expected Object Type and Object ID.
Suggested change
procedure LookupAPIObjectsOKHandler(var MCPAPIObjectLookup: TestPage "MCP API Object Lookup")
[ModalPageHandler]
procedure LookupAPIObjectsOKHandler(var MCPAPIObjectLookup: TestPage "MCP API Object Lookup")
begin
MCPAPIObjectLookup.GoToKey(MCPAPIObjectLookup."Object Type"::Page, Page::"Mock API");
MCPAPIObjectLookup.OK().Invoke();
end;
[Test]
[HandlerFunctions('LookupAPIObjectsOKHandler')]
procedure TestLookupAPIObjects()
var
TempSelectedObjects: Record "MCP API Object Buffer";
begin
Assert.IsTrue(MCPConfigTestLibrary.LookupAPIObjects(TempSelectedObjects), 'Lookup returned no selection');
TempSelectedObjects.FindFirst();
Assert.AreEqual(TempSelectedObjects."Object Type"::Page, TempSelectedObjects."Object Type", 'Wrong object type');
Assert.AreEqual(Page::"Mock API", TempSelectedObjects."Object ID", 'Wrong object ID');
end;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

MCPConfiguration: Record "MCP Configuration";
UpgradeTag: Codeunit "Upgrade Tag";
begin
if UpgradeTag.HasDatabaseUpgradeTag(GetMCPEnableApiToolsUpgradeTag()) then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Upgrade} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Upgrade enables API Tools on active configs

EnableApiToolsOnExistingConfigurations calls MCPConfiguration.ModifyAll(EnableApiTools, true), which bypasses all record-level validation and modifies every configuration record—including currently active ones. Enabling a feature flag on an active configuration could immediately change the behaviour of live MCP sessions without the administrator's explicit consent.

Recommendation:

  • Exclude active configurations from the bulk update, or mark the upgrade so it only applies to inactive configurations. If enabling for active configs is intentional, add a comment explaining the decision and confirm there are no runtime side-effects.
Suggested change
if UpgradeTag.HasDatabaseUpgradeTag(GetMCPEnableApiToolsUpgradeTag()) then
internal procedure EnableApiToolsOnExistingConfigurations()
var
MCPConfiguration: Record "MCP Configuration";
UpgradeTag: Codeunit "Upgrade Tag";
begin
if UpgradeTag.HasDatabaseUpgradeTag(GetMCPEnableApiToolsUpgradeTag()) then
exit;
MCPConfiguration.SetRange(Active, false);
MCPConfiguration.ModifyAll(EnableApiTools, true);
UpgradeTag.SetUpgradeTag(GetMCPEnableApiToolsUpgradeTag());
end;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

],
"target": "OnPrem",
"contextSensitiveHelpUrl": "https://learn.microsoft.com/dynamics365/business-central/"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Upgrade} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

app.json missing newline at end of file

The new version of app.json ends without a trailing newline ('No newline at end of file'), which can cause issues with diff tools, version control, and some CI checks that enforce POSIX compliance. This is a regression introduced by this PR.

Recommendation:

  • Add a trailing newline to the end of app.json.
Suggested change
}
"contextSensitiveHelpUrl": "https://learn.microsoft.com/dynamics365/business-central/"
}

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Upgrade} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Upgrade procedure is internal instead of local

EnableApiToolsOnExistingConfigurations is declared internal, making it callable from other codeunits in the same app (e.g. test libraries). If called outside the upgrade pipeline—without the UpgradeTag guard being respected or with the tag already set—the ModifyAll would either silently no-op or unexpectedly re-enable API Tools. Making it internal also exposes the upgrade step to potential misuse in production.

Recommendation:

  • Declare the procedure local unless it genuinely needs to be called from outside this codeunit. If tests need to invoke it directly, create a thin internal wrapper in a test library instead.
local procedure EnableApiToolsOnExistingConfigurations()
var
    MCPConfiguration: Record "MCP Configuration";
    UpgradeTag: Codeunit "Upgrade Tag";
begin
    if UpgradeTag.HasDatabaseUpgradeTag(GetMCPEnableApiToolsUpgradeTag()) then
        exit;
    MCPConfiguration.ModifyAll(EnableApiTools, true);
    UpgradeTag.SetUpgradeTag(GetMCPEnableApiToolsUpgradeTag());
end;

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@onbuyuka onbuyuka closed this Jun 8, 2026
@onbuyuka onbuyuka reopened this Jun 8, 2026
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Performance} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

RefreshSubPages fires DB reads on every record navigation

RefreshSubPages() is called from OnAfterGetCurrRecord, which fires every time the user navigates to a different row on the configuration card. Each call invokes IsActive(ConfigId) for every feature enum value (currently 3 features), issuing at least 3 database reads per navigation. Users rapidly scrolling through configurations will trigger a burst of reads.

Recommendation:

  • Move the full RefreshSubPages reload out of OnAfterGetCurrRecord and keep it only in OnAfterGetRecord (triggered once per record load) plus the specific OnValidate handlers that actually change feature state, rather than on every cursor movement.
trigger OnAfterGetRecord()
begin
    IsDefault := MCPConfigImplementation.IsDefaultConfiguration(Rec);
    RefreshSubPages();
end;

trigger OnAfterGetCurrRecord()
begin
    // do not reload sub-pages here; already handled in OnAfterGetRecord and OnValidate triggers
end;

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

xMCPConfiguration: Record "MCP Configuration";
begin
if not MCPConfiguration.GetBySystemId(ConfigId) then
Error(ConfigurationNotFoundErr);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Security} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

API Tools cascade bypasses default-config guard

EnableAPITools(ConfigId, false) directly sets EnableDynamicToolMode := false without calling EnableDynamicToolMode, bypassing the DynamicToolModeCannotBeDisabledErr guard that prevents disabling Dynamic Tool Mode on the default configuration. A caller using the public MCPConfig.EnableAPITools facade can therefore silently disable Dynamic Tool Mode on the default config, breaking the invariant that EnableDynamicToolMode enforces.

Recommendation:

  • Add an IsDefaultConfiguration guard to EnableAPITools before the cascade—similar to the one in EnableDynamicToolMode—or call EnableDynamicToolMode(ConfigId, false) through the existing validated procedure rather than setting the field directly.
Suggested change
Error(ConfigurationNotFoundErr);
if not Enable then begin
if IsDefaultConfiguration(MCPConfiguration) then
Error(DesignatedDefaultCannotBeDeactivatedErr);
MCPConfiguration.EnableDynamicToolMode := false;
MCPConfiguration.DiscoverReadOnlyObjects := false;
end;

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

local procedure GetMCPEnableApiToolsUpgradeTag(): Text[250]
begin
exit('MS-631012-MCPEnableApiTools-20260603');
end;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

File missing trailing newline

Both MCPUpgrade.Codeunit.al and app.json are saved without a trailing newline (git diff shows \ No newline at end of file). This is inconsistent with the rest of the codebase, can cause diff noise in future patches, and may trigger linting warnings.

Recommendation:

  • Ensure each file ends with exactly one trailing newline (\n).
Suggested change
end;
// Add a blank line after the final `}` in MCPUpgrade.Codeunit.al

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Upgrade} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Import allows inconsistent enableApiTools=false + enableDynamicToolMode=true

During ImportConfiguration, both EnableApiTools and EnableDynamicToolMode are written directly to the record before Insert(), with no cross-field validation. A JSON payload containing enableApiTools: false and enableDynamicToolMode: true will produce a configuration that violates the new invariant—Dynamic Tool Mode requires API Tools—without raising any error.

Recommendation:

  • After reading all boolean flags from JSON and before MCPConfiguration.Insert(), add a consistency check: if EnableDynamicToolMode is true but EnableApiTools is false, either force EnableApiTools := true or throw an import error.
if MCPConfiguration.EnableDynamicToolMode and not MCPConfiguration.EnableApiTools then
    MCPConfiguration.EnableApiTools := true; // enforce prerequisite on import

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

MCPConfiguration.Init(); // not persisted yet (new config): reflect the table default (InitValue)
exit(MCPConfiguration.EnableAlQueryTools);
end;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Upgrade} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

EnableAPITools logs modified event even when no fields changed

EnableAPITools calls LogConfigurationModified(MCPConfiguration, xMCPConfiguration) unconditionally after MCPConfiguration.Modify(). If the caller passes the same value as already stored (e.g., enabling API Tools when it is already enabled), the audit log and telemetry will record a spurious modification event with no changed dimensions, polluting the audit trail. The same pattern exists in EnableDataQueryTools.

Recommendation:

  • Guard the Modify() and LogConfigurationModified() calls with a dirty-check: only proceed if the new value differs from the existing one, consistent with how other procedures in this codeunit behave.
Suggested change
if MCPConfiguration.EnableApiTools = Enable then
exit;
MCPConfiguration.EnableApiTools := Enable;
...

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

begin
SetStatusStyle();
end;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Upgrade} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Reload always iterates all features even when none changed

Reload is called after every SetActive call (to reflect cascaded changes). However, it also calls InsertRow for every enum ordinal, which calls ServerFeature.IsActive(ConfigId) (a DB read) for each feature on every activation/deactivation. If the feature list grows, this becomes O(n) DB calls on every user interaction, with no caching.

Recommendation:

  • Consider reading all required field values from the MCP Configuration record in a single GetBySystemId call inside Reload, then populating the temp table from in-memory values rather than dispatching an IsActive call per feature.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Upgrade} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

Missing negative test for Dynamic Tool Mode + API Tools gate

The test TestEnableDynamicToolMode was updated to first enable API Tools before enabling Dynamic Tool Mode, ensuring the positive path succeeds. However, there is no test that verifies calling EnableDynamicToolMode(ConfigId, true) when API Tools is disabled raises APIToolsRequiredForDynamicErr. Without a negative test, a future regression removing or relocating the guard would go undetected.

Recommendation:

  • Add a test procedure (e.g., TestEnableDynamicToolModeRequiresAPITools) that calls MCPConfig.EnableDynamicToolMode(ConfigId, true) on a config where API Tools is disabled and asserts that the expected error is thrown.
[Test]
procedure TestEnableDynamicToolModeRequiresAPITools()
var
    ConfigId: Guid;
begin
    ConfigId := CreateMCPConfig(false, false, true, false);
    // API Tools is off by default on new config
    asserterror MCPConfig.EnableDynamicToolMode(ConfigId, true);
    Assert.ExpectedError('API Tools must be enabled');
end;

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

page 8352 "MCP Config Tool List"
{
Caption = 'Available Tools';
Caption = 'Available APIs';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the rest of the tooltips etc also say API instead of tool?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants