Improve MCP settings auth flow, remote config support, and diagnostics UX (#953)
* feat: improve MCP settings auth workflow * fix: complete MCP settings auth flow * fix: harden MCP settings auth flow * fix: add MCP settings refresh control * fix: stabilize MCP authorization and status handling * fix: clarify MCP advanced remote options toggle * fix: improve MCP import and diagnostics * feat: improve MCP settings panel visual hierarchy and UX * fix: expose MCP auth actions in connected state * fix: remove MCP import snippet helper text * fix: address MCP review feedback * fix: correct MCP page transport layout after rebase
This commit is contained in:
@@ -5,8 +5,14 @@ import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
|
||||
export type McpStatusMap = Record<string, McpStatus>;
|
||||
export type McpRuntimeDiagnostic = {
|
||||
status: 'failed';
|
||||
error: string;
|
||||
};
|
||||
export type McpRuntimeDiagnosticMap = Record<string, McpRuntimeDiagnostic>;
|
||||
|
||||
const EMPTY_STATUS: McpStatusMap = {};
|
||||
const EMPTY_DIAGNOSTICS: McpRuntimeDiagnosticMap = {};
|
||||
|
||||
type McpHealth = {
|
||||
connected: number;
|
||||
@@ -47,20 +53,34 @@ type RefreshOptions = {
|
||||
silent?: boolean;
|
||||
};
|
||||
|
||||
type TestConnectionResult = {
|
||||
status?: McpStatus;
|
||||
error?: string;
|
||||
warning?: string;
|
||||
};
|
||||
|
||||
interface McpStore {
|
||||
byDirectory: Record<string, McpStatusMap>;
|
||||
diagnosticsByDirectory: Record<string, McpRuntimeDiagnosticMap>;
|
||||
loadingKeys: Record<string, boolean>;
|
||||
lastErrorKeys: Record<string, string | null>;
|
||||
|
||||
getStatusForDirectory: (directory?: string | null) => McpStatusMap;
|
||||
getDiagnosticForDirectory: (directory?: string | null) => McpRuntimeDiagnosticMap;
|
||||
getErrorForDirectory: (directory?: string | null) => string | null;
|
||||
refresh: (options?: RefreshOptions) => Promise<void>;
|
||||
connect: (name: string, directory?: string | null) => Promise<void>;
|
||||
disconnect: (name: string, directory?: string | null) => Promise<void>;
|
||||
startAuth: (name: string, directory?: string | null) => Promise<string>;
|
||||
completeAuth: (name: string, code: string, directory?: string | null) => Promise<void>;
|
||||
clearAuth: (name: string, directory?: string | null) => Promise<void>;
|
||||
testConnection: (name: string, directory?: string | null) => Promise<TestConnectionResult>;
|
||||
}
|
||||
|
||||
export const useMcpStore = create<McpStore>()(
|
||||
devtools((set, get) => ({
|
||||
byDirectory: {},
|
||||
diagnosticsByDirectory: {},
|
||||
loadingKeys: {},
|
||||
lastErrorKeys: {},
|
||||
|
||||
@@ -69,6 +89,16 @@ export const useMcpStore = create<McpStore>()(
|
||||
return get().byDirectory[key] ?? EMPTY_STATUS;
|
||||
},
|
||||
|
||||
getDiagnosticForDirectory: (directory) => {
|
||||
const key = toKey(directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
return get().diagnosticsByDirectory[key] ?? EMPTY_DIAGNOSTICS;
|
||||
},
|
||||
|
||||
getErrorForDirectory: (directory) => {
|
||||
const key = toKey(directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
return get().lastErrorKeys[key] ?? null;
|
||||
},
|
||||
|
||||
refresh: async (options) => {
|
||||
const directory = normalizeDirectory(options?.directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
const key = toKey(directory);
|
||||
@@ -87,6 +117,12 @@ export const useMcpStore = create<McpStore>()(
|
||||
|
||||
set((state) => ({
|
||||
byDirectory: { ...state.byDirectory, [key]: data },
|
||||
diagnosticsByDirectory: {
|
||||
...state.diagnosticsByDirectory,
|
||||
[key]: Object.fromEntries(
|
||||
Object.entries(state.diagnosticsByDirectory[key] ?? {}).filter(([name]) => !data[name])
|
||||
),
|
||||
},
|
||||
loadingKeys: { ...state.loadingKeys, [key]: false },
|
||||
lastErrorKeys: { ...state.lastErrorKeys, [key]: null },
|
||||
}));
|
||||
@@ -101,8 +137,23 @@ export const useMcpStore = create<McpStore>()(
|
||||
|
||||
connect: async (name, directory) => {
|
||||
const normalized = normalizeDirectory(directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
const key = toKey(normalized);
|
||||
const api = getMcpApiClient(normalized);
|
||||
await api.mcp.connect({ name }, { throwOnError: true });
|
||||
try {
|
||||
await api.mcp.connect({ name }, { throwOnError: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Connection failed';
|
||||
set((state) => ({
|
||||
diagnosticsByDirectory: {
|
||||
...state.diagnosticsByDirectory,
|
||||
[key]: {
|
||||
...(state.diagnosticsByDirectory[key] ?? {}),
|
||||
[name]: { status: 'failed', error: message },
|
||||
},
|
||||
},
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
await get().refresh({ directory: normalized, silent: true });
|
||||
},
|
||||
|
||||
@@ -113,5 +164,77 @@ export const useMcpStore = create<McpStore>()(
|
||||
await get().refresh({ directory: normalized, silent: true });
|
||||
},
|
||||
|
||||
startAuth: async (name, directory) => {
|
||||
const normalized = normalizeDirectory(directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
const api = getMcpApiClient(normalized);
|
||||
const result = await api.mcp.auth.start({ name }, { throwOnError: true });
|
||||
const authorizationUrl = result.data?.authorizationUrl;
|
||||
|
||||
if (!authorizationUrl) {
|
||||
throw new Error('Authorization URL was not returned');
|
||||
}
|
||||
|
||||
return authorizationUrl;
|
||||
},
|
||||
|
||||
completeAuth: async (name, code, directory) => {
|
||||
const normalized = normalizeDirectory(directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
const api = getMcpApiClient(normalized);
|
||||
await api.mcp.auth.callback({ name, code }, { throwOnError: true });
|
||||
await get().refresh({ directory: normalized, silent: true });
|
||||
},
|
||||
|
||||
clearAuth: async (name, directory) => {
|
||||
const normalized = normalizeDirectory(directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
const api = getMcpApiClient(normalized);
|
||||
await api.mcp.auth.remove({ name }, { throwOnError: true });
|
||||
await get().refresh({ directory: normalized, silent: true });
|
||||
},
|
||||
|
||||
testConnection: async (name, directory) => {
|
||||
const normalized = normalizeDirectory(directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
const key = toKey(normalized);
|
||||
const api = getMcpApiClient(normalized);
|
||||
const previousStatus = get().getStatusForDirectory(normalized)[name];
|
||||
const wasConnected = previousStatus?.status === 'connected';
|
||||
let errorMessage: string | undefined;
|
||||
let warningMessage: string | undefined;
|
||||
|
||||
try {
|
||||
await api.mcp.connect({ name }, { throwOnError: true });
|
||||
} catch (error) {
|
||||
errorMessage = error instanceof Error ? error.message : 'Connection failed';
|
||||
set((state) => ({
|
||||
diagnosticsByDirectory: {
|
||||
...state.diagnosticsByDirectory,
|
||||
[key]: {
|
||||
...(state.diagnosticsByDirectory[key] ?? {}),
|
||||
[name]: { status: 'failed', error: errorMessage ?? 'Connection failed' },
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
await get().refresh({ directory: normalized, silent: true });
|
||||
const currentStatus = get().getStatusForDirectory(normalized)[name];
|
||||
const observedStatus = currentStatus;
|
||||
|
||||
if (!wasConnected && currentStatus?.status === 'connected') {
|
||||
try {
|
||||
await api.mcp.disconnect({ name }, { throwOnError: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Disconnect failed';
|
||||
warningMessage = `Connection test succeeded, but cleanup disconnect failed: ${message}`;
|
||||
}
|
||||
await get().refresh({ directory: normalized, silent: true });
|
||||
}
|
||||
|
||||
return {
|
||||
status: observedStatus ?? get().getStatusForDirectory(normalized)[name],
|
||||
error: errorMessage,
|
||||
warning: warningMessage,
|
||||
};
|
||||
},
|
||||
|
||||
}))
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user