Skip to content

📌 feat: Seed Default Pinned Tools and MCP Dropdown via Interface Config#13865

Merged
danny-avila merged 3 commits into
devfrom
claude/gallant-sanderson-ca8d90
Jun 20, 2026
Merged

📌 feat: Seed Default Pinned Tools and MCP Dropdown via Interface Config#13865
danny-avila merged 3 commits into
devfrom
claude/gallant-sanderson-ca8d90

Conversation

@danny-avila

Copy link
Copy Markdown
Owner

✨ feat: Add defaultPinnedTools interface config for default tool & MCP pinning

📋 Summary

Adds a single interface.defaultPinnedTools string array that lets admins control which tools — and the MCP servers dropdown — are pinned to the prompt bar by default for all users.

This unifies two prior community contributions into one config key:

🎯 Problem

Users currently have to manually pin each tool (Artifacts, Code Interpreter, Web Search, etc.) one-by-one, every environment, and the MCP dropdown's default pinned state isn't configurable. Admins had no way to set sensible defaults org-wide.

✨ Solution

interface:
  defaultPinnedTools:
    - 'artifacts'
    - 'execute_code'
    - 'mcp'
  • Tools — listing a tool key (artifacts, execute_code, web_search, file_search, skills) pins that badge by default, wired through useToolToggle.
  • MCP — listing the keyword 'mcp' (or a specific configured MCP server name) pins the MCP servers dropdown, wired through useMCPSelect.
  • Default-only semantics — this only seeds the initial pinned state. Once a user pins/unpins anything, their stored preference always takes precedence (mirrors how other default* options behave).
  • Backward compatible — when the option is omitted entirely, behavior is unchanged: tools start unpinned and the MCP dropdown keeps its legacy default (pinned). Configuring the array without 'mcp' is how an admin unpins MCP by default (the old defaultPinMcp: false use case).

How it flows

interface.defaultPinnedToolsinterfaceSchema (data-provider) → loadDefaultInterface (data-schemas) → startupConfig.interface → consumed by useToolToggle / useMCPSelect on the client.

🧪 Testing

  • packages/data-provider config-schemas.spec.ts — schema accepts the array / leaves it undefined when unset.
  • packages/data-schemas interface.spec.ts — passthrough when configured, omitted when not.
  • client useToolToggle.test.tsx — seeds pinned state from the configured tool keys.
  • client useMCPSelect.test.tsx'mcp'/server-name pins, configured-without-mcp unpins, legacy default preserved, stored user preference wins.

All affected suites pass; eslint and client tsc --noEmit are clean.

Change Type

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • My changes do not introduce new warnings
  • Local unit tests pass with my changes
  • New tests added covering the feature
  • A docs PR for librechat.ai will follow

…MCP pinning

Adds an `interface.defaultPinnedTools` string array letting admins pin tools and the MCP servers dropdown to the prompt bar by default for all users.

- Tool keys (artifacts, execute_code, web_search, file_search, skills) pin their badge via `useToolToggle`.

- The keyword `'mcp'` or a configured MCP server name pins the MCP dropdown via `useMCPSelect`.

- Only seeds initial state; a user's stored pin preference always wins. When unset, tools start unpinned and the MCP dropdown keeps its legacy default (pinned).

Unifies the approaches from #11646 (pinnedTools) and #9251 (defaultPinMcp) into one config key.
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 761379bbf5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +87 to +90
const [isPinned, setIsPinned] = useLocalStorage<boolean>(
`${localStorageKey}pinned`,
defaultPinned,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Migrate existing auto-seeded unpinned values

For users who have loaded the app before this feature, ${localStorageKey}pinned already exists as false because the previous hook call used useLocalStorage(..., false) and useLocalStorageAlt writes the default when the key is absent. Passing defaultPinned only as the hook's initial default means those auto-created false values are treated as user preferences, so an admin setting interface.defaultPinnedTools: ['execute_code'] will not pin Code Interpreter/Web/File/etc. for existing users who never chose a pin state. Add a migration or a way to distinguish auto-seeded defaults from explicit user choices before honoring the stored value.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Thanks — this is accurate, and it's an intentional design choice here rather than a bug.

defaultPinnedTools is scoped to seed the initial pin state only; it deliberately does not override a value already persisted on the client. That matches how the rest of LibreChat's interface defaults behave (we don't retroactively rewrite a user's localStorage-persisted UI state) and the intended semantics of this option: it "only affects the initial/default state and doesn't override user preferences once they've interacted with pinning."

The constraint you've identified is real: in the pre-existing data model an auto-seeded false is indistinguishable from a deliberate unpin. So any migration that force-applies the configured default to existing false values would also re-pin tools that users intentionally unpinned — a worse regression than the gap it closes. The maintainer's call is to not override existing client state.

Net effect: the defaults apply to new users and fresh storage; existing users keep their current pins and can still pin manually as today. If broad retroactive application is ever wanted, it would be a separate, versioned pin-storage migration rather than something folded into this PR.

@danny-avila danny-avila changed the title ✨ feat: Add defaultPinnedTools interface config for default tool & MCP pinning 📌 feat: Seed Default Pinned Tools and MCP Dropdown via Interface Config Jun 20, 2026
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 761379bbf5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +87 to +90
const [isPinned, setIsPinned] = useLocalStorage<boolean>(
`${localStorageKey}pinned`,
defaultPinned,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Defer pin initialization until startup config loads

In a cold load where useGetStartupConfig() is still fetching, defaultPinned is false and useLocalStorageAlt persists that default when the pin key is absent; its initialization effect also does not rerun for later defaultValue changes. When the config eventually returns with defaultPinnedTools: ['execute_code'] (or any other tool), the stored false is now treated as the user's preference, so the admin-configured default pin is missed for fresh users whenever startup config was not already cached.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in e180cc3.

useToolToggle now captures whether a pin preference already existed before mount (pre-seed), and a one-shot effect applies the configured default once startupConfig resolves — for users with no prior preference only. So on a cold load where useGetStartupConfig() is still fetching, the eagerly-seeded false is corrected to the admin default when the config arrives, while an existing stored pin is never overridden (keeping the conservative behavior for returning users).

For reference, the MCP path in useMCPSelect was already guarded this way: its init effect early-returns until startupConfig is present, and mcpPinnedAtom (atomWithStorage) doesn't eager-seed, so absence of a stored value stays meaningful.

Added an integration test (useToolToggle.pinDefault.test.tsx, using the real useLocalStorageAlt + jsdom localStorage) covering: the cold-load race (config resolves after mount → default applied), the already-cached path, and the no-override guarantee for an existing stored unpin.

On a cold load, useToolToggle can mount before useGetStartupConfig() resolves, so defaultPinned starts false and useLocalStorageAlt eagerly persists it; its init effect never re-runs for the later config-driven default. Fresh users would then miss the admin-configured default pin whenever startup config was not already cached.

Capture whether a pin preference existed before mount (pre-seed) and, once startupConfig arrives, apply the real default for users with no prior preference. Runs once and never overrides an existing stored pin, so the conservative behavior for existing users is preserved.
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e180cc30be

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +105 to +109
if (hadStoredPin) {
return;
}
if (defaultPinned !== isPinned) {
setIsPinned(defaultPinned);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve pin clicks made before startup config loads

When startupConfig is still null on the first render, a user can toggle a tool's pin before the query resolves. Because hadStoredPin is captured only once at mount, this block still treats the user as having no preference and calls setIsPinned(defaultPinned) when config arrives, overwriting that click (for example, a manually pinned code tool becomes unpinned if it is not listed). Re-check the current storage value or mark user updates before applying the admin default.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb9a086.

Explicit pin toggles now flip a ref (set through the returned setter), and the admin-default effect skips application whenever the user has interacted in-session — alongside the existing stored-preference guard. So a pin clicked during the cold-load window (before startupConfig resolves) is preserved instead of being overwritten by the default.

Added a test for exactly this case: pin the tool before config resolves, then have config arrive without the tool listed — the user's pin is kept (useToolToggle.pinDefault.test.tsx).


/** Captured before mount (pre-seed): did the user already have a stored pin preference?
* Distinguishes a real choice from `useLocalStorage`'s eager default-seed below. */
const [hadStoredPin] = useState(() => localStorage.getItem(`${localStorageKey}pinned`) != null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't treat old auto-seeded pin values as user choices

On upgraded clients that have previously visited chat, useLocalStorageAlt has already written ${localStorageKey}pinned with false even when the user never touched the pin. Capturing only key presence here makes hadStoredPin true for those legacy auto-seeded values, so an admin setting defaultPinnedTools: ['execute_code'] will be ignored for most existing users. Use an explicit user-action marker or migrate/remove the old default seed before deciding the user has a stored preference.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is the same behavior discussed on the earlier thread about auto-seeded values, and it's intentional rather than a bug to fix in this PR.

defaultPinnedTools deliberately seeds the initial state only and does not override a value already persisted on the client. On upgraded clients, the legacy auto-seeded ${localStorageKey}pinned is treated as the user's existing state and respected, so the option applies to new users and fresh storage.

The key constraint is that an auto-seeded false and a deliberate unpin are indistinguishable in the pre-existing data model. A migration that force-applied the configured default to legacy false values would therefore also re-pin tools that users intentionally unpinned — a worse regression than the gap it closes. The maintainer's decision is to keep this conservative (don't override existing client state); broad retroactive application, if ever wanted, would be a separate versioned pin-storage migration.

The cold-load default-seeding effect captured the stored-pin state only at mount, so a pin toggled before startupConfig resolved was treated as no-preference and overwritten when the admin default applied.

Track explicit pin toggles via a ref (set through the returned setter) and skip the default application when the user has interacted in-session — in addition to the existing stored-preference guard.
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: fb9a086429

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila danny-avila merged commit f76a5fa into dev Jun 20, 2026
30 checks passed
@danny-avila danny-avila deleted the claude/gallant-sanderson-ca8d90 branch June 20, 2026 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant