Figura 1. O fluxo completo. Os passos a ciano vêm com o Passport e o laravel/mcp. Os passos a laranja são seus.
Passo 1. O Passport 13 é o servidor de autorização
O Passport 13 fala OAuth 2.1 de origem: authorization code com PKCE, e os grants password e implicit ficam desligados a menos que os ative. Não os ative. Instale o Passport, corra as migrações e defina um scope para o endpoint MCP.
// app/Providers/AppServiceProvider.php
use Laravel\Passport\Passport;
Passport::tokensCan([
'mcp:use' => 'Use the MCP tools for the selected workspaces',
]);Um scope chega. A restrição de tenant vive no token, não no scope. Isso é o passo 3.
Passo 2. Discovery e registo de clientes, zero código
O laravel/mcp traz o documento de discovery e o endpoint de registo. Uma chamada publica os dois, uma segunda monta o servidor.
// routes/ai.php
use Laravel\Mcp\Facades\Mcp;
Mcp::oauthRoutes();
Mcp::web('/mcp', AppMcpServer::class)
->middleware(AuthenticateMcpRequest::class);Depois disto, GET /.well-known/oauth-authorization-server responde, POST /oauth/register cria um cliente Passport, e o Claude e o ChatGPT encontram o caminho sozinhos. Resista a registar os dois clientes à mão. Qualquer outro cliente MCP que apareça no próximo ano deixaria de funcionar.
Passo 3. O ecrã de consentimento e a armadilha
O Passport dá-lhe o /oauth/authorize. O ecrã é seu. Numa aplicação single-tenant é um botão. No Semantika, um workspace guarda as marcas que uma equipa monitoriza, por isso o utilizador escolhe que workspaces o assistente pode ler. Um ou mais.
Aqui está a armadilha. O consentimento acontece no browser, dentro da sessão do utilizador. A troca de token que vem a seguir é um POST /oauth/token de servidor para servidor, vindo do backend da Anthropic ou da OpenAI. Sem cookie, sem sessão, sem memória do que o utilizador escolheu. Guarde a escolha na sessão e leia-a quando o token é criado, e não lê nada.
A solução é carimbar a escolha no código de autorização, no momento do consentimento, no pedido que cria o código.
// app/Http/Controllers/OAuth/ApproveAuthorizationController.php
public function approve(Request $request)
{
// Only workspaces this user can actually access.
$allowed = $request->user()->workspaces()
->whereIn('id', $request->input('workspace_ids', []))
->pluck('id')
->all();
abort_if($allowed === [], 422, 'Select at least one workspace.');
$request->session()->put('oauth.consent.workspace_ids', $allowed);
return parent::approve($request);
}// app/Models/Passport/AuthCode.php
protected static function booted(): void
{
static::creating(function (self $code) {
$code->workspace_ids = session('oauth.consent.workspace_ids', []);
});
}Registe o modelo com Passport::useAuthCodeModel(AuthCode::class) e acrescente uma coluna JSON. A linha do código passa a transportar a lista de tenants, e o código é a única coisa que o cliente devolve. Valide os ids contra o acesso real do utilizador, no servidor. O formulário pode ser editado.