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:
Bohdan Triapitsyn
2026-08-10 20:23:45 +03:00
parent 3feee346da
commit 75978cf188
19 changed files with 466 additions and 22 deletions
@@ -1417,6 +1417,8 @@ export const settingsDict = {
'settings.mcp.page.status.label.failed': 'Fehlgeschlagen',
'settings.mcp.page.status.label.needsAuth': 'Autorisierung erforderlich',
'settings.mcp.page.status.label.needsRegistration': 'Registrierung erforderlich',
'settings.mcp.page.status.label.awaitingRestart': 'Wartet auf Neustart',
'settings.mcp.page.status.description.awaitingRestart': 'Dieser Server ist gespeichert, aber noch nicht übernommen. Verwenden Sie „Apply & Restart“, damit OpenCode ihn lädt — danach sind Verbindung und Autorisierung verfügbar.',
'settings.mcp.page.status.description.connected': 'Verbunden und bereit, Tools und Ressourcen zu entdecken.',
'settings.mcp.page.status.description.failedDefault': 'OpenCode konnte diesen MCP-Server nicht erreichen.',
'settings.mcp.page.status.description.needsAuth': 'Dieser entfernte MCP-Server erfordert Autorisierung, bevor eine Verbindung hergestellt werden kann.',
@@ -1482,6 +1482,8 @@ export const settingsDict = {
'settings.mcp.page.status.label.failed': 'Failed',
'settings.mcp.page.status.label.needsAuth': 'Needs auth',
'settings.mcp.page.status.label.needsRegistration': 'Needs registration',
'settings.mcp.page.status.label.awaitingRestart': 'Awaiting restart',
'settings.mcp.page.status.description.awaitingRestart': 'This server is saved but not applied yet. Use Apply & Restart to load it into OpenCode — connect and authorization become available after that.',
'settings.mcp.page.status.description.connected': 'Connected and ready for OpenCode to discover tools and resources.',
'settings.mcp.page.status.description.failedDefault': 'OpenCode could not reach this MCP server.',
'settings.mcp.page.status.description.needsAuth': 'This remote MCP server requires authorization before it can connect.',
@@ -1459,6 +1459,8 @@ export const settingsDict = {
"settings.mcp.page.status.label.failed": "Fallido",
"settings.mcp.page.status.label.needsAuth": "Necesita autenticación",
"settings.mcp.page.status.label.needsRegistration": "Necesita registro",
"settings.mcp.page.status.label.awaitingRestart": "Esperando reinicio",
"settings.mcp.page.status.description.awaitingRestart": "Este servidor está guardado pero aún no aplicado. Usa «Apply & Restart» para cargarlo en OpenCode; después estarán disponibles la conexión y la autorización.",
"settings.mcp.page.status.description.connected": "Conectado y listo para que OpenCode descubra herramientas y recursos.",
"settings.mcp.page.status.description.failedDefault": "OpenCode no pudo alcanzar este servidor MCP.",
"settings.mcp.page.status.description.needsAuth": "Este servidor MCP remoto requiere autorización antes de poder conectarse.",
@@ -1377,6 +1377,8 @@ export const settingsDict = {
'settings.mcp.page.status.label.failed': 'Échoué',
'settings.mcp.page.status.label.needsAuth': 'Nécessite une authentification',
'settings.mcp.page.status.label.needsRegistration': 'Nécessite une inscription',
'settings.mcp.page.status.label.awaitingRestart': 'En attente de redémarrage',
'settings.mcp.page.status.description.awaitingRestart': 'Ce serveur est enregistré mais pas encore appliqué. Utilisez « Apply & Restart » pour le charger dans OpenCode — la connexion et l\'autorisation seront ensuite disponibles.',
'settings.mcp.page.status.description.connected': 'Connecté et prêt pour OpenCode pour découvrir des outils et des ressources.',
'settings.mcp.page.status.description.failedDefault': 'OpenCode n\'a pas pu atteindre ce serveur MCP.',
'settings.mcp.page.status.description.needsAuth': 'Ce serveur MCP distant nécessite une autorisation avant de pouvoir se connecter.',
@@ -1492,6 +1492,8 @@ export const settingsDict = {
'settings.mcp.page.status.label.failed': '失敗',
'settings.mcp.page.status.label.needsAuth': '認証が必要',
'settings.mcp.page.status.label.needsRegistration': '登録が必要',
'settings.mcp.page.status.label.awaitingRestart': '再起動待ち',
'settings.mcp.page.status.description.awaitingRestart': 'このサーバーは保存されていますが、まだ適用されていません。「Apply & Restart」で OpenCode に読み込むと、接続と認可が利用できるようになります。',
'settings.mcp.page.status.description.connected': '接続済み。OpenCode がツールとリソースを検出できます。',
'settings.mcp.page.status.description.failedDefault': 'OpenCode がこの MCP サーバーに到達できませんでした。',
'settings.mcp.page.status.description.needsAuth': 'このリモート MCP サーバーは接続する前に認証が必要です。',
@@ -1459,6 +1459,8 @@ export const settingsDict = {
'settings.mcp.page.status.label.failed': '실패',
'settings.mcp.page.status.label.needsAuth': '인증 필요',
'settings.mcp.page.status.label.needsRegistration': '등록 필요',
'settings.mcp.page.status.label.awaitingRestart': '재시작 대기 중',
'settings.mcp.page.status.description.awaitingRestart': '이 서버는 저장되었지만 아직 적용되지 않았습니다. Apply & Restart를 사용해 OpenCode에 불러오면 연결과 인증을 사용할 수 있습니다.',
'settings.mcp.page.status.description.connected': '연결되었습니다. OpenCode가 도구와 리소스를 찾을 준비가 됐습니다.',
'settings.mcp.page.status.description.failedDefault': 'OpenCode가 이 MCP 서버에 연결할 수 없습니다.',
'settings.mcp.page.status.description.needsAuth': '이 원격 MCP 서버는 연결 전에 권한 부여가 필요합니다.',
@@ -503,6 +503,8 @@ export const settingsDict = {
'settings.mcp.page.status.label.failed': 'Błąd',
'settings.mcp.page.status.label.needsAuth': 'Wymaga autoryzacji',
'settings.mcp.page.status.label.needsRegistration': 'Wymaga rejestracji',
'settings.mcp.page.status.label.awaitingRestart': 'Oczekuje na restart',
'settings.mcp.page.status.description.awaitingRestart': 'Ten serwer został zapisany, ale nie został jeszcze zastosowany. Użyj Apply & Restart, aby OpenCode go wczytał — potem będą dostępne połączenie i autoryzacja.',
'settings.mcp.page.status.projectScopedTo': 'Ograniczony do projektu w {directory}',
'settings.mcp.page.status.runtimeStatus': 'Status uruchomieniowy',
'settings.mcp.page.status.userScoped': 'Konfiguracja użytkownika',
@@ -1459,6 +1459,8 @@ export const settingsDict = {
"settings.mcp.page.status.label.failed": "Falhou",
"settings.mcp.page.status.label.needsAuth": "Precisa de autenticação",
"settings.mcp.page.status.label.needsRegistration": "Precisa de registro",
"settings.mcp.page.status.label.awaitingRestart": "Aguardando reinício",
"settings.mcp.page.status.description.awaitingRestart": "Este servidor foi salvo, mas ainda não aplicado. Use Apply & Restart para carregá-lo no OpenCode — depois disso, conexão e autorização ficam disponíveis.",
"settings.mcp.page.status.description.connected": "Conectado e pronto para que OpenCode descubra ferramentas e recursos.",
"settings.mcp.page.status.description.failedDefault": "OpenCode não conseguiu acessar este servidor MCP.",
"settings.mcp.page.status.description.needsAuth": "Este servidor MCP remoto exige autorização antes de poder se conectar.",
@@ -1459,6 +1459,8 @@ export const settingsDict = {
"settings.mcp.page.status.label.failed": "Не вдалося",
"settings.mcp.page.status.label.needsAuth": "Потрібна авторизація",
"settings.mcp.page.status.label.needsRegistration": "Потрібна реєстрація",
"settings.mcp.page.status.label.awaitingRestart": "Очікує перезапуску",
"settings.mcp.page.status.description.awaitingRestart": "Сервер збережено, але ще не застосовано. Натисніть Apply & Restart, щоб OpenCode його завантажив — після цього стануть доступні підключення та авторизація.",
"settings.mcp.page.status.description.connected": "Підключено, OpenCode може відкривати інструменти й ресурси.",
"settings.mcp.page.status.description.failedDefault": "OpenCode не вдалося отримати доступ до цього сервера MCP.",
"settings.mcp.page.status.description.needsAuth": "Цей віддалений сервер MCP потребує авторизації, перш ніж він зможе підключитися.",
@@ -1459,6 +1459,8 @@ export const settingsDict = {
'settings.mcp.page.status.label.failed': '失败',
'settings.mcp.page.status.label.needsAuth': '需要授权',
'settings.mcp.page.status.label.needsRegistration': '需要注册',
'settings.mcp.page.status.label.awaitingRestart': '等待重启',
'settings.mcp.page.status.description.awaitingRestart': '该服务器已保存但尚未生效。使用 Apply & Restart 让 OpenCode 加载它——之后即可进行连接和授权。',
'settings.mcp.page.status.description.connected': '已连接,OpenCode 可发现工具和资源。',
'settings.mcp.page.status.description.failedDefault': 'OpenCode 无法连接到此 MCP 服务器。',
'settings.mcp.page.status.description.needsAuth': '此远程 MCP 服务器在连接前需要授权。',
@@ -1365,6 +1365,8 @@
'settings.mcp.page.status.label.failed': '失敗',
'settings.mcp.page.status.label.needsAuth': '需要授權',
'settings.mcp.page.status.label.needsRegistration': '需要註冊',
'settings.mcp.page.status.label.awaitingRestart': '等待重新啟動',
'settings.mcp.page.status.description.awaitingRestart': '該伺服器已儲存但尚未生效。使用 Apply & Restart 讓 OpenCode 載入它——之後即可進行連線與授權。',
'settings.mcp.page.status.description.connected': '已連線,OpenCode 可發現工具和資源。',
'settings.mcp.page.status.description.failedDefault': 'OpenCode 無法連線到此 MCP 伺服器。',
'settings.mcp.page.status.description.needsAuth': '此遠端 MCP 伺服器在連線前需要授權。',