[MCP] Server Features in MCP configuration: API Tools, Dynamic Tool Mode, Data Query Tools (Preview)#8085
[MCP] Server Features in MCP configuration: API Tools, Dynamic Tool Mode, Data Query Tools (Preview)#8085onbuyuka wants to merge 22 commits into
Conversation
…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>
…12-al-query-mcp-config-ui
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>
There was a problem hiding this comment.
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.
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>
…12-al-query-mcp-config-ui
…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>
…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>
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>
3a3217d to
49b399f
Compare
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>
| if not Enable and IsDefaultConfiguration(MCPConfiguration) then | ||
| Error(DynamicToolModeCannotBeDisabledErr); | ||
|
|
||
| if Enable and not IsAPIToolsEnabled(ConfigId) then |
There was a problem hiding this comment.
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.'
| 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"))); |
There was a problem hiding this comment.
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.
| 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
Name OnValidate fires CurrPage.Update on every keystroke for new recordsThe Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
SystemToolList FactBox always reloads on cursor move
Recommendation:
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'; |
There was a problem hiding this comment.
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.ActivetoSetAsDefault(only active configs should be set as default, consistent with the tooltip text) and document the active requirement explicitly.
| 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 |
There was a problem hiding this comment.
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 theOnLookuptrigger. Use a flag or only delete when called from theOnLookuptrigger path.
| 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) | ||
| { |
There was a problem hiding this comment.
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
ParentFeatureActiveinReloadthat is set per-row, or extend theMCP Server Featuretable with aParentActivefield, and include it in theVisible/Enabledexpression for theActivateaction.
| { | |
| 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 |
There was a problem hiding this comment.
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.
| 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'); |
There was a problem hiding this comment.
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 withDynToolMode=true, DiscoverReadOnly=true, APITools=trueand verifies all three cascade off.
| 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] |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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.
| 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
Export action missing AccessByPermission guardThe 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:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
EnableAPITools allows modifying active configurationsEnableAPITools 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:
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") |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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/" | ||
| } |
There was a problem hiding this comment.
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.
| } | |
| "contextSensitiveHelpUrl": "https://learn.microsoft.com/dynamics365/business-central/" | |
| } |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
Upgrade procedure is internal instead of localEnableApiToolsOnExistingConfigurations 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:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
RefreshSubPages fires DB reads on every record navigation
Recommendation:
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); |
There was a problem hiding this comment.
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
IsDefaultConfigurationguard toEnableAPIToolsbefore the cascade—similar to the one inEnableDynamicToolMode—or callEnableDynamicToolMode(ConfigId, false)through the existing validated procedure rather than setting the field directly.
| 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; |
There was a problem hiding this comment.
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).
| end; | |
| // Add a blank line after the final `}` in MCPUpgrade.Codeunit.al |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
Import allows inconsistent enableApiTools=false + enableDynamicToolMode=trueDuring Recommendation:
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; | ||
|
|
There was a problem hiding this comment.
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()andLogConfigurationModified()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.
| if MCPConfiguration.EnableApiTools = Enable then | |
| exit; | |
| MCPConfiguration.EnableApiTools := Enable; | |
| ... |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| begin | ||
| SetStatusStyle(); | ||
| end; | ||
|
|
There was a problem hiding this comment.
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 Configurationrecord in a singleGetBySystemIdcall insideReload, then populating the temp table from in-memory values rather than dispatching anIsActivecall per feature.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
Missing negative test for Dynamic Tool Mode + API Tools gateThe test Recommendation:
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'; |
There was a problem hiding this comment.
Should the rest of the tooltips etc also say API instead of tool?
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 Configurationfields shipped in BC-Platform #44811; only the Data Query Tools server runtime lands separately (see Platform dependency).Feature handlers (interface-based)
interface "MCP Server Features"—SetActive/IsActive/HasSettings/OpenSettings/Description/LoadSystemTools/TryGetParentFeature.enum "MCP Server Feature"implements it, binding each value to a handler codeunit; resolvingServerFeature := Rec.Featurereplaces the old per-featurecaseblocks.MCP Server Feature ListandMCP System Tool Listpages are fully generic: each walksServerFeature.Ordinals()+FromInteger(...)(the same pattern asMCP 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.SetActivedelegates tocodeunit "MCP Config Implementation", which reads/writes the realMCP Configurationfields (EnableDynamicToolMode,EnableApiTools,EnableAlQueryTools);IsActivereads them back.Card UX
Server Featureslist-part with per-row Activate / Deactivate / Configure, mirroringpage 7775 "Copilot AI Capabilities"→Copilot Capabilities GA. Configure appears only for features that have settings — gated on a per-rowConfigurableflag set from the handler'sHasSettings(). Dynamic Tool Mode renders indented as a sub-feature of API Tools (TryGetParentFeature+ anIndentationcolumn on the temp table).Visible = not IsDefault and APIToolsActive), where the admin curates which API pages / queries the client can reach. No settings.Discover Read-Only Objects; activation flows toEnableDynamicToolMode, deactivation cascade-clearsDiscoverReadOnlyObjects. Enabling it requires API Tools to be enabled first —impl.EnableDynamicToolModeenforces the gate (APIToolsRequiredForDynamicErr).page 8352) — the curated API list. ItsSelect APIsaction opens a single lookup listing both API pages and API queries (new temptable "MCP API Object Buffer"+page "MCP API Object Lookup", populated byimpl.LookupAPIObjects); the per-rowObject Idlookup shares the same path. The part was renamed'Available Tools'→'API Tools'→'Available APIs'to disambiguate it from the API Tools feature row.StandardDialog(page 8369 "MCP Server Feature Settings") is used only by Dynamic Tool Mode; its handler runs it viaOpenSettings, writing on OK.LoadSystemTools.Configuration list (
page 8350)Defaultfield is read-only on the card; Set as Default / Clear Default actions (promoted) live on the configuration list instead.Rec.EnableApiTools/Rec.EnableAlQueryTools) replaced the previous Dynamic Tool Mode / Discover Read-Only Objects columns.Public API
codeunit 8350 "MCP Config"exposesEnableAPIToolsandEnableDataQueryToolsalongside the existingEnableDynamicToolMode/EnableDiscoverReadOnlyObjects. The two feature procedures read/write theMCP Configuration.EnableApiTools/.EnableAlQueryToolsfields.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; carriesConfigurable+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"8369 "MCP API Tools Feature",8368 "MCP Data Query Tools Feature",8370 "MCP Dyn. Tool Mode Feature"Deprecated
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 unifiedMCP API Object Lookup(8376). They're public System Application objects, so they're deprecated rather than deleted (removing them breaks dependent extensions —AS0029). Their internalMCP Config Implementationlookup procedures (LookupAPIPageTools/LookupAPIQueryTools) and the test-library wrappers were removed (neither is public surface).Permissions
MCP - Objectslists 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).MCP Configuration.EnableApiTools(field 8,InitValue = true) /.EnableAlQueryTools(field 9), read/written through fourMCP Config Implementationprocedures. An upgrade (MCP Upgrade.EnableApiToolsOnExistingConfigurations) turns API Tools on for existing configurations (InitValueonly covers new ones)."MCP Utilities".GetSystemToolsInDataQuery().The platform uses the "AL Query Tools" field name; our UX stays "Data Query Tools" (it maps onto
EnableAlQueryTools).Test plan
MCP Config Test(codeunit 130130): activation tests asserting the realEnableApiTools/EnableAlQueryToolsfield state; aServer FeaturesTestPage set onpage 8351 "MCP Config Card"(the list shows all three features in order,Configureis enabled only on Dynamic Tool Mode, activating Dynamic Tool Mode flips its row Status and setsEnableDynamicToolMode, and the part is hidden on the default configuration; API Tools is enabled before Dynamic Tool Mode so the gate is satisfied);TestLookupAPIObjectsexercising the unified API lookup; and Export/Import round-trip tests asserting the two new fields survive.Fixes AB#631012
🤖 Generated with Claude Code