Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/Http/Middleware/App/SetLocale.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ public function handle(Request $request, Closure $next): Response

$response = $next($request);

// Passport OAuth errors return a raw Symfony Response (no withCookie()).
// Attach via headers so both Illuminate and Symfony responses work.
if (! $isValid) {
$response->withCookie(
$response->headers->setCookie(
cookie()->forever('locale', config('languages.default'), '/', config('session.domain')),
);
}
Expand Down
79 changes: 79 additions & 0 deletions app/Passport/AuthorizationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,103 @@
namespace App\Passport;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Laravel\Passport\Client;
use Laravel\Passport\Contracts\AuthorizationViewResponse;
use Laravel\Passport\Exceptions\OAuthServerException;
use Laravel\Passport\Http\Controllers\AuthorizationController as PassportAuthorizationController;
use Laravel\Passport\Scope;
use League\OAuth2\Server\Exception\OAuthServerException as LeagueOAuthServerException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\HttpFoundation\Response;

/**
* Always show the MCP consent screen so the user can pick a workspace.
*
* Passport otherwise skips consent when the user already granted the same
* scopes (silent re-consent), which would bind the auth code via
* current_workspace without an explicit pick.
*
* Guests are sent to login before client validation so a stale MCP Inspector
* client_id does not return invalid_client JSON instead of the login page.
*
* Browser / Inertia requests that fail OAuth validation get an Inertia error
* page instead of raw JSON (which breaks the post-login Inertia redirect).
*/
class AuthorizationController extends PassportAuthorizationController
{
/**
* Authorize a client to access the user's account.
*/
public function authorize(
ServerRequestInterface $psrRequest,
Request $request,
ResponseInterface $psrResponse,
AuthorizationViewResponse $viewResponse
): Response|AuthorizationViewResponse {
if ($this->guard->guest()) {
$prompt = $request->string('prompt')->explode(' ')->map(trim(...))->filter()->values();

// prompt=none must not show a login UI — fall through to Passport
// validation so the client receives login_required / invalid_client.
if ($prompt->doesntContain('none')) {
$this->promptForLogin($request);
}
}

try {
return parent::authorize($psrRequest, $request, $psrResponse, $viewResponse);
} catch (OAuthServerException $exception) {
if (! $this->shouldRenderAuthorizationErrorPage($request)) {
throw $exception;
}

return $this->authorizationErrorPage($request, $exception);
}
}

/**
* @param Scope[] $scopes
*/
protected function hasGrantedScopes(Authenticatable $user, Client $client, array $scopes): bool
{
return false;
}

/**
* Browser navigations (including Inertia) get an error page. JSON clients
* that explicitly expect JSON keep the OAuth error payload.
*/
private function shouldRenderAuthorizationErrorPage(Request $request): bool
{
return ! $request->expectsJson();
}

private function authorizationErrorPage(Request $request, OAuthServerException $exception): Response
{
$payload = $this->oauthErrorPayload($exception);

return Inertia::render('mcp/AuthorizeError', [
'error' => (string) ($payload['error'] ?? 'server_error'),
'errorDescription' => (string) ($payload['error_description'] ?? __('mcp.authorize.error_body')),
])->toResponse($request);
}

/**
* @return array<string, mixed>
*/
private function oauthErrorPayload(OAuthServerException $exception): array
{
$previous = $exception->getPrevious();

if ($previous instanceof LeagueOAuthServerException) {
return $previous->getPayload();
}

$decoded = json_decode($exception->getResponse()->getContent() ?: '[]', true);

return is_array($decoded) ? $decoded : [];
}
}
4 changes: 4 additions & 0 deletions lang/ar/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => 'فشل التفويض - TryPost',
'error_title' => 'تعذر الاتصال',
'error_body' => 'طلب التفويض هذا غير صالح أو منتهٍ. أغلق هذه النافذة وحاول الاتصال مرة أخرى من عميل MCP.',
'error_code' => 'خطأ: :error',
],

'other_clients_title' => 'تطبيقات أخرى',
Expand Down
4 changes: 4 additions & 0 deletions lang/de/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => 'Autorisierung fehlgeschlagen - TryPost',
'error_title' => 'Verbindung fehlgeschlagen',
'error_body' => 'Diese Autorisierungsanfrage ist ungültig oder abgelaufen. Schließen Sie dieses Fenster und verbinden Sie sich erneut über Ihren MCP-Client.',
'error_code' => 'Fehler: :error',
],

'other_clients_title' => 'Andere Apps',
Expand Down
4 changes: 4 additions & 0 deletions lang/el/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => 'Αποτυχία εξουσιοδότησης - TryPost',
'error_title' => 'Αδυναμία σύνδεσης',
'error_body' => 'Αυτό το αίτημα εξουσιοδότησης είναι μη έγκυρο ή έχει λήξει. Κλείστε αυτό το παράθυρο και δοκιμάστε ξανά από τον πελάτη MCP.',
'error_code' => 'Σφάλμα: :error',
],

'other_clients_title' => 'Άλλες εφαρμογές',
Expand Down
4 changes: 4 additions & 0 deletions lang/en/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => 'Authorization failed - TryPost',
'error_title' => 'Could not connect',
'error_body' => 'This authorization request is invalid or expired. Close this window and try connecting again from your MCP client.',
'error_code' => 'Error: :error',
],

'other_clients_title' => 'Other apps',
Expand Down
4 changes: 4 additions & 0 deletions lang/es/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => 'Error de autorización - TryPost',
'error_title' => 'No se pudo conectar',
'error_body' => 'Esta solicitud de autorización no es válida o ha caducado. Cierra esta ventana e intenta conectar de nuevo desde tu cliente MCP.',
'error_code' => 'Error: :error',
],

'other_clients_title' => 'Otras apps',
Expand Down
4 changes: 4 additions & 0 deletions lang/fr/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => 'Échec de l\'autorisation - TryPost',
'error_title' => 'Impossible de se connecter',
'error_body' => 'Cette demande d\'autorisation est invalide ou a expiré. Fermez cette fenêtre et réessayez depuis votre client MCP.',
'error_code' => 'Erreur : :error',
],

'other_clients_title' => 'Autres apps',
Expand Down
4 changes: 4 additions & 0 deletions lang/it/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => 'Autorizzazione non riuscita - TryPost',
'error_title' => 'Impossibile connettersi',
'error_body' => 'Questa richiesta di autorizzazione non è valida o è scaduta. Chiudi questa finestra e riprova dal tuo client MCP.',
'error_code' => 'Errore: :error',
],

'other_clients_title' => 'Altre app',
Expand Down
4 changes: 4 additions & 0 deletions lang/ja/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => '認可に失敗しました - TryPost',
'error_title' => '接続できませんでした',
'error_body' => 'この認可リクエストは無効か期限切れです。このウィンドウを閉じ、MCPクライアントからもう一度接続してください。',
'error_code' => 'エラー: :error',
],

'other_clients_title' => 'その他のアプリ',
Expand Down
4 changes: 4 additions & 0 deletions lang/ko/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => '승인 실패 - TryPost',
'error_title' => '연결할 수 없음',
'error_body' => '이 승인 요청이 잘못되었거나 만료되었습니다. 이 창을 닫고 MCP 클라이언트에서 다시 연결하세요.',
'error_code' => '오류: :error',
],

'other_clients_title' => '다른 앱',
Expand Down
4 changes: 4 additions & 0 deletions lang/nl/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => 'Autorisatie mislukt - TryPost',
'error_title' => 'Verbinden mislukt',
'error_body' => 'Dit autorisatieverzoek is ongeldig of verlopen. Sluit dit venster en probeer opnieuw te verbinden via je MCP-client.',
'error_code' => 'Fout: :error',
],

'other_clients_title' => 'Andere apps',
Expand Down
4 changes: 4 additions & 0 deletions lang/pl/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => 'Autoryzacja nie powiodła się - TryPost',
'error_title' => 'Nie udało się połączyć',
'error_body' => 'To żądanie autoryzacji jest nieprawidłowe lub wygasło. Zamknij to okno i spróbuj ponownie połączyć się z poziomu klienta MCP.',
'error_code' => 'Błąd: :error',
],

'other_clients_title' => 'Inne aplikacje',
Expand Down
4 changes: 4 additions & 0 deletions lang/pt-BR/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Buscar workspaces...',
'no_workspace_found' => 'Nenhum workspace encontrado',
'scope_mcp_use' => 'Usar o servidor MCP',
'error_page_title' => 'Falha na autorização - TryPost',
'error_title' => 'Não foi possível conectar',
'error_body' => 'Este pedido de autorização é inválido ou expirou. Feche esta janela e tente conectar de novo pelo seu cliente MCP.',
'error_code' => 'Erro: :error',
],

'other_clients_title' => 'Outros apps',
Expand Down
4 changes: 4 additions & 0 deletions lang/ru/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => 'Ошибка авторизации - TryPost',
'error_title' => 'Не удалось подключиться',
'error_body' => 'Этот запрос авторизации недействителен или устарел. Закройте это окно и попробуйте подключиться снова из MCP-клиента.',
'error_code' => 'Ошибка: :error',
],

'other_clients_title' => 'Другие приложения',
Expand Down
4 changes: 4 additions & 0 deletions lang/tr/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => 'Yetkilendirme başarısız - TryPost',
'error_title' => 'Bağlantı kurulamadı',
'error_body' => 'Bu yetkilendirme isteği geçersiz veya süresi dolmuş. Bu pencereyi kapatın ve MCP istemcinizden yeniden bağlanmayı deneyin.',
'error_code' => 'Hata: :error',
],

'other_clients_title' => 'Diğer uygulamalar',
Expand Down
4 changes: 4 additions & 0 deletions lang/uk/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => 'Помилка авторизації - TryPost',
'error_title' => 'Не вдалося підключитися',
'error_body' => 'Цей запит на авторизацію недійсний або застарів. Закрийте це вікно й спробуйте підключитися знову з MCP-клієнта.',
'error_code' => 'Помилка: :error',
],

'other_clients_title' => 'Інші застосунки',
Expand Down
4 changes: 4 additions & 0 deletions lang/zh/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
'search_workspace' => 'Search workspaces...',
'no_workspace_found' => 'No workspace found',
'scope_mcp_use' => 'Use MCP server',
'error_page_title' => '授权失败 - TryPost',
'error_title' => '无法连接',
'error_body' => '此授权请求无效或已过期。请关闭此窗口,然后从 MCP 客户端重新连接。',
'error_code' => '错误::error',
],

'other_clients_title' => '其他应用',
Expand Down
46 changes: 46 additions & 0 deletions resources/js/layouts/mcp/AuthorizeLayout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';

defineProps<{
title: string;
description?: string;
pageTitle?: string;
}>();
</script>

<template>
<div
class="flex min-h-svh flex-col items-center justify-center bg-background p-6 md:p-10"
>
<Head v-if="pageTitle" :title="pageTitle" />

<div class="w-full max-w-md space-y-8">
<div class="flex flex-col items-center gap-4 text-center">
<img
src="/images/trypost/logo-light.png"
alt="TryPost"
class="h-10 w-auto"
/>
<div class="space-y-2">
<h1
class="text-xl font-semibold tracking-tight text-foreground"
>
{{ title }}
</h1>
<p
v-if="description"
class="text-sm text-muted-foreground"
>
{{ description }}
</p>
</div>
</div>

<div
class="space-y-6 rounded-xl border-2 border-foreground bg-card p-6 shadow-sm"
>
<slot />
</div>
</div>
</div>
</template>
Loading