feat: auto-discover MCP servers from project and user config files

Scan .vscode/mcp.json, .mcp.json, .copilot/mcp.json (project-level) and
~/.copilot/mcp.json (user-level) for MCP server definitions. Discovered
servers require explicit user acceptance before activation.

Backend:
- Add ConfigScannerRegistry with per-format scanners and  substitution
- Add discovered tooling domain model with fingerprint-based change detection
- Integrate scanning into workspace load, project add, and project selection
- Merge accepted discovered MCPs into effective runtime tooling
- Extend sidecar contracts with env/headers for real-world MCP configs
- Add IPC channels for accept/dismiss/rescan operations

Frontend:
- Add DiscoveredToolingModal for reviewing pending MCP servers
- Auto-show modal when pending discoveries exist on load or project switch
- Add amber badge on sidebar project headers for pending discoveries
- Add discovered MCP section in Settings panel with accept/dismiss/rescan
- Group InlinePills tool dropdown by Workspace MCP / User MCP / Project MCP
- Pass effective project tooling (including accepted discoveries) to ChatPane

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-25 21:54:06 +01:00
co-authored by Copilot
parent 08a1132d86
commit e5878ba9e8
27 changed files with 2466 additions and 45 deletions
+61 -20
View File
@@ -225,6 +225,10 @@ export function InlineToolsPill({
const enabledCount = selection.enabledMcpServerIds.length + selection.enabledLspProfileIds.length;
const totalCount = mcpServers.length + lspProfiles.length;
const workspaceMcpServers = mcpServers.filter((s) => !s.id.startsWith('discovered_'));
const discoveredUserMcpServers = mcpServers.filter((s) => s.id.startsWith('discovered_user_'));
const discoveredProjectMcpServers = mcpServers.filter((s) => s.id.startsWith('discovered_project_'));
return (
<div className="relative" ref={ref}>
<button
@@ -246,26 +250,29 @@ export function InlineToolsPill({
{open && !disabled && (
<div className="absolute bottom-full left-0 z-40 mb-1.5 w-64 overflow-y-auto rounded-lg border border-zinc-700 bg-zinc-900 py-1 shadow-2xl">
{mcpServers.length > 0 && (
<div>
<div className="px-3 pb-1 pt-2 text-[9px] font-semibold uppercase tracking-wider text-zinc-600">
MCP Servers
</div>
{mcpServers.map((server) => (
<PopoverToggleRow
detail={server.transport === 'local' ? server.command : server.url}
enabled={selection.enabledMcpServerIds.includes(server.id)}
key={server.id}
label={server.name}
onToggle={() =>
onToggle({
...selection,
enabledMcpServerIds: toggleInArray(selection.enabledMcpServerIds, server.id),
})
}
/>
))}
</div>
{workspaceMcpServers.length > 0 && (
<McpServerGroup
label="Workspace MCP"
onToggle={onToggle}
selection={selection}
servers={workspaceMcpServers}
/>
)}
{discoveredUserMcpServers.length > 0 && (
<McpServerGroup
label="User MCP"
onToggle={onToggle}
selection={selection}
servers={discoveredUserMcpServers}
/>
)}
{discoveredProjectMcpServers.length > 0 && (
<McpServerGroup
label="Project MCP"
onToggle={onToggle}
selection={selection}
servers={discoveredProjectMcpServers}
/>
)}
{lspProfiles.length > 0 && (
<div>
@@ -294,6 +301,40 @@ export function InlineToolsPill({
);
}
function McpServerGroup({
label,
servers,
selection,
onToggle,
}: {
label: string;
servers: ReadonlyArray<McpServerDefinition>;
selection: SessionToolingSelection;
onToggle: (selection: SessionToolingSelection) => void;
}) {
return (
<div>
<div className="px-3 pb-1 pt-2 text-[9px] font-semibold uppercase tracking-wider text-zinc-600">
{label}
</div>
{servers.map((server) => (
<PopoverToggleRow
detail={server.transport === 'local' ? server.command : server.url}
enabled={selection.enabledMcpServerIds.includes(server.id)}
key={server.id}
label={server.name}
onToggle={() =>
onToggle({
...selection,
enabledMcpServerIds: toggleInArray(selection.enabledMcpServerIds, server.id),
})
}
/>
))}
</div>
);
}
/* ── InlineApprovalPill ────────────────────────────────────── */
const approvalKindOrder: ApprovalToolKind[] = ['builtin', 'mcp', 'lsp', 'mixed'];