trypost/tests/Feature/McpSettingsControllerTest.php

375 lines
14 KiB
PHP
Raw Normal View History

MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
<?php
declare(strict_types=1);
use App\Enums\UserWorkspace\Role;
use App\Models\AccessToken;
use App\Models\User;
use App\Models\Workspace;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
beforeEach(function (): void {
config(['trypost.self_hosted' => false]);
$this->user = User::factory()->create();
$this->workspace = Workspace::factory()->create([
'account_id' => $this->user->account_id,
'user_id' => $this->user->id,
]);
$this->workspace->members()->attach($this->user->id, ['role' => Role::Admin->value]);
$this->user->update(['current_workspace_id' => $this->workspace->id]);
$this->user->refresh();
// SaaS mode: the MCP settings live behind EnsureAccountReady.
subscribeAccount($this->user->account);
});
it('shows the mcp settings page', function (): void {
$this->actingAs($this->user)
->get(route('app.mcp.index'))
->assertOk()
->assertInertia(fn ($page) => $page
->component('settings/workspace/Mcp')
->where('mcpUrl', route('mcp.trypost'))
->missing('docsUrl')
->missing('mcpClients')
->has('connectedClients'));
});
it('shows the mcp settings page without a subscription in self-hosted mode', function (): void {
config(['trypost.self_hosted' => true]);
$this->user->account->subscriptions()->delete();
$this->actingAs($this->user->fresh())
->get(route('app.mcp.index'))
->assertOk()
->assertInertia(fn ($page) => $page
->component('settings/workspace/Mcp')
->where('mcpUrl', route('mcp.trypost')));
});
it('lists only the current users oauth clients as connected, excluding personal access tokens', function (): void {
$member = User::factory()->create(['account_id' => $this->user->account_id]);
$this->workspace->members()->attach($member->id, ['role' => Role::Member->value]);
$member->update(['current_workspace_id' => $this->workspace->id]);
$ownerClientId = mcpOauthClient('Owner Agent');
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
mcpAccessToken($this->user, $ownerClientId, $this->workspace);
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$memberClientId = mcpOauthClient('Member Agent');
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
mcpAccessToken($member, $memberClientId, $this->workspace);
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$pat = $this->user->createToken('API Key');
AccessToken::query()->findOrFail($pat->token->id)
->forceFill(['workspace_id' => $this->workspace->id])
->saveQuietly();
$this->actingAs($this->user)
->get(route('app.mcp.index'))
->assertInertia(fn ($page) => $page
->has('connectedClients', 1)
->where('connectedClients.0.name', 'Owner Agent')
->where('connectedClients.0.can_disconnect', true)
->where('connectedClients', fn ($clients): bool => collect($clients)->every(
fn (array $client): bool => $client['name'] !== 'Member Agent',
)));
});
it('excludes unscoped oauth grants from connected clients', function (): void {
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
mcpAccessToken($this->user, mcpOauthClient('Unscoped Agent'), $this->workspace, scopes: []);
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$this->actingAs($this->user)
->get(route('app.mcp.index'))
->assertInertia(fn ($page) => $page->where('connectedClients', []));
});
it('lists viewer own oauth grants as connected clients', function (): void {
$viewer = User::factory()->create(['account_id' => $this->user->account_id]);
$this->workspace->members()->attach($viewer->id, ['role' => Role::Viewer->value]);
$viewer->update(['current_workspace_id' => $this->workspace->id]);
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
mcpAccessToken($viewer, mcpOauthClient('Viewer Agent'), $this->workspace);
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$this->actingAs($viewer->fresh())
->get(route('app.mcp.index'))
->assertInertia(fn ($page) => $page
->has('connectedClients', 1)
->where('connectedClients.0.name', 'Viewer Agent')
->where('connectedClients.0.can_disconnect', true));
});
it('disconnects a client by revoking its tokens', function (): void {
$clientId = mcpOauthClient();
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
$token = mcpAccessToken($this->user, $clientId, $this->workspace);
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$this->actingAs($this->user)
->delete(route('app.mcp.disconnect', ['client' => $clientId]))
->assertRedirect()
->assertSessionHas('flash.success', __('mcp.disconnected'));
expect($token->fresh()->revoked)->toBeTrue();
});
it('disconnects a client when its access token expired but its refresh token is live', function (): void {
$clientId = mcpOauthClient();
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
$token = mcpAccessToken($this->user, $clientId, $this->workspace);
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$token->forceFill(['expires_at' => now()->subMinute()])->saveQuietly();
$refreshTokenId = Str::random(80);
DB::table('oauth_refresh_tokens')->insert([
'id' => $refreshTokenId,
'access_token_id' => $token->id,
'revoked' => false,
'expires_at' => now()->addMonth(),
]);
$this->actingAs($this->user)
->delete(route('app.mcp.disconnect', ['client' => $clientId]))
->assertRedirect()
->assertSessionHas('flash.success');
expect($token->fresh()->revoked)->toBeTrue()
->and(DB::table('oauth_refresh_tokens')->where('id', $refreshTokenId)->value('revoked'))->toBeTrue();
});
it('lists a client when its access token expired but its refresh token is live', function (): void {
$clientId = mcpOauthClient('Recoverable Agent');
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
$token = mcpAccessToken($this->user, $clientId, $this->workspace);
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$token->forceFill(['expires_at' => now()->subMinute()])->saveQuietly();
DB::table('oauth_refresh_tokens')->insert([
'id' => Str::random(80),
'access_token_id' => $token->id,
'revoked' => false,
'expires_at' => now()->addMonth(),
]);
$this->actingAs($this->user)
->get(route('app.mcp.index'))
->assertOk()
->assertInertia(fn ($page) => $page
->has('connectedClients', 1)
->where('connectedClients.0.name', 'Recoverable Agent')
->where('connectedClients.0.can_disconnect', true));
});
it('hides a client when both access and refresh tokens are expired', function (): void {
$clientId = mcpOauthClient('Dead Agent');
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
$token = mcpAccessToken($this->user, $clientId, $this->workspace);
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$token->forceFill(['expires_at' => now()->subMinute()])->saveQuietly();
DB::table('oauth_refresh_tokens')->insert([
'id' => Str::random(80),
'access_token_id' => $token->id,
'revoked' => false,
'expires_at' => now()->subMinute(),
]);
$this->actingAs($this->user)
->get(route('app.mcp.index'))
->assertOk()
->assertInertia(fn ($page) => $page->where('connectedClients', []));
});
it('does not flash success when disconnecting an unknown client', function (): void {
$this->actingAs($this->user)
->delete(route('app.mcp.disconnect', ['client' => (string) Str::uuid()]))
->assertRedirect()
->assertSessionMissing('flash.success');
});
it('does not list a teammates mcp connection', function (): void {
$member = User::factory()->create(['account_id' => $this->user->account_id]);
$this->workspace->members()->attach($member->id, ['role' => Role::Member->value]);
$member->update(['current_workspace_id' => $this->workspace->id]);
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
mcpAccessToken($this->user, mcpOauthClient('Owner Agent'), $this->workspace);
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$this->actingAs($member->fresh())
->get(route('app.mcp.index'))
->assertOk()
->assertInertia(fn ($page) => $page->where('connectedClients', []));
});
it('allows workspace members to view and disconnect their own mcp clients', function (): void {
$member = User::factory()->create(['account_id' => $this->user->account_id]);
$this->workspace->members()->attach($member->id, ['role' => Role::Member->value]);
$member->update(['current_workspace_id' => $this->workspace->id]);
$clientId = mcpOauthClient('Member Agent');
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
$token = mcpAccessToken($member, $clientId, $this->workspace);
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$this->actingAs($member->fresh())
->get(route('app.mcp.index'))
->assertOk()
->assertInertia(fn ($page) => $page
->has('connectedClients', 1)
->where('connectedClients.0.name', 'Member Agent'));
$this->actingAs($member->fresh())
->delete(route('app.mcp.disconnect', ['client' => $clientId]))
->assertRedirect()
->assertSessionHas('flash.success');
expect($token->fresh()->revoked)->toBeTrue();
});
it('allows workspace viewers to view and disconnect their own mcp clients', function (): void {
$viewer = User::factory()->create(['account_id' => $this->user->account_id]);
$this->workspace->members()->attach($viewer->id, ['role' => Role::Viewer->value]);
$viewer->update(['current_workspace_id' => $this->workspace->id]);
$clientId = mcpOauthClient('Viewer Agent');
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
$token = mcpAccessToken($viewer, $clientId, $this->workspace);
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$this->actingAs($viewer->fresh())
->get(route('app.mcp.index'))
->assertOk()
->assertInertia(fn ($page) => $page
->component('settings/workspace/Mcp')
->has('connectedClients', 1)
->where('connectedClients.0.name', 'Viewer Agent')
->where('connectedClients.0.can_disconnect', true));
$this->actingAs($viewer->fresh())
->delete(route('app.mcp.disconnect', ['client' => $clientId]))
->assertRedirect()
->assertSessionHas('flash.success');
expect($token->fresh()->revoked)->toBeTrue();
});
it('does not revoke another users mcp client tokens', function (): void {
$member = User::factory()->create(['account_id' => $this->user->account_id]);
$this->workspace->members()->attach($member->id, ['role' => Role::Member->value]);
$member->update(['current_workspace_id' => $this->workspace->id]);
$clientId = mcpOauthClient('Owner Agent');
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
$token = mcpAccessToken($this->user, $clientId, $this->workspace);
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$this->actingAs($member->fresh())
->delete(route('app.mcp.disconnect', ['client' => $clientId]))
->assertRedirect()
->assertSessionMissing('flash.success');
expect($token->fresh()->revoked)->toBeFalse();
});
it('does not revoke personal access tokens via disconnect', function (): void {
$pat = $this->user->createToken('API Key');
$token = AccessToken::query()->findOrFail($pat->token->id);
$token->forceFill(['workspace_id' => $this->workspace->id])->saveQuietly();
$this->actingAs($this->user)
->delete(route('app.mcp.disconnect', ['client' => $token->client_id]))
->assertRedirect()
->assertSessionMissing('flash.success');
expect($token->fresh()->revoked)->toBeFalse();
});
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
it('lists only mcp connections for the current workspace', function (): void {
$workspaceB = Workspace::factory()->create([
'account_id' => $this->user->account_id,
'user_id' => $this->user->id,
'name' => 'Workspace B',
]);
$workspaceB->members()->attach($this->user->id, ['role' => Role::Admin->value]);
$clientId = mcpOauthClient('Claude');
mcpAccessToken($this->user, $clientId, $this->workspace);
mcpAccessToken($this->user, $clientId, $workspaceB);
$this->actingAs($this->user)
->get(route('app.mcp.index'))
->assertOk()
->assertInertia(fn ($page) => $page
->has('connectedClients', 1)
->where('connectedClients.0.client_id', $clientId)
->where('connectedClients.0.name', 'Claude'));
$this->user->update(['current_workspace_id' => $workspaceB->id]);
$this->actingAs($this->user->fresh())
->get(route('app.mcp.index'))
->assertOk()
->assertInertia(fn ($page) => $page
->has('connectedClients', 1)
->where('connectedClients.0.client_id', $clientId)
->where('connectedClients.0.name', 'Claude'));
});
it('orders connected mcp clients by last used descending', function (): void {
$older = mcpAccessToken($this->user, mcpOauthClient('Older Agent'), $this->workspace);
$newer = mcpAccessToken($this->user, mcpOauthClient('Newer Agent'), $this->workspace);
$older->forceFill(['last_used_at' => now()->subDay()])->saveQuietly();
$newer->forceFill(['last_used_at' => now()])->saveQuietly();
$this->actingAs($this->user)
->get(route('app.mcp.index'))
->assertOk()
->assertInertia(fn ($page) => $page
->has('connectedClients', 2)
->where('connectedClients.0.name', 'Newer Agent')
->where('connectedClients.1.name', 'Older Agent'));
});
it('does not list unbound mcp grants', function (): void {
mcpAccessToken($this->user, mcpOauthClient('Legacy Agent'), workspace: null);
$this->actingAs($this->user)
->get(route('app.mcp.index'))
->assertOk()
->assertInertia(fn ($page) => $page->where('connectedClients', []));
});
it('disconnects a client only on the current workspace', function (): void {
$workspaceB = Workspace::factory()->create([
'account_id' => $this->user->account_id,
'user_id' => $this->user->id,
]);
$workspaceB->members()->attach($this->user->id, ['role' => Role::Admin->value]);
$clientId = mcpOauthClient('Claude');
$onA = mcpAccessToken($this->user, $clientId, $this->workspace);
$onB = mcpAccessToken($this->user, $clientId, $workspaceB);
$this->actingAs($this->user)
->delete(route('app.mcp.disconnect', ['client' => $clientId]))
->assertRedirect()
->assertSessionHas('flash.success');
expect($onA->fresh()->revoked)->toBeTrue()
->and($onB->fresh()->revoked)->toBeFalse();
$this->user->update(['current_workspace_id' => $workspaceB->id]);
$this->actingAs($this->user->fresh())
->get(route('app.mcp.index'))
->assertOk()
->assertInertia(fn ($page) => $page
->has('connectedClients', 1)
->where('connectedClients.0.client_id', $clientId));
});
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
it('requires authentication', function (): void {
$this->get(route('app.mcp.index'))->assertRedirect();
});
it('forbids users without workspace access', function (): void {
$outsider = User::factory()->create();
subscribeAccount($outsider->account);
$outsider->update(['current_workspace_id' => $this->workspace->id]);
$this->actingAs($outsider->fresh())
->get(route('app.mcp.index'))
->assertForbidden();
});
Welcome: pre-subscription funnel and member subscription-required screen (#243) * Rename pre-subscription onboarding funnel to Welcome. Move the ICP steps to /welcome, drop the social-connect checkout gate, hold unpaid members on a subscription-required screen, and keep legacy /onboarding URLs working until the post-subscription checklist lands. Closes #237 Co-authored-by: Cursor <cursoragent@cursor.com> * Drop legacy /onboarding ICP URL aliases. Unfinished users re-enter Welcome via EnsureAccountReady on next login; /onboarding stays free for the post-subscription checklist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify Welcome PostHog event names. Use welcome.persona/goals/referral and drop the unused checkout case — begin checkout stays on the frontend as checkout.started / begin_checkout. Co-authored-by: Cursor <cursoragent@cursor.com> * Slim welcome goal options to match the #204 set. Drop team_collaboration, automate_api, and track_performance so the goals step stays at nine choices. Co-authored-by: Cursor <cursoragent@cursor.com> * Split Welcome AI goals into TryPost AI and MCP assistants. Rewrite ai_content for in-app generation and add use_mcp so Claude/ChatGPT/Cursor intent is captured separately across locales. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden Welcome goals gate and drop dead checkout UI. Treat removed goal values as incomplete so mid-funnel users re-select, remove the unused canCheckout branch, and fix the pt-BR welcome progress label. Co-authored-by: Cursor <cursoragent@cursor.com> * Add password visibility toggle to the login form. Match the register eye control so users can reveal their password while signing in. Co-authored-by: Cursor <cursoragent@cursor.com> * Point the sidebar community link to Discord. Replace the X stay-updated entry with Join Discord and the trypost.it/discord invite. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename the sidebar Discord link to Discord community. Softer label that matches the other support nav items. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix broken Turkish Discord community translation. An unescaped apostrophe left a parse error in lang/tr/sidebar.php. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 14:34:50 +00:00
it('redirects to welcome when the account has no app access', function (): void {
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
$this->user->account->subscriptions()->delete();
$this->actingAs($this->user->fresh())
->get(route('app.mcp.index'))
Welcome: pre-subscription funnel and member subscription-required screen (#243) * Rename pre-subscription onboarding funnel to Welcome. Move the ICP steps to /welcome, drop the social-connect checkout gate, hold unpaid members on a subscription-required screen, and keep legacy /onboarding URLs working until the post-subscription checklist lands. Closes #237 Co-authored-by: Cursor <cursoragent@cursor.com> * Drop legacy /onboarding ICP URL aliases. Unfinished users re-enter Welcome via EnsureAccountReady on next login; /onboarding stays free for the post-subscription checklist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify Welcome PostHog event names. Use welcome.persona/goals/referral and drop the unused checkout case — begin checkout stays on the frontend as checkout.started / begin_checkout. Co-authored-by: Cursor <cursoragent@cursor.com> * Slim welcome goal options to match the #204 set. Drop team_collaboration, automate_api, and track_performance so the goals step stays at nine choices. Co-authored-by: Cursor <cursoragent@cursor.com> * Split Welcome AI goals into TryPost AI and MCP assistants. Rewrite ai_content for in-app generation and add use_mcp so Claude/ChatGPT/Cursor intent is captured separately across locales. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden Welcome goals gate and drop dead checkout UI. Treat removed goal values as incomplete so mid-funnel users re-select, remove the unused canCheckout branch, and fix the pt-BR welcome progress label. Co-authored-by: Cursor <cursoragent@cursor.com> * Add password visibility toggle to the login form. Match the register eye control so users can reveal their password while signing in. Co-authored-by: Cursor <cursoragent@cursor.com> * Point the sidebar community link to Discord. Replace the X stay-updated entry with Join Discord and the trypost.it/discord invite. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename the sidebar Discord link to Discord community. Softer label that matches the other support nav items. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix broken Turkish Discord community translation. An unescaped apostrophe left a parse error in lang/tr/sidebar.php. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 14:34:50 +00:00
->assertRedirect(route('app.welcome.persona'));
MCP: workspace settings, viewer read access, and token access (#241) * Add workspace MCP settings and token access controls. Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring, and workspace.token:mcp gating so assistants can connect without pulling in welcome/onboarding from the parent epic. Co-authored-by: Cursor <cursoragent@cursor.com> * Type MCP client config shapes instead of string checks. Encode http/config-root on each advanced client and tighten primary client ids so snippet generation does not branch on magic strings. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP settings follow-ups from review. Translate Ukrainian MCP copy, deep-link ChatGPT into connector creation, drop an unused asset and revoke arg, and assert PATs are rejected on the MCP endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP connected clients, revoke scope, and OAuth consent. List recoverable sessions with live refresh tokens, revoke only PATs, throttle registration alone, and block viewers from authorizing MCP. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify MCP OAuth route throttling to a single middleware group. Co-authored-by: Cursor <cursoragent@cursor.com> * Allow workspace viewers read-only MCP access with web policy writes. Mirror the web app: MCP connects on view + OAuth mcp:use, write tools enforce createPost/update/delete/manageAccounts/manageTeam, and demotion to Viewer keeps grants. Cover role denials, consent, and disconnect. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden MCP tool authz with shared workspace helpers. Route ApiKey tools through AuthorizesMcpTool, fail closed on null user or policy argument, and resolve the current workspace before mutating. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant string casts on validated request data. Enum::from and validated() fields are already strings, so the casts add noise without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Show only the current user's MCP connections in settings. Match API keys privacy: list and disconnect your own OAuth clients, not teammates' across the account. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop redundant is_string guard before UpdatePostTool find. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined. * Refresh MCP connected clients with Inertia usePoll. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump laravel/mcp to 0.9.1 and add the TryPost server icon. Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop no-op ReflectionClass import in TryPostServerTest. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:51 +00:00
});