fix(mcp): reliable OAuth across runtimes and honest pre-restart UI
MCP authorization was broken in several stacked ways. The browser return leg landed on the SPA behind the auth gate, so the system browser saw a login page instead of finishing; the pending-context store silently saved nothing because its route had no JSON body parser; and the callback-URL config write started deferring behind Apply & Restart, so authorization ran against a runtime without the URL and dead-ended on OpenCode's loopback listener. The return leg is now completed entirely server-side by an unauthenticated GET /mcp/oauth/callback that only forwards a code whose state matches a parked context. Desktop with the local server and VS Code switch to OpenCode's native flow over its fixed loopback port — no config writes or restarts at all, with a one-time cleanup of the previously written callback URL — and its completion signal drives the page instead of blind status polling. Remote, hosted-web, and mobile keep the server-callback flow, applying a queued callback-URL write immediately since authorization cannot wait for a manual restart. Also: a server queued behind Apply & Restart now shows an Awaiting restart badge and explanation instead of connect/reauthorize buttons that can only fail, and Reauthorize is offered only while the server is actually connected.
This commit is contained in:
@@ -72,6 +72,12 @@ interface McpStore {
|
||||
connect: (name: string, directory?: string | null) => Promise<void>;
|
||||
disconnect: (name: string, directory?: string | null) => Promise<void>;
|
||||
startAuth: (name: string, directory?: string | null) => Promise<string>;
|
||||
/**
|
||||
* OpenCode's native full OAuth flow: OpenCode opens the browser, receives
|
||||
* the callback on its own fixed loopback listener, and exchanges the code
|
||||
* itself. Resolves only when the whole flow finishes (minutes, not ms).
|
||||
*/
|
||||
authenticate: (name: string, directory?: string | null) => Promise<void>;
|
||||
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>;
|
||||
@@ -178,6 +184,28 @@ export const useMcpStore = create<McpStore>()(
|
||||
},
|
||||
|
||||
|
||||
authenticate: async (name, directory) => {
|
||||
const normalized = normalizeDirectory(directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
const key = toKey(normalized);
|
||||
const api = getMcpApiClient(normalized);
|
||||
try {
|
||||
await api.mcp.auth.authenticate({ name }, { throwOnError: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Authorization failed';
|
||||
set((state) => ({
|
||||
diagnosticsByDirectory: {
|
||||
...state.diagnosticsByDirectory,
|
||||
[key]: {
|
||||
...(state.diagnosticsByDirectory[key] ?? {}),
|
||||
[name]: { status: 'failed', error: message },
|
||||
},
|
||||
},
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
await get().refresh({ directory: normalized, silent: true });
|
||||
},
|
||||
|
||||
completeAuth: async (name, code, directory) => {
|
||||
const normalized = normalizeDirectory(directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
const api = getMcpApiClient(normalized);
|
||||
|
||||
Reference in New Issue
Block a user