This is page 7 of 8. Use http://codebase.md/tiberriver256/azure-devops-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .clinerules ├── .env.example ├── .eslintrc.json ├── .github │ ├── FUNDING.yml │ ├── release-please-config.json │ ├── release-please-manifest.json │ └── workflows │ ├── main.yml │ └── release-please.yml ├── .gitignore ├── .husky │ ├── commit-msg │ └── pre-commit ├── .kilocode │ └── mcp.json ├── .prettierrc ├── .vscode │ └── settings.json ├── CHANGELOG.md ├── commitlint.config.js ├── CONTRIBUTING.md ├── create_branch.sh ├── docs │ ├── authentication.md │ ├── azure-identity-authentication.md │ ├── ci-setup.md │ ├── examples │ │ ├── azure-cli-authentication.env │ │ ├── azure-identity-authentication.env │ │ ├── pat-authentication.env │ │ └── README.md │ ├── testing │ │ ├── README.md │ │ └── setup.md │ └── tools │ ├── core-navigation.md │ ├── organizations.md │ ├── pipelines.md │ ├── projects.md │ ├── pull-requests.md │ ├── README.md │ ├── repositories.md │ ├── resources.md │ ├── search.md │ ├── user-tools.md │ ├── wiki.md │ └── work-items.md ├── finish_task.sh ├── jest.e2e.config.js ├── jest.int.config.js ├── jest.unit.config.js ├── LICENSE ├── memory │ └── tasks_memory_2025-05-26T16-18-03.json ├── package-lock.json ├── package.json ├── project-management │ ├── planning │ │ ├── architecture-guide.md │ │ ├── azure-identity-authentication-design.md │ │ ├── project-plan.md │ │ ├── project-structure.md │ │ ├── tech-stack.md │ │ └── the-dream-team.md │ ├── startup.xml │ ├── tdd-cycle.xml │ └── troubleshooter.xml ├── README.md ├── setup_env.sh ├── shrimp-rules.md ├── src │ ├── clients │ │ └── azure-devops.ts │ ├── features │ │ ├── organizations │ │ │ ├── __test__ │ │ │ │ └── test-helpers.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── list-organizations │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── schemas.ts │ │ │ ├── tool-definitions.ts │ │ │ └── types.ts │ │ ├── pipelines │ │ │ ├── get-pipeline │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── list-pipelines │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── tool-definitions.ts │ │ │ ├── trigger-pipeline │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ └── types.ts │ │ ├── projects │ │ │ ├── __test__ │ │ │ │ └── test-helpers.ts │ │ │ ├── get-project │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-project-details │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── list-projects │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── schemas.ts │ │ │ ├── tool-definitions.ts │ │ │ └── types.ts │ │ ├── pull-requests │ │ │ ├── add-pull-request-comment │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ └── index.ts │ │ │ ├── create-pull-request │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-pull-request-comments │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ └── index.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── list-pull-requests │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── schemas.ts │ │ │ ├── tool-definitions.ts │ │ │ ├── types.ts │ │ │ └── update-pull-request │ │ │ ├── feature.spec.int.ts │ │ │ ├── feature.spec.unit.ts │ │ │ ├── feature.ts │ │ │ └── index.ts │ │ ├── repositories │ │ │ ├── __test__ │ │ │ │ └── test-helpers.ts │ │ │ ├── get-all-repositories-tree │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── feature.spec.unit.ts.snap │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-file-content │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-repository │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-repository-details │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── list-repositories │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── schemas.ts │ │ │ ├── tool-definitions.ts │ │ │ └── types.ts │ │ ├── search │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── schemas.ts │ │ │ ├── search-code │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ └── index.ts │ │ │ ├── search-wiki │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ └── index.ts │ │ │ ├── search-work-items │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ └── index.ts │ │ │ ├── tool-definitions.ts │ │ │ └── types.ts │ │ ├── users │ │ │ ├── get-me │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── schemas.ts │ │ │ ├── tool-definitions.ts │ │ │ └── types.ts │ │ ├── wikis │ │ │ ├── create-wiki │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── create-wiki-page │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-wiki-page │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-wikis │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── list-wiki-pages │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── tool-definitions.ts │ │ │ └── update-wiki-page │ │ │ ├── feature.spec.int.ts │ │ │ ├── feature.ts │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ └── work-items │ │ ├── __test__ │ │ │ ├── fixtures.ts │ │ │ ├── test-helpers.ts │ │ │ └── test-utils.ts │ │ ├── create-work-item │ │ │ ├── feature.spec.int.ts │ │ │ ├── feature.spec.unit.ts │ │ │ ├── feature.ts │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ ├── get-work-item │ │ │ ├── feature.spec.int.ts │ │ │ ├── feature.spec.unit.ts │ │ │ ├── feature.ts │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ ├── index.spec.unit.ts │ │ ├── index.ts │ │ ├── list-work-items │ │ │ ├── feature.spec.int.ts │ │ │ ├── feature.spec.unit.ts │ │ │ ├── feature.ts │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ ├── manage-work-item-link │ │ │ ├── feature.spec.int.ts │ │ │ ├── feature.spec.unit.ts │ │ │ ├── feature.ts │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ ├── schemas.ts │ │ ├── tool-definitions.ts │ │ ├── types.ts │ │ └── update-work-item │ │ ├── feature.spec.int.ts │ │ ├── feature.spec.unit.ts │ │ ├── feature.ts │ │ ├── index.ts │ │ └── schema.ts │ ├── index.spec.unit.ts │ ├── index.ts │ ├── server.spec.e2e.ts │ ├── server.ts │ ├── shared │ │ ├── api │ │ │ ├── client.ts │ │ │ └── index.ts │ │ ├── auth │ │ │ ├── auth-factory.ts │ │ │ ├── client-factory.ts │ │ │ └── index.ts │ │ ├── config │ │ │ ├── index.ts │ │ │ └── version.ts │ │ ├── enums │ │ │ ├── index.spec.unit.ts │ │ │ └── index.ts │ │ ├── errors │ │ │ ├── azure-devops-errors.ts │ │ │ ├── handle-request-error.ts │ │ │ └── index.ts │ │ ├── test │ │ │ └── test-helpers.ts │ │ └── types │ │ ├── config.ts │ │ ├── index.ts │ │ ├── request-handler.ts │ │ └── tool-definition.ts │ └── utils │ ├── environment.spec.unit.ts │ └── environment.ts ├── tasks.json ├── tests │ └── setup.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /tasks.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "tasks": [ 3 | { 4 | "id": "42e6533a-f407-4286-be04-4d76fdfd8734", 5 | "name": "Create list-wiki-pages directory structure and schema", 6 | "description": "Create the folder structure src/features/wikis/list-wiki-pages/ with schema.ts and index.ts files. Implement Zod schema validation for ListWikiPagesSchema with organizationId, projectId, wikiId, path, and recursionLevel parameters following existing wiki patterns.", 7 | "status": "completed", 8 | "dependencies": [], 9 | "createdAt": "2025-05-26T16:18:03.641Z", 10 | "updatedAt": "2025-05-26T16:18:03.641Z", 11 | "relatedFiles": [ 12 | { 13 | "path": "src/features/wikis/list-wiki-pages/schema.ts", 14 | "type": "CREATE", 15 | "description": "Zod schema for list wiki pages parameters" 16 | }, 17 | { 18 | "path": "src/features/wikis/list-wiki-pages/index.ts", 19 | "type": "CREATE", 20 | "description": "Export file for list wiki pages feature" 21 | }, 22 | { 23 | "path": "src/features/wikis/get-wikis/schema.ts", 24 | "type": "REFERENCE", 25 | "description": "Reference pattern for schema structure" 26 | }, 27 | { 28 | "path": "src/utils/environment.ts", 29 | "type": "REFERENCE", 30 | "description": "Default organization and project utilities" 31 | } 32 | ], 33 | "implementationGuide": "1. Create directory: src/features/wikis/list-wiki-pages/\n2. Create schema.ts with ListWikiPagesSchema using z.object():\n - organizationId: z.string().optional().describe()\n - projectId: z.string().optional().describe()\n - wikiId: z.string().describe()\n - path: z.string().optional().describe()\n - recursionLevel: z.number().int().min(1).max(50).optional().describe()\n3. Import defaultOrg, defaultProject from utils/environment\n4. Create index.ts with exports for schema and future feature function\n5. Follow exact patterns from src/features/wikis/get-wikis/schema.ts", 34 | "verificationCriteria": "Schema compiles without errors, exports are properly defined, follows existing naming conventions, includes proper TypeScript types and Zod validation", 35 | "analysisResult": "Implement list_wiki_pages tool for Azure DevOps MCP server following GitHub issue #184 requirements. The implementation extends existing WikiClient with Azure DevOps Pages Batch API support, includes comprehensive pagination handling, and maintains consistency with established codebase patterns for schema validation, error handling, and testing." 36 | }, 37 | { 38 | "id": "6b895c15-b337-444b-908a-e50a5ae07da3", 39 | "name": "Extend WikiClient with listWikiPages method", 40 | "description": "Add listWikiPages method to WikiClient class in src/clients/azure-devops.ts. Implement Azure DevOps Pages Batch API call with POST request, pagination loop using continuationToken, and proper error handling.", 41 | "status": "completed", 42 | "dependencies": [], 43 | "createdAt": "2025-05-26T16:18:03.641Z", 44 | "updatedAt": "2025-05-26T21:57:06.473Z", 45 | "relatedFiles": [ 46 | { 47 | "path": "src/clients/azure-devops.ts", 48 | "type": "TO_MODIFY", 49 | "description": "Add listWikiPages method to WikiClient class", 50 | "lineStart": 45, 51 | "lineEnd": 532 52 | } 53 | ], 54 | "implementationGuide": "1. Add listWikiPages method to WikiClient class\n2. Method signature: async listWikiPages(projectId: string, wikiId: string, options?: {path?: string, recursionLevel?: number})\n3. Implement POST request to: {baseUrl}/{project}/_apis/wiki/wikis/{wikiId}/pagesbatch?api-version=7.1\n4. Request body: {top: 1000, continuationToken?, path?, recursionLevel?}\n5. Pagination loop: while continuationToken exists, make subsequent requests\n6. Concatenate all results from response.data.value arrays\n7. Error handling: 404 -> AzureDevOpsResourceNotFoundError, 401/403 -> AzureDevOpsPermissionError\n8. Return WikiPageSummary[] with {id, path, url, order} fields\n9. Sort results by order then path", 55 | "verificationCriteria": "Method compiles without errors, implements proper pagination loop, handles all error cases, returns correctly typed results, follows existing WikiClient method patterns", 56 | "analysisResult": "Implement list_wiki_pages tool for Azure DevOps MCP server following GitHub issue #184 requirements. The implementation extends existing WikiClient with Azure DevOps Pages Batch API support, includes comprehensive pagination handling, and maintains consistency with established codebase patterns for schema validation, error handling, and testing.", 57 | "summary": "Successfully implemented the listWikiPages method in WikiClient class. The implementation includes: 1) Added WikiPageSummary interface with id, path, url, and order fields as required. 2) Implemented POST request to Azure DevOps Pages Batch API with proper pagination using continuationToken. 3) Added comprehensive error handling for 404 (AzureDevOpsResourceNotFoundError) and 401/403 (AzureDevOpsPermissionError) status codes. 4) Implemented sorting by order then path as specified. 5) Method signature matches requirements with optional path and recursionLevel parameters. 6) Code compiles without errors and follows existing WikiClient patterns. 7) All TypeScript types are properly defined and exported.", 58 | "completedAt": "2025-05-26T21:57:06.472Z" 59 | }, 60 | { 61 | "id": "6f042d63-fa61-42c9-b7b0-820495aec9ba", 62 | "name": "Implement list-wiki-pages feature function", 63 | "description": "Create feature.ts with listWikiPages function that uses the WikiClient method. Define WikiPageSummary interface and implement the main feature logic with proper error handling and type safety.", 64 | "status": "completed", 65 | "dependencies": [ 66 | { 67 | "taskId": "42e6533a-f407-4286-be04-4d76fdfd8734" 68 | }, 69 | { 70 | "taskId": "6b895c15-b337-444b-908a-e50a5ae07da3" 71 | } 72 | ], 73 | "createdAt": "2025-05-26T16:18:03.641Z", 74 | "updatedAt": "2025-05-26T22:44:06.001Z", 75 | "relatedFiles": [ 76 | { 77 | "path": "src/features/wikis/list-wiki-pages/feature.ts", 78 | "type": "CREATE", 79 | "description": "Main feature implementation" 80 | } 81 | ], 82 | "implementationGuide": "1. Create src/features/wikis/list-wiki-pages/feature.ts\n2. Define WikiPageSummary interface: {id: number, path: string, url: string, order?: number}\n3. Define ListWikiPagesOptions interface matching schema\n4. Implement listWikiPages function:\n - Import WikiClient from clients/azure-devops\n - Use organizationId || defaultOrg, projectId || defaultProject\n - Call wikiClient.listWikiPages() with proper parameters\n - Handle errors with try/catch and proper error type conversion\n - Return WikiPageSummary[] array\n5. Follow patterns from src/features/wikis/get-wiki-page/feature.ts", 83 | "verificationCriteria": "Feature function compiles and exports correctly, proper error handling, type safety maintained, follows existing feature patterns, integrates properly with WikiClient", 84 | "analysisResult": "Implement list_wiki_pages tool for Azure DevOps MCP server following GitHub issue #184 requirements. The implementation extends existing WikiClient with Azure DevOps Pages Batch API support, includes comprehensive pagination handling, and maintains consistency with established codebase patterns for schema validation, error handling, and testing.", 85 | "summary": "Successfully implemented the list-wiki-pages feature function with all required components: Created src/features/wikis/list-wiki-pages/feature.ts with WikiPageSummary interface {id: number, path: string, url: string, order?: number}, imported ListWikiPagesOptions from schema, implemented listWikiPages function using WikiClient.listWikiPages() method with proper error handling, default organization/project handling, and type conversion from client's string id to number id. The implementation follows established patterns from get-wiki-page feature, compiles without TypeScript errors, and integrates properly with the existing WikiClient.", 86 | "completedAt": "2025-05-26T22:44:06.000Z" 87 | }, 88 | { 89 | "id": "29f527f5-a069-4c2d-900b-eb3c7ac478d2", 90 | "name": "Add tool definition and update wikis module exports", 91 | "description": "Add list_wiki_pages tool definition to tool-definitions.ts and update the main wikis index.ts to include the new feature exports and request handler case.", 92 | "status": "completed", 93 | "dependencies": [ 94 | { 95 | "taskId": "42e6533a-f407-4286-be04-4d76fdfd8734" 96 | }, 97 | { 98 | "taskId": "6f042d63-fa61-42c9-b7b0-820495aec9ba" 99 | } 100 | ], 101 | "createdAt": "2025-05-26T16:18:03.641Z", 102 | "updatedAt": "2025-05-26T22:52:55.297Z", 103 | "relatedFiles": [ 104 | { 105 | "path": "src/features/wikis/tool-definitions.ts", 106 | "type": "TO_MODIFY", 107 | "description": "Add list_wiki_pages tool definition" 108 | }, 109 | { 110 | "path": "src/features/wikis/index.ts", 111 | "type": "TO_MODIFY", 112 | "description": "Add exports and request handler case" 113 | } 114 | ], 115 | "implementationGuide": "1. Update src/features/wikis/tool-definitions.ts:\n - Import ListWikiPagesSchema\n - Add tool definition: {name: 'list_wiki_pages', description: 'List pages within an Azure DevOps wiki', inputSchema: zodToJsonSchema(ListWikiPagesSchema)}\n2. Update src/features/wikis/index.ts:\n - Add exports: export {listWikiPages, ListWikiPagesSchema} from './list-wiki-pages'\n - Add 'list_wiki_pages' to isWikisRequest array\n - Add case in handleWikisRequest switch statement\n - Parse args with ListWikiPagesSchema.parse()\n - Call listWikiPages with proper parameters\n - Return JSON.stringify(result, null, 2) in content array", 116 | "verificationCriteria": "Tool definition is properly added, exports are correct, request handler case works, follows existing patterns for tool registration and handling", 117 | "analysisResult": "Implement list_wiki_pages tool for Azure DevOps MCP server following GitHub issue #184 requirements. The implementation extends existing WikiClient with Azure DevOps Pages Batch API support, includes comprehensive pagination handling, and maintains consistency with established codebase patterns for schema validation, error handling, and testing.", 118 | "summary": "Successfully implemented the list_wiki_pages tool definition and updated wikis module exports. Added ListWikiPagesSchema import to tool-definitions.ts, created the tool definition with proper name, description, and schema. Updated main wikis index.ts to export listWikiPages and ListWikiPagesSchema, added 'list_wiki_pages' to the request identifier array, and implemented the request handler case with proper argument parsing and function call. Also fixed the missing export in list-wiki-pages/index.ts. All changes follow existing patterns and the build compiles successfully without errors.", 119 | "completedAt": "2025-05-26T22:52:55.296Z" 120 | }, 121 | { 122 | "id": "457c0d1b-3635-49d7-916e-0e9aeb4f370f", 123 | "name": "Implement comprehensive integration tests", 124 | "description": "Create feature.spec.int.ts with comprehensive integration tests that test against real Azure DevOps API. This is the primary testing approach, covering happy path, error scenarios, and edge cases with real API responses.", 125 | "status": "completed", 126 | "dependencies": [ 127 | { 128 | "taskId": "6f042d63-fa61-42c9-b7b0-820495aec9ba" 129 | } 130 | ], 131 | "createdAt": "2025-05-26T16:18:03.641Z", 132 | "updatedAt": "2025-05-26T23:24:09.973Z", 133 | "relatedFiles": [ 134 | { 135 | "path": "src/features/wikis/list-wiki-pages/feature.spec.int.ts", 136 | "type": "CREATE", 137 | "description": "Integration tests for list wiki pages feature" 138 | } 139 | ], 140 | "implementationGuide": "1. Create src/features/wikis/list-wiki-pages/feature.spec.int.ts\n2. Add environment guard: process.env.AZDO_INT_TESTS === 'true'\n3. Comprehensive test cases with real Azure DevOps API:\n - List pages in real test wiki (happy path)\n - Handle invalid wikiId (expect 404 error)\n - Test path filtering with real wiki structure\n - Test recursionLevel parameter with various values\n - Test pagination with large wiki structures\n - Verify returned data structure matches WikiPageSummary interface\n - Test edge cases like empty wikis, deeply nested paths\n - Error scenarios: permission errors, network issues\n4. Follow patterns from src/features/wikis/get-wikis/feature.spec.int.ts\n5. Use real Azure DevOps connection and test data\n6. Include proper cleanup and comprehensive error handling", 141 | "verificationCriteria": "Integration tests provide comprehensive coverage with real Azure DevOps API, proper environment guards, tests validate real data structure and all major scenarios, follows existing integration test patterns", 142 | "analysisResult": "Implement list_wiki_pages tool for Azure DevOps MCP server following GitHub issue #184 requirements. The implementation extends existing WikiClient with Azure DevOps Pages Batch API support, includes comprehensive pagination handling, and maintains consistency with established codebase patterns for schema validation, error handling, and testing.", 143 | "summary": "Successfully implemented comprehensive integration tests for list-wiki-pages feature. Created src/features/wikis/list-wiki-pages/feature.spec.int.ts with complete test coverage including: environment guard (AZDO_INT_TESTS === 'true'), happy path tests with real Azure DevOps API, error scenarios for invalid wikiId/projectId/organizationId, edge cases for empty wikis and deeply nested paths, data structure validation matching WikiPageSummary interface, performance tests for large wiki structures, path filtering tests, recursionLevel parameter testing with boundary values (1-50), and proper cleanup with comprehensive error handling. All tests follow existing integration test patterns from get-wikis and get-wiki-page features.", 144 | "completedAt": "2025-05-26T23:24:09.973Z" 145 | }, 146 | { 147 | "id": "68804833-8bdd-4dda-ab6b-dc22f540a0e3", 148 | "name": "Implement unit tests for coverage gaps", 149 | "description": "Create feature.spec.unit.ts with unit tests to fill coverage gaps not covered by integration tests. Use mocks only when absolutely necessary for scenarios that cannot be tested with real Azure DevOps API.", 150 | "status": "completed", 151 | "dependencies": [ 152 | { 153 | "taskId": "457c0d1b-3635-49d7-916e-0e9aeb4f370f" 154 | } 155 | ], 156 | "createdAt": "2025-05-26T16:18:03.641Z", 157 | "updatedAt": "2025-05-26T23:31:32.356Z", 158 | "relatedFiles": [ 159 | { 160 | "path": "src/features/wikis/list-wiki-pages/feature.spec.unit.ts", 161 | "type": "CREATE", 162 | "description": "Unit tests for list wiki pages feature" 163 | } 164 | ], 165 | "implementationGuide": "1. Create src/features/wikis/list-wiki-pages/feature.spec.unit.ts\n2. Mock WikiClient and its listWikiPages method only for scenarios not covered by integration tests\n3. Focus on edge cases and error scenarios that are difficult to reproduce with real API:\n - Network failures and timeouts\n - Malformed API responses\n - Edge cases in pagination logic\n - Input validation edge cases\n4. Follow patterns from src/features/wikis/get-wikis/feature.spec.unit.ts\n5. Use jest.mock() for WikiClient only when necessary\n6. Complement integration tests rather than duplicate coverage", 166 | "verificationCriteria": "Unit tests fill gaps in integration test coverage, minimal use of mocks, tests focus on scenarios that cannot be tested with real API, follows existing test patterns", 167 | "analysisResult": "Implement list_wiki_pages tool for Azure DevOps MCP server following GitHub issue #184 requirements. The implementation extends existing WikiClient with Azure DevOps Pages Batch API support, includes comprehensive pagination handling, and maintains consistency with established codebase patterns for schema validation, error handling, and testing.", 168 | "summary": "Successfully implemented comprehensive unit tests for list-wiki-pages feature. Created src/features/wikis/list-wiki-pages/feature.spec.unit.ts with 394 lines of focused unit tests that complement integration tests. Tests cover scenarios not easily testable with real API: network failures/timeouts, malformed API responses, edge cases in pagination logic, input validation edge cases, large datasets (10,000 pages), special characters in paths, boundary recursionLevel values, client creation failures, and data transformation scenarios. Used minimal mocking (only WikiClient when necessary) following patterns from existing unit tests. All tests focus on scenarios that cannot be reliably tested with real Azure DevOps API while avoiding duplication of integration test coverage.", 169 | "completedAt": "2025-05-26T23:31:32.355Z" 170 | } 171 | ] 172 | } ``` -------------------------------------------------------------------------------- /docs/tools/repositories.md: -------------------------------------------------------------------------------- ```markdown 1 | # Azure DevOps Repositories Tools 2 | 3 | This document describes the tools available for working with Azure DevOps Git repositories. 4 | 5 | ## get_repository_details 6 | 7 | Gets detailed information about a specific Git repository, including optional branch statistics and refs. 8 | 9 | ### Description 10 | 11 | The `get_repository_details` tool retrieves comprehensive information about a specific Git repository in Azure DevOps. It can optionally include branch statistics (ahead/behind counts, commit information) and repository refs (branches, tags). This is useful for tasks like branch management, policy configuration, and repository statistics tracking. 12 | 13 | ### Parameters 14 | 15 | ```json 16 | { 17 | "projectId": "MyProject", // Required: The ID or name of the project 18 | "repositoryId": "MyRepo", // Required: The ID or name of the repository 19 | "includeStatistics": true, // Optional: Whether to include branch statistics (default: false) 20 | "includeRefs": true, // Optional: Whether to include repository refs (default: false) 21 | "refFilter": "heads/", // Optional: Filter for refs (e.g., "heads/" or "tags/") 22 | "branchName": "main" // Optional: Name of specific branch to get statistics for 23 | } 24 | ``` 25 | 26 | | Parameter | Type | Required | Description | 27 | | --------- | ---- | -------- | ----------- | 28 | | `projectId` | string | Yes | The ID or name of the project containing the repository | 29 | | `repositoryId` | string | Yes | The ID or name of the repository to get details for | 30 | | `includeStatistics` | boolean | No | Whether to include branch statistics (default: false) | 31 | | `includeRefs` | boolean | No | Whether to include repository refs (default: false) | 32 | | `refFilter` | string | No | Optional filter for refs (e.g., "heads/" or "tags/") | 33 | | `branchName` | string | No | Name of specific branch to get statistics for (if includeStatistics is true) | 34 | 35 | ### Response 36 | 37 | The tool returns a `RepositoryDetails` object containing: 38 | 39 | - `repository`: The basic repository information (same as returned by `get_repository`) 40 | - `statistics` (optional): Branch statistics if requested 41 | - `refs` (optional): Repository refs if requested 42 | 43 | Example response: 44 | 45 | ```json 46 | { 47 | "repository": { 48 | "id": "repo-guid", 49 | "name": "MyRepository", 50 | "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/MyRepository", 51 | "project": { 52 | "id": "project-guid", 53 | "name": "MyProject", 54 | "url": "https://dev.azure.com/organization/_apis/projects/project-guid" 55 | }, 56 | "defaultBranch": "refs/heads/main", 57 | "size": 25478, 58 | "remoteUrl": "https://dev.azure.com/organization/MyProject/_git/MyRepository", 59 | "sshUrl": "[email protected]:v3/organization/MyProject/MyRepository", 60 | "webUrl": "https://dev.azure.com/organization/MyProject/_git/MyRepository" 61 | }, 62 | "statistics": { 63 | "branches": [ 64 | { 65 | "name": "refs/heads/main", 66 | "aheadCount": 0, 67 | "behindCount": 0, 68 | "isBaseVersion": true, 69 | "commit": { 70 | "commitId": "commit-guid", 71 | "author": { 72 | "name": "John Doe", 73 | "email": "[email protected]", 74 | "date": "2023-01-01T12:00:00Z" 75 | }, 76 | "committer": { 77 | "name": "John Doe", 78 | "email": "[email protected]", 79 | "date": "2023-01-01T12:00:00Z" 80 | }, 81 | "comment": "Initial commit" 82 | } 83 | } 84 | ] 85 | }, 86 | "refs": { 87 | "value": [ 88 | { 89 | "name": "refs/heads/main", 90 | "objectId": "commit-guid", 91 | "creator": { 92 | "displayName": "John Doe", 93 | "id": "user-guid" 94 | }, 95 | "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/repo-guid/refs/heads/main" 96 | } 97 | ], 98 | "count": 1 99 | } 100 | } 101 | ``` 102 | 103 | ### Error Handling 104 | 105 | The tool may throw the following errors: 106 | 107 | - General errors: If the API call fails or other unexpected errors occur 108 | - Authentication errors: If the authentication credentials are invalid or expired 109 | - Permission errors: If the authenticated user doesn't have permission to access the repository 110 | - ResourceNotFound errors: If the specified project or repository doesn't exist 111 | 112 | Error messages will be formatted as text and provide details about what went wrong. 113 | 114 | ### Example Usage 115 | 116 | ```typescript 117 | // Basic example - just repository info 118 | const repoDetails = await mcpClient.callTool('get_repository_details', { 119 | projectId: 'MyProject', 120 | repositoryId: 'MyRepo' 121 | }); 122 | console.log(repoDetails); 123 | 124 | // Example with branch statistics 125 | const repoWithStats = await mcpClient.callTool('get_repository_details', { 126 | projectId: 'MyProject', 127 | repositoryId: 'MyRepo', 128 | includeStatistics: true 129 | }); 130 | console.log(repoWithStats); 131 | 132 | // Example with refs filtered to branches 133 | const repoWithBranches = await mcpClient.callTool('get_repository_details', { 134 | projectId: 'MyProject', 135 | repositoryId: 'MyRepo', 136 | includeRefs: true, 137 | refFilter: 'heads/' 138 | }); 139 | console.log(repoWithBranches); 140 | 141 | // Example with all options 142 | const fullRepoDetails = await mcpClient.callTool('get_repository_details', { 143 | projectId: 'MyProject', 144 | repositoryId: 'MyRepo', 145 | includeStatistics: true, 146 | includeRefs: true, 147 | refFilter: 'heads/', 148 | branchName: 'main' 149 | }); 150 | console.log(fullRepoDetails); 151 | ``` 152 | 153 | ### Implementation Details 154 | 155 | This tool uses the Azure DevOps Node API's Git API to retrieve repository details: 156 | 157 | 1. It gets a connection to the Azure DevOps WebApi client 158 | 2. It calls the `getGitApi()` method to get a handle to the Git API 159 | 3. It retrieves the basic repository information using `getRepository()` 160 | 4. If requested, it retrieves branch statistics using `getBranches()` 161 | 5. If requested, it retrieves repository refs using `getRefs()` 162 | 6. The combined results are returned to the caller 163 | 164 | ## list_repositories 165 | 166 | Lists all Git repositories in a specific project. 167 | 168 | ### Description 169 | 170 | The `list_repositories` tool retrieves all Git repositories within a specified Azure DevOps project. This is useful for discovering which repositories are available for cloning, accessing files, or creating branches and pull requests. 171 | 172 | This tool uses the Azure DevOps WebApi client to interact with the Git API. 173 | 174 | ### Parameters 175 | 176 | ```json 177 | { 178 | "projectId": "MyProject", // Required: The ID or name of the project 179 | "includeLinks": true // Optional: Whether to include reference links 180 | } 181 | ``` 182 | 183 | | Parameter | Type | Required | Description | 184 | | -------------- | ------- | -------- | ------------------------------------------------------------ | 185 | | `projectId` | string | Yes | The ID or name of the project containing the repositories | 186 | | `includeLinks` | boolean | No | Whether to include reference links in the repository objects | 187 | 188 | ### Response 189 | 190 | The tool returns an array of `GitRepository` objects, each containing: 191 | 192 | - `id`: The unique identifier of the repository 193 | - `name`: The name of the repository 194 | - `url`: The URL of the repository 195 | - `project`: Object containing basic project information 196 | - `defaultBranch`: The default branch of the repository (e.g., "refs/heads/main") 197 | - `size`: The size of the repository 198 | - `remoteUrl`: The remote URL for cloning the repository 199 | - `sshUrl`: The SSH URL for cloning the repository 200 | - `webUrl`: The web URL for browsing the repository in browser 201 | - ... and potentially other repository properties 202 | 203 | Example response: 204 | 205 | ```json 206 | [ 207 | { 208 | "id": "repo-guid-1", 209 | "name": "FirstRepository", 210 | "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/FirstRepository", 211 | "project": { 212 | "id": "project-guid", 213 | "name": "MyProject", 214 | "url": "https://dev.azure.com/organization/_apis/projects/project-guid" 215 | }, 216 | "defaultBranch": "refs/heads/main", 217 | "size": 25478, 218 | "remoteUrl": "https://dev.azure.com/organization/MyProject/_git/FirstRepository", 219 | "sshUrl": "[email protected]:v3/organization/MyProject/FirstRepository", 220 | "webUrl": "https://dev.azure.com/organization/MyProject/_git/FirstRepository" 221 | }, 222 | { 223 | "id": "repo-guid-2", 224 | "name": "SecondRepository", 225 | "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/SecondRepository", 226 | "project": { 227 | "id": "project-guid", 228 | "name": "MyProject", 229 | "url": "https://dev.azure.com/organization/_apis/projects/project-guid" 230 | }, 231 | "defaultBranch": "refs/heads/main", 232 | "size": 15789, 233 | "remoteUrl": "https://dev.azure.com/organization/MyProject/_git/SecondRepository", 234 | "sshUrl": "[email protected]:v3/organization/MyProject/SecondRepository", 235 | "webUrl": "https://dev.azure.com/organization/MyProject/_git/SecondRepository" 236 | } 237 | ] 238 | ``` 239 | 240 | ### Error Handling 241 | 242 | The tool may throw the following errors: 243 | 244 | - General errors: If the API call fails or other unexpected errors occur 245 | - Authentication errors: If the authentication credentials are invalid or expired 246 | - Permission errors: If the authenticated user doesn't have permission to list repositories 247 | - ResourceNotFound errors: If the specified project doesn't exist 248 | 249 | Error messages will be formatted as text and provide details about what went wrong. 250 | 251 | ### Example Usage 252 | 253 | ```typescript 254 | // Basic example 255 | const repositories = await mcpClient.callTool('list_repositories', { 256 | projectId: 'MyProject', 257 | }); 258 | console.log(repositories); 259 | 260 | // Example with includeLinks parameter 261 | const repositoriesWithLinks = await mcpClient.callTool('list_repositories', { 262 | projectId: 'MyProject', 263 | includeLinks: true, 264 | }); 265 | console.log(repositoriesWithLinks); 266 | ``` 267 | 268 | ### Implementation Details 269 | 270 | This tool uses the Azure DevOps Node API's Git API to retrieve repositories: 271 | 272 | 1. It gets a connection to the Azure DevOps WebApi client 273 | 2. It calls the `getGitApi()` method to get a handle to the Git API 274 | 3. It then calls `getRepositories()` with the specified project ID and optional include links parameter 275 | 4. The results are returned directly to the caller 276 | 277 | ### Related Tools 278 | 279 | - `get_repository`: Get details of a specific repository 280 | - `get_repository_details`: Get detailed information about a repository including statistics and refs 281 | - `list_projects`: List all projects in the organization (to find project IDs) 282 | 283 | ## get_file_content 284 | 285 | Retrieves the content of a file or directory from a Git repository. 286 | 287 | ### Description 288 | 289 | The `get_file_content` tool allows you to access the contents of files and directories within a Git repository. This is useful for examining code, documentation, or other files stored in repositories without having to clone the entire repository. It supports fetching file content from the default branch or from specific branches, tags, or commits. 290 | 291 | ### Parameters 292 | 293 | ```json 294 | { 295 | "projectId": "MyProject", // Required: The ID or name of the project 296 | "repositoryId": "MyRepo", // Required: The ID or name of the repository 297 | "path": "/src/index.ts", // Required: The path to the file or directory 298 | "versionType": "branch", // Optional: The type of version (branch, tag, or commit) 299 | "version": "main" // Optional: The name of the branch/tag, or commit ID 300 | } 301 | ``` 302 | 303 | | Parameter | Type | Required | Description | 304 | | --------- | ---- | -------- | ----------- | 305 | | `projectId` | string | Yes | The ID or name of the project containing the repository | 306 | | `repositoryId` | string | Yes | The ID or name of the repository | 307 | | `path` | string | Yes | The path to the file or directory (starting with "/") | 308 | | `versionType` | enum | No | The type of version: "branch", "tag", or "commit" (GitVersionType) | 309 | | `version` | string | No | The name of the branch/tag, or the commit ID | 310 | 311 | ### Response 312 | 313 | The tool returns a `FileContentResponse` object containing: 314 | 315 | - `content`: The content of the file as a string, or a JSON string of items for directories 316 | - `isDirectory`: Boolean indicating whether the path refers to a directory 317 | 318 | Example response for a file: 319 | 320 | ```json 321 | { 322 | "content": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'app-root',\n templateUrl: './app.component.html',\n styleUrls: ['./app.component.css']\n})\nexport class AppComponent {\n title = 'My App';\n}\n", 323 | "isDirectory": false 324 | } 325 | ``` 326 | 327 | Example response for a directory: 328 | 329 | ```json 330 | { 331 | "content": "[{\"objectId\":\"c7be24d3\",\"gitObjectType\":\"blob\",\"commitId\":\"d5b8e757\",\"path\":\"/src/app/app.component.ts\",\"contentMetadata\":{\"fileName\":\"app.component.ts\"}},{\"objectId\":\"a8c2e5f1\",\"gitObjectType\":\"blob\",\"commitId\":\"d5b8e757\",\"path\":\"/src/app/app.module.ts\",\"contentMetadata\":{\"fileName\":\"app.module.ts\"}}]", 332 | "isDirectory": true 333 | } 334 | ``` 335 | 336 | ### Error Handling 337 | 338 | The tool may throw the following errors: 339 | 340 | - General errors: If the API call fails or other unexpected errors occur 341 | - Authentication errors: If the authentication credentials are invalid or expired 342 | - Permission errors: If the authenticated user doesn't have permission to access the repository 343 | - ResourceNotFound errors: If the specified project, repository, or path doesn't exist 344 | 345 | Error messages will be formatted as text and provide details about what went wrong. 346 | 347 | ### Example Usage 348 | 349 | ```typescript 350 | // Basic example - get file from default branch 351 | const fileContent = await mcpClient.callTool('get_file_content', { 352 | projectId: 'MyProject', 353 | repositoryId: 'MyRepo', 354 | path: '/src/index.ts' 355 | }); 356 | console.log(fileContent.content); 357 | 358 | // Get directory content 359 | const directoryContent = await mcpClient.callTool('get_file_content', { 360 | projectId: 'MyProject', 361 | repositoryId: 'MyRepo', 362 | path: '/src' 363 | }); 364 | if (directoryContent.isDirectory) { 365 | const items = JSON.parse(directoryContent.content); 366 | console.log(`Directory contains ${items.length} items`); 367 | } 368 | 369 | // Get file from specific branch 370 | const branchFileContent = await mcpClient.callTool('get_file_content', { 371 | projectId: 'MyProject', 372 | repositoryId: 'MyRepo', 373 | path: '/src/index.ts', 374 | versionType: 'branch', 375 | version: 'feature/new-ui' 376 | }); 377 | console.log(branchFileContent.content); 378 | 379 | // Get file from specific commit 380 | const commitFileContent = await mcpClient.callTool('get_file_content', { 381 | projectId: 'MyProject', 382 | repositoryId: 'MyRepo', 383 | path: '/src/index.ts', 384 | versionType: 'commit', 385 | version: 'a1b2c3d4e5f6g7h8i9j0' 386 | }); 387 | console.log(commitFileContent.content); 388 | ``` 389 | 390 | ### Implementation Details 391 | 392 | This tool uses the Azure DevOps Node API's Git API to retrieve file or directory content: 393 | 394 | 1. It gets a connection to the Azure DevOps WebApi client 395 | 2. It calls the `getGitApi()` method to get a handle to the Git API 396 | 3. It determines if the path is a file or directory by attempting to fetch items 397 | 4. For directories, it returns the list of items as a JSON string 398 | 5. For files, it fetches the file content and returns it as a string 399 | 6. The results are wrapped in a `FileContentResponse` object with the appropriate `isDirectory` flag 400 | 401 | ### Resource URI Access 402 | 403 | In addition to using this tool, file content can also be accessed via resource URIs with the following patterns: 404 | 405 | - Default branch: `ado://{organization}/{project}/{repo}/contents/{path}` 406 | - Specific branch: `ado://{organization}/{project}/{repo}/branches/{branch}/contents/{path}` 407 | - Specific commit: `ado://{organization}/{project}/{repo}/commits/{commit}/contents/{path}` 408 | - Specific tag: `ado://{organization}/{project}/{repo}/tags/{tag}/contents/{path}` 409 | - Pull request: `ado://{organization}/{project}/{repo}/pullrequests/{prId}/contents/{path}` 410 | 411 | ### Related Tools 412 | 413 | - `list_repositories`: List all repositories in a project 414 | - `get_repository`: Get details of a specific repository 415 | - `get_repository_details`: Get detailed information about a repository including statistics and refs 416 | - `search_code`: Search for code across repositories in a project 417 | 418 | ## get_all_repositories_tree 419 | 420 | Displays a hierarchical tree view of files and directories across multiple Azure DevOps repositories within a project, based on their default branches. 421 | 422 | ### Description 423 | 424 | The `get_all_repositories_tree` tool provides a broad overview of file and directory structure across multiple repositories in a project. It uses a tree-like structure similar to the Unix `tree` command, with each repository's tree displayed sequentially. 425 | 426 | Key features: 427 | - Views multiple repositories at once 428 | - Filter repositories by name pattern 429 | - Filter files by pattern 430 | - Control depth to balance performance and detail 431 | - Shows directories and files in a hierarchical view 432 | - Provides statistics (count of files and directories) 433 | - Works with the default branch of each repository 434 | - Handles errors gracefully 435 | 436 | ### Parameters 437 | 438 | ```json 439 | { 440 | "organizationId": "MyOrg", 441 | "projectId": "MyProject", 442 | "repositoryPattern": "API*", 443 | "depth": 0, 444 | "pattern": "*.yaml" 445 | } 446 | ``` 447 | 448 | - `organizationId` (string, required): The ID or name of the Azure DevOps organization. 449 | - `projectId` (string, required): The ID or name of the project containing the repositories. 450 | - `repositoryPattern` (string, optional): Pattern to filter repositories by name (PowerShell wildcard). 451 | - `depth` (number, optional, default: 0): Maximum depth to traverse in each repository's file hierarchy. Use 0 for unlimited depth (more efficient server-side recursion), or a specific number (1-10) for limited depth. 452 | - `pattern` (string, optional): Pattern to filter files by name (PowerShell wildcard). Note: Directories are always shown regardless of this filter. 453 | 454 | ### Response 455 | 456 | The response is a formatted ASCII tree showing the file and directory structure of each repository: 457 | 458 | ``` 459 | Repo-API-1/ 460 | |-- src/ 461 | | |-- config.yaml 462 | | `-- utils/ 463 | `-- deploy.yaml 464 | 1 directory, 2 files 465 | 466 | Repo-API-Gateway/ 467 | |-- charts/ 468 | | `-- values.yaml 469 | `-- README.md 470 | 1 directory, 2 files 471 | 472 | Repo-Data-Service/ 473 | (Repository is empty or default branch not found) 474 | 0 directories, 0 files 475 | ``` 476 | 477 | ### Examples 478 | 479 | #### Basic Example - View All Repositories with Maximum Depth 480 | 481 | ```javascript 482 | const result = await mcpClient.callTool('get_all_repositories_tree', { 483 | organizationId: 'MyOrg', 484 | projectId: 'MyProject' 485 | }); 486 | console.log(result); 487 | ``` 488 | 489 | #### Filter Repositories by Name Pattern 490 | 491 | ```javascript 492 | const result = await mcpClient.callTool('get_all_repositories_tree', { 493 | organizationId: 'MyOrg', 494 | projectId: 'MyProject', 495 | repositoryPattern: 'API*' 496 | }); 497 | console.log(result); 498 | ``` 499 | 500 | #### Limited Depth and File Pattern Filter 501 | 502 | ```javascript 503 | const result = await mcpClient.callTool('get_all_repositories_tree', { 504 | organizationId: 'MyOrg', 505 | projectId: 'MyProject', 506 | depth: 1, // Only one level deep 507 | pattern: '*.yaml' 508 | }); 509 | console.log(result); 510 | ``` 511 | 512 | ### Performance Considerations 513 | 514 | - For maximum depth (depth=0), the tool uses server-side recursion (VersionControlRecursionType.Full) which is more efficient for retrieving deep directory structures. 515 | - For limited depth (depth=1 to 10), the tool uses client-side recursion which is better for controlled exploration. 516 | - When viewing very large repositories, consider using a limited depth or file pattern to reduce response time. 517 | 518 | ### Related Tools 519 | 520 | - `list_repositories`: Lists all repositories in a project (summary only) 521 | - `get_repository_details`: Gets detailed info about a single repository 522 | - `get_repository_tree`: Explores structure within a single repository (more detailed) 523 | - `get_file_content`: Gets content of a specific file 524 | ``` -------------------------------------------------------------------------------- /shrimp-rules.md: -------------------------------------------------------------------------------- ```markdown 1 | # Development Guidelines for AI Agents - mcp-server-azure-devops 2 | 3 | **This document is exclusively for AI Agent operational use. DO NOT include general development knowledge.** 4 | 5 | ## 1. Project Overview 6 | 7 | ### Purpose 8 | - This project, `@tiberriver256/mcp-server-azure-devops`, is an MCP (Model Context Protocol) server. 9 | - Its primary function is to provide tools for interacting with Azure DevOps services. 10 | 11 | ### Technology Stack 12 | - **Core**: TypeScript, Node.js 13 | - **Key Libraries**: 14 | - `@modelcontextprotocol/sdk`: For MCP server and type definitions. 15 | - `azure-devops-node-api`: For interacting with Azure DevOps. 16 | - `@azure/identity`: For Azure authentication. 17 | - `zod`: For schema definition and validation. 18 | - `zod-to-json-schema`: For converting Zod schemas to JSON schemas for MCP tools. 19 | - **Testing**: Jest (for unit, integration, and e2e tests). 20 | - **Linting/Formatting**: ESLint, Prettier. 21 | - **Environment Management**: `dotenv`. 22 | 23 | ### Core Functionality 24 | - Provides MCP tools to interact with Azure DevOps features including, but not limited to: 25 | - Organizations 26 | - Projects (list, get, get details) 27 | - Repositories (list, get, get content, get tree) 28 | - Work Items (list, get, create, update, manage links) 29 | - Pull Requests (list, get, create, update, add/get comments) 30 | - Pipelines (list, trigger) 31 | - Search (code, wiki, work items) 32 | - Users (get current user) 33 | - Wikis (list, get page, create, update page) 34 | 35 | ## 2. Project Architecture 36 | 37 | ### Main Directory Structure 38 | - **`./` (Root)**: 39 | - [`package.json`](package.json:0): Project metadata, dependencies, and NPM scripts. **REFER** to this for available commands and dependencies. 40 | - [`tsconfig.json`](tsconfig.json:0): TypeScript compiler configuration. **ADHERE** to its settings. 41 | - [`.eslintrc.json`](.eslintrc.json:0): ESLint configuration for code linting. **ADHERE** to its rules. 42 | - [`README.md`](README.md:0): General project information. 43 | - `setup_env.sh`: Shell script for environment setup. 44 | - `CHANGELOG.md` (if present): Tracks changes between versions. 45 | - **`src/`**: Contains all TypeScript source code. 46 | - **`src/features/`**: Core application logic. Each subdirectory represents a distinct Azure DevOps feature set (e.g., `projects`, `repositories`). 47 | - `src/features/[feature-name]/`: Contains all files related to a specific feature. 48 | - `src/features/[feature-name]/index.ts`: Main export file for the feature. Exports request handlers (`isFeatureRequest`, `handleFeatureRequest`), tool definitions array (`featureTools`), schemas, types, and individual tool implementation functions. **MODIFY** this file when adding new tools or functionalities to the feature. 49 | - `src/features/[feature-name]/schemas.ts`: Defines Zod input/output schemas for all tools within this feature. **DEFINE** new schemas here. 50 | - `src/features/[feature-name]/tool-definitions.ts`: Defines MCP tools for the feature using `@modelcontextprotocol/sdk` and `zodToJsonSchema`. **ADD** new tool definitions here. 51 | - `src/features/[feature-name]/types.ts`: Contains TypeScript type definitions specific to this feature. **DEFINE** feature-specific types here. 52 | - `src/features/[feature-name]/[tool-name]/`: Subdirectory for a specific tool/action within the feature. 53 | - `src/features/[feature-name]/[tool-name]/feature.ts`: Implements the core logic for the specific tool (e.g., API calls, data transformation). **IMPLEMENT** tool logic here. 54 | - `src/features/[feature-name]/[tool-name]/index.ts`: Exports the `feature.ts` logic and potentially tool-specific schemas/types if not in the parent feature files. 55 | - `src/features/[feature-name]/[tool-name]/schema.ts` (optional, often re-exports from feature-level `schemas.ts`): Defines or re-exports Zod schemas for this specific tool. 56 | - `src/features/organizations/`, `src/features/pipelines/`, `src/features/projects/`, `src/features/pull-requests/`, `src/features/repositories/`, `src/features/search/`, `src/features/users/`, `src/features/wikis/`, `src/features/work-items/`: Existing feature modules. **REFER** to these for patterns. 57 | - **`src/shared/`**: Contains shared modules and utilities used across features. 58 | - `src/shared/api/`: Azure DevOps API client setup (e.g., `client.ts`). 59 | - `src/shared/auth/`: Authentication logic for Azure DevOps (e.g., `auth-factory.ts`, `client-factory.ts`). **USE** these factories; DO NOT implement custom auth. 60 | - `src/shared/config/`: Configuration management (e.g., `version.ts`). 61 | - `src/shared/errors/`: Shared error handling classes and utilities (e.g., `azure-devops-errors.ts`, `handle-request-error.ts`). **USE** these for consistent error handling. 62 | - `src/shared/types/`: Global TypeScript type definitions (e.g., `config.ts`, `request-handler.ts`, `tool-definition.ts`). 63 | - **`src/utils/`**: General utility functions. 64 | - `src/utils/environment.ts`: Provides default values for environment variables (e.g., `defaultProject`, `defaultOrg`). 65 | - [`src/index.ts`](src/index.ts:1): Main application entry point. Handles environment variable loading and server initialization. **Exports** server components. 66 | - [`src/server.ts`](src/server.ts:1): MCP server core logic. Initializes the server, registers all tool handlers from features, and sets up request routing. **MODIFY** this file to register new feature modules (their `isFeatureRequest` and `handleFeatureRequest` handlers, and `featureTools` array). 67 | - **`docs/`**: Currently empty. If documentation is added, **MAINTAIN** it in sync with code changes. 68 | - **`project-management/`**: Contains project planning and design documents. **REFER** to `architecture-guide.md` for high-level design. 69 | - **`tests/`**: Directory for global test setup or utilities if any. Most tests are co-located with source files (e.g., `*.spec.unit.ts`, `*.spec.int.ts`, `*.spec.e2e.ts`). 70 | 71 | ## 3. Code Standards 72 | 73 | ### Naming Conventions 74 | - **Files and Directories**: USE kebab-case (e.g., `my-feature`, `get-project-details.ts`). 75 | - **Variables and Functions**: USE camelCase (e.g., `projectId`, `listProjects`). 76 | - **Classes, Interfaces, Enums, Types**: USE PascalCase (e.g., `AzureDevOpsClient`, `TeamProject`, `AuthenticationMethod`). 77 | - **Test Files**: 78 | - Unit tests: `[filename].spec.unit.ts` (e.g., [`get-project.spec.unit.ts`](src/features/projects/get-project/feature.spec.unit.ts:0)). 79 | - Integration tests: `[filename].spec.int.ts` (e.g., [`get-project.spec.int.ts`](src/features/projects/get-project/feature.spec.int.ts:0)). 80 | - E2E tests: `[filename].spec.e2e.ts` (e.g., [`server.spec.e2e.ts`](src/server.spec.e2e.ts:0)). 81 | - **Feature Modules**: Place under `src/features/[feature-name]/`. 82 | - **Tool Logic**: Place in `src/features/[feature-name]/[tool-name]/feature.ts`. 83 | - **Schemas**: Define in `src/features/[feature-name]/schemas.ts`. 84 | - **Tool Definitions (MCP)**: Define in `src/features/[feature-name]/tool-definitions.ts`. 85 | - **Types**: Feature-specific types in `src/features/[feature-name]/types.ts`; global types in `src/shared/types/`. 86 | 87 | ### Formatting 88 | - **Prettier**: Enforced via ESLint and lint-staged. 89 | - **Rule**: ADHERE to formatting rules defined by Prettier (implicitly via [`.eslintrc.json`](.eslintrc.json:1) which extends `prettier`). 90 | - **Action**: ALWAYS run `npm run format` (or rely on lint-staged) before committing. 91 | 92 | ### Linting 93 | - **ESLint**: Configuration in [`.eslintrc.json`](.eslintrc.json:1). 94 | - **Rule**: ADHERE to linting rules. 95 | - **Action**: ALWAYS run `npm run lint` (or `npm run lint:fix`) and RESOLVE all errors/warnings before committing. 96 | - **Key Lint Rules (from [`.eslintrc.json`](.eslintrc.json:1))**: 97 | - `prettier/prettier: "error"` (Prettier violations are ESLint errors). 98 | - `@typescript-eslint/no-explicit-any: "warn"` (Avoid `any` where possible; it's "off" for `*.spec.unit.ts` and `tests/**/*.ts`). 99 | - `@typescript-eslint/no-unused-vars: ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }]` (No unused variables, allowing `_` prefix for ignored ones). 100 | 101 | ### Comments 102 | - **TSDoc**: USE TSDoc for documenting public functions, classes, interfaces, and types (e.g., `/** ... */`). 103 | - **Inline Comments**: For complex logic blocks, ADD inline comments (`// ...`) explaining the purpose. 104 | 105 | ### TypeScript Specifics (from [`tsconfig.json`](tsconfig.json:1)) 106 | - `strict: true`: ADHERE to strict mode. 107 | - `noImplicitAny: true`: DO NOT use implicit `any`. Explicitly type all entities. 108 | - `noUnusedLocals: true`, `noUnusedParameters: true`: ENSURE no unused local variables or parameters. 109 | - `moduleResolution: "Node16"`: Be aware of Node.js ESM module resolution specifics. 110 | - `paths: { "@/*": ["src/*"] }`: USE path alias `@/*` for imports from `src/`. 111 | 112 | ## 4. Functionality Implementation Standards 113 | 114 | ### Adding a New Tool/Functionality to an Existing Feature 115 | 1. **Identify Feature**: Determine the relevant feature directory in `src/features/[feature-name]/`. 116 | 2. **Create Tool Directory**: Inside the feature directory, CREATE a new subdirectory for your tool, e.g., `src/features/[feature-name]/[new-tool-name]/`. 117 | 3. **Implement Logic**: CREATE `[new-tool-name]/feature.ts`. Implement the core Azure DevOps interaction logic here. 118 | - USE `getClient()` from `src/shared/api/client.ts` or `getConnection()` from [`src/server.ts`](src/server.ts:1) to get `WebApi`. 119 | - USE error handling from `src/shared/errors/`. 120 | 4. **Define Schema**: 121 | - ADD Zod schema for the tool's input to `src/features/[feature-name]/schemas.ts`. 122 | - EXPORT it. 123 | - If needed, CREATE `[new-tool-name]/schema.ts` and re-export the specific schema from the feature-level `schemas.ts`. 124 | 5. **Define MCP Tool**: 125 | - ADD tool definition to `src/features/[feature-name]/tool-definitions.ts`. 126 | - Import the Zod schema and use `zodToJsonSchema` for `inputSchema`. 127 | - Ensure `name` matches the intended tool name for MCP. 128 | 6. **Update Feature Index**: 129 | - In `src/features/[feature-name]/index.ts`: 130 | - EXPORT your new tool's logic function (from `[new-tool-name]/feature.ts` or its `index.ts`). 131 | - ADD your new tool's name to the `includes()` check in `isFeatureRequest` function. 132 | - ADD a `case` for your new tool in the `handleFeatureRequest` function to call your logic. Parse arguments using the Zod schema. 133 | 7. **Update Server**: No changes usually needed in [`src/server.ts`](src/server.ts:1) if the feature module is already registered. The feature's `tool-definitions.ts` and `handleFeatureRequest` will be picked up. 134 | 8. **Add Tests**: CREATE `[new-tool-name]/feature.spec.unit.ts` and `[new-tool-name]/feature.spec.int.ts`. 135 | 136 | ### Adding a New Feature Module (e.g., for a new Azure DevOps Service Area) 137 | 1. **Create Feature Directory**: CREATE `src/features/[new-feature-module-name]/`. 138 | 2. **Implement Tools**: Follow "Adding a New Tool" steps above for each tool within this new feature module. This includes creating `schemas.ts`, `tool-definitions.ts`, `types.ts` (if needed), and subdirectories for each tool's `feature.ts`. 139 | 3. **Create Feature Index**: CREATE `src/features/[new-feature-module-name]/index.ts`. 140 | - EXPORT all schemas, types, tool logic functions. 141 | - EXPORT the `[new-feature-module-name]Tools` array from `tool-definitions.ts`. 142 | - CREATE and EXPORT `is[NewFeatureModuleName]Request` (e.g., `isMyNewFeatureRequest`) type guard. 143 | - CREATE and EXPORT `handle[NewFeatureModuleName]Request` (e.g., `handleMyNewFeatureRequest`) request handler function. 144 | 4. **Register Feature in Server**: 145 | - In [`src/server.ts`](src/server.ts:1): 146 | - IMPORT `[new-feature-module-name]Tools`, `is[NewFeatureModuleName]Request`, and `handle[NewFeatureModuleName]Request` from your new feature's `index.ts`. 147 | - ADD `...[new-feature-module-name]Tools` to the `tools` array in the `ListToolsRequestSchema` handler. 148 | - ADD an `if (is[NewFeatureModuleName]Request(request)) { return await handle[NewFeatureModuleName]Request(connection, request); }` block in the `CallToolRequestSchema` handler. 149 | 5. **Add Tests**: Ensure comprehensive tests for the new feature module. 150 | 151 | ## 5. Framework/Plugin/Third-party Library Usage Standards 152 | 153 | - **`@modelcontextprotocol/sdk`**: 154 | - USE `Server` class from `@modelcontextprotocol/sdk/server/index.js` to create the MCP server ([`src/server.ts`](src/server.ts:1)). 155 | - USE `StdioServerTransport` for transport ([`src/index.ts`](src/index.ts:1)). 156 | - USE schema types like `CallToolRequestSchema` from `@modelcontextprotocol/sdk/types.js`. 157 | - DEFINE tools as `ToolDefinition[]` (see `src/shared/types/tool-definition.ts` and feature `tool-definitions.ts` files). 158 | - **`azure-devops-node-api`**: 159 | - This is the primary library for Azure DevOps interactions. 160 | - OBTAIN `WebApi` connection object via `getConnection()` from [`src/server.ts`](src/server.ts:1) or `AzureDevOpsClient` from `src/shared/auth/client-factory.ts`. 161 | - USE specific APIs from the connection (e.g., `connection.getCoreApi()`, `connection.getWorkItemTrackingApi()`). 162 | - **`@azure/identity`**: 163 | - Used for Azure authentication (e.g., `DefaultAzureCredential`). 164 | - Primarily abstracted via `AzureDevOpsClient` in `src/shared/auth/`. PREFER using this abstraction. 165 | - **`zod`**: 166 | - USE for all input/output schema definition and validation. 167 | - DEFINE schemas in `src/features/[feature-name]/schemas.ts`. 168 | - USE `z.object({...})`, `z.string()`, `z.boolean()`, etc. 169 | - USE `.optional()`, `.default()`, `.describe()` for schema fields. 170 | - **`zod-to-json-schema`**: 171 | - USE to convert Zod schemas to JSON schemas for MCP `inputSchema` in `tool-definitions.ts`. 172 | - **`dotenv`**: 173 | - Used in [`src/index.ts`](src/index.ts:1) to load environment variables from a `.env` file. 174 | - **Jest**: 175 | - Test files co-located with source files or in feature-specific `__test__` directories. 176 | - Configuration in `jest.unit.config.js`, `jest.int.config.js`, `jest.e2e.config.js`. 177 | - **ESLint/Prettier**: See "Code Standards". 178 | 179 | ## 6. Workflow Standards 180 | 181 | ### Development Workflow 182 | 1. **Branch**: CREATE or CHECKOUT a feature/bugfix branch from `main` (or relevant development branch). 183 | 2. **Implement**: WRITE code and corresponding tests. 184 | 3. **Test**: 185 | - RUN unit tests: `npm run test:unit`. 186 | - RUN integration tests: `npm run test:int`. 187 | - RUN E2E tests: `npm run test:e2e`. 188 | - Or run all tests: `npm test`. 189 | - ENSURE all tests pass. 190 | 4. **Lint & Format**: 191 | - RUN `npm run lint` (or `npm run lint:fix`). RESOLVE all issues. 192 | - RUN `npm run format`. 193 | 5. **Commit**: 194 | - USE Conventional Commits specification (e.g., `feat: ...`, `fix: ...`). 195 | - RECOMMENDED: Use `npm run commit` (uses `cz-conventional-changelog`) for guided commit messages. 196 | 6. **Pull Request**: PUSH branch and CREATE Pull Request against `main` (or relevant development branch). 197 | 198 | ### NPM Scripts (from [`package.json`](package.json:1)) 199 | - `build`: `tsc` (Compiles TypeScript to `dist/`). 200 | - `dev`: `ts-node-dev --respawn --transpile-only src/index.ts` (Runs server in development with auto-restart). 201 | - `start`: `node dist/index.js` (Runs compiled server). 202 | - `inspector`: `npm run build && npx @modelcontextprotocol/inspector node dist/index.js` (Runs server with MCP Inspector). 203 | - `test:unit`, `test:int`, `test:e2e`, `test`: Run respective test suites. 204 | - `lint`, `lint:fix`: Run ESLint. 205 | - `format`: Run Prettier. 206 | - `prepare`: `husky install` (Sets up Git hooks). 207 | - `commit`: `cz` (Interactive commitizen). 208 | 209 | ### CI/CD 210 | - No explicit CI/CD pipeline configuration files (e.g., `azure-pipelines.yml`, `.github/workflows/`) were found in the file listing. If added, **REFER** to them. 211 | 212 | ## 7. Key File Interaction Standards 213 | 214 | - **Adding/Modifying a Tool**: 215 | - TOUCH `src/features/[feature-name]/[tool-name]/feature.ts` (logic). 216 | - TOUCH `src/features/[feature-name]/schemas.ts` (Zod schema). 217 | - TOUCH `src/features/[feature-name]/tool-definitions.ts` (MCP tool definition). 218 | - TOUCH `src/features/[feature-name]/index.ts` (export logic, update request handler and guard). 219 | - TOUCH corresponding `*.spec.unit.ts` and `*.spec.int.ts` files. 220 | - **Adding a New Feature Module**: 221 | - CREATE files within `src/features/[new-feature-module-name]/` as per "Functionality Implementation Standards". 222 | - MODIFY [`src/server.ts`](src/server.ts:1) to import and register the new feature module's tools and handlers. 223 | - **Configuration Changes**: 224 | - Environment variables: Managed via `.env` file (loaded by `dotenv` in [`src/index.ts`](src/index.ts:1)). 225 | - TypeScript config: [`tsconfig.json`](tsconfig.json:1). 226 | - Linting config: [`.eslintrc.json`](.eslintrc.json:1). 227 | - **Dependency Management**: 228 | - MODIFY [`package.json`](package.json:1) to add/update dependencies. 229 | - RUN `npm install` or `npm ci`. 230 | - **Documentation**: 231 | - `docs/` directory is currently empty. If project documentation is added (e.g., `docs/feature-x.md`), **UPDATE** it when the corresponding feature `src/features/feature-x/` is modified. 232 | - [`README.md`](README.md:0): UPDATE for significant high-level changes. 233 | 234 | ## 8. AI Decision-making Standards 235 | 236 | ### When Adding a New Azure DevOps API Interaction: 237 | 1. **Goal**: To expose a new Azure DevOps API endpoint as an MCP tool. 238 | 2. **Decision: New or Existing Feature?** 239 | - IF the API relates to an existing service area (e.g., adding a new work item query type to `work-items` feature), MODIFY the existing feature module. 240 | - ELSE (e.g., interacting with Azure DevOps Audit Logs, a new service area), CREATE a new feature module. (See "Functionality Implementation Standards"). 241 | 3. **Pattern Adherence**: 242 | - FOLLOW the established pattern: 243 | - `src/features/[feature]/[tool]/feature.ts` for logic. 244 | - `src/features/[feature]/schemas.ts` for Zod schemas. 245 | - `src/features/[feature]/tool-definitions.ts` for MCP tool definitions. 246 | - `src/features/[feature]/index.ts` for feature-level exports, request guard (`isFeatureRequest`), and request handler (`handleFeatureRequest`). 247 | - **Example**: To add `get_pipeline_run_logs` to `pipelines` feature: 248 | - CREATE `src/features/pipelines/get-pipeline-run-logs/feature.ts`. 249 | - ADD `GetPipelineRunLogsSchema` to `src/features/pipelines/schemas.ts`. 250 | - ADD `get_pipeline_run_logs` definition to `src/features/pipelines/tool-definitions.ts`. 251 | - UPDATE `src/features/pipelines/index.ts` to export the new function, add to `isPipelinesRequest`, and handle in `handlePipelinesRequest`. 252 | 4. **Error Handling**: 253 | - ALWAYS use custom error classes from `src/shared/errors/azure-devops-errors.ts` (e.g., `AzureDevOpsResourceNotFoundError`). 254 | - WRAP external API calls in try/catch blocks. 255 | - USE `handleResponseError` from `src/shared/errors/handle-request-error.ts` in the top-level request handler in [`src/server.ts`](src/server.ts:1) (already done for existing features). Feature-specific handlers should re-throw custom errors. 256 | 5. **Testing**: 257 | - ALWAYS write unit tests for the new logic in `[tool-name]/feature.spec.unit.ts`. 258 | - ALWAYS write integration tests (NEVER mocking anything) in `[tool-name]/feature.spec.int.ts`. Prefer integration tests over unit tests. 259 | 260 | ### When Modifying Existing Functionality: 261 | 1. **Identify Impact**: DETERMINE all files affected by the change (logic, schemas, tool definitions, tests, potentially documentation). 262 | 2. **Maintain Consistency**: ENSURE changes are consistent with existing patterns within that feature module. 263 | 3. **Update Tests**: MODIFY existing tests or ADD new ones to cover the changes. ENSURE all tests pass. 264 | 4. **Version Bumping**: For significant changes, consider if a version bump in [`package.json`](package.json:1) is warranted (usually handled by `release-please`). 265 | 266 | ## 9. Prohibited Actions 267 | 268 | - **DO NOT** include general development knowledge or LLM-known facts in this `shrimp-rules.md` document. This document is for project-specific operational rules for AI. 269 | - **DO NOT** explain project functionality in terms of *what it does for an end-user*. Focus on *how to modify or add to it* for an AI developer. 270 | - **DO NOT** use `any` type implicitly. [`tsconfig.json`](tsconfig.json:1) enforces `noImplicitAny: true`. [`.eslintrc.json`](.eslintrc.json:1) warns on explicit `any` (`@typescript-eslint/no-explicit-any: "warn"`), except in unit tests. MINIMIZE explicit `any`. 271 | - **DO NOT** bypass linting (`npm run lint`) or formatting (`npm run format`) checks. Code MUST adhere to these standards. 272 | - **DO NOT** commit code that fails tests (`npm test`). 273 | - **DO NOT** implement custom Azure DevOps authentication logic. USE the provided `AzureDevOpsClient` from `src/shared/auth/`. 274 | - **DO NOT** hardcode configuration values (like PATs, Org URLs, Project IDs). These should come from environment variables (see [`src/index.ts`](src/index.ts:1) `getConfig` and `src/utils/environment.ts`). 275 | - **DO NOT** directly call Azure DevOps REST APIs if a corresponding function already exists in the `azure-devops-node-api` library or in shared project code (e.g., `src/shared/api/`). 276 | - **DO NOT** modify files in `dist/` directory directly. This directory is auto-generated by `npm run build`. 277 | - **DO NOT** ignore the `project-management/` directory for understanding architectural guidelines, but DO NOT replicate its content here. 278 | - **DO NOT** use mocks within integration tests. ``` -------------------------------------------------------------------------------- /src/clients/azure-devops.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axios, { AxiosError } from 'axios'; 2 | import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity'; 3 | import { 4 | AzureDevOpsError, 5 | AzureDevOpsResourceNotFoundError, 6 | AzureDevOpsValidationError, 7 | AzureDevOpsPermissionError, 8 | } from '../shared/errors'; 9 | import { defaultOrg, defaultProject } from '../utils/environment'; 10 | 11 | interface AzureDevOpsApiErrorResponse { 12 | message?: string; 13 | typeKey?: string; 14 | errorCode?: number; 15 | eventId?: number; 16 | } 17 | 18 | interface ClientOptions { 19 | organizationId?: string; 20 | } 21 | 22 | interface WikiCreateParameters { 23 | name: string; 24 | projectId: string; 25 | type: 'projectWiki' | 'codeWiki'; 26 | repositoryId?: string; 27 | mappedPath?: string; 28 | version?: { 29 | version: string; 30 | versionType?: 'branch' | 'tag' | 'commit'; 31 | }; 32 | } 33 | 34 | interface WikiPageContent { 35 | content: string; 36 | } 37 | 38 | export interface WikiPageSummary { 39 | id: number; 40 | path: string; 41 | url?: string; 42 | order?: number; 43 | } 44 | 45 | interface WikiPagesBatchRequest { 46 | top: number; 47 | continuationToken?: string; 48 | } 49 | 50 | interface WikiPagesBatchResponse { 51 | value: WikiPageSummary[]; 52 | continuationToken?: string; 53 | } 54 | 55 | interface PageUpdateOptions { 56 | comment?: string; 57 | versionDescriptor?: { 58 | version?: string; 59 | }; 60 | } 61 | 62 | export class WikiClient { 63 | private baseUrl: string; 64 | private organizationId: string; 65 | 66 | constructor(organizationId: string) { 67 | this.organizationId = organizationId || defaultOrg; 68 | this.baseUrl = `https://dev.azure.com/${this.organizationId}`; 69 | } 70 | 71 | /** 72 | * Gets a project's ID from its name or verifies a project ID 73 | * @param projectNameOrId - Project name or ID 74 | * @returns The project ID 75 | */ 76 | private async getProjectId(projectNameOrId: string): Promise<string> { 77 | try { 78 | // Try to get project details using the provided name or ID 79 | const url = `${this.baseUrl}/_apis/projects/${projectNameOrId}`; 80 | const authHeader = await getAuthorizationHeader(); 81 | 82 | const response = await axios.get(url, { 83 | params: { 84 | 'api-version': '7.1', 85 | }, 86 | headers: { 87 | Authorization: authHeader, 88 | 'Content-Type': 'application/json', 89 | }, 90 | }); 91 | 92 | // Return the project ID from the response 93 | return response.data.id; 94 | } catch (error) { 95 | const axiosError = error as AxiosError; 96 | 97 | if (axiosError.response) { 98 | const status = axiosError.response.status; 99 | const errorMessage = 100 | typeof axiosError.response.data === 'object' && 101 | axiosError.response.data 102 | ? (axiosError.response.data as AzureDevOpsApiErrorResponse) 103 | .message || axiosError.message 104 | : axiosError.message; 105 | 106 | if (status === 404) { 107 | throw new AzureDevOpsResourceNotFoundError( 108 | `Project not found: ${projectNameOrId}`, 109 | ); 110 | } 111 | 112 | if (status === 401 || status === 403) { 113 | throw new AzureDevOpsPermissionError( 114 | `Permission denied to access project: ${projectNameOrId}`, 115 | ); 116 | } 117 | 118 | throw new AzureDevOpsError( 119 | `Failed to get project details: ${errorMessage}`, 120 | ); 121 | } 122 | 123 | throw new AzureDevOpsError( 124 | `Network error when getting project details: ${axiosError.message}`, 125 | ); 126 | } 127 | } 128 | 129 | /** 130 | * Creates a new wiki in Azure DevOps 131 | * @param projectId - Project ID or name 132 | * @param params - Parameters for creating the wiki 133 | * @returns The created wiki 134 | */ 135 | async createWiki(projectId: string, params: WikiCreateParameters) { 136 | // Use the default project if not provided 137 | const project = projectId || defaultProject; 138 | 139 | try { 140 | // Get the actual project ID (whether the input was a name or ID) 141 | const actualProjectId = await this.getProjectId(project); 142 | 143 | // Construct the URL to create the wiki 144 | const url = `${this.baseUrl}/${project}/_apis/wiki/wikis`; 145 | 146 | // Get authorization header 147 | const authHeader = await getAuthorizationHeader(); 148 | 149 | // Make the API request 150 | const response = await axios.post( 151 | url, 152 | { 153 | name: params.name, 154 | type: params.type, 155 | projectId: actualProjectId, 156 | ...(params.type === 'codeWiki' && { 157 | repositoryId: params.repositoryId, 158 | mappedPath: params.mappedPath, 159 | version: params.version, 160 | }), 161 | }, 162 | { 163 | params: { 164 | 'api-version': '7.1', 165 | }, 166 | headers: { 167 | Authorization: authHeader, 168 | 'Content-Type': 'application/json', 169 | }, 170 | }, 171 | ); 172 | 173 | return response.data; 174 | } catch (error) { 175 | const axiosError = error as AxiosError; 176 | 177 | // Handle specific error cases 178 | if (axiosError.response) { 179 | const status = axiosError.response.status; 180 | const errorMessage = 181 | typeof axiosError.response.data === 'object' && 182 | axiosError.response.data 183 | ? (axiosError.response.data as AzureDevOpsApiErrorResponse) 184 | .message || axiosError.message 185 | : axiosError.message; 186 | 187 | // Handle 404 Not Found 188 | if (status === 404) { 189 | throw new AzureDevOpsResourceNotFoundError( 190 | `Project not found: ${projectId}`, 191 | ); 192 | } 193 | 194 | // Handle 401 Unauthorized or 403 Forbidden 195 | if (status === 401 || status === 403) { 196 | throw new AzureDevOpsPermissionError( 197 | `Permission denied to create wiki in project: ${projectId}`, 198 | ); 199 | } 200 | 201 | // Handle validation errors 202 | if (status === 400) { 203 | throw new AzureDevOpsValidationError( 204 | `Invalid wiki creation parameters: ${errorMessage}`, 205 | ); 206 | } 207 | 208 | // Handle other error statuses 209 | throw new AzureDevOpsError(`Failed to create wiki: ${errorMessage}`); 210 | } 211 | 212 | // Handle network errors 213 | throw new AzureDevOpsError( 214 | `Network error when creating wiki: ${axiosError.message}`, 215 | ); 216 | } 217 | } 218 | 219 | /** 220 | * Gets a wiki page's content 221 | * @param projectId - Project ID or name 222 | * @param wikiId - Wiki ID or name 223 | * @param pagePath - Path of the wiki page 224 | * @param options - Additional options like version 225 | * @returns The wiki page content and ETag 226 | */ 227 | async getPage(projectId: string, wikiId: string, pagePath: string) { 228 | // Use the default project if not provided 229 | const project = projectId || defaultProject; 230 | 231 | // Ensure pagePath starts with a forward slash 232 | const normalizedPath = pagePath.startsWith('/') ? pagePath : `/${pagePath}`; 233 | 234 | // Construct the URL to get the wiki page 235 | const url = `${this.baseUrl}/${project}/_apis/wiki/wikis/${wikiId}/pages`; 236 | const params: Record<string, string> = { 237 | 'api-version': '7.1', 238 | path: normalizedPath, 239 | }; 240 | 241 | try { 242 | // Get authorization header 243 | const authHeader = await getAuthorizationHeader(); 244 | 245 | // Make the API request for plain text content 246 | const response = await axios.get(url, { 247 | params, 248 | headers: { 249 | Authorization: authHeader, 250 | Accept: 'text/plain', 251 | 'Content-Type': 'application/json', 252 | }, 253 | responseType: 'text', 254 | }); 255 | 256 | // Return both the content and the ETag 257 | return { 258 | content: response.data, 259 | eTag: response.headers.etag?.replace(/"/g, ''), // Remove quotes from ETag 260 | }; 261 | } catch (error) { 262 | const axiosError = error as AxiosError; 263 | 264 | // Handle specific error cases 265 | if (axiosError.response) { 266 | const status = axiosError.response.status; 267 | const errorMessage = 268 | typeof axiosError.response.data === 'object' && 269 | axiosError.response.data 270 | ? (axiosError.response.data as AzureDevOpsApiErrorResponse) 271 | .message || axiosError.message 272 | : axiosError.message; 273 | 274 | // Handle 404 Not Found 275 | if (status === 404) { 276 | throw new AzureDevOpsResourceNotFoundError( 277 | `Wiki page not found: ${pagePath} in wiki ${wikiId}`, 278 | ); 279 | } 280 | 281 | // Handle 401 Unauthorized or 403 Forbidden 282 | if (status === 401 || status === 403) { 283 | throw new AzureDevOpsPermissionError( 284 | `Permission denied to access wiki page: ${pagePath}`, 285 | ); 286 | } 287 | 288 | // Handle other error statuses 289 | throw new AzureDevOpsError( 290 | `Failed to get wiki page: ${errorMessage} ${axiosError.response?.data}`, 291 | ); 292 | } 293 | 294 | // Handle network errors 295 | throw new AzureDevOpsError( 296 | `Network error when getting wiki page: ${axiosError.message}`, 297 | ); 298 | } 299 | } 300 | 301 | /** 302 | * Creates a new wiki page with the provided content 303 | * @param content - Content for the new wiki page 304 | * @param projectId - Project ID or name 305 | * @param wikiId - Wiki ID or name 306 | * @param pagePath - Path of the wiki page to create 307 | * @param options - Additional options like comment 308 | * @returns The created wiki page 309 | */ 310 | async createPage( 311 | content: string, 312 | projectId: string, 313 | wikiId: string, 314 | pagePath: string, 315 | options?: { comment?: string }, 316 | ) { 317 | // Use the default project if not provided 318 | const project = projectId || defaultProject; 319 | 320 | // Encode the page path, handling forward slashes properly 321 | const encodedPagePath = encodeURIComponent(pagePath).replace(/%2F/g, '/'); 322 | 323 | // Construct the URL to create the wiki page 324 | const url = `${this.baseUrl}/${project}/_apis/wiki/wikis/${wikiId}/pages`; 325 | 326 | const params: Record<string, string> = { 327 | 'api-version': '7.1', 328 | path: encodedPagePath, 329 | }; 330 | 331 | // Prepare the request payload 332 | const payload: Record<string, string> = { 333 | content, 334 | }; 335 | 336 | // Add comment if provided 337 | if (options?.comment) { 338 | payload.comment = options.comment; 339 | } 340 | 341 | try { 342 | // Get authorization header 343 | const authHeader = await getAuthorizationHeader(); 344 | 345 | // Make the API request 346 | const response = await axios.put(url, payload, { 347 | params, 348 | headers: { 349 | Authorization: authHeader, 350 | 'Content-Type': 'application/json', 351 | Accept: 'application/json', 352 | }, 353 | }); 354 | 355 | // The ETag header contains the version 356 | const eTag = response.headers.etag; 357 | 358 | // Return the page content along with metadata 359 | return { 360 | ...response.data, 361 | version: eTag ? eTag.replace(/"/g, '') : undefined, // Remove quotes from ETag 362 | }; 363 | } catch (error) { 364 | const axiosError = error as AxiosError; 365 | 366 | // Handle specific error cases 367 | if (axiosError.response) { 368 | const status = axiosError.response.status; 369 | const errorMessage = 370 | typeof axiosError.response.data === 'object' && 371 | axiosError.response.data 372 | ? (axiosError.response.data as AzureDevOpsApiErrorResponse) 373 | .message || axiosError.message 374 | : axiosError.message; 375 | 376 | // Handle 404 Not Found - usually means the parent path doesn't exist 377 | if (status === 404) { 378 | throw new AzureDevOpsResourceNotFoundError( 379 | `Cannot create wiki page: parent path for ${pagePath} does not exist`, 380 | ); 381 | } 382 | 383 | // Handle 401 Unauthorized or 403 Forbidden 384 | if (status === 401 || status === 403) { 385 | throw new AzureDevOpsPermissionError( 386 | `Permission denied to create wiki page: ${pagePath}`, 387 | ); 388 | } 389 | 390 | // Handle 412 Precondition Failed - page might already exist 391 | if (status === 412) { 392 | throw new AzureDevOpsValidationError( 393 | `Wiki page already exists: ${pagePath}`, 394 | ); 395 | } 396 | 397 | // Handle 400 Bad Request - usually validation errors 398 | if (status === 400) { 399 | throw new AzureDevOpsValidationError( 400 | `Invalid request when creating wiki page: ${errorMessage}`, 401 | ); 402 | } 403 | 404 | // Handle other error statuses 405 | throw new AzureDevOpsError( 406 | `Failed to create wiki page: ${errorMessage}`, 407 | ); 408 | } 409 | 410 | // Handle network errors 411 | throw new AzureDevOpsError( 412 | `Network error when creating wiki page: ${axiosError.message}`, 413 | ); 414 | } 415 | } 416 | 417 | /** 418 | * Updates a wiki page with the provided content 419 | * @param content - Content for the wiki page 420 | * @param projectId - Project ID or name 421 | * @param wikiId - Wiki ID or name 422 | * @param pagePath - Path of the wiki page 423 | * @param options - Additional options like comment and version 424 | * @returns The updated wiki page 425 | */ 426 | async updatePage( 427 | content: WikiPageContent, 428 | projectId: string, 429 | wikiId: string, 430 | pagePath: string, 431 | options?: PageUpdateOptions, 432 | ) { 433 | // Use the default project if not provided 434 | const project = projectId || defaultProject; 435 | 436 | // First get the current page version 437 | let currentETag; 438 | try { 439 | const currentPage = await this.getPage(project, wikiId, pagePath); 440 | currentETag = currentPage.eTag; 441 | } catch (error) { 442 | if (error instanceof AzureDevOpsResourceNotFoundError) { 443 | // If page doesn't exist, we'll create it (no If-Match header needed) 444 | currentETag = undefined; 445 | } else { 446 | throw error; 447 | } 448 | } 449 | 450 | // Encode the page path, handling forward slashes properly 451 | const encodedPagePath = encodeURIComponent(pagePath).replace(/%2F/g, '/'); 452 | 453 | // Construct the URL to update the wiki page 454 | const url = `${this.baseUrl}/${project}/_apis/wiki/wikis/${wikiId}/pages`; 455 | const params: Record<string, string> = { 456 | 'api-version': '7.1', 457 | path: encodedPagePath, 458 | }; 459 | 460 | // Add optional comment parameter if provided 461 | if (options?.comment) { 462 | params.comment = options.comment; 463 | } 464 | 465 | try { 466 | // Get authorization header 467 | const authHeader = await getAuthorizationHeader(); 468 | 469 | // Prepare request headers 470 | const headers: Record<string, string> = { 471 | Authorization: authHeader, 472 | 'Content-Type': 'application/json', 473 | }; 474 | 475 | // Add If-Match header if we have an ETag (for updates) 476 | if (currentETag) { 477 | headers['If-Match'] = `"${currentETag}"`; // Wrap in quotes as required by API 478 | } 479 | 480 | // Create a properly typed payload 481 | const payload: Record<string, string> = { 482 | content: content.content, 483 | }; 484 | 485 | // Make the API request 486 | const response = await axios.put(url, payload, { 487 | params, 488 | headers, 489 | }); 490 | 491 | // The ETag header contains the version 492 | const eTag = response.headers.etag; 493 | 494 | // Return the page content along with metadata 495 | return { 496 | ...response.data, 497 | version: eTag ? eTag.replace(/"/g, '') : undefined, // Remove quotes from ETag 498 | message: 499 | response.status === 201 500 | ? 'Page created successfully' 501 | : 'Page updated successfully', 502 | }; 503 | } catch (error) { 504 | const axiosError = error as AxiosError; 505 | 506 | // Handle specific error cases 507 | if (axiosError.response) { 508 | const status = axiosError.response.status; 509 | const errorMessage = 510 | typeof axiosError.response.data === 'object' && 511 | axiosError.response.data 512 | ? (axiosError.response.data as AzureDevOpsApiErrorResponse) 513 | .message || axiosError.message 514 | : axiosError.message; 515 | 516 | // Handle 404 Not Found 517 | if (status === 404) { 518 | throw new AzureDevOpsResourceNotFoundError( 519 | `Wiki page not found: ${pagePath} in wiki ${wikiId}`, 520 | ); 521 | } 522 | 523 | // Handle 401 Unauthorized or 403 Forbidden 524 | if (status === 401 || status === 403) { 525 | throw new AzureDevOpsPermissionError( 526 | `Permission denied to update wiki page: ${pagePath}`, 527 | ); 528 | } 529 | 530 | // Handle 412 Precondition Failed (version conflict) 531 | if (status === 412) { 532 | throw new AzureDevOpsValidationError( 533 | `Version conflict: The wiki page has been modified since you retrieved it. Please get the latest version and try again.`, 534 | ); 535 | } 536 | 537 | // Handle other error statuses 538 | throw new AzureDevOpsError( 539 | `Failed to update wiki page: ${errorMessage}`, 540 | ); 541 | } 542 | 543 | // Handle network errors 544 | throw new AzureDevOpsError( 545 | `Network error when updating wiki page: ${axiosError.message}`, 546 | ); 547 | } 548 | } 549 | 550 | /** 551 | * Lists wiki pages from a wiki using the Pages Batch API 552 | * @param projectId - Project ID or name 553 | * @param wikiId - Wiki ID or name 554 | * @returns Array of wiki page summaries sorted by order then path 555 | */ 556 | async listWikiPages( 557 | projectId: string, 558 | wikiId: string, 559 | ): Promise<WikiPageSummary[]> { 560 | // Use the default project if not provided 561 | const project = projectId || defaultProject; 562 | 563 | // Construct the URL for the Pages Batch API 564 | const url = `${this.baseUrl}/${project}/_apis/wiki/wikis/${wikiId}/pagesbatch`; 565 | 566 | const allPages: WikiPageSummary[] = []; 567 | let continuationToken: string | undefined; 568 | 569 | try { 570 | // Get authorization header 571 | const authHeader = await getAuthorizationHeader(); 572 | 573 | do { 574 | // Prepare the request body 575 | const requestBody: WikiPagesBatchRequest = { 576 | top: 100, 577 | ...(continuationToken && { continuationToken }), 578 | }; 579 | 580 | // Make the API request 581 | const response = await axios.post<WikiPagesBatchResponse>( 582 | url, 583 | requestBody, 584 | { 585 | params: { 586 | 'api-version': '7.1', 587 | }, 588 | headers: { 589 | Authorization: authHeader, 590 | 'Content-Type': 'application/json', 591 | }, 592 | }, 593 | ); 594 | 595 | // Add the pages from this batch to our collection 596 | if (response.data.value && Array.isArray(response.data.value)) { 597 | allPages.push(...response.data.value); 598 | } 599 | 600 | // Update continuation token for next iteration 601 | continuationToken = response.data.continuationToken; 602 | } while (continuationToken); 603 | 604 | // Sort results by order then path 605 | return allPages.sort((a, b) => { 606 | // Handle optional order field 607 | const aOrder = a.order ?? Number.MAX_SAFE_INTEGER; 608 | const bOrder = b.order ?? Number.MAX_SAFE_INTEGER; 609 | 610 | if (aOrder !== bOrder) { 611 | return aOrder - bOrder; 612 | } 613 | return a.path.localeCompare(b.path); 614 | }); 615 | } catch (error) { 616 | const axiosError = error as AxiosError; 617 | 618 | // Handle specific error cases 619 | if (axiosError.response) { 620 | const status = axiosError.response.status; 621 | const errorMessage = 622 | typeof axiosError.response.data === 'object' && 623 | axiosError.response.data 624 | ? (axiosError.response.data as AzureDevOpsApiErrorResponse) 625 | .message || axiosError.message 626 | : axiosError.message; 627 | 628 | // Handle 404 Not Found 629 | if (status === 404) { 630 | throw new AzureDevOpsResourceNotFoundError( 631 | `Wiki not found: ${wikiId} in project ${projectId}`, 632 | ); 633 | } 634 | 635 | // Handle 401 Unauthorized or 403 Forbidden 636 | if (status === 401 || status === 403) { 637 | throw new AzureDevOpsPermissionError( 638 | `Permission denied to list wiki pages in wiki: ${wikiId}`, 639 | ); 640 | } 641 | 642 | // Handle other error statuses 643 | throw new AzureDevOpsError( 644 | `Failed to list wiki pages: ${errorMessage}`, 645 | ); 646 | } 647 | 648 | // Handle network errors 649 | throw new AzureDevOpsError( 650 | `Network error when listing wiki pages: ${axiosError.message}`, 651 | ); 652 | } 653 | } 654 | } 655 | 656 | /** 657 | * Creates a Wiki client for Azure DevOps operations 658 | * @param options - Options for creating the client 659 | * @returns A Wiki client instance 660 | */ 661 | export async function getWikiClient( 662 | options: ClientOptions, 663 | ): Promise<WikiClient> { 664 | const { organizationId } = options; 665 | 666 | return new WikiClient(organizationId || defaultOrg); 667 | } 668 | 669 | /** 670 | * Get the authorization header for Azure DevOps API requests 671 | * @returns The authorization header 672 | */ 673 | export async function getAuthorizationHeader(): Promise<string> { 674 | try { 675 | // For PAT authentication, we can construct the header directly 676 | if ( 677 | process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'pat' && 678 | process.env.AZURE_DEVOPS_PAT 679 | ) { 680 | // For PAT auth, we can construct the Basic auth header directly 681 | const token = process.env.AZURE_DEVOPS_PAT; 682 | const base64Token = Buffer.from(`:${token}`).toString('base64'); 683 | return `Basic ${base64Token}`; 684 | } 685 | 686 | // For Azure Identity / Azure CLI auth, we need to get a token 687 | // using the Azure DevOps resource ID 688 | // Choose the appropriate credential based on auth method 689 | const credential = 690 | process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-cli' 691 | ? new AzureCliCredential() 692 | : new DefaultAzureCredential(); 693 | 694 | // Azure DevOps resource ID for token acquisition 695 | const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798'; 696 | 697 | // Get token for Azure DevOps 698 | const token = await credential.getToken( 699 | `${AZURE_DEVOPS_RESOURCE_ID}/.default`, 700 | ); 701 | 702 | if (!token || !token.token) { 703 | throw new Error('Failed to acquire token for Azure DevOps'); 704 | } 705 | 706 | return `Bearer ${token.token}`; 707 | } catch (error) { 708 | throw new AzureDevOpsValidationError( 709 | `Failed to get authorization header: ${error instanceof Error ? error.message : String(error)}`, 710 | ); 711 | } 712 | } 713 | ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [0.1.42](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.41...mcp-server-azure-devops-v0.1.42) (2025-07-15) 6 | 7 | 8 | ### Features 9 | 10 | * implement human-readable string enums for Azure DevOps API responses ([8168bcb](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/8168bcbe8e4957e9632927f57ecbe9632c911735)) 11 | 12 | ## [0.1.41](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.40...mcp-server-azure-devops-v0.1.41) (2025-07-14) 13 | 14 | 15 | ### Features 16 | 17 | * **pull-requests:** enhance get_pull_request_comments response with … ([#229](https://github.com/Tiberriver256/mcp-server-azure-devops/issues/229)) ([6997a04](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6997a04e92b4fe453354b8fd9f0f25c974fcad2b)) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **work-items:** make expand enum compatible with Gemini CLI ([#240](https://github.com/Tiberriver256/mcp-server-azure-devops/issues/240)) ([ac1dcac](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ac1dcace4cd6f63d5decd4820307b52a4d0d431d)) 23 | 24 | ## [0.1.40](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.39...mcp-server-azure-devops-v0.1.40) (2025-06-20) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * simplify listWikiPages API by removing unused parameters ([fff7238](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/fff72384f69433942ee8439de0dda90d7fc85c38)) 30 | 31 | ## [0.1.39](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.38...mcp-server-azure-devops-v0.1.39) (2025-06-03) 32 | 33 | 34 | ### Features 35 | 36 | * add listWikiPages functionality to Azure DevOps wiki client ([bb9ddc0](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/bb9ddc077e80be0caeda106a7e75dc336a62c9ae)) 37 | * implement create_wiki_page feature for Azure DevOps wiki API integration ([#225](https://github.com/Tiberriver256/mcp-server-azure-devops/issues/225)) ([7e3294d](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/7e3294d1f1b6e82d5ca34cf86f3eaa51579dad02)) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * enhanced the get-pull-request-comments tool to include path and line number ([f6017e5](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/f6017e5dd352b63b189e761c9cf27d103dd24b9d)) 43 | * remove uuid() validator to resolve unknown format error ([b251252](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/b251252c7c455ee11d8076380b70a546ebf40a6e)) 44 | 45 | ## [0.1.38](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.37...mcp-server-azure-devops-v0.1.38) (2025-05-25) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * improve org name extraction from url ([496abc7](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/496abc7a9c5fd0867cf8484f8c33c47a3cc42edf)) 51 | 52 | ## [0.1.37](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.36...mcp-server-azure-devops-v0.1.37) (2025-05-14) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * get_me support for visualstudio.com urls ([ffd3c8a](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ffd3c8a34bcee1856911e5eed7b719d524c25fef)) 58 | 59 | ## [0.1.36](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.35...mcp-server-azure-devops-v0.1.36) (2025-05-07) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * implement pagination for list-pull-requests to prevent infinite loop ([#196](https://github.com/Tiberriver256/mcp-server-azure-devops/issues/196)) ([e3d7f32](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/e3d7f321f11241bd7b45a4f0e6810509cc8c01c1)) 65 | 66 | ## [0.1.35](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.34...mcp-server-azure-devops-v0.1.35) (2025-05-07) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * update creatorId and reviewerId to require UUIDs instead of allowing emails ([09e82ef](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/09e82ef5e7dfdcd07d1851450ea8c488ea8bb82a)) 72 | * use default project for code search when no projectId is specified ([#202](https://github.com/Tiberriver256/mcp-server-azure-devops/issues/202)) ([3bf118f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/3bf118f45b4222bbfaf888deb67d546b87afc2fe)) 73 | 74 | ## [0.1.34](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.33...mcp-server-azure-devops-v0.1.34) (2025-05-02) 75 | 76 | 77 | ### Features 78 | 79 | * add update-pull-request tool to tool-definitions and implement reviewer management ([b7b5398](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/b7b539813baadb84e15022fdb93d24f440491d94)) 80 | 81 | ## [0.1.33](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.32...mcp-server-azure-devops-v0.1.33) (2025-04-28) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * add guidance for HTML formatting in multi-line text fields ([#188](https://github.com/Tiberriver256/mcp-server-azure-devops/issues/188)) ([25751cd](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/25751cd0d7cb8919a7bca80d0796784935f0fbed)), closes [#179](https://github.com/Tiberriver256/mcp-server-azure-devops/issues/179) 87 | 88 | ## [0.1.32](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.31...mcp-server-azure-devops-v0.1.32) (2025-04-26) 89 | 90 | 91 | ### Features 92 | 93 | * add get_pull_request_comments ([2b7fb3a](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2b7fb3a885466c633d2d2dfdd8906cc9573483d9)) 94 | * add_pull_request_comment ([1df6161](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/1df616161835e11c0039bc344ccdc57742f79507)) 95 | 96 | ## [0.1.31](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.30...mcp-server-azure-devops-v0.1.31) (2025-04-23) 97 | 98 | 99 | ### Features 100 | 101 | * **pull-requests:** implement list-pull-requests functionality ([3f8cac4](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/3f8cac448e2adacaddeb069bc0116b8526577624)) 102 | * **wikis:** add create and update wiki functionalities ([27edd6d](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/27edd6d7786748548f4a0123ff19be43b30265c4)) 103 | 104 | 105 | ### Bug Fixes 106 | 107 | * **pull-requests:** update repository name environment variable ([d2fde5f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/d2fde5f94280f08056f94b613a673d4bbe9c0192)) 108 | 109 | ## [0.1.30](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.29...mcp-server-azure-devops-v0.1.30) (2025-04-21) 110 | 111 | 112 | ### Features 113 | 114 | * **wikis:** implement `get_wiki_page` tool ([7ba5fd7](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/7ba5fd7830fefa17c014aa2b80f0bad04d8fcbf7)) 115 | * **wikis:** implement `get_wikis` tool ([3120479](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/3120479b5c31bfaeb50a507791056066f33b6534)) 116 | 117 | ## [0.1.29](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.28...mcp-server-azure-devops-v0.1.29) (2025-04-19) 118 | 119 | 120 | ### Features 121 | 122 | * **pipelines:** implement trigger-pipeline functionality ([e9ba71b](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/e9ba71bfeb2c3a2dc0e1e314698a453d5995d099)) 123 | 124 | ## [0.1.28](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.27...mcp-server-azure-devops-v0.1.28) (2025-04-17) 125 | 126 | 127 | ### Features 128 | 129 | * **pipeline:** implement get-pipeline functionality ([#166](https://github.com/Tiberriver256/mcp-server-azure-devops/issues/166)) ([e307340](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/e3073401e141b566191be16ed4f9b7925c2849eb)) 130 | 131 | ## [0.1.27](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.26...mcp-server-azure-devops-v0.1.27) (2025-04-16) 132 | 133 | 134 | ### Features 135 | 136 | * **pipeline:** implement list-pipelines ([#161](https://github.com/Tiberriver256/mcp-server-azure-devops/issues/161)) ([89ce473](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/89ce4732ba754632540ffb45ceae323f9675c023)), closes [#94](https://github.com/Tiberriver256/mcp-server-azure-devops/issues/94) 137 | 138 | ## [0.1.26](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.25...mcp-server-azure-devops-v0.1.26) (2025-04-15) 139 | 140 | 141 | ### Features 142 | 143 | * **getWorkItem:** enhance get_work_item to include all available fields ([3810660](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/38106600f04842a44e5e5b6e824716ebb6f69e61)) 144 | * support default project and organization in all tools ([5beca06](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/5beca063057bdbc2dd869c865fb01e0d311c8917)) 145 | 146 | 147 | ### Bug Fixes 148 | 149 | * return actual field information from get_project_details tool ([64a030a](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/64a030a8c14fd1f9e7f871ae409f0dded23dbe98)) 150 | 151 | ## [0.1.25](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.24...mcp-server-azure-devops-v0.1.25) (2025-04-11) 152 | 153 | 154 | ### Features 155 | 156 | * create pull request ([ab9c255](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ab9c2554ea82a497dead8131a6479ba6fe7c5ba8)) 157 | 158 | ## [0.1.24](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.23...mcp-server-azure-devops-v0.1.24) (2025-04-10) 159 | 160 | 161 | ### Bug Fixes 162 | 163 | * add missing minimatch module ([ee1ffa3](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ee1ffa34afb0da9cdac31da140c17dbd9c589c2b)) 164 | 165 | ## [0.1.23](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.22...mcp-server-azure-devops-v0.1.23) (2025-04-10) 166 | 167 | 168 | ### Features 169 | 170 | * **repositories:** add get_all_repositories_tree tool for viewing multi-repository file structure ([adbe206](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/adbe206300d55ba06063c675492b3a8153b688f7)) 171 | * support default project and organization in all tools ([96d61bd](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/96d61bd1098146dfafd1faf7dade1a37725cd7b7)) 172 | 173 | ## [0.1.22](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.21...mcp-server-azure-devops-v0.1.22) (2025-04-08) 174 | 175 | 176 | ### Bug Fixes 177 | 178 | * allow parameterless tools to be called without arguments ([9ce88c3](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/9ce88c3afd4454b8a65392a98e7e2ffb45192584)) 179 | 180 | ## [0.1.21](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.20...mcp-server-azure-devops-v0.1.21) (2025-04-08) 181 | 182 | 183 | ### Features 184 | 185 | * add get-file-content feature to access repository content ([a282f75](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/a282f75383ffc362e5b2d1ecbccebb0047e21571)) 186 | * restore get_file_content tool and update documentation ([f71013a](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/f71013a962fb5fbe5d121eaf7f1901e58cf70482)) 187 | 188 | ## [0.1.20](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.19...mcp-server-azure-devops-v0.1.20) (2025-04-06) 189 | 190 | 191 | ### Bug Fixes 192 | 193 | * add explicit permissions to workflow file ([ae85b95](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ae85b953d2467538e42d8c6853b93e1af3c8ed51)) 194 | * refine WIQL query in integration test ([eb32e43](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/eb32e43a9064485d29661bcae99a987e3b863464)) 195 | * remove schema validation for parameterless tools ([031a71d](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/031a71d71083649216e5b67eb6d67c18c78702bd)) 196 | 197 | ## [0.1.19](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.18...mcp-server-azure-devops-v0.1.19) (2025-04-05) 198 | 199 | 200 | ### Bug Fixes 201 | 202 | * package.json & package-lock.json to reduce vulnerabilities ([2fb1e72](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2fb1e725120edc75c9897bc81f57381c20ad880a)) 203 | 204 | ## [0.1.18](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.17...mcp-server-azure-devops-v0.1.18) (2025-04-05) 205 | 206 | 207 | ### Bug Fixes 208 | 209 | * getMe profile bug ([ceca909](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ceca909beaa74b0dd150ce1688a498281fd0b9e8)) 210 | 211 | ## [0.1.17](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.16...mcp-server-azure-devops-v0.1.17) (2025-04-05) 212 | 213 | 214 | ### Features 215 | 216 | * implement get_me tool ([2a3849d](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2a3849da063f6ce0877dd672992a8bc19f88230e)) 217 | 218 | ## [0.1.16](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.15...mcp-server-azure-devops-v0.1.16) (2025-04-05) 219 | 220 | 221 | ### Features 222 | 223 | * limit search results to 10 when includeContent is true ([827e4e6](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/827e4e65be353125f5ae595b7e68d80f614f8c07)) 224 | * make projectId optional in search features for organization-wide search ([1ca1e0e](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/1ca1e0e146bf880d367078b02a2ddaebf6f54a2a)) 225 | 226 | 227 | ### Bug Fixes 228 | 229 | * correct [Object Object] display in search_code includeContent ([bdabd6b](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/bdabd6bbeb3f60347c37499bdcb621f5c206dfe0)) 230 | * resolve parameter conflict in getItemContent function ([38d624c](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/38d624c10dcfad26bab6d04a9290ad05097f5052)) 231 | * simplify content handling in search_code to properly process ReadableStream ([136a90a](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/136a90a94f446e2c4227d87286b8d71ef8223212)) 232 | 233 | 234 | ### Performance Improvements 235 | 236 | * optimize git hooks with lint-staged ([ba953d8](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ba953d84706893d56a82573c8d9e8ecdf3b09591)) 237 | 238 | ## [0.1.15](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.14...mcp-server-azure-devops-v0.1.15) (2025-04-02) 239 | 240 | 241 | ### Bug Fixes 242 | 243 | * search_work_items authentication with Azure Identity ([cdb2e72](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/cdb2e722ee3abf6be465adcad7dc294f7c623103)) 244 | 245 | ## [0.1.14](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.13...mcp-server-azure-devops-v0.1.14) (2025-04-02) 246 | 247 | 248 | ### Bug Fixes 249 | 250 | * add zod-to-json-schema dependency and remove unused packages from package-lock.json ([c9c117f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/c9c117fd388e228c1116d9249698d931557877b7)) 251 | 252 | ## [0.1.13](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.12...mcp-server-azure-devops-v0.1.13) (2025-04-02) 253 | 254 | 255 | ### Features 256 | 257 | * add 'expand' option to get_work_item ([6bee365](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6bee365d9b37f7e197eaff03065e713ab0ee1c5f)) 258 | * Add npm publish to release.yml ([50d0368](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/50d0368c090adc39a9b3ece67d198cabcd18c6ce)) 259 | * add pre-commit hook for prettier and eslint ([1b4ddff](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/1b4ddff90e3c3ab9954d041398d224f03c632f63)) 260 | * enhance GitHub release notes with changelog content ([2fb275d](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2fb275d38acbc9c092584573a549466ccd5482bc)) 261 | * implement automated release workflow ([9e5a5df](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/9e5a5dfacdd87ca933ed02efbd0aa8035239332d)) 262 | * implement get_project_details core functionality ([6d93d98](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6d93d9820c4bd3ce8bc257d05ff04b39d1370a19)), closes [#101](https://github.com/Tiberriver256/mcp-server-azure-devops/issues/101) 263 | * implement get_repository_details core functionality ([dcef80b](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/dcef80b922ef338f6d3704ab30f59c1b126c70ee)) 264 | * implement manage work item link handler ([72cd641](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/72cd6419cf804eb0d72d5ba7763ad5b46bc35650)) 265 | * implement search_wiki handler with tests ([286598c](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/286598c47052ade3b6a524938046b3e3b9341b3a)) 266 | * implement search_work_items handler with tests ([e244658](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/e2446587e6f82fb7e2dbfe47d2d034ecfdfc3189)) 267 | * **search:** add code search functionality for Azure DevOps repos ([0680102](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/068010236b10d8ed444ec01bd6820b27c5c9dcdc)) 268 | 269 | 270 | ### Bug Fixes 271 | 272 | * add bin field to make package executable with npx ([2d3d5fa](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2d3d5fa31a9ba741c4a85d7ef21d72ff46270695)) 273 | * add build step to workflow and ensure dist files are included in package ([6e12d3c](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6e12d3ca666937c7b24c7c5d8b161fbb8e34798c)) 274 | * add parent-child relationship support for createWorkItem ([31d5efe](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/31d5efef49c162772e64eabd1e4012d8143dc270)) 275 | * add tag_name parameter to GitHub release action ([68cfa43](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/68cfa43839c5975cdf9c2ec8a5348ace6138d1c2)) 276 | * improve cross-platform CLI compatibility for Windows ([0f6ed3f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/0f6ed3fe7c72ba63ec5485047ce52e06278457ab)) 277 | * make AZURE_DEVOPS_AUTH_METHOD parameter case-insensitive ([9bbf53f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/9bbf53ffcc1a9170e6ba038fee182da0621be777)) 278 | * only request max 200 by default ([296de35](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/296de3584346bd05c14dec3b39dff9a5ec0036a5)) 279 | * resolve npm publish authentication and package content issues ([96e91d0](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/96e91d04ec620ad77fc35fea31c2b7795fb73d9e)) 280 | * restore tests/setup.ts to fix test suite ([5e23eab](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/5e23eab1228f3949c431f1b8509ad5fbf829e528)) 281 | * revert to direct execution of index.js to fix main module detection ([82efa90](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/82efa90852f56db3a0b028ec50eb5230072da88a)) 282 | * Typo in release.yaml workflow ([e0de15f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/e0de15fd220ef2141466cf0530383921ed99253d)) 283 | 284 | ## [0.1.12](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/mcp-server-azure-devops-v0.1.11...mcp-server-azure-devops-v0.1.12) (2025-04-02) 285 | 286 | 287 | ### Features 288 | 289 | * add 'expand' option to get_work_item ([6bee365](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6bee365d9b37f7e197eaff03065e713ab0ee1c5f)) 290 | * Add npm publish to release.yml ([50d0368](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/50d0368c090adc39a9b3ece67d198cabcd18c6ce)) 291 | * add pre-commit hook for prettier and eslint ([1b4ddff](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/1b4ddff90e3c3ab9954d041398d224f03c632f63)) 292 | * enhance GitHub release notes with changelog content ([2fb275d](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2fb275d38acbc9c092584573a549466ccd5482bc)) 293 | * implement automated release workflow ([9e5a5df](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/9e5a5dfacdd87ca933ed02efbd0aa8035239332d)) 294 | * implement get_project_details core functionality ([6d93d98](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6d93d9820c4bd3ce8bc257d05ff04b39d1370a19)), closes [#101](https://github.com/Tiberriver256/mcp-server-azure-devops/issues/101) 295 | * implement get_repository_details core functionality ([dcef80b](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/dcef80b922ef338f6d3704ab30f59c1b126c70ee)) 296 | * implement manage work item link handler ([72cd641](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/72cd6419cf804eb0d72d5ba7763ad5b46bc35650)) 297 | * implement search_wiki handler with tests ([286598c](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/286598c47052ade3b6a524938046b3e3b9341b3a)) 298 | * implement search_work_items handler with tests ([e244658](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/e2446587e6f82fb7e2dbfe47d2d034ecfdfc3189)) 299 | * **search:** add code search functionality for Azure DevOps repos ([0680102](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/068010236b10d8ed444ec01bd6820b27c5c9dcdc)) 300 | 301 | 302 | ### Bug Fixes 303 | 304 | * add bin field to make package executable with npx ([2d3d5fa](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2d3d5fa31a9ba741c4a85d7ef21d72ff46270695)) 305 | * add build step to workflow and ensure dist files are included in package ([6e12d3c](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6e12d3ca666937c7b24c7c5d8b161fbb8e34798c)) 306 | * add parent-child relationship support for createWorkItem ([31d5efe](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/31d5efef49c162772e64eabd1e4012d8143dc270)) 307 | * add tag_name parameter to GitHub release action ([68cfa43](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/68cfa43839c5975cdf9c2ec8a5348ace6138d1c2)) 308 | * improve cross-platform CLI compatibility for Windows ([0f6ed3f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/0f6ed3fe7c72ba63ec5485047ce52e06278457ab)) 309 | * make AZURE_DEVOPS_AUTH_METHOD parameter case-insensitive ([9bbf53f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/9bbf53ffcc1a9170e6ba038fee182da0621be777)) 310 | * only request max 200 by default ([296de35](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/296de3584346bd05c14dec3b39dff9a5ec0036a5)) 311 | * resolve npm publish authentication and package content issues ([96e91d0](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/96e91d04ec620ad77fc35fea31c2b7795fb73d9e)) 312 | * restore tests/setup.ts to fix test suite ([5e23eab](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/5e23eab1228f3949c431f1b8509ad5fbf829e528)) 313 | * revert to direct execution of index.js to fix main module detection ([82efa90](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/82efa90852f56db3a0b028ec50eb5230072da88a)) 314 | * Typo in release.yaml workflow ([e0de15f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/e0de15fd220ef2141466cf0530383921ed99253d)) 315 | 316 | ### [0.1.11](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/v0.1.10...v0.1.11) (2025-04-01) 317 | 318 | 319 | ### Features 320 | 321 | * **search:** add code search functionality for Azure DevOps repos ([0680102](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/068010236b10d8ed444ec01bd6820b27c5c9dcdc)) 322 | 323 | ### [0.1.10](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/v0.1.9...v0.1.10) (2025-04-01) 324 | 325 | 326 | ### Features 327 | 328 | * add 'expand' option to get_work_item ([6bee365](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6bee365d9b37f7e197eaff03065e713ab0ee1c5f)) 329 | 330 | 331 | ### Bug Fixes 332 | 333 | * only request max 200 by default ([296de35](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/296de3584346bd05c14dec3b39dff9a5ec0036a5)) 334 | 335 | ### [0.1.9](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/v0.1.8...v0.1.9) (2025-03-31) 336 | 337 | 338 | ### Features 339 | 340 | * add pre-commit hook for prettier and eslint ([1b4ddff](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/1b4ddff90e3c3ab9954d041398d224f03c632f63)) 341 | * implement manage work item link handler ([72cd641](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/72cd6419cf804eb0d72d5ba7763ad5b46bc35650)) 342 | 343 | 344 | ### Bug Fixes 345 | 346 | * add parent-child relationship support for createWorkItem ([31d5efe](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/31d5efef49c162772e64eabd1e4012d8143dc270)) 347 | * make AZURE_DEVOPS_AUTH_METHOD parameter case-insensitive ([9bbf53f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/9bbf53ffcc1a9170e6ba038fee182da0621be777)) 348 | * restore tests/setup.ts to fix test suite ([5e23eab](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/5e23eab1228f3949c431f1b8509ad5fbf829e528)) 349 | 350 | ### [0.1.8](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.7...v0.1.8) (2025-03-26) 351 | 352 | 353 | ### Bug Fixes 354 | 355 | * revert to direct execution of index.js to fix main module detection ([82efa90](https://github.com/Tiberriver256/azure-devops-mcp/commit/82efa90852f56db3a0b028ec50eb5230072da88a)) 356 | 357 | ### [0.1.7](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.6...v0.1.7) (2025-03-26) 358 | 359 | 360 | ### Bug Fixes 361 | 362 | * add build step to workflow and ensure dist files are included in package ([6e12d3c](https://github.com/Tiberriver256/azure-devops-mcp/commit/6e12d3ca666937c7b24c7c5d8b161fbb8e34798c)) 363 | 364 | ### [0.1.6](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.5...v0.1.6) (2025-03-26) 365 | 366 | 367 | ### Bug Fixes 368 | 369 | * improve cross-platform CLI compatibility for Windows ([0f6ed3f](https://github.com/Tiberriver256/azure-devops-mcp/commit/0f6ed3fe7c72ba63ec5485047ce52e06278457ab)) 370 | 371 | ### [0.1.5](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.4...v0.1.5) (2025-03-26) 372 | 373 | 374 | ### Bug Fixes 375 | 376 | * add bin field to make package executable with npx ([2d3d5fa](https://github.com/Tiberriver256/azure-devops-mcp/commit/2d3d5fa31a9ba741c4a85d7ef21d72ff46270695)) 377 | 378 | ### [0.1.4](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.3...v0.1.4) (2025-03-26) 379 | 380 | 381 | ### Bug Fixes 382 | 383 | * resolve npm publish authentication and package content issues ([96e91d0](https://github.com/Tiberriver256/azure-devops-mcp/commit/96e91d04ec620ad77fc35fea31c2b7795fb73d9e)) 384 | 385 | ### [0.1.3](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.2...v0.1.3) (2025-03-26) 386 | 387 | 388 | ### Features 389 | 390 | * Add npm publish to release.yml ([50d0368](https://github.com/Tiberriver256/azure-devops-mcp/commit/50d0368c090adc39a9b3ece67d198cabcd18c6ce)) 391 | 392 | 393 | ### Bug Fixes 394 | 395 | * Typo in release.yaml workflow ([e0de15f](https://github.com/Tiberriver256/azure-devops-mcp/commit/e0de15fd220ef2141466cf0530383921ed99253d)) 396 | 397 | ### [0.1.2](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.1...v0.1.2) (2025-03-26) 398 | 399 | 400 | ### Bug Fixes 401 | 402 | * add tag_name parameter to GitHub release action ([68cfa43](https://github.com/Tiberriver256/azure-devops-mcp/commit/68cfa43839c5975cdf9c2ec8a5348ace6138d1c2)) 403 | 404 | ### 0.1.1 (2025-03-26) 405 | 406 | 407 | ### Features 408 | 409 | * enhance GitHub release notes with changelog content ([2fb275d](https://github.com/Tiberriver256/azure-devops-mcp/commit/2fb275d38acbc9c092584573a549466ccd5482bc)) 410 | * implement automated release workflow ([9e5a5df](https://github.com/Tiberriver256/azure-devops-mcp/commit/9e5a5dfacdd87ca933ed02efbd0aa8035239332d)) 411 | 412 | ## 0.1.0 (2025-03-26) 413 | 414 | 415 | ### Features 416 | 417 | * enhance GitHub release notes with changelog content ([dcaf554](https://github.com/Tiberriver256/azure-devops-mcp/commit/dcaf5542fc08cbb9bd665623d305ae7879758f4e)) 418 | * implement automated release workflow ([6fbf41e](https://github.com/Tiberriver256/azure-devops-mcp/commit/6fbf41e5a52c4db054355d4aced33744f6b1a6eb)) 419 | ``` -------------------------------------------------------------------------------- /src/features/search/search-code/feature.spec.unit.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axios from 'axios'; 2 | import { searchCode } from './feature'; 3 | import { WebApi } from 'azure-devops-node-api'; 4 | import { AzureDevOpsError } from '../../../shared/errors'; 5 | import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces'; 6 | 7 | // Mock Azure Identity 8 | jest.mock('@azure/identity', () => { 9 | const mockGetToken = jest.fn().mockResolvedValue({ token: 'mock-token' }); 10 | return { 11 | DefaultAzureCredential: jest.fn().mockImplementation(() => ({ 12 | getToken: mockGetToken, 13 | })), 14 | AzureCliCredential: jest.fn().mockImplementation(() => ({ 15 | getToken: mockGetToken, 16 | })), 17 | }; 18 | }); 19 | 20 | // Mock axios 21 | jest.mock('axios'); 22 | const mockedAxios = axios as jest.Mocked<typeof axios>; 23 | 24 | describe('searchCode unit', () => { 25 | // Mock WebApi connection 26 | const mockConnection = { 27 | getGitApi: jest.fn().mockImplementation(() => ({ 28 | getItemContent: jest.fn().mockImplementation((_repoId, path) => { 29 | // Return different content based on the path to simulate different files 30 | if (path === '/src/example.ts') { 31 | return Buffer.from('export function example() { return "test"; }'); 32 | } 33 | return Buffer.from('// Empty file'); 34 | }), 35 | })), 36 | _getHttpClient: jest.fn().mockReturnValue({ 37 | getAuthorizationHeader: jest.fn().mockReturnValue('Bearer mock-token'), 38 | }), 39 | getCoreApi: jest.fn().mockImplementation(() => ({ 40 | getProjects: jest 41 | .fn() 42 | .mockResolvedValue([{ name: 'TestProject', id: 'project-id' }]), 43 | })), 44 | serverUrl: 'https://dev.azure.com/testorg', 45 | } as unknown as WebApi; 46 | 47 | // Store original console.error 48 | const originalConsoleError = console.error; 49 | 50 | beforeEach(() => { 51 | jest.clearAllMocks(); 52 | // Mock console.error to prevent error messages from being displayed during tests 53 | console.error = jest.fn(); 54 | }); 55 | 56 | afterEach(() => { 57 | // Restore original console.error 58 | console.error = originalConsoleError; 59 | }); 60 | 61 | test('should return search results with content', async () => { 62 | // Arrange 63 | const mockSearchResponse = { 64 | data: { 65 | count: 1, 66 | results: [ 67 | { 68 | fileName: 'example.ts', 69 | path: '/src/example.ts', 70 | matches: { 71 | content: [ 72 | { 73 | charOffset: 17, 74 | length: 7, 75 | }, 76 | ], 77 | }, 78 | collection: { 79 | name: 'DefaultCollection', 80 | }, 81 | project: { 82 | name: 'TestProject', 83 | id: 'project-id', 84 | }, 85 | repository: { 86 | name: 'TestRepo', 87 | id: 'repo-id', 88 | type: 'git', 89 | }, 90 | versions: [ 91 | { 92 | branchName: 'main', 93 | changeId: 'commit-hash', 94 | }, 95 | ], 96 | contentId: 'content-hash', 97 | }, 98 | ], 99 | }, 100 | }; 101 | 102 | mockedAxios.post.mockResolvedValueOnce(mockSearchResponse); 103 | 104 | // Create a mock stream with content 105 | const fileContent = 'export function example() { return "test"; }'; 106 | const mockStream = { 107 | on: jest.fn().mockImplementation((event, callback) => { 108 | if (event === 'data') { 109 | // Call the callback with the data 110 | callback(Buffer.from(fileContent)); 111 | } else if (event === 'end') { 112 | // Call the end callback asynchronously 113 | setTimeout(callback, 0); 114 | } 115 | return mockStream; // Return this for chaining 116 | }), 117 | }; 118 | 119 | // Mock Git API to return content 120 | const mockGitApi = { 121 | getItemContent: jest.fn().mockResolvedValue(mockStream), 122 | }; 123 | 124 | const mockConnectionWithContent = { 125 | ...mockConnection, 126 | getGitApi: jest.fn().mockResolvedValue(mockGitApi), 127 | serverUrl: 'https://dev.azure.com/testorg', 128 | } as unknown as WebApi; 129 | 130 | // Act 131 | const result = await searchCode(mockConnectionWithContent, { 132 | searchText: 'example', 133 | projectId: 'TestProject', 134 | includeContent: true, 135 | }); 136 | 137 | // Assert 138 | expect(result).toBeDefined(); 139 | expect(result.count).toBe(1); 140 | expect(result.results).toHaveLength(1); 141 | expect(result.results[0].fileName).toBe('example.ts'); 142 | expect(result.results[0].content).toBe( 143 | 'export function example() { return "test"; }', 144 | ); 145 | expect(mockedAxios.post).toHaveBeenCalledTimes(1); 146 | expect(mockGitApi.getItemContent).toHaveBeenCalledTimes(1); 147 | expect(mockGitApi.getItemContent).toHaveBeenCalledWith( 148 | 'repo-id', 149 | '/src/example.ts', 150 | 'TestProject', 151 | undefined, 152 | undefined, 153 | undefined, 154 | undefined, 155 | false, 156 | { 157 | version: 'commit-hash', 158 | versionType: GitVersionType.Commit, 159 | }, 160 | true, 161 | ); 162 | }); 163 | 164 | test('should not fetch content when includeContent is false', async () => { 165 | // Arrange 166 | const mockSearchResponse = { 167 | data: { 168 | count: 1, 169 | results: [ 170 | { 171 | fileName: 'example.ts', 172 | path: '/src/example.ts', 173 | matches: { 174 | content: [ 175 | { 176 | charOffset: 17, 177 | length: 7, 178 | }, 179 | ], 180 | }, 181 | collection: { 182 | name: 'DefaultCollection', 183 | }, 184 | project: { 185 | name: 'TestProject', 186 | id: 'project-id', 187 | }, 188 | repository: { 189 | name: 'TestRepo', 190 | id: 'repo-id', 191 | type: 'git', 192 | }, 193 | versions: [ 194 | { 195 | branchName: 'main', 196 | changeId: 'commit-hash', 197 | }, 198 | ], 199 | contentId: 'content-hash', 200 | }, 201 | ], 202 | }, 203 | }; 204 | 205 | mockedAxios.post.mockResolvedValueOnce(mockSearchResponse); 206 | 207 | // Act 208 | const result = await searchCode(mockConnection, { 209 | searchText: 'example', 210 | projectId: 'TestProject', 211 | includeContent: false, 212 | }); 213 | 214 | // Assert 215 | expect(result).toBeDefined(); 216 | expect(result.count).toBe(1); 217 | expect(result.results).toHaveLength(1); 218 | expect(result.results[0].fileName).toBe('example.ts'); 219 | expect(result.results[0].content).toBeUndefined(); 220 | expect(mockConnection.getGitApi).not.toHaveBeenCalled(); 221 | }); 222 | 223 | test('should handle empty search results', async () => { 224 | // Arrange 225 | const mockSearchResponse = { 226 | data: { 227 | count: 0, 228 | results: [], 229 | }, 230 | }; 231 | 232 | mockedAxios.post.mockResolvedValueOnce(mockSearchResponse); 233 | 234 | // Act 235 | const result = await searchCode(mockConnection, { 236 | searchText: 'nonexistent', 237 | projectId: 'TestProject', 238 | }); 239 | 240 | // Assert 241 | expect(result).toBeDefined(); 242 | expect(result.count).toBe(0); 243 | expect(result.results).toHaveLength(0); 244 | }); 245 | 246 | test('should handle API errors', async () => { 247 | // Arrange 248 | const axiosError = new Error('API Error'); 249 | (axiosError as any).isAxiosError = true; 250 | (axiosError as any).response = { 251 | status: 404, 252 | data: { 253 | message: 'Project not found', 254 | }, 255 | }; 256 | 257 | mockedAxios.post.mockRejectedValueOnce(axiosError); 258 | 259 | // Act & Assert 260 | await expect( 261 | searchCode(mockConnection, { 262 | searchText: 'example', 263 | projectId: 'NonExistentProject', 264 | }), 265 | ).rejects.toThrow(AzureDevOpsError); 266 | }); 267 | 268 | test('should propagate custom errors when thrown internally', async () => { 269 | // Arrange 270 | const customError = new AzureDevOpsError('Custom error'); 271 | 272 | // Mock axios to properly return the custom error 273 | mockedAxios.post.mockImplementationOnce(() => { 274 | throw customError; 275 | }); 276 | 277 | // Act & Assert 278 | await expect( 279 | searchCode(mockConnection, { 280 | searchText: 'example', 281 | projectId: 'TestProject', 282 | }), 283 | ).rejects.toThrow(AzureDevOpsError); 284 | 285 | // Reset mock and set it up again for the second test 286 | mockedAxios.post.mockReset(); 287 | mockedAxios.post.mockImplementationOnce(() => { 288 | throw customError; 289 | }); 290 | 291 | await expect( 292 | searchCode(mockConnection, { 293 | searchText: 'example', 294 | projectId: 'TestProject', 295 | }), 296 | ).rejects.toThrow('Custom error'); 297 | }); 298 | 299 | test('should apply filters when provided', async () => { 300 | // Arrange 301 | const mockSearchResponse = { 302 | data: { 303 | count: 1, 304 | results: [ 305 | { 306 | fileName: 'example.ts', 307 | path: '/src/example.ts', 308 | matches: { 309 | content: [ 310 | { 311 | charOffset: 17, 312 | length: 7, 313 | }, 314 | ], 315 | }, 316 | collection: { 317 | name: 'DefaultCollection', 318 | }, 319 | project: { 320 | name: 'TestProject', 321 | id: 'project-id', 322 | }, 323 | repository: { 324 | name: 'TestRepo', 325 | id: 'repo-id', 326 | type: 'git', 327 | }, 328 | versions: [ 329 | { 330 | branchName: 'main', 331 | changeId: 'commit-hash', 332 | }, 333 | ], 334 | contentId: 'content-hash', 335 | }, 336 | ], 337 | }, 338 | }; 339 | 340 | mockedAxios.post.mockResolvedValueOnce(mockSearchResponse); 341 | 342 | // Act 343 | await searchCode(mockConnection, { 344 | searchText: 'example', 345 | projectId: 'TestProject', 346 | filters: { 347 | Repository: ['TestRepo'], 348 | Path: ['/src'], 349 | Branch: ['main'], 350 | CodeElement: ['function'], 351 | }, 352 | }); 353 | 354 | // Assert 355 | expect(mockedAxios.post).toHaveBeenCalledWith( 356 | expect.any(String), 357 | expect.objectContaining({ 358 | filters: { 359 | Project: ['TestProject'], 360 | Repository: ['TestRepo'], 361 | Path: ['/src'], 362 | Branch: ['main'], 363 | CodeElement: ['function'], 364 | }, 365 | }), 366 | expect.any(Object), 367 | ); 368 | }); 369 | 370 | test('should handle pagination parameters', async () => { 371 | // Arrange 372 | const mockSearchResponse = { 373 | data: { 374 | count: 100, 375 | results: Array(10) 376 | .fill(0) 377 | .map((_, i) => ({ 378 | fileName: `example${i}.ts`, 379 | path: `/src/example${i}.ts`, 380 | matches: { 381 | content: [ 382 | { 383 | charOffset: 17, 384 | length: 7, 385 | }, 386 | ], 387 | }, 388 | collection: { 389 | name: 'DefaultCollection', 390 | }, 391 | project: { 392 | name: 'TestProject', 393 | id: 'project-id', 394 | }, 395 | repository: { 396 | name: 'TestRepo', 397 | id: 'repo-id', 398 | type: 'git', 399 | }, 400 | versions: [ 401 | { 402 | branchName: 'main', 403 | changeId: 'commit-hash', 404 | }, 405 | ], 406 | contentId: `content-hash-${i}`, 407 | })), 408 | }, 409 | }; 410 | 411 | mockedAxios.post.mockResolvedValueOnce(mockSearchResponse); 412 | 413 | // Act 414 | await searchCode(mockConnection, { 415 | searchText: 'example', 416 | projectId: 'TestProject', 417 | top: 10, 418 | skip: 20, 419 | }); 420 | 421 | // Assert 422 | expect(mockedAxios.post).toHaveBeenCalledWith( 423 | expect.any(String), 424 | expect.objectContaining({ 425 | $top: 10, 426 | $skip: 20, 427 | }), 428 | expect.any(Object), 429 | ); 430 | }); 431 | 432 | test('should handle errors when fetching file content', async () => { 433 | // Arrange 434 | const mockSearchResponse = { 435 | data: { 436 | count: 1, 437 | results: [ 438 | { 439 | fileName: 'example.ts', 440 | path: '/src/example.ts', 441 | matches: { 442 | content: [ 443 | { 444 | charOffset: 17, 445 | length: 7, 446 | }, 447 | ], 448 | }, 449 | collection: { 450 | name: 'DefaultCollection', 451 | }, 452 | project: { 453 | name: 'TestProject', 454 | id: 'project-id', 455 | }, 456 | repository: { 457 | name: 'TestRepo', 458 | id: 'repo-id', 459 | type: 'git', 460 | }, 461 | versions: [ 462 | { 463 | branchName: 'main', 464 | changeId: 'commit-hash', 465 | }, 466 | ], 467 | contentId: 'content-hash', 468 | }, 469 | ], 470 | }, 471 | }; 472 | 473 | mockedAxios.post.mockResolvedValueOnce(mockSearchResponse); 474 | 475 | // Mock Git API to throw an error 476 | const mockGitApi = { 477 | getItemContent: jest 478 | .fn() 479 | .mockRejectedValue(new Error('Failed to fetch content')), 480 | }; 481 | const mockConnectionWithError = { 482 | ...mockConnection, 483 | getGitApi: jest.fn().mockResolvedValue(mockGitApi), 484 | } as unknown as WebApi; 485 | 486 | // Act 487 | const result = await searchCode(mockConnectionWithError, { 488 | searchText: 'example', 489 | projectId: 'TestProject', 490 | includeContent: true, 491 | }); 492 | 493 | // Assert 494 | expect(result).toBeDefined(); 495 | expect(result.count).toBe(1); 496 | expect(result.results).toHaveLength(1); 497 | // Content should be undefined when there's an error fetching it 498 | expect(result.results[0].content).toBeUndefined(); 499 | }); 500 | 501 | test('should use default project when projectId is not provided', async () => { 502 | // Arrange 503 | // Set up environment variable for default project 504 | const originalEnv = process.env.AZURE_DEVOPS_DEFAULT_PROJECT; 505 | process.env.AZURE_DEVOPS_DEFAULT_PROJECT = 'DefaultProject'; 506 | 507 | const mockSearchResponse = { 508 | data: { 509 | count: 2, 510 | results: [ 511 | { 512 | fileName: 'example1.ts', 513 | path: '/src/example1.ts', 514 | matches: { 515 | content: [ 516 | { 517 | charOffset: 17, 518 | length: 7, 519 | }, 520 | ], 521 | }, 522 | collection: { 523 | name: 'DefaultCollection', 524 | }, 525 | project: { 526 | name: 'DefaultProject', 527 | id: 'default-project-id', 528 | }, 529 | repository: { 530 | name: 'Repo1', 531 | id: 'repo-id-1', 532 | type: 'git', 533 | }, 534 | versions: [ 535 | { 536 | branchName: 'main', 537 | changeId: 'commit-hash-1', 538 | }, 539 | ], 540 | contentId: 'content-hash-1', 541 | }, 542 | { 543 | fileName: 'example2.ts', 544 | path: '/src/example2.ts', 545 | matches: { 546 | content: [ 547 | { 548 | charOffset: 17, 549 | length: 7, 550 | }, 551 | ], 552 | }, 553 | collection: { 554 | name: 'DefaultCollection', 555 | }, 556 | project: { 557 | name: 'DefaultProject', 558 | id: 'default-project-id', 559 | }, 560 | repository: { 561 | name: 'Repo2', 562 | id: 'repo-id-2', 563 | type: 'git', 564 | }, 565 | versions: [ 566 | { 567 | branchName: 'main', 568 | changeId: 'commit-hash-2', 569 | }, 570 | ], 571 | contentId: 'content-hash-2', 572 | }, 573 | ], 574 | }, 575 | }; 576 | 577 | mockedAxios.post.mockResolvedValueOnce(mockSearchResponse); 578 | 579 | try { 580 | // Act 581 | const result = await searchCode(mockConnection, { 582 | searchText: 'example', 583 | includeContent: false, 584 | }); 585 | 586 | // Assert 587 | expect(result).toBeDefined(); 588 | expect(result.count).toBe(2); 589 | expect(result.results).toHaveLength(2); 590 | expect(result.results[0].project.name).toBe('DefaultProject'); 591 | expect(result.results[1].project.name).toBe('DefaultProject'); 592 | expect(mockedAxios.post).toHaveBeenCalledTimes(1); 593 | expect(mockedAxios.post).toHaveBeenCalledWith( 594 | expect.stringContaining( 595 | 'https://almsearch.dev.azure.com/testorg/DefaultProject/_apis/search/codesearchresults', 596 | ), 597 | expect.objectContaining({ 598 | filters: expect.objectContaining({ 599 | Project: ['DefaultProject'], 600 | }), 601 | }), 602 | expect.any(Object), 603 | ); 604 | } finally { 605 | // Restore original environment variable 606 | process.env.AZURE_DEVOPS_DEFAULT_PROJECT = originalEnv; 607 | } 608 | }); 609 | 610 | test('should throw error when no projectId is provided and no default project is set', async () => { 611 | // Arrange 612 | // Ensure no default project is set 613 | const originalEnv = process.env.AZURE_DEVOPS_DEFAULT_PROJECT; 614 | process.env.AZURE_DEVOPS_DEFAULT_PROJECT = ''; 615 | 616 | try { 617 | // Act & Assert 618 | await expect( 619 | searchCode(mockConnection, { 620 | searchText: 'example', 621 | includeContent: false, 622 | }), 623 | ).rejects.toThrow('Project ID is required'); 624 | } finally { 625 | // Restore original environment variable 626 | process.env.AZURE_DEVOPS_DEFAULT_PROJECT = originalEnv; 627 | } 628 | }); 629 | 630 | test('should handle includeContent for different content types', async () => { 631 | // Arrange 632 | const mockSearchResponse = { 633 | data: { 634 | count: 4, 635 | results: [ 636 | // Result 1 - Buffer content 637 | { 638 | fileName: 'example1.ts', 639 | path: '/src/example1.ts', 640 | matches: { 641 | content: [ 642 | { 643 | charOffset: 17, 644 | length: 7, 645 | }, 646 | ], 647 | }, 648 | collection: { 649 | name: 'DefaultCollection', 650 | }, 651 | project: { 652 | name: 'TestProject', 653 | id: 'project-id', 654 | }, 655 | repository: { 656 | name: 'TestRepo', 657 | id: 'repo-id-1', 658 | type: 'git', 659 | }, 660 | versions: [ 661 | { 662 | branchName: 'main', 663 | changeId: 'commit-hash-1', 664 | }, 665 | ], 666 | contentId: 'content-hash-1', 667 | }, 668 | // Result 2 - String content 669 | { 670 | fileName: 'example2.ts', 671 | path: '/src/example2.ts', 672 | matches: { 673 | content: [ 674 | { 675 | charOffset: 17, 676 | length: 7, 677 | }, 678 | ], 679 | }, 680 | collection: { 681 | name: 'DefaultCollection', 682 | }, 683 | project: { 684 | name: 'TestProject', 685 | id: 'project-id', 686 | }, 687 | repository: { 688 | name: 'TestRepo', 689 | id: 'repo-id-2', 690 | type: 'git', 691 | }, 692 | versions: [ 693 | { 694 | branchName: 'main', 695 | changeId: 'commit-hash-2', 696 | }, 697 | ], 698 | contentId: 'content-hash-2', 699 | }, 700 | // Result 3 - Object content 701 | { 702 | fileName: 'example3.ts', 703 | path: '/src/example3.ts', 704 | matches: { 705 | content: [ 706 | { 707 | charOffset: 17, 708 | length: 7, 709 | }, 710 | ], 711 | }, 712 | collection: { 713 | name: 'DefaultCollection', 714 | }, 715 | project: { 716 | name: 'TestProject', 717 | id: 'project-id', 718 | }, 719 | repository: { 720 | name: 'TestRepo', 721 | id: 'repo-id-3', 722 | type: 'git', 723 | }, 724 | versions: [ 725 | { 726 | branchName: 'main', 727 | changeId: 'commit-hash-3', 728 | }, 729 | ], 730 | contentId: 'content-hash-3', 731 | }, 732 | // Result 4 - Uint8Array content 733 | { 734 | fileName: 'example4.ts', 735 | path: '/src/example4.ts', 736 | matches: { 737 | content: [ 738 | { 739 | charOffset: 17, 740 | length: 7, 741 | }, 742 | ], 743 | }, 744 | collection: { 745 | name: 'DefaultCollection', 746 | }, 747 | project: { 748 | name: 'TestProject', 749 | id: 'project-id', 750 | }, 751 | repository: { 752 | name: 'TestRepo', 753 | id: 'repo-id-4', 754 | type: 'git', 755 | }, 756 | versions: [ 757 | { 758 | branchName: 'main', 759 | changeId: 'commit-hash-4', 760 | }, 761 | ], 762 | contentId: 'content-hash-4', 763 | }, 764 | ], 765 | }, 766 | }; 767 | 768 | mockedAxios.post.mockResolvedValueOnce(mockSearchResponse); 769 | 770 | // Create mock contents for each type - all as streams, since that's what getItemContent returns 771 | // These are all streams but with different content to demonstrate handling different data types from the stream 772 | const createMockStream = (content: string) => ({ 773 | on: jest.fn().mockImplementation((event, callback) => { 774 | if (event === 'data') { 775 | callback(Buffer.from(content)); 776 | } else if (event === 'end') { 777 | setTimeout(callback, 0); 778 | } 779 | return createMockStream(content); // Return this for chaining 780 | }), 781 | }); 782 | 783 | // Create four different mock streams with different content 784 | const mockStream1 = createMockStream('Buffer content'); 785 | const mockStream2 = createMockStream('String content'); 786 | const mockStream3 = createMockStream( 787 | JSON.stringify({ foo: 'bar', baz: 42 }), 788 | ); 789 | const mockStream4 = createMockStream('hello'); 790 | 791 | // Mock Git API to return our different mock streams for each repository 792 | const mockGitApi = { 793 | getItemContent: jest 794 | .fn() 795 | .mockImplementationOnce(() => Promise.resolve(mockStream1)) 796 | .mockImplementationOnce(() => Promise.resolve(mockStream2)) 797 | .mockImplementationOnce(() => Promise.resolve(mockStream3)) 798 | .mockImplementationOnce(() => Promise.resolve(mockStream4)), 799 | }; 800 | 801 | const mockConnectionWithStreams = { 802 | ...mockConnection, 803 | getGitApi: jest.fn().mockResolvedValue(mockGitApi), 804 | serverUrl: 'https://dev.azure.com/testorg', 805 | } as unknown as WebApi; 806 | 807 | // Act 808 | const result = await searchCode(mockConnectionWithStreams, { 809 | searchText: 'example', 810 | projectId: 'TestProject', 811 | includeContent: true, 812 | }); 813 | 814 | // Assert 815 | expect(result).toBeDefined(); 816 | expect(result.count).toBe(4); 817 | expect(result.results).toHaveLength(4); 818 | 819 | // Check each result has appropriate content from the streams 820 | // Result 1 - Buffer content stream 821 | expect(result.results[0].content).toBe('Buffer content'); 822 | 823 | // Result 2 - String content stream 824 | expect(result.results[1].content).toBe('String content'); 825 | 826 | // Result 3 - JSON object content stream 827 | expect(result.results[2].content).toBe('{"foo":"bar","baz":42}'); 828 | 829 | // Result 4 - Text content stream 830 | expect(result.results[3].content).toBe('hello'); 831 | 832 | // Git API should have been called 4 times 833 | expect(mockGitApi.getItemContent).toHaveBeenCalledTimes(4); 834 | // Verify the parameters for the first call 835 | expect(mockGitApi.getItemContent.mock.calls[0]).toEqual([ 836 | 'repo-id-1', 837 | '/src/example1.ts', 838 | 'TestProject', 839 | undefined, 840 | undefined, 841 | undefined, 842 | undefined, 843 | false, 844 | { 845 | version: 'commit-hash-1', 846 | versionType: GitVersionType.Commit, 847 | }, 848 | true, 849 | ]); 850 | }); 851 | 852 | test('should properly convert content stream to string', async () => { 853 | // Arrange 854 | const mockSearchResponse = { 855 | data: { 856 | count: 1, 857 | results: [ 858 | { 859 | fileName: 'example.ts', 860 | path: '/src/example.ts', 861 | matches: { 862 | content: [ 863 | { 864 | charOffset: 17, 865 | length: 7, 866 | }, 867 | ], 868 | }, 869 | collection: { 870 | name: 'DefaultCollection', 871 | }, 872 | project: { 873 | name: 'TestProject', 874 | id: 'project-id', 875 | }, 876 | repository: { 877 | name: 'TestRepo', 878 | id: 'repo-id', 879 | type: 'git', 880 | }, 881 | versions: [ 882 | { 883 | branchName: 'main', 884 | changeId: 'commit-hash', 885 | }, 886 | ], 887 | contentId: 'content-hash', 888 | }, 889 | ], 890 | }, 891 | }; 892 | 893 | mockedAxios.post.mockResolvedValueOnce(mockSearchResponse); 894 | 895 | // Create a mock ReadableStream 896 | const mockContent = 'This is the file content'; 897 | 898 | // Create a simplified mock stream that emits the content 899 | const mockStream = { 900 | on: jest.fn().mockImplementation((event, callback) => { 901 | if (event === 'data') { 902 | // Call the callback with the data 903 | callback(Buffer.from(mockContent)); 904 | } else if (event === 'end') { 905 | // Call the end callback asynchronously 906 | setTimeout(callback, 0); 907 | } 908 | return mockStream; // Return this for chaining 909 | }), 910 | }; 911 | 912 | // Mock Git API to return our mock stream 913 | const mockGitApi = { 914 | getItemContent: jest.fn().mockResolvedValue(mockStream), 915 | }; 916 | 917 | const mockConnectionWithStream = { 918 | ...mockConnection, 919 | getGitApi: jest.fn().mockResolvedValue(mockGitApi), 920 | serverUrl: 'https://dev.azure.com/testorg', 921 | } as unknown as WebApi; 922 | 923 | // Act 924 | const result = await searchCode(mockConnectionWithStream, { 925 | searchText: 'example', 926 | projectId: 'TestProject', 927 | includeContent: true, 928 | }); 929 | 930 | // Assert 931 | expect(result).toBeDefined(); 932 | expect(result.count).toBe(1); 933 | expect(result.results).toHaveLength(1); 934 | 935 | // Check that the content was properly converted from stream to string 936 | expect(result.results[0].content).toBe(mockContent); 937 | 938 | // Verify the stream event handlers were attached 939 | expect(mockStream.on).toHaveBeenCalledWith('data', expect.any(Function)); 940 | expect(mockStream.on).toHaveBeenCalledWith('end', expect.any(Function)); 941 | expect(mockStream.on).toHaveBeenCalledWith('error', expect.any(Function)); 942 | 943 | // Verify the parameters for getItemContent 944 | expect(mockGitApi.getItemContent).toHaveBeenCalledWith( 945 | 'repo-id', 946 | '/src/example.ts', 947 | 'TestProject', 948 | undefined, 949 | undefined, 950 | undefined, 951 | undefined, 952 | false, 953 | { 954 | version: 'commit-hash', 955 | versionType: GitVersionType.Commit, 956 | }, 957 | true, 958 | ); 959 | }); 960 | 961 | test('should limit top to 10 when includeContent is true', async () => { 962 | // Arrange 963 | const mockSearchResponse = { 964 | data: { 965 | count: 10, 966 | results: Array(10) 967 | .fill(0) 968 | .map((_, i) => ({ 969 | fileName: `example${i}.ts`, 970 | path: `/src/example${i}.ts`, 971 | matches: { 972 | content: [ 973 | { 974 | charOffset: 17, 975 | length: 7, 976 | }, 977 | ], 978 | }, 979 | collection: { 980 | name: 'DefaultCollection', 981 | }, 982 | project: { 983 | name: 'TestProject', 984 | id: 'project-id', 985 | }, 986 | repository: { 987 | name: 'TestRepo', 988 | id: 'repo-id', 989 | type: 'git', 990 | }, 991 | versions: [ 992 | { 993 | branchName: 'main', 994 | changeId: 'commit-hash', 995 | }, 996 | ], 997 | contentId: `content-hash-${i}`, 998 | })), 999 | }, 1000 | }; 1001 | 1002 | mockedAxios.post.mockResolvedValueOnce(mockSearchResponse); 1003 | 1004 | // For this test, we don't need to mock the Git API since we're only testing the top parameter 1005 | // We'll create a connection that doesn't have includeContent functionality 1006 | const mockConnectionWithoutContent = { 1007 | ...mockConnection, 1008 | getGitApi: jest.fn().mockImplementation(() => { 1009 | throw new Error('Git API not available'); 1010 | }), 1011 | serverUrl: 'https://dev.azure.com/testorg', 1012 | } as unknown as WebApi; 1013 | 1014 | // Act 1015 | await searchCode(mockConnectionWithoutContent, { 1016 | searchText: 'example', 1017 | projectId: 'TestProject', 1018 | top: 50, // User tries to get 50 results 1019 | includeContent: true, // But includeContent is true 1020 | }); 1021 | 1022 | // Assert 1023 | expect(mockedAxios.post).toHaveBeenCalledWith( 1024 | expect.any(String), 1025 | expect.objectContaining({ 1026 | $top: 10, // Should be limited to 10 1027 | }), 1028 | expect.any(Object), 1029 | ); 1030 | }); 1031 | 1032 | test('should not limit top when includeContent is false', async () => { 1033 | // Arrange 1034 | const mockSearchResponse = { 1035 | data: { 1036 | count: 50, 1037 | results: Array(50) 1038 | .fill(0) 1039 | .map((_, i) => ({ 1040 | // ... simplified result object 1041 | fileName: `example${i}.ts`, 1042 | })), 1043 | }, 1044 | }; 1045 | 1046 | mockedAxios.post.mockResolvedValueOnce(mockSearchResponse); 1047 | 1048 | // Act 1049 | await searchCode(mockConnection, { 1050 | searchText: 'example', 1051 | projectId: 'TestProject', 1052 | top: 50, // User wants 50 results 1053 | includeContent: false, // includeContent is false 1054 | }); 1055 | 1056 | // Assert 1057 | expect(mockedAxios.post).toHaveBeenCalledWith( 1058 | expect.any(String), 1059 | expect.objectContaining({ 1060 | $top: 50, // Should use requested value 1061 | }), 1062 | expect.any(Object), 1063 | ); 1064 | }); 1065 | }); 1066 | ```