# Workspace Context Workspace isolation and context resolution for MCP tools. ## Overview Workspace context ensures that MCP tools operate within the correct workspace boundary, preventing data leaks and unauthorized access. ## Context Resolution ### From Request Headers ```php use Core\Mcp\Context\WorkspaceContext; // Resolve from X-Workspace-ID header $context = WorkspaceContext::fromRequest($request); // Returns WorkspaceContext with: // - workspace: Workspace model // - user: Current user // - namespace: Current namespace (if applicable) ``` **Request Example:** ```bash curl -H "Authorization: Bearer sk_live_..." \ -H "X-Workspace-ID: ws_abc123" \ https://api.example.com/mcp/query ``` ### From API Key ```php use Mod\Api\Models\ApiKey; $apiKey = ApiKey::findByKey($providedKey); // API key is scoped to workspace $context = WorkspaceContext::fromApiKey($apiKey); ``` ### Manual Creation ```php use Mod\Tenant\Models\Workspace; $workspace = Workspace::find($id); $context = new WorkspaceContext( workspace: $workspace, user: $user, namespace: $namespace ); ``` ## Requiring Context ### Tool Implementation ```php validateWorkspaceContext(); // Access workspace $workspace = $this->workspaceContext->workspace; // Query scoped to workspace return Post::where('workspace_id', $workspace->id) ->where('status', $params['status'] ?? 'published') ->get() ->toArray(); } } ``` ### Middleware ```php use Core\Mcp\Middleware\ValidateWorkspaceContext; Route::middleware([ValidateWorkspaceContext::class]) ->post('/mcp/tools/{tool}', [McpController::class, 'execute']); ``` **Validation:** - Header `X-Workspace-ID` is present - Workspace exists - User has access to workspace - API key is scoped to workspace ## Automatic Query Scoping ### SELECT Queries ```php // Query without workspace filter $result = $tool->execute([ 'query' => 'SELECT * FROM posts WHERE status = ?', 'bindings' => ['published'], ]); // Automatically becomes: // SELECT * FROM posts // WHERE status = ? // AND workspace_id = ? // With bindings: ['published', $workspaceId] ``` ### BelongsToWorkspace Models ```php use Core\Mod\Tenant\Concerns\BelongsToWorkspace; class Post extends Model { use BelongsToWorkspace; // Automatically scoped to workspace } // All queries automatically filtered: Post::all(); // Only current workspace's posts Post::where('status', 'published')->get(); // Scoped Post::find($id); // Returns null if wrong workspace ``` ## Context Properties ### Workspace ```php $workspace = $context->workspace; $workspace->id; // Workspace ID $workspace->name; // Workspace name $workspace->slug; // URL slug $workspace->settings; // Workspace settings $workspace->subscription; // Subscription plan ``` ### User ```php $user = $context->user; $user->id; // User ID $user->name; // User name $user->email; // User email $user->workspace_id; // Primary workspace $user->permissions; // User permissions ``` ### Namespace ```php $namespace = $context->namespace; if ($namespace) { $namespace->id; // Namespace ID $namespace->name; // Namespace name $namespace->entitlements; // Feature access } ``` ## Multi-Workspace Access ### Switching Context ```php // User with access to multiple workspaces $workspaces = $user->workspaces; foreach ($workspaces as $workspace) { $context = new WorkspaceContext($workspace, $user); // Execute in workspace context $result = $tool->execute($params, $context); } ``` ### Cross-Workspace Queries (Admin) ```php // Requires admin permission $result = $tool->execute([ 'query' => 'SELECT * FROM posts', 'bypass_workspace_scope' => true, ], $context); // Returns posts from all workspaces ``` ## Error Handling ### Missing Context ```php use Core\Mcp\Exceptions\MissingWorkspaceContextException; try { $tool->execute($params); // No context provided } catch (MissingWorkspaceContextException $e) { return response()->json([ 'error' => 'Workspace context required', 'message' => 'Please provide X-Workspace-ID header', ], 400); } ``` ### Invalid Workspace ```php use Core\Mod\Tenant\Exceptions\WorkspaceNotFoundException; try { $context = WorkspaceContext::fromRequest($request); } catch (WorkspaceNotFoundException $e) { return response()->json([ 'error' => 'Invalid workspace', 'message' => 'Workspace not found', ], 404); } ``` ### Unauthorized Access ```php use Illuminate\Auth\Access\AuthorizationException; try { $context = WorkspaceContext::fromRequest($request); } catch (AuthorizationException $e) { return response()->json([ 'error' => 'Unauthorized', 'message' => 'You do not have access to this workspace', ], 403); } ``` ## Testing ```php use Tests\TestCase; use Core\Mcp\Context\WorkspaceContext; class WorkspaceContextTest extends TestCase { public function test_resolves_from_header(): void { $workspace = Workspace::factory()->create(); $response = $this->withHeaders([ 'X-Workspace-ID' => $workspace->id, ])->postJson('/mcp/query', [...]); $response->assertStatus(200); } public function test_scopes_queries_to_workspace(): void { $workspace1 = Workspace::factory()->create(); $workspace2 = Workspace::factory()->create(); Post::factory()->create(['workspace_id' => $workspace1->id]); Post::factory()->create(['workspace_id' => $workspace2->id]); $context = new WorkspaceContext($workspace1); $result = $tool->execute([ 'query' => 'SELECT * FROM posts', ], $context); $this->assertCount(1, $result['rows']); $this->assertEquals($workspace1->id, $result['rows'][0]['workspace_id']); } public function test_throws_when_context_missing(): void { $this->expectException(MissingWorkspaceContextException::class); $tool->execute(['query' => 'SELECT * FROM posts']); } } ``` ## Best Practices ### 1. Always Validate Context ```php // ✅ Good - validate context public function execute(array $params) { $this->validateWorkspaceContext(); // ... } // ❌ Bad - no validation public function execute(array $params) { // Potential workspace bypass } ``` ### 2. Use BelongsToWorkspace Trait ```php // ✅ Good - automatic scoping class Post extends Model { use BelongsToWorkspace; } // ❌ Bad - manual filtering Post::where('workspace_id', $workspace->id)->get(); ``` ### 3. Provide Clear Errors ```php // ✅ Good - helpful error throw new MissingWorkspaceContextException( 'Please provide X-Workspace-ID header' ); // ❌ Bad - generic error throw new Exception('Error'); ``` ### 4. Test Context Isolation ```php // ✅ Good - test workspace boundaries public function test_cannot_access_other_workspace(): void { $workspace1 = Workspace::factory()->create(); $workspace2 = Workspace::factory()->create(); $context = new WorkspaceContext($workspace1); $post = Post::factory()->create(['workspace_id' => $workspace2->id]); $result = Post::find($post->id); // Should be null $this->assertNull($result); } ``` ## Learn More - [Multi-Tenancy →](/packages/core/tenancy) - [Security →](/packages/mcp/security) - [Creating Tools →](/packages/mcp/tools)