77 */
88
99import { workspaces } from '@angular-devkit/core' ;
10+ import { mkdirSync , mkdtempSync , rmSync , writeFileSync } from 'node:fs' ;
11+ import { tmpdir } from 'node:os' ;
1012import { join } from 'node:path' ;
13+ import { pathToFileURL } from 'node:url' ;
1114import { AngularWorkspace } from '../../utilities/config' ;
1215import { LocalWorkspaceHost } from './host' ;
1316import { addProjectToWorkspace , createMockContext , createMockHost } from './testing/test-utils' ;
@@ -101,11 +104,24 @@ describe('MCP Workspace Utils', () => {
101104 describe ( 'resolveWorkspaceAndProject' , ( ) => {
102105 let mockHost : ReturnType < typeof createMockHost > ;
103106 let mockWorkspace : AngularWorkspace ;
107+ let mockServer : NonNullable < Parameters < typeof resolveWorkspaceAndProject > [ 0 ] [ 'server' ] > ;
108+ let tempDir : string ;
109+ let allowedRoot : string ;
110+ let allowedWorkspace : string ;
111+ let outsideWorkspace : string ;
104112 const cwd = './' ;
105113
106114 beforeEach ( ( ) => {
107115 mockHost = createMockHost ( ) ;
108116 spyOn ( process , 'cwd' ) . and . returnValue ( cwd ) ;
117+ tempDir = mkdtempSync ( join ( tmpdir ( ) , 'mcp-workspace-utils-' ) ) ;
118+ allowedRoot = join ( tempDir , 'allowed-root' ) ;
119+ allowedWorkspace = join ( allowedRoot , 'workspace' ) ;
120+ outsideWorkspace = join ( tempDir , 'outside-workspace' ) ;
121+ mkdirSync ( allowedWorkspace , { recursive : true } ) ;
122+ mkdirSync ( outsideWorkspace , { recursive : true } ) ;
123+ writeFileSync ( join ( allowedWorkspace , 'angular.json' ) , '{}' ) ;
124+ writeFileSync ( join ( outsideWorkspace , 'angular.json' ) , '{}' ) ;
109125
110126 // Setup default mocks
111127 mockHost . existsSync . and . callFake ( ( p ) => {
@@ -120,6 +136,18 @@ describe('MCP Workspace Utils', () => {
120136 if ( p === '/my/workspace/angular.json' ) {
121137 return true ;
122138 }
139+ if ( p === allowedWorkspace ) {
140+ return true ;
141+ }
142+ if ( p === join ( allowedWorkspace , 'angular.json' ) ) {
143+ return true ;
144+ }
145+ if ( p === outsideWorkspace ) {
146+ return true ;
147+ }
148+ if ( p === join ( outsideWorkspace , 'angular.json' ) ) {
149+ return true ;
150+ }
123151
124152 return false ;
125153 } ) ;
@@ -139,6 +167,21 @@ describe('MCP Workspace Utils', () => {
139167 } as unknown as AngularWorkspace ;
140168
141169 spyOn ( AngularWorkspace , 'load' ) . and . resolveTo ( mockWorkspace ) ;
170+
171+ mockServer = {
172+ server : {
173+ getClientCapabilities : jasmine . createSpy ( 'getClientCapabilities' ) . and . returnValue ( {
174+ roots : { listChanged : false } ,
175+ } ) ,
176+ listRoots : jasmine . createSpy ( 'listRoots' ) . and . resolveTo ( {
177+ roots : [ { uri : pathToFileURL ( allowedRoot ) . href , name : 'allowed-root' } ] ,
178+ } ) ,
179+ } ,
180+ } as unknown as NonNullable < Parameters < typeof resolveWorkspaceAndProject > [ 0 ] [ 'server' ] > ;
181+ } ) ;
182+
183+ afterEach ( ( ) => {
184+ rmSync ( tempDir , { recursive : true , force : true } ) ;
142185 } ) ;
143186
144187 it ( 'should resolve workspace from CWD if not provided and mcpWorkspace is absent' , async ( ) => {
@@ -179,6 +222,27 @@ describe('MCP Workspace Utils', () => {
179222 expect ( AngularWorkspace . load ) . toHaveBeenCalledWith ( '/my/workspace/angular.json' ) ;
180223 } ) ;
181224
225+ it ( 'should allow provided workspace within allowed MCP roots' , async ( ) => {
226+ const result = await resolveWorkspaceAndProject ( {
227+ host : mockHost ,
228+ server : mockServer ,
229+ workspacePathInput : allowedWorkspace ,
230+ } ) ;
231+ expect ( result . workspacePath ) . toBe ( allowedWorkspace ) ;
232+ expect ( AngularWorkspace . load ) . toHaveBeenCalledWith ( join ( allowedWorkspace , 'angular.json' ) ) ;
233+ expect ( mockServer . server . listRoots ) . toHaveBeenCalled ( ) ;
234+ } ) ;
235+
236+ it ( 'should reject provided workspace outside allowed MCP roots' , async ( ) => {
237+ await expectAsync (
238+ resolveWorkspaceAndProject ( {
239+ host : mockHost ,
240+ server : mockServer ,
241+ workspacePathInput : outsideWorkspace ,
242+ } ) ,
243+ ) . toBeRejectedWithError ( / W o r k s p a c e p a t h i s o u t s i d e t h e a l l o w e d M C P r o o t s / ) ;
244+ } ) ;
245+
182246 it ( 'should throw if provided workspace does not exist' , async ( ) => {
183247 mockHost . existsSync . and . returnValue ( false ) ;
184248 await expectAsync (
0 commit comments