Skip to content

feat(site): add MCP server picker to agent chat UI#23470

Merged
kylecarbs merged 26 commits intomainfrom
kylecarbs/mcp-client-ui
Mar 24, 2026
Merged

feat(site): add MCP server picker to agent chat UI#23470
kylecarbs merged 26 commits intomainfrom
kylecarbs/mcp-client-ui

Conversation

@kylecarbs
Copy link
Copy Markdown
Member

Summary

Adds a user-facing MCP server configuration panel to the chat input toolbar. Users can toggle which MCP servers provide tools for their chat sessions, and authenticate with OAuth2 servers via popup windows.

Changes

New Components

  • MCPServerPicker (MCPServerPicker.tsx): Popover-based picker that appears in the chat input toolbar next to the model selector. Shows all enabled MCP servers with toggles.
  • MCPServerPicker.stories.tsx: 13 Storybook stories covering all states.

Availability Policies

Respects the admin-configured availability for each server:

  • force_on: Always active, toggle disabled, lock icon shown. User cannot disable.
  • default_on: Pre-selected by default, user can opt out via toggle.
  • default_off: Not selected by default, user must opt in via toggle.

OAuth2 Authentication

For servers with auth_type: "oauth2":

  • Shows auth status (connected/not connected)
  • "Connect to authenticate" link opens a popup window to /api/experimental/mcp/servers/{id}/oauth2/connect
  • Listens for postMessage with {type: "mcp-oauth2-complete"} from the callback page
  • Same UX pattern as external auth on the Create Workspace screen

Integration Points

  • AgentChatInput: MCP picker appears in the toolbar after the model selector
  • AgentDetail: Manages MCP selection state, initializes from chat.mcp_server_ids or defaults
  • AgentDetailView / AgentDetailContent: Props plumbed through to input
  • AgentCreatePage / AgentCreateForm: MCP selection for new chats
  • mcp_server_ids now sent with CreateChatMessageRequest and CreateChatRequest

Helper

  • getDefaultMCPSelection(): Computes default selection from availability policies (force_on + default_on)

Storybook Stories

Story Description
NoServers No servers - picker hidden
AllDisabled All disabled servers - picker hidden
SingleForceOn Force-on server with locked toggle
SingleDefaultOnNoAuth Default-on with no auth required
SingleDefaultOff Optional server not selected
OAuthNeedsAuth OAuth2 server needing authentication
OAuthConnected OAuth2 server already connected
MixedServers Multiple servers with mixed availability/auth
AllConnected All OAuth2 servers authenticated
Disabled Picker in disabled state
WithDisabledServer Disabled servers filtered out
AllOptedOut All toggled off except force_on
OptionalOAuthNeedsAuth Optional OAuth2 needing auth

Adds a user-facing MCP server configuration panel to the chat input toolbar.
Users can toggle which MCP servers provide tools for their chat sessions.

Key behaviors:
- force_on servers: Always active, toggle disabled, lock icon shown
- default_on servers: Pre-selected by default, user can opt out
- default_off servers: Not selected by default, user can opt in
- OAuth2 servers: Shows auth status and 'Connect' button that opens
  a popup window (same pattern as Create Workspace external auth).
  Listens for postMessage from the OAuth2 callback.
- mcp_server_ids are now sent with CreateChatMessageRequest and
  CreateChatRequest

Components:
- MCPServerPicker: Popover-based picker with server toggles
- Integration into AgentChatInput toolbar (next to model selector)
- Wired through AgentDetailView -> AgentDetailContent -> AgentChatInput
- AgentDetail manages MCP state and sends IDs with messages
- AgentCreatePage/Form passes MCP selection for new chats

Includes 13 Storybook stories covering:
- No servers / all disabled
- Single force_on / default_on / default_off
- OAuth2 needing auth vs connected
- Mixed servers with various states
- Disabled picker state
- All opted out
- Remove verbose 'Connect to authenticate' text link
- Add compact 'Auth' button with LogIn icon on the right side
- Reduce row height and icon size for denser layout
- Wrap each server row in a Tooltip showing description,
  availability policy, and auth status on hover
- Use TooltipProvider with 300ms delay matching model selector
- Narrow popover from w-80 to w-72
- Simplify header to single line
- Match model selector style: borderless transparent trigger with
  chevron, h-8, text-xs
- Show overlapping icon stack in trigger when multiple servers
  are active, with +N overflow badge for >3
- Show plain plug icon when no servers are active
- Remove 'MCP Servers' header from popover
- Auth button replaces the toggle switch when OAuth2 auth is
  needed (not shown side-by-side)
- Narrow popover from w-72 to w-64
- Add MultipleActiveIcons and IconStackOverflow stories
- Remove useCallback wrappers (React Compiler handles memoization)
- Add event.origin validation on postMessage handler
- Disable all Auth buttons when any auth flow is in-flight
- Remove stray whitespace node and scale-[0.8] on Switch
- Fix MCP defaults not applying on create page (useState
  initializer runs before query resolves, added useEffect seed)
- Fix deliberate opt-out being overwritten on reload (respect
  empty mcp_server_ids array from chat record)
- Remove unnecessary array spread in AgentChatInput
- Use Tailwind class in story decorator
- Reduce Auth button horizontal padding from px-1 to px-0.5
- Add WithMCPServers story: multiple servers selected, icon stack visible
- Add WithMCPNeedingAuth story: OAuth server shows Auth button
- Add WithMCPNoneActive story: no servers active, plain plug icon
@coder-tasks
Copy link
Copy Markdown
Contributor

coder-tasks bot commented Mar 24, 2026

Documentation Check

Updates Needed

  • docs/ai-coder/agents/platform-controls/index.md — The "Tool customization" section describes MCP server tool configuration as a future/planned feature. Now that users can select MCP servers from the chat toolbar, this section should describe the availability policies (force_on, default_on, default_off) as a current capability and link to how admins configure them.

New Documentation Needed

  • docs/ai-coder/agents/ (new page or section in an existing page) — Document the user-facing MCP server picker: what it shows, how availability policies control which servers appear, that force_on servers cannot be deselected, and how OAuth2-authenticated servers display connection status and a flow for connecting.

Automated review via Coder Tasks

Export mcpServerConfigsKey from chats queries and seed it as
empty in the AgentDetail story query cache so the MCP picker
doesn't attempt a real API call and cause visual diffs.
The IIFE computing effectiveMCPServerIds referenced chatRecord
before its const declaration, causing a TDZ (Temporal Dead Zone)
ReferenceError at runtime. Move the block after the chatRecord
assignment on line 420. Also add getMCPServerConfigs API spy to
the stories beforeEach to prevent unhandled query errors.
Remove the fallback PlugIcon from the MCP trigger button when no
servers are active. Now only the "MCP" label and chevron are shown.
Override both servers to default_off so force_on Sentry does not
appear in the trigger icon stack when no servers are selected.
Previously the Auth button only appeared when the server was both
unauthenticated and selected. Now it shows whenever auth is needed,
regardless of toggle state, so users can authenticate optional
servers without toggling them on first.
Override Linear to oauth2 with auth_connected: false and Sentry
to auth_connected: false so both rows display the Auth button
instead of a toggle.
/>
mcpServers={mcpServersQuery.data ?? []}
onMCPAuthComplete={() => void mcpServersQuery.refetch()}
/>{" "}
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.

Stray whitespace

// If the chat has MCP server IDs recorded (even empty, meaning
// the user deliberately opted out), use those.
if (chatRecord?.mcp_server_ids) {
return [...chatRecord.mcp_server_ids];
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.

How come we're doing a copy here btw

model_config_id: selectedModelConfigID,
mcp_server_ids:
effectiveMCPServerIds.length > 0
? [...effectiveMCPServerIds]
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.

And here (not saying we shouldn't to either, just curious)

{isConnecting ? (
<Spinner loading className="h-2.5 w-2.5" />
) : null}
Auth{" "}
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.

stray whitespace intentional?

disabled={disabled || isForceOn}
aria-label={`${isSelected ? "Disable" : "Enable"} ${server.display_name}`}
/>
)}{" "}
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.

stray whitespace intentional?

Comment on lines +251 to +261
const [selectedMCPServerIds, setSelectedMCPServerIds] = useState<string[]>(
() => (mcpServers ? getDefaultMCPSelection(mcpServers) : []),
);
// Seed default selection once the MCP server query resolves.
const mcpDefaultsApplied = useRef(false);
useEffect(() => {
if (!mcpDefaultsApplied.current && mcpServers && mcpServers.length > 0) {
mcpDefaultsApplied.current = true;
setSelectedMCPServerIds(getDefaultMCPSelection(mcpServers));
}
}, [mcpServers]);
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.

Can you noodle with your agent on this, I'm not convinced with this code.

We're lazy initializing selectedMCPServerIds only to useEffect sync it with mcpServers once loaded. My best guess is that we probably don't need this useEffect if designed properly.

Comment on lines +56 to +81
if (iconUrl) {
return (
<div
className={cn(
"flex shrink-0 items-center justify-center rounded-full bg-surface-secondary",
className,
)}
>
<ExternalImage
src={iconUrl}
alt={`${name} icon`}
className="h-3/5 w-3/5"
/>
</div>
);
}
return (
<div
className={cn(
"flex shrink-0 items-center justify-center rounded-full bg-surface-secondary",
className,
)}
>
<ServerIcon className="h-3/5 w-3/5 text-content-secondary" />
</div>
);
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.

Suggested change
if (iconUrl) {
return (
<div
className={cn(
"flex shrink-0 items-center justify-center rounded-full bg-surface-secondary",
className,
)}
>
<ExternalImage
src={iconUrl}
alt={`${name} icon`}
className="h-3/5 w-3/5"
/>
</div>
);
}
return (
<div
className={cn(
"flex shrink-0 items-center justify-center rounded-full bg-surface-secondary",
className,
)}
>
<ServerIcon className="h-3/5 w-3/5 text-content-secondary" />
</div>
);
let icon;
if (iconUrl) {
icon = (
<ExternalImage
src={iconUrl}
alt={`${name} icon`}
className="h-3/5 w-3/5"
/>
);
} else {
icon = <ServerIcon className="h-3/5 w-3/5 text-content-secondary" />;
}
return (
<div
className={cn(
"flex shrink-0 items-center justify-center rounded-full bg-surface-secondary",
className,
)}
>
{icon}
</div>
);

Comment on lines +178 to +187
useEffect(() => {
if (!connectingServerId || !popupRef.current) return;
const interval = setInterval(() => {
if (popupRef.current?.closed) {
setConnectingServerId(null);
popupRef.current = null;
}
}, 500);
return () => clearInterval(interval);
}, [connectingServerId]);
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.

My agent thinks we might be leaking a popup ref, + this code feels very sketchy

- Refactor MCPIcon to deduplicate wrapper div
- Remove stray whitespace in AgentCreatePage, MCPServerPicker
- Remove unnecessary array spread in AgentDetail (keep at API
  boundary where readonly->mutable conversion is needed)
- Close popup on unmount to prevent leaked browser windows
- Simplify AgentCreateForm MCP selection: replace useState +
  useEffect + useRef guard with null sentinel + derived state
@kylecarbs kylecarbs merged commit f62f2ff into main Mar 24, 2026
27 checks passed
@kylecarbs kylecarbs deleted the kylecarbs/mcp-client-ui branch March 24, 2026 12:13
@github-actions github-actions bot locked and limited conversation to collaborators Mar 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants