Skip to content

ui: Replace per-conversation MCP overrides with per-conversation tool policy - #27745

Merged
allozaur merged 34 commits into
masterfrom
allozaur/feat/chat-form-actions/tool-policy
Aug 27, 2026
Merged

ui: Replace per-conversation MCP overrides with per-conversation tool policy#27745
allozaur merged 34 commits into
masterfrom
allozaur/feat/chat-form-actions/tool-policy

Conversation

@allozaur

@allozaur allozaur commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Overview

  • MCP server enabled state is now purely global (server.enabled); per-conversation toggles in the dropdown/sheet are removed
  • Per-conversation control moves to a tool policy on the conversation row: disabled tool categories + disabled tool keys (incl. server-scoped mcp:<serverId> group keys), applied to the tool list sent to the LLM
  • New conversations are seeded from the global defaults at creation; edits afterwards live on the conversation row only and do not flow back into the defaults
  • The policy gates more than tools: MCP connections aren't opened for a conversation whose policy leaves no usable server (category off, or every enabled server's group key disabled), and prompt/resource capability checks plus the server avatars follow the same policy, so a disabled server's prompts/resources no longer appear in the pickers (per-chat gating the old overrides provided); the CWD and mention pickers respect the policy for file_glob_search
  • Tools submenu and mobile add sheet split into category groups and per-server MCP groups (grayed out while the MCP category is off), with mixed-state group checkboxes; the sheet regains the Reasoning section
  • Sheet's per-server toggle list replaced by a single "MCP Servers" entry that opens the dialog from the previous PR
  • Settings > Chat > Tools edits the global defaults explicitly (category checkboxes added; "applies to new conversations" note) — the in-chat tools panels edit the per-conversation policy
  • One-off migration rewrites existing conversations' legacy mcpServerOverrides into the new policy (field deprecated, kept for downgrade compatibility)

Requirements

@allozaur
allozaur force-pushed the allozaur/feat/chat-form-actions/tool-policy branch from 0e6c5c0 to 927bae6 Compare August 26, 2026 14:38
@allozaur allozaur changed the title ui: replace per-conversation MCP overrides with per-conversation tool policy ui: Replace per-conversation MCP overrides with per-conversation tool policy Aug 26, 2026
@allozaur
allozaur marked this pull request as ready for review August 26, 2026 14:49
Copilot AI lite review requested due to automatic review settings August 26, 2026 14:49
@allozaur
allozaur requested a review from a team as a code owner August 26, 2026 14:49

Copilot AI 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.

Pull request overview

This PR refactors the UI’s MCP enablement model by removing per-conversation MCP server overrides and replacing them with a per-conversation “tool policy” (disabled tool categories + disabled tool keys, including server-scoped mcp:<serverId> keys) that is seeded from global defaults at conversation creation and editable afterwards. It also adds a one-off migration to preserve existing behavior by translating legacy mcpServerOverrides into the new policy shape.

Changes:

  • Introduces per-conversation tool policy fields (disabledTools, disabledToolCategories) and updates agentic flow startup to capture/apply that policy.
  • Simplifies MCP store enablement to be purely global (server.enabled), removing per-chat server checks throughout the MCP store.
  • Adds a migration to seed existing conversation rows from global disabled-tool defaults plus legacy MCP overrides.

Reviewed changes

Copilot reviewed 19 out of 21 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tools/ui/tests/unit/mcp-override-fallback.test.ts Removes regression tests tied to legacy per-chat MCP override behavior.
tools/ui/src/lib/types/database.d.ts Deprecates mcpServerOverrides and adds per-conversation tool policy fields.
tools/ui/src/lib/types/agentic.d.ts Replaces per-chat MCP overrides with an optional toolPolicy captured at flow start.
tools/ui/src/lib/stores/tools.svelte.ts Adds global default disabled categories, adds MCP server-group key support, and makes tool enablement policy-driven.
tools/ui/src/lib/stores/mcp/index.svelte.ts Removes per-chat enablement; MCP server enabled state becomes purely global.
tools/ui/src/lib/stores/conversations/preferences.svelte.ts Adds per-conversation tool policy accessors/toggles and threads policy snapshots into new conversations.
tools/ui/src/lib/stores/conversations/index.svelte.ts Seeds new conversations with a snapshot of the current global tool defaults.
tools/ui/src/lib/stores/chat/index.svelte.ts Captures the current conversation’s tool policy and passes it into agentic flows.
tools/ui/src/lib/stores/agentic/index.svelte.ts Applies per-flow tool policy when building the tool list; MCP init now depends on global enablement.
tools/ui/src/lib/services/migration.service.ts Adds migration rewriting legacy MCP overrides into per-conversation disabled tool keys.
tools/ui/src/lib/hooks/use-tools-panel.svelte.ts Refactors tools panel grouping into categories + per-server MCP groups and routes toggles via conversation preferences.
tools/ui/src/lib/constants/storage.constants.ts Adds localStorage key for global default disabled tool categories.
tools/ui/src/lib/components/app/settings/SettingsMcpServers.svelte Makes MCP server enablement purely global in settings UI.
tools/ui/src/lib/components/app/mcp/McpActiveServersAvatars.svelte Updates “enabled servers for chat” to no longer use legacy per-chat overrides.
tools/ui/src/lib/components/app/dialogs/DialogMcpServerAddNew.svelte Removes per-chat override side effect when adding a new server.
tools/ui/src/lib/components/app/dialogs/DialogMcpResourcesBrowser.svelte Removes per-chat overrides from MCP initialization for resource browsing.
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte Removes per-chat overrides from MCP initialization for prompt browsing.
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte Capability checks now depend only on global MCP enablement.
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte Renders tools as category groups + per-server MCP groups and routes toggles via conversation preferences.
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte Reworks the mobile add sheet tools section and MCP entry behavior.
tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte Routes “has cwd tools” through per-conversation preferences; MCP prompts capability check is global.
Suppressed comments (2)

tools/ui/src/lib/stores/conversations/preferences.svelte.ts:311

  • This section header says "Working Directory" but it now sits above persistDisabledTools(), which is tool-policy persistence. Consider updating/removing this header to match the code below it.
	 * Working Directory

tools/ui/src/lib/stores/conversations/preferences.svelte.ts:57

  • Same clarity issue as the disabledTools comment: this is effectively "conversation row, or defaults when no conversation" (not a live fallback when the field is undefined on an existing conversation).
/** Effective disabled tool categories: conversation row, falling back to defaults. */

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tools/ui/src/lib/services/migration.service.ts
Comment thread tools/ui/src/lib/stores/conversations/preferences.svelte.ts Outdated
Comment thread tools/ui/src/lib/stores/agentic/index.svelte.ts Outdated
Comment thread tools/ui/src/lib/stores/agentic/index.svelte.ts Outdated
Comment thread tools/ui/src/lib/components/app/mcp/McpActiveServersAvatars.svelte
Comment thread tools/ui/src/lib/stores/conversations/preferences.svelte.ts Outdated
@allozaur
allozaur force-pushed the allozaur/feat/chat-form-actions/tool-policy branch from 927bae6 to 9ff5415 Compare August 26, 2026 15:37
@allozaur
allozaur force-pushed the allozaur/feat/chat-form-actions/tool-policy branch from 9ff5415 to b119d55 Compare August 26, 2026 18:19
Base automatically changed from allozaur/feat/chat-form-actions/settings-mcp-dialogs to master August 26, 2026 19:07
@allozaur
allozaur force-pushed the allozaur/feat/chat-form-actions/tool-policy branch 2 times, most recently from f26b5c1 to 73a94a7 Compare August 26, 2026 19:10
… policy

MCP server enabled state is now global (server.enabled); per-conversation
control moves to disabled tool keys and categories seeded into each new
conversation. Aligns the add sheet with the dropdown options and flattens
MCP tool groups in the tools submenu.

Assisted-by: pi
A corrupt disabledToolKeys localStorage entry no longer aborts the
migration; it falls through with empty defaults so legacy MCP server
overrides still get converted.

Assisted-by: pi
Passing empty disabled sets bypassed the global defaults and could
enable tools for callers that do not pass a policy yet.

Assisted-by: pi
The Reasoning Effort and Working Directory headers sat above tool
policy methods; move them above setCwd and setReasoningEffort. Also
clarify the disabled tools JSDoc: existing rows with an unset field
have an empty policy, defaults apply only when there is no active
conversation.

Assisted-by: pi
Servers whose tools are disabled for the current conversation (MCP
category or server-scoped key) no longer show as enabled for the chat.

Assisted-by: pi
Per-conversation MCP control is server-granular; no component renders
a whole-category toggle, so remove the dead API.

Assisted-by: pi
Resolve the effective tool policy before deciding whether to
initialize MCP so flows that will not send any MCP tools skip the
init work. Callers without a policy keep falling back to global
defaults.

Assisted-by: pi
The sheet rewrite dropped it; the desktop dropdown still has it.
MCP Prompts and Resources stay out of the sheet on purpose.

Assisted-by: pi
The group key disables every tool of the server regardless of
per-tool keys, so re-enabling a server from Settings did nothing
while it was set.

Assisted-by: pi
Extends the category-level check: the flow also skips MCP init when
every globally-enabled server has its server-scoped group key
disabled in the tool policy.

Assisted-by: pi
Adds per-category checkboxes and a caption stating the tab applies
to new conversations; tool picks inside a chat only affect that
chat.

Assisted-by: pi
Both checked the global disabled set directly, so a conversation
that disabled file_search still showed search as available.

Assisted-by: pi
Documents getEnabledToolsForLLM properly, unstacks the JSDoc at
isEntryEnabled, makes setToolEnabled persist like setCategoryEnabled
(toggleTool now delegates to it), and routes the serverId-less MCP
branch of toolKey through getMcpServerToolsKey so both key formats
come from one place. Preferences banner comments become plain
comments so they no longer read as class member docs.

Assisted-by: pi
A category that is on with nothing enabled under it now shows the
mixed checkbox state instead of a checked box next to 0/N. Rows
grayed out by a disabled parent no longer stay clickable behind
opacity.

Assisted-by: pi
hasPromptsCapability and hasResourcesCapability accept an optional
set of usable server ids; ChatFormActions resolves it from global
enablement minus the active conversation's policy. Restores the
per-chat gating the old mcpServerOverrides provided; callers without
arguments keep global behavior.

Assisted-by: pi
Never rendered anywhere; its entries are duplicates (prompts and
resources live in the attachment menu, servers in the add menu and
sheet) that would need capability wiring maintained for nothing.

Assisted-by: pi
The dialog sets container-type: inline-size, so auto width ignores
its contents and collapses to padding. Give it an explicit viewport
width on mobile and cap at 60rem on desktop.

Assisted-by: pi
Long unbreakable Jinja tokens blew out the table and dialog width;
the block now scrolls horizontally instead of stretching.

Assisted-by: pi
Auto table layout sizes columns to content min-content, so the chat
template's long lines kept inflating the dialog despite the scroll
wrapper. Fixed layout pins the first column and gives the value
column a definite width the wrapper can scroll within. min-w-0 on
the grid item guards the same path on the grid side.

Assisted-by: pi
Matches the settings dialog pattern: full viewport below md,
calc-sized and capped at 60rem on desktop.

Assisted-by: pi
Label above the block in a single full-width cell, so the template
gets the whole table width and its horizontal scroll is usable on
narrow screens.

Assisted-by: pi
The base dialog header is sticky; this dialog overrides it to
relative so the title and description scroll away with the body.
relative keeps the header as the close button's containing block.

Assisted-by: pi
A // line inside the Svelte snippet rendered as visible text; use an
HTML comment.

Assisted-by: pi
The checkbox indicator snippet renders the check icon whenever
checked, so the mixed state never showed. Pass the checked prop
as false while indeterminate.

Assisted-by: pi
ensureInitialized accepts an optional server id set; the agentic
flow passes the servers its tool policy leaves usable, so servers
disabled for the conversation no longer get connected. Callers
without arguments keep the global behavior.

Assisted-by: pi
Moves the mixed-state derivation out of the submenu and sheet
snippets into one getGroupCheckState accessor; the snippets just
consume checked and indeterminate.

Assisted-by: pi
The slash command's availability now follows the same rule as the
agentic flow instead of the global capability check, so it disables
itself when the conversation's policy leaves no usable MCP server.

Assisted-by: pi
The /prompt slash command is the surviving trigger; the menu-button
path (onMcpPromptClick, hasMcpPromptsSupport, showMcpPromptButton,
the MCP_PROMPT attachment item and its unrendered item arrays) has
no consumer left. Message display for inserted prompts is untouched.

Assisted-by: pi
The accessor refactor dropped the checked-and-not-indeterminate
guard, so the category-on flag won and the dash never showed. The
tooltip keeps using the raw parent flag since clicking a mixed
group still disables it.

Assisted-by: pi
Clicking a mixed-state group box let bits-ui optimistically flip
its internal checked flag; the derived checked prop did not change
across the transition (both mixed and off map to checked=false),
so Svelte never applied the settled value and the check icon stuck
while the count already read 0/7.

Pass the parent flag as checked and the mix as indeterminate, so
every group toggle changes checked; render the dash on top of a
checked box for the mixed state.

Assisted-by: pi
@allozaur
allozaur force-pushed the allozaur/feat/chat-form-actions/tool-policy branch from 47852d0 to 9b521a7 Compare August 27, 2026 09:03
ensureInitialized folds the policy into its config signature, so
alternating two conversations with different policies tore down and
reconnected every server with health checks included. Tool collection
already filters by the flow policy, so initialize every
settings-enabled server instead and never pass a policy into the MCP
config. The duplicated policy-server check becomes one accessor on
ConversationPreferences.

Assisted-by: pi
Same shape as the earlier prompt trigger cleanup: nothing renders the
MCP resources menu button, and the only live entry into resource
browsing is Settings > MCP Servers plus the attachment resource
picker. Drop onMcpResourcesClick, hasMcpResourcesSupport,
MCP_RESOURCES_CLICK, the AttachmentItemVisibleWhen enum and
hasResourcesCapability; the resources display, browser and picker
components are untouched.

Assisted-by: pi
@allozaur allozaur added the merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. label Aug 27, 2026
@allozaur
allozaur merged commit fe235f4 into master Aug 27, 2026
7 checks passed
ppenatra pushed a commit to ppenatra/llama.cpp that referenced this pull request Aug 27, 2026
… policy (ggml-org#27745)

* ui: replace per-conversation MCP overrides with per-conversation tool policy

MCP server enabled state is now global (server.enabled); per-conversation
control moves to disabled tool keys and categories seeded into each new
conversation. Aligns the add sheet with the dropdown options and flattens
MCP tool groups in the tools submenu.

Assisted-by: pi

* ui: keep tool policy migration running when defaults parse fails

A corrupt disabledToolKeys localStorage entry no longer aborts the
migration; it falls through with empty defaults so legacy MCP server
overrides still get converted.

Assisted-by: pi

* ui: fall back to global defaults when agentic flow has no tool policy

Passing empty disabled sets bypassed the global defaults and could
enable tools for callers that do not pass a policy yet.

Assisted-by: pi

* ui: align preferences section headers with their methods

The Reasoning Effort and Working Directory headers sat above tool
policy methods; move them above setCwd and setReasoningEffort. Also
clarify the disabled tools JSDoc: existing rows with an unset field
have an empty policy, defaults apply only when there is no active
conversation.

Assisted-by: pi

* ui: gate MCP server avatars on conversation tool policy

Servers whose tools are disabled for the current conversation (MCP
category or server-scoped key) no longer show as enabled for the chat.

Assisted-by: pi

* ui: drop unused MCP category toggle from tools panel hook

Per-conversation MCP control is server-granular; no component renders
a whole-category toggle, so remove the dead API.

Assisted-by: pi

* ui: skip MCP init when flow policy disables the MCP category

Resolve the effective tool policy before deciding whether to
initialize MCP so flows that will not send any MCP tools skip the
init work. Callers without a policy keep falling back to global
defaults.

Assisted-by: pi

* chore: format

* ui: restore reasoning section in mobile add sheet

The sheet rewrite dropped it; the desktop dropdown still has it.
MCP Prompts and Resources stay out of the sheet on purpose.

Assisted-by: pi

* ui: clear MCP server group key in enableAllToolsForServer

The group key disables every tool of the server regardless of
per-tool keys, so re-enabling a server from Settings did nothing
while it was set.

Assisted-by: pi

* ui: skip MCP init when no policy-enabled server remains

Extends the category-level check: the flow also skips MCP init when
every globally-enabled server has its server-scoped group key
disabled in the tool policy.

Assisted-by: pi

* ui: make Settings tools tab edit defaults with category toggles

Adds per-category checkboxes and a caption stating the tab applies
to new conversations; tool picks inside a chat only affect that
chat.

Assisted-by: pi

* ui: gate cwd picker and mention picker on effective tool policy

Both checked the global disabled set directly, so a conversation
that disabled file_search still showed search as available.

Assisted-by: pi

* ui: clean up tool key helpers and store docs

Documents getEnabledToolsForLLM properly, unstacks the JSDoc at
isEntryEnabled, makes setToolEnabled persist like setCategoryEnabled
(toggleTool now delegates to it), and routes the serverId-less MCP
branch of toolKey through getMcpServerToolsKey so both key formats
come from one place. Preferences banner comments become plain
comments so they no longer read as class member docs.

Assisted-by: pi

* ui: indeterminate group checkboxes and inert grayed rows

A category that is on with nothing enabled under it now shows the
mixed checkbox state instead of a checked box next to 0/N. Rows
grayed out by a disabled parent no longer stay clickable behind
opacity.

Assisted-by: pi

* ui: gate MCP prompt and resource capabilities on tool policy

hasPromptsCapability and hasResourcesCapability accept an optional
set of usable server ids; ChatFormActions resolves it from global
enablement minus the active conversation's policy. Restores the
per-chat gating the old mcpServerOverrides provided; callers without
arguments keep global behavior.

Assisted-by: pi

* ui: remove unmounted MCP submenu component

Never rendered anywhere; its entries are duplicates (prompts and
resources live in the attachment menu, servers in the add menu and
sheet) that would need capability wiring maintained for nothing.

Assisted-by: pi

* ui: fix model information dialog width on all screen sizes

The dialog sets container-type: inline-size, so auto width ignores
its contents and collapses to padding. Give it an explicit viewport
width on mobile and cap at 60rem on desktop.

Assisted-by: pi

* ui: scroll wide chat template in model information dialog

Long unbreakable Jinja tokens blew out the table and dialog width;
the block now scrolls horizontally instead of stretching.

Assisted-by: pi

* ui: use fixed table layout in model information dialog

Auto table layout sizes columns to content min-content, so the chat
template's long lines kept inflating the dialog despite the scroll
wrapper. Fixed layout pins the first column and gives the value
column a definite width the wrapper can scroll within. min-w-0 on
the grid item guards the same path on the grid side.

Assisted-by: pi

* ui: make model information dialog full-screen on mobile

Matches the settings dialog pattern: full viewport below md,
calc-sized and capped at 60rem on desktop.

Assisted-by: pi

* ui: stack chat template row in model information dialog

Label above the block in a single full-width cell, so the template
gets the whole table width and its horizontal scroll is usable on
narrow screens.

Assisted-by: pi

* ui: scroll model information header with the content

The base dialog header is sticky; this dialog overrides it to
relative so the title and description scroll away with the body.
relative keeps the header as the close button's containing block.

Assisted-by: pi

* ui: replace literal comment text in sheet group snippet

A // line inside the Svelte snippet rendered as visible text; use an
HTML comment.

Assisted-by: pi

* ui: let indeterminate state win over checked in group checkboxes

The checkbox indicator snippet renders the check icon whenever
checked, so the mixed state never showed. Pass the checked prop
as false while indeterminate.

Assisted-by: pi

* ui: initialize only policy-enabled MCP servers for a flow

ensureInitialized accepts an optional server id set; the agentic
flow passes the servers its tool policy leaves usable, so servers
disabled for the conversation no longer get connected. Callers
without arguments keep the global behavior.

Assisted-by: pi

* ui: derive group checkbox state in useToolsPanel

Moves the mixed-state derivation out of the submenu and sheet
snippets into one getGroupCheckState accessor; the snippets just
consume checked and indeterminate.

Assisted-by: pi

* ui: gate /prompt command on the conversation tool policy

The slash command's availability now follows the same rule as the
agentic flow instead of the global capability check, so it disables
itself when the conversation's policy leaves no usable MCP server.

Assisted-by: pi

* ui: remove dead MCP prompt menu trigger chain

The /prompt slash command is the surviving trigger; the menu-button
path (onMcpPromptClick, hasMcpPromptsSupport, showMcpPromptButton,
the MCP_PROMPT attachment item and its unrendered item arrays) has
no consumer left. Message display for inserted prompts is untouched.

Assisted-by: pi

* ui: render dash for mixed-state group checkboxes

The accessor refactor dropped the checked-and-not-indeterminate
guard, so the category-on flag won and the dash never showed. The
tooltip keeps using the raw parent flag since clicking a mixed
group still disables it.

Assisted-by: pi

* ui: fix group checkbox sticking checked after disable

Clicking a mixed-state group box let bits-ui optimistically flip
its internal checked flag; the derived checked prop did not change
across the transition (both mixed and off map to checked=false),
so Svelte never applied the settled value and the check icon stuck
while the count already read 0/7.

Pass the parent flag as checked and the mix as indeterminate, so
every group toggle changes checked; render the dash on top of a
checked box for the mixed state.

Assisted-by: pi

* fix: UI for Model Information dialog

* ui: keep MCP connections stable across policy switches

ensureInitialized folds the policy into its config signature, so
alternating two conversations with different policies tore down and
reconnected every server with health checks included. Tool collection
already filters by the flow policy, so initialize every
settings-enabled server instead and never pass a policy into the MCP
config. The duplicated policy-server check becomes one accessor on
ConversationPreferences.

Assisted-by: pi

* ui: remove dead MCP resources menu trigger chain

Same shape as the earlier prompt trigger cleanup: nothing renders the
MCP resources menu button, and the only live entry into resource
browsing is Settings > MCP Servers plus the attachment resource
picker. Drop onMcpResourcesClick, hasMcpResourcesSupport,
MCP_RESOURCES_CLICK, the AttachmentItemVisibleWhen enum and
hasResourcesCapability; the resources display, browser and picker
components are untouched.

Assisted-by: pi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. server/ui

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants