@@ -9,6 +9,41 @@ import fs from 'fs';
99// Load environment variables from .env file
1010dotenv . config ( ) ;
1111
12+ /**
13+ * Helper function to create a client with custom CLI arguments
14+ */
15+ async function createClientWithArgs (
16+ args : string [ ] = [ ] ,
17+ ) : Promise < { client : Client ; transport : StdioClientTransport } > {
18+ const serverPath = join ( process . cwd ( ) , 'dist' , 'index.js' ) ;
19+ const tempEnvFile = join ( process . cwd ( ) , '.env.e2e-test' ) ;
20+
21+ const transport = new StdioClientTransport ( {
22+ command : 'node' ,
23+ args : [ '-r' , 'dotenv/config' , serverPath , ...args ] ,
24+ env : {
25+ ...process . env ,
26+ NODE_ENV : 'test' ,
27+ DOTENV_CONFIG_PATH : tempEnvFile ,
28+ } ,
29+ } ) ;
30+
31+ const client = new Client (
32+ {
33+ name : 'e2e-test-client' ,
34+ version : '1.0.0' ,
35+ } ,
36+ {
37+ capabilities : {
38+ tools : { } ,
39+ } ,
40+ } ,
41+ ) ;
42+
43+ await client . connect ( transport ) ;
44+ return { client, transport } ;
45+ }
46+
1247describe ( 'Azure DevOps MCP Server E2E Tests' , ( ) => {
1348 let client : Client ;
1449 let serverProcess : ReturnType < typeof spawn > ;
@@ -134,6 +169,218 @@ AZURE_DEVOPS_AUTH_METHOD=${authMethod}
134169 } ) ;
135170 } ) ;
136171
172+ describe ( 'Domain Filtering' , ( ) => {
173+ test ( 'should load all 43 tools by default (no filtering)' , async ( ) => {
174+ // Arrange - use default client without any domain args
175+ const tools = await client . listTools ( ) ;
176+
177+ // Assert
178+ expect ( tools . tools ) . toBeDefined ( ) ;
179+ expect ( tools . tools . length ) . toBe ( 43 ) ;
180+ } ) ;
181+
182+ test ( 'should load only 5 work-items tools when --domains work-items' , async ( ) => {
183+ // Arrange
184+ const { client : filteredClient , transport : filteredTransport } =
185+ await createClientWithArgs ( [ '--domains' , 'work-items' ] ) ;
186+
187+ try {
188+ // Act
189+ const tools = await filteredClient . listTools ( ) ;
190+
191+ // Assert
192+ expect ( tools . tools ) . toBeDefined ( ) ;
193+ expect ( tools . tools . length ) . toBe ( 5 ) ;
194+
195+ const toolNames = tools . tools . map ( ( t ) => t . name ) ;
196+ expect ( toolNames ) . toContain ( 'list_work_items' ) ;
197+ expect ( toolNames ) . toContain ( 'get_work_item' ) ;
198+ expect ( toolNames ) . toContain ( 'create_work_item' ) ;
199+ expect ( toolNames ) . toContain ( 'update_work_item' ) ;
200+ expect ( toolNames ) . toContain ( 'manage_work_item_link' ) ;
201+
202+ // Should not contain tools from other domains
203+ expect ( toolNames ) . not . toContain ( 'list_repositories' ) ;
204+ expect ( toolNames ) . not . toContain ( 'list_projects' ) ;
205+ expect ( toolNames ) . not . toContain ( 'list_pipelines' ) ;
206+ } finally {
207+ await filteredClient . close ( ) ;
208+ await filteredTransport . close ( ) ;
209+ }
210+ } ) ;
211+
212+ test ( 'should load 9 tools when --domains core work-items' , async ( ) => {
213+ // Arrange
214+ const { client : filteredClient , transport : filteredTransport } =
215+ await createClientWithArgs ( [ '--domains' , 'core' , 'work-items' ] ) ;
216+
217+ try {
218+ // Act
219+ const tools = await filteredClient . listTools ( ) ;
220+
221+ // Assert
222+ expect ( tools . tools ) . toBeDefined ( ) ;
223+ expect ( tools . tools . length ) . toBe ( 9 ) ;
224+
225+ const toolNames = tools . tools . map ( ( t ) => t . name ) ;
226+
227+ // Core domain tools (4)
228+ expect ( toolNames ) . toContain ( 'list_organizations' ) ;
229+ expect ( toolNames ) . toContain ( 'list_projects' ) ;
230+ expect ( toolNames ) . toContain ( 'get_project' ) ;
231+ expect ( toolNames ) . toContain ( 'get_me' ) ;
232+
233+ // Work items domain tools (5)
234+ expect ( toolNames ) . toContain ( 'list_work_items' ) ;
235+ expect ( toolNames ) . toContain ( 'get_work_item' ) ;
236+
237+ // Should not contain tools from other domains
238+ expect ( toolNames ) . not . toContain ( 'list_repositories' ) ;
239+ expect ( toolNames ) . not . toContain ( 'list_pipelines' ) ;
240+ } finally {
241+ await filteredClient . close ( ) ;
242+ await filteredTransport . close ( ) ;
243+ }
244+ } ) ;
245+
246+ test ( 'should support comma-separated domains' , async ( ) => {
247+ // Arrange
248+ const { client : filteredClient , transport : filteredTransport } =
249+ await createClientWithArgs ( [ '--domains' , 'core,work-items' ] ) ;
250+
251+ try {
252+ // Act
253+ const tools = await filteredClient . listTools ( ) ;
254+
255+ // Assert - should be same as space-separated
256+ expect ( tools . tools ) . toBeDefined ( ) ;
257+ expect ( tools . tools . length ) . toBe ( 9 ) ;
258+ } finally {
259+ await filteredClient . close ( ) ;
260+ await filteredTransport . close ( ) ;
261+ }
262+ } ) ;
263+ } ) ;
264+
265+ describe ( 'Read-Only Mode' , ( ) => {
266+ test ( 'should filter out write operations when --read-only' , async ( ) => {
267+ // Arrange
268+ const { client : readOnlyClient , transport : readOnlyTransport } =
269+ await createClientWithArgs ( [ '--read-only' ] ) ;
270+
271+ try {
272+ // Act
273+ const tools = await readOnlyClient . listTools ( ) ;
274+
275+ // Assert
276+ const toolNames = tools . tools . map ( ( t ) => t . name ) ;
277+
278+ // Should contain read-only tools
279+ expect ( toolNames ) . toContain ( 'list_work_items' ) ;
280+ expect ( toolNames ) . toContain ( 'get_work_item' ) ;
281+ expect ( toolNames ) . toContain ( 'list_repositories' ) ;
282+ expect ( toolNames ) . toContain ( 'get_repository' ) ;
283+ expect ( toolNames ) . toContain ( 'list_projects' ) ;
284+ expect ( toolNames ) . toContain ( 'get_project' ) ;
285+
286+ // Should NOT contain write operations
287+ expect ( toolNames ) . not . toContain ( 'create_work_item' ) ;
288+ expect ( toolNames ) . not . toContain ( 'update_work_item' ) ;
289+ expect ( toolNames ) . not . toContain ( 'create_branch' ) ;
290+ expect ( toolNames ) . not . toContain ( 'create_commit' ) ;
291+ expect ( toolNames ) . not . toContain ( 'create_pull_request' ) ;
292+ expect ( toolNames ) . not . toContain ( 'update_pull_request' ) ;
293+ expect ( toolNames ) . not . toContain ( 'trigger_pipeline' ) ;
294+ expect ( toolNames ) . not . toContain ( 'create_wiki' ) ;
295+ expect ( toolNames ) . not . toContain ( 'update_wiki_page' ) ;
296+
297+ // Verify we filtered out approximately half the tools
298+ expect ( tools . tools . length ) . toBeGreaterThanOrEqual ( 20 ) ;
299+ expect ( tools . tools . length ) . toBeLessThanOrEqual ( 25 ) ;
300+ } finally {
301+ await readOnlyClient . close ( ) ;
302+ await readOnlyTransport . close ( ) ;
303+ }
304+ } ) ;
305+ } ) ;
306+
307+ describe ( 'Combined Filtering (Domain + Read-Only)' , ( ) => {
308+ test ( 'should load only 2 tools when --domains work-items --read-only' , async ( ) => {
309+ // Arrange
310+ const { client : filteredClient , transport : filteredTransport } =
311+ await createClientWithArgs ( [
312+ '--domains' ,
313+ 'work-items' ,
314+ '--read-only' ,
315+ ] ) ;
316+
317+ try {
318+ // Act
319+ const tools = await filteredClient . listTools ( ) ;
320+
321+ // Assert - 95% reduction! (43 -> 2)
322+ expect ( tools . tools ) . toBeDefined ( ) ;
323+ expect ( tools . tools . length ) . toBe ( 2 ) ;
324+
325+ const toolNames = tools . tools . map ( ( t ) => t . name ) ;
326+ expect ( toolNames ) . toContain ( 'list_work_items' ) ;
327+ expect ( toolNames ) . toContain ( 'get_work_item' ) ;
328+
329+ // Should not contain write operations
330+ expect ( toolNames ) . not . toContain ( 'create_work_item' ) ;
331+ expect ( toolNames ) . not . toContain ( 'update_work_item' ) ;
332+
333+ // Should not contain tools from other domains
334+ expect ( toolNames ) . not . toContain ( 'list_repositories' ) ;
335+ expect ( toolNames ) . not . toContain ( 'list_projects' ) ;
336+ } finally {
337+ await filteredClient . close ( ) ;
338+ await filteredTransport . close ( ) ;
339+ }
340+ } ) ;
341+
342+ test ( 'should handle multiple domains with read-only mode' , async ( ) => {
343+ // Arrange
344+ const { client : filteredClient , transport : filteredTransport } =
345+ await createClientWithArgs ( [
346+ '--domains' ,
347+ 'core' ,
348+ 'repositories' ,
349+ '--read-only' ,
350+ ] ) ;
351+
352+ try {
353+ // Act
354+ const tools = await filteredClient . listTools ( ) ;
355+
356+ // Assert
357+ const toolNames = tools . tools . map ( ( t ) => t . name ) ;
358+
359+ // Core domain read-only tools
360+ expect ( toolNames ) . toContain ( 'list_organizations' ) ;
361+ expect ( toolNames ) . toContain ( 'list_projects' ) ;
362+ expect ( toolNames ) . toContain ( 'get_project' ) ;
363+ expect ( toolNames ) . toContain ( 'get_me' ) ;
364+
365+ // Repositories domain read-only tools
366+ expect ( toolNames ) . toContain ( 'list_repositories' ) ;
367+ expect ( toolNames ) . toContain ( 'get_repository' ) ;
368+ expect ( toolNames ) . toContain ( 'get_file_content' ) ;
369+
370+ // Should not contain write operations
371+ expect ( toolNames ) . not . toContain ( 'create_branch' ) ;
372+ expect ( toolNames ) . not . toContain ( 'create_commit' ) ;
373+
374+ // Should not contain tools from other domains
375+ expect ( toolNames ) . not . toContain ( 'list_work_items' ) ;
376+ expect ( toolNames ) . not . toContain ( 'list_pipelines' ) ;
377+ } finally {
378+ await filteredClient . close ( ) ;
379+ await filteredTransport . close ( ) ;
380+ }
381+ } ) ;
382+ } ) ;
383+
137384 describe ( 'Organizations' , ( ) => {
138385 test ( 'should list organizations' , async ( ) => {
139386 // Arrange
0 commit comments