This is page 5 of 6. Use http://codebase.md/tiberriver256/azure-devops-mcp?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 -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- ```typescript import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { WebApi } from 'azure-devops-node-api'; import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { VERSION } from './shared/config'; import { AzureDevOpsConfig } from './shared/types'; import { AzureDevOpsAuthenticationError, AzureDevOpsError, AzureDevOpsResourceNotFoundError, AzureDevOpsValidationError, } from './shared/errors'; import { handleResponseError } from './shared/errors/handle-request-error'; import { AuthenticationMethod, AzureDevOpsClient } from './shared/auth'; // Import environment defaults when needed in feature handlers // Import feature modules with request handlers and tool definitions import { workItemsTools, isWorkItemsRequest, handleWorkItemsRequest, } from './features/work-items'; import { projectsTools, isProjectsRequest, handleProjectsRequest, } from './features/projects'; import { repositoriesTools, isRepositoriesRequest, handleRepositoriesRequest, } from './features/repositories'; import { organizationsTools, isOrganizationsRequest, handleOrganizationsRequest, } from './features/organizations'; import { searchTools, isSearchRequest, handleSearchRequest, } from './features/search'; import { usersTools, isUsersRequest, handleUsersRequest, } from './features/users'; import { pullRequestsTools, isPullRequestsRequest, handlePullRequestsRequest, } from './features/pull-requests'; import { pipelinesTools, isPipelinesRequest, handlePipelinesRequest, } from './features/pipelines'; import { wikisTools, isWikisRequest, handleWikisRequest, } from './features/wikis'; // Create a safe console logging function that won't interfere with MCP protocol function safeLog(message: string) { process.stderr.write(`${message}\n`); } /** * Type definition for the Azure DevOps MCP Server */ export type AzureDevOpsServer = Server; /** * Create an Azure DevOps MCP Server * * @param config The Azure DevOps configuration * @returns A configured MCP server instance */ export function createAzureDevOpsServer(config: AzureDevOpsConfig): Server { // Validate the configuration validateConfig(config); // Initialize the MCP server const server = new Server( { name: 'azure-devops-mcp', version: VERSION, }, { capabilities: { tools: {}, resources: {}, }, }, ); // Register the ListTools request handler server.setRequestHandler(ListToolsRequestSchema, () => { // Combine tools from all features const tools = [ ...usersTools, ...organizationsTools, ...projectsTools, ...repositoriesTools, ...workItemsTools, ...searchTools, ...pullRequestsTools, ...pipelinesTools, ...wikisTools, ]; return { tools }; }); // Register the resource handlers // ListResources - register available resource templates server.setRequestHandler(ListResourcesRequestSchema, async () => { // Create resource templates for repository content const templates = [ // Default branch content { uriTemplate: 'ado://{organization}/{project}/{repo}/contents{/path*}', name: 'Repository Content', description: 'Content from the default branch of a repository', }, // Branch specific content { uriTemplate: 'ado://{organization}/{project}/{repo}/branches/{branch}/contents{/path*}', name: 'Branch Content', description: 'Content from a specific branch of a repository', }, // Commit specific content { uriTemplate: 'ado://{organization}/{project}/{repo}/commits/{commit}/contents{/path*}', name: 'Commit Content', description: 'Content from a specific commit in a repository', }, // Tag specific content { uriTemplate: 'ado://{organization}/{project}/{repo}/tags/{tag}/contents{/path*}', name: 'Tag Content', description: 'Content from a specific tag in a repository', }, // Pull request specific content { uriTemplate: 'ado://{organization}/{project}/{repo}/pullrequests/{prId}/contents{/path*}', name: 'Pull Request Content', description: 'Content from a specific pull request in a repository', }, ]; return { resources: [], templates, }; }); // ReadResource - handle reading content from the templates server.setRequestHandler(ReadResourceRequestSchema, async (request) => { try { const uri = new URL(request.params.uri); // Parse the URI to extract components const segments = uri.pathname.split('/').filter(Boolean); // Check if it's an Azure DevOps resource URI if (uri.protocol !== 'ado:') { throw new AzureDevOpsResourceNotFoundError( `Unsupported protocol: ${uri.protocol}`, ); } // Extract organization, project, and repo // const organization = segments[0]; // Currently unused but kept for future use const project = segments[1]; const repo = segments[2]; // Get a connection to Azure DevOps const connection = await getConnection(config); // Default path is root if not specified let path = '/'; // Extract path from the remaining segments, if there are at least 5 segments (org/project/repo/contents/path) if (segments.length >= 5 && segments[3] === 'contents') { path = '/' + segments.slice(4).join('/'); } // Determine version control parameters based on URI pattern let versionType: number | undefined; let version: string | undefined; if (segments[3] === 'branches' && segments.length >= 5) { versionType = GitVersionType.Branch; version = segments[4]; // Extract path if present if (segments.length >= 7 && segments[5] === 'contents') { path = '/' + segments.slice(6).join('/'); } } else if (segments[3] === 'commits' && segments.length >= 5) { versionType = GitVersionType.Commit; version = segments[4]; // Extract path if present if (segments.length >= 7 && segments[5] === 'contents') { path = '/' + segments.slice(6).join('/'); } } else if (segments[3] === 'tags' && segments.length >= 5) { versionType = GitVersionType.Tag; version = segments[4]; // Extract path if present if (segments.length >= 7 && segments[5] === 'contents') { path = '/' + segments.slice(6).join('/'); } } else if (segments[3] === 'pullrequests' && segments.length >= 5) { // TODO: For PR head, we need to get the source branch or commit // Currently just use the default branch as a fallback // versionType = GitVersionType.Branch; // version = 'PR-' + segments[4]; // Extract path if present if (segments.length >= 7 && segments[5] === 'contents') { path = '/' + segments.slice(6).join('/'); } } // Get the content const versionDescriptor = versionType && version ? { versionType, version } : undefined; // Import the getFileContent function from repositories feature const { getFileContent } = await import( './features/repositories/get-file-content/index.js' ); const fileContent = await getFileContent( connection, project, repo, path, versionDescriptor, ); // Return the content based on whether it's a file or directory return { contents: [ { uri: request.params.uri, mimeType: fileContent.isDirectory ? 'application/json' : getMimeType(path), text: fileContent.content, }, ], }; } catch (error) { safeLog(`Error reading resource: ${error}`); if (error instanceof AzureDevOpsError) { throw error; } throw new AzureDevOpsResourceNotFoundError( `Failed to read resource: ${error instanceof Error ? error.message : String(error)}`, ); } }); // Register the CallTool request handler server.setRequestHandler(CallToolRequestSchema, async (request) => { try { // Note: We don't need to validate the presence of arguments here because: // 1. The schema validations (via zod.parse) will check for required parameters // 2. Default values from environment.ts are applied for optional parameters (projectId, organizationId) // 3. Arguments can be omitted entirely for tools with no required parameters // Get a connection to Azure DevOps const connection = await getConnection(config); // Route the request to the appropriate feature handler if (isWorkItemsRequest(request)) { return await handleWorkItemsRequest(connection, request); } if (isProjectsRequest(request)) { return await handleProjectsRequest(connection, request); } if (isRepositoriesRequest(request)) { return await handleRepositoriesRequest(connection, request); } if (isOrganizationsRequest(request)) { // Organizations feature doesn't need the config object anymore return await handleOrganizationsRequest(connection, request); } if (isSearchRequest(request)) { return await handleSearchRequest(connection, request); } if (isUsersRequest(request)) { return await handleUsersRequest(connection, request); } if (isPullRequestsRequest(request)) { return await handlePullRequestsRequest(connection, request); } if (isPipelinesRequest(request)) { return await handlePipelinesRequest(connection, request); } if (isWikisRequest(request)) { return await handleWikisRequest(connection, request); } // If we get here, the tool is not recognized by any feature handler throw new Error(`Unknown tool: ${request.params.name}`); } catch (error) { return handleResponseError(error); } }); return server; } /** * Get a mime type based on file extension * * @param path File path * @returns Mime type string */ function getMimeType(path: string): string { const extension = path.split('.').pop()?.toLowerCase(); switch (extension) { case 'txt': return 'text/plain'; case 'html': case 'htm': return 'text/html'; case 'css': return 'text/css'; case 'js': return 'application/javascript'; case 'json': return 'application/json'; case 'xml': return 'application/xml'; case 'md': return 'text/markdown'; case 'png': return 'image/png'; case 'jpg': case 'jpeg': return 'image/jpeg'; case 'gif': return 'image/gif'; case 'webp': return 'image/webp'; case 'svg': return 'image/svg+xml'; case 'pdf': return 'application/pdf'; case 'ts': case 'tsx': return 'application/typescript'; case 'py': return 'text/x-python'; case 'cs': return 'text/x-csharp'; case 'java': return 'text/x-java'; case 'c': return 'text/x-c'; case 'cpp': case 'cc': return 'text/x-c++'; case 'go': return 'text/x-go'; case 'rs': return 'text/x-rust'; case 'rb': return 'text/x-ruby'; case 'sh': return 'text/x-sh'; case 'yaml': case 'yml': return 'text/yaml'; default: return 'text/plain'; } } /** * Validate the Azure DevOps configuration * * @param config The configuration to validate * @throws {AzureDevOpsValidationError} If the configuration is invalid */ function validateConfig(config: AzureDevOpsConfig): void { if (!config.organizationUrl) { process.stderr.write( 'ERROR: Organization URL is required but was not provided.\n', ); process.stderr.write( `Config: ${JSON.stringify( { organizationUrl: config.organizationUrl, authMethod: config.authMethod, defaultProject: config.defaultProject, // Hide PAT for security personalAccessToken: config.personalAccessToken ? 'REDACTED' : undefined, apiVersion: config.apiVersion, }, null, 2, )}\n`, ); throw new AzureDevOpsValidationError('Organization URL is required'); } // Set default authentication method if not specified if (!config.authMethod) { config.authMethod = AuthenticationMethod.AzureIdentity; } // Validate PAT if using PAT authentication if ( config.authMethod === AuthenticationMethod.PersonalAccessToken && !config.personalAccessToken ) { throw new AzureDevOpsValidationError( 'Personal access token is required when using PAT authentication', ); } } /** * Create a connection to Azure DevOps * * @param config The configuration to use * @returns A WebApi connection */ export async function getConnection( config: AzureDevOpsConfig, ): Promise<WebApi> { try { // Create a client with the appropriate authentication method const client = new AzureDevOpsClient({ method: config.authMethod || AuthenticationMethod.AzureIdentity, organizationUrl: config.organizationUrl, personalAccessToken: config.personalAccessToken, }); // Test the connection by getting the Core API await client.getCoreApi(); // Return the underlying WebApi client return await client.getWebApiClient(); } catch (error) { throw new AzureDevOpsAuthenticationError( `Failed to connect to Azure DevOps: ${error instanceof Error ? error.message : String(error)}`, ); } } ``` -------------------------------------------------------------------------------- /src/features/pull-requests/update-pull-request/feature.spec.int.ts: -------------------------------------------------------------------------------- ```typescript import { WebApi } from 'azure-devops-node-api'; import { updatePullRequest } from './feature'; import { createPullRequest } from '../create-pull-request/feature'; import { listWorkItems } from '../../work-items/list-work-items/feature'; import { getTestConnection, shouldSkipIntegrationTest, } from '@/shared/test/test-helpers'; describe('updatePullRequest integration', () => { let connection: WebApi | null = null; let projectName: string; let repositoryName: string; let pullRequestId: number; let workItemId: number | null = null; // Generate unique identifiers using timestamp const timestamp = Date.now(); const randomSuffix = Math.floor(Math.random() * 1000); const uniqueBranchName = `test-branch-${timestamp}-${randomSuffix}`; const uniqueTitle = `Test PR ${timestamp}-${randomSuffix}`; const updatedTitle = `Updated PR ${timestamp}-${randomSuffix}`; beforeAll(async () => { // Skip if integration tests should be skipped if (shouldSkipIntegrationTest()) { return; } // Get a real connection using environment variables connection = await getTestConnection(); // Get project and repository names from environment variables projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject'; repositoryName = process.env.AZURE_DEVOPS_DEFAULT_REPOSITORY || 'DefaultRepo'; // Find an existing work item to use in tests if (!connection) { throw new Error('Connection is null'); } const workItems = await listWorkItems(connection, { projectId: projectName, top: 1, // Just need one work item }); if (workItems && workItems.length > 0 && workItems[0].id) { workItemId = workItems[0].id; } // Create a test pull request or find an existing one const gitApi = await connection.getGitApi(); // Get the default branch's object ID const repository = await gitApi.getRepository(repositoryName, projectName); const defaultBranch = repository.defaultBranch?.replace('refs/heads/', '') || 'main'; // Get the latest commit on the default branch const commits = await gitApi.getCommits( repositoryName, { $top: 1, itemVersion: { version: defaultBranch, versionType: 0, // 0 = branch }, }, projectName, ); if (!commits || commits.length === 0) { throw new Error('No commits found in repository'); } // Create a new branch const refUpdate = { name: `refs/heads/${uniqueBranchName}`, oldObjectId: '0000000000000000000000000000000000000000', newObjectId: commits[0].commitId, }; const updateResult = await gitApi.updateRefs( [refUpdate], repositoryName, projectName, ); if ( !updateResult || updateResult.length === 0 || !updateResult[0].success ) { throw new Error('Failed to create new branch'); } // Create a test pull request const testPullRequest = await createPullRequest( connection, projectName, repositoryName, { title: uniqueTitle, description: 'Test pull request for integration testing', sourceRefName: `refs/heads/${uniqueBranchName}`, targetRefName: repository.defaultBranch || 'refs/heads/main', isDraft: true, }, ); pullRequestId = testPullRequest.pullRequestId!; }); afterAll(async () => { // Clean up created resources if (!shouldSkipIntegrationTest() && connection && pullRequestId) { try { // Check the current state of the pull request const gitApi = await connection.getGitApi(); const pullRequest = await gitApi.getPullRequestById( pullRequestId, projectName, ); // Only try to abandon if it's still active (status 1) if (pullRequest && pullRequest.status === 1) { await gitApi.updatePullRequest( { status: 2, // 2 = Abandoned }, repositoryName, pullRequestId, projectName, ); } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (_) { // Ignore cleanup errors } } }); test('should update pull request title and description', async () => { // Skip if integration tests should be skipped if (shouldSkipIntegrationTest() || !connection) { console.log('Skipping test due to missing connection'); return; } const updatedDescription = 'Updated description for integration testing'; const result = await updatePullRequest({ projectId: projectName, repositoryId: repositoryName, pullRequestId, title: updatedTitle, description: updatedDescription, }); // Verify the update was successful expect(result).toBeDefined(); expect(result.pullRequestId).toBe(pullRequestId); expect(result.title).toBe(updatedTitle); expect(result.description).toBe(updatedDescription); }, 30000); // 30 second timeout for integration test test('should update pull request draft status', async () => { // Skip if integration tests should be skipped if (shouldSkipIntegrationTest() || !connection) { console.log('Skipping test due to missing connection'); return; } // Mark as not a draft const result = await updatePullRequest({ projectId: projectName, repositoryId: repositoryName, pullRequestId, isDraft: false, }); // Verify the update was successful expect(result).toBeDefined(); expect(result.pullRequestId).toBe(pullRequestId); expect(result.isDraft).toBe(false); }, 30000); // 30 second timeout for integration test test('should add work item links to pull request', async () => { // Skip if no work items were found if (shouldSkipIntegrationTest()) { console.log('Skipping test due to missing connection or work item'); return; } // Add the work item link const result = await updatePullRequest({ projectId: projectName, repositoryId: repositoryName, pullRequestId, addWorkItemIds: [workItemId!], }); // Verify the update was successful expect(result).toBeDefined(); expect(result.pullRequestId).toBe(pullRequestId); // Get the pull request work items using the proper API const gitApi = await connection!.getGitApi(); // Add a delay to allow Azure DevOps to process the work item link await new Promise((resolve) => setTimeout(resolve, 5000)); // Use the getPullRequestWorkItemRefs method to get the work items const workItemRefs = await gitApi.getPullRequestWorkItemRefs( repositoryName, pullRequestId, projectName, ); // Verify that work items are linked expect(workItemRefs).toBeDefined(); expect(Array.isArray(workItemRefs)).toBe(true); // Check if our work item is in the list const hasWorkItem = workItemRefs.some( (ref) => ref.id !== undefined && Number(ref.id) === workItemId, ); expect(hasWorkItem).toBe(true); }, 60000); // 60 second timeout for integration test test('should remove work item links from pull request', async () => { // Skip if no work items were found if (shouldSkipIntegrationTest()) { console.log('Skipping test due to missing connection or work item'); return; } // First ensure the work item is linked try { await updatePullRequest({ projectId: projectName, repositoryId: repositoryName, pullRequestId, addWorkItemIds: [workItemId!], }); // Add a delay to allow Azure DevOps to process the work item link await new Promise((resolve) => setTimeout(resolve, 3000)); } catch (error) { // If there's an error adding the link, that's okay console.log( "Error adding work item (already be linked so that's 👍):", error instanceof Error ? error.message : String(error), ); } // Then remove the work item link const result = await updatePullRequest({ projectId: projectName, repositoryId: repositoryName, pullRequestId, removeWorkItemIds: [workItemId!], }); // Verify the update was successful expect(result).toBeDefined(); expect(result.pullRequestId).toBe(pullRequestId); // Get the pull request work items using the proper API const gitApi = await connection!.getGitApi(); // Add a delay to allow Azure DevOps to process the work item unlink await new Promise((resolve) => setTimeout(resolve, 5000)); // Use the getPullRequestWorkItemRefs method to get the work items const workItemRefs = await gitApi.getPullRequestWorkItemRefs( repositoryName, pullRequestId, projectName, ); // Verify that work items are properly unlinked expect(workItemRefs).toBeDefined(); expect(Array.isArray(workItemRefs)).toBe(true); // Check if our work item is not in the list const hasWorkItem = workItemRefs.some( (ref) => ref.id !== undefined && Number(ref.id) === workItemId, ); expect(hasWorkItem).toBe(false); }, 60000); // 60 second timeout for integration test test('should add reviewers to pull request', async () => { // Skip if integration tests should be skipped if (shouldSkipIntegrationTest() || !connection) { console.log('Skipping test due to missing connection'); return; } // Find an actual user in the organization to use as a reviewer const gitApi = await connection.getGitApi(); // Get the pull request creator as a reviewer (they always exist) const pullRequest = await gitApi.getPullRequestById( pullRequestId, projectName, )!; // Use the pull request creator's ID as the reviewer const reviewer = pullRequest.createdBy!.id!; // Add the reviewer const result = await updatePullRequest({ projectId: projectName, repositoryId: repositoryName, pullRequestId, addReviewers: [reviewer], }); // Verify the update was successful expect(result).toBeDefined(); expect(result.pullRequestId).toBe(pullRequestId); // Add a delay to allow Azure DevOps to process the reviewer addition await new Promise((resolve) => setTimeout(resolve, 1000)); const reviewers = await gitApi.getPullRequestReviewers( repositoryName, pullRequestId, projectName, ); // Verify that the reviewer was added expect(reviewers).toBeDefined(); expect(Array.isArray(reviewers)).toBe(true); // Check if our reviewer is in the list by ID const hasReviewer = reviewers.some((r) => r.id === reviewer); expect(hasReviewer).toBe(true); }, 60000); // 60 second timeout for integration test test('should remove reviewers from pull request', async () => { // Skip if integration tests should be skipped if (shouldSkipIntegrationTest() || !connection) { console.log('Skipping test due to missing connection'); return; } // Find an actual user in the organization to use as a reviewer const gitApi = await connection.getGitApi(); // Get the pull request creator as a reviewer (they always exist) const pullRequest = await gitApi.getPullRequestById( pullRequestId, projectName, ); if (!pullRequest || !pullRequest.createdBy || !pullRequest.createdBy.id) { throw new Error('Could not determine pull request creator'); } // Use the pull request creator's ID as the reviewer const reviewer = pullRequest.createdBy.id; // First ensure the reviewer is added try { await updatePullRequest({ projectId: projectName, repositoryId: repositoryName, pullRequestId, addReviewers: [reviewer], }); // Add a delay to allow Azure DevOps to process the reviewer addition await new Promise((resolve) => setTimeout(resolve, 3000)); } catch (error) { // If there's an error adding the reviewer, that's okay console.log( 'Error adding reviewer (might already be added):', error instanceof Error ? error.message : String(error), ); } // Then remove the reviewer const result = await updatePullRequest({ projectId: projectName, repositoryId: repositoryName, pullRequestId, removeReviewers: [reviewer], }); // Verify the update was successful expect(result).toBeDefined(); expect(result.pullRequestId).toBe(pullRequestId); // Add a delay to allow Azure DevOps to process the reviewer removal await new Promise((resolve) => setTimeout(resolve, 3000)); const reviewers = await gitApi.getPullRequestReviewers( repositoryName, pullRequestId, projectName, ); // Verify that the reviewer was removed expect(reviewers).toBeDefined(); expect(Array.isArray(reviewers)).toBe(true); // Check if our reviewer is not in the list const hasReviewer = reviewers.some((r) => r.id === reviewer); expect(hasReviewer).toBe(false); }, 60000); // 60 second timeout for integration test test('should update pull request with additional properties', async () => { // Skip if integration tests should be skipped if (shouldSkipIntegrationTest() || !connection) { console.log('Skipping test due to missing connection'); return; } // Use a custom property that Azure DevOps supports const customProperty = 'autoComplete'; const customValue = true; const result = await updatePullRequest({ projectId: projectName, repositoryId: repositoryName, pullRequestId, additionalProperties: { [customProperty]: customValue, }, }); // Verify the update was successful expect(result).toBeDefined(); expect(result.pullRequestId).toBe(pullRequestId); // For autoComplete specifically, we can check if it's in the response if (customProperty in result) { expect(result[customProperty]).toBe(customValue); } }, 30000); // 30 second timeout for integration test test('should update pull request status to abandoned', async () => { // Skip if integration tests should be skipped if (shouldSkipIntegrationTest() || !connection) { console.log('Skipping test due to missing connection'); return; } // Abandon the pull request instead of completing it // Completing requires additional setup that's complex for integration tests const result = await updatePullRequest({ projectId: projectName, repositoryId: repositoryName, pullRequestId, status: 'abandoned', }); // Verify the update was successful expect(result).toBeDefined(); expect(result.pullRequestId).toBe(pullRequestId); expect(result.status).toBe(2); // 2 = Abandoned }, 30000); // 30 second timeout for integration test }); ``` -------------------------------------------------------------------------------- /src/features/wikis/list-wiki-pages/feature.spec.unit.ts: -------------------------------------------------------------------------------- ```typescript import { listWikiPages, WikiPageSummary } from './feature'; import * as azureDevOpsClient from '../../../clients/azure-devops'; import { AzureDevOpsError, AzureDevOpsResourceNotFoundError, AzureDevOpsPermissionError, } from '../../../shared/errors/azure-devops-errors'; // Mock the Azure DevOps client jest.mock('../../../clients/azure-devops'); // Mock the environment utilities to avoid dependency on environment variables jest.mock('../../../utils/environment', () => ({ defaultOrg: 'azure-devops-mcp-testing', defaultProject: 'eShopOnWeb', })); describe('listWikiPages unit', () => { // Mock WikiClient const mockWikiClient = { listWikiPages: jest.fn(), }; // Mock getWikiClient function const mockGetWikiClient = azureDevOpsClient.getWikiClient as jest.MockedFunction< typeof azureDevOpsClient.getWikiClient >; beforeEach(() => { // Clear mock calls between tests jest.clearAllMocks(); // Setup default mock implementation mockGetWikiClient.mockResolvedValue(mockWikiClient as any); }); describe('Happy Path Scenarios', () => { test('should return wiki pages successfully', async () => { // Mock data const mockPages: WikiPageSummary[] = [ { id: 1, path: '/Home', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/1', order: 1, }, { id: 2, path: '/Getting-Started', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/2', order: 2, }, ]; // Setup mock responses mockWikiClient.listWikiPages.mockResolvedValue(mockPages); // Call the function const result = await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }); // Assertions expect(mockGetWikiClient).toHaveBeenCalledWith({ organizationId: 'test-org', }); expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith( 'test-project', 'test-wiki', ); expect(result).toEqual(mockPages); expect(result.length).toBe(2); }); test('should handle basic listing without parameters', async () => { const mockPages: WikiPageSummary[] = [ { id: 3, path: '/docs/api', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/3', order: 1, }, ]; mockWikiClient.listWikiPages.mockResolvedValue(mockPages); const result = await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }); expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith( 'test-project', 'test-wiki', ); expect(result).toEqual(mockPages); }); test('should handle nested pages correctly', async () => { const mockPages: WikiPageSummary[] = [ { id: 4, path: '/deep/nested/page', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/4', order: 1, }, ]; mockWikiClient.listWikiPages.mockResolvedValue(mockPages); const result = await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }); expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith( 'test-project', 'test-wiki', ); expect(result).toEqual(mockPages); }); test('should handle empty wiki correctly', async () => { const mockPages: WikiPageSummary[] = []; mockWikiClient.listWikiPages.mockResolvedValue(mockPages); const result = await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }); expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith( 'test-project', 'test-wiki', ); expect(result).toEqual(mockPages); }); test('should return empty array when no pages found', async () => { mockWikiClient.listWikiPages.mockResolvedValue([]); const result = await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'empty-wiki', }); expect(result).toEqual([]); expect(Array.isArray(result)).toBe(true); }); test('should use default organization and project when not provided', async () => { const mockPages: WikiPageSummary[] = [ { id: 5, path: '/default-page', url: 'https://dev.azure.com/default-org/default-project/_wiki/wikis/wiki1/5', order: 1, }, ]; mockWikiClient.listWikiPages.mockResolvedValue(mockPages); const result = await listWikiPages({ wikiId: 'test-wiki', }); expect(mockGetWikiClient).toHaveBeenCalledWith({ organizationId: 'azure-devops-mcp-testing', // Uses default from environment }); expect(result).toEqual(mockPages); }); }); describe('Error Scenarios', () => { test('should handle network timeout errors', async () => { const timeoutError = new Error('Network timeout'); timeoutError.name = 'ETIMEDOUT'; mockWikiClient.listWikiPages.mockRejectedValue(timeoutError); await expect( listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }), ).rejects.toThrow(AzureDevOpsError); }); test('should handle connection refused errors', async () => { const connectionError = new Error('Connection refused'); connectionError.name = 'ECONNREFUSED'; mockWikiClient.listWikiPages.mockRejectedValue(connectionError); await expect( listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }), ).rejects.toThrow(AzureDevOpsError); }); test('should propagate AzureDevOpsResourceNotFoundError from client', async () => { const notFoundError = new AzureDevOpsResourceNotFoundError( 'Wiki not found: test-wiki', ); mockWikiClient.listWikiPages.mockRejectedValue(notFoundError); await expect( listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'non-existent-wiki', }), ).rejects.toThrow(AzureDevOpsResourceNotFoundError); }); test('should propagate AzureDevOpsPermissionError from client', async () => { const permissionError = new AzureDevOpsPermissionError( 'Permission denied to access wiki', ); mockWikiClient.listWikiPages.mockRejectedValue(permissionError); await expect( listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'restricted-wiki', }), ).rejects.toThrow(AzureDevOpsPermissionError); }); test('should wrap unknown errors in AzureDevOpsError', async () => { const unknownError = new Error('Unknown error occurred'); mockWikiClient.listWikiPages.mockRejectedValue(unknownError); await expect( listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }), ).rejects.toThrow(AzureDevOpsError); try { await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }); } catch (error) { expect(error).toBeInstanceOf(AzureDevOpsError); expect((error as AzureDevOpsError).message).toBe( 'Failed to list wiki pages', ); } }); test('should handle client creation failure', async () => { const clientError = new Error('Failed to create client'); mockGetWikiClient.mockRejectedValue(clientError); await expect( listWikiPages({ organizationId: 'invalid-org', projectId: 'test-project', wikiId: 'test-wiki', }), ).rejects.toThrow(AzureDevOpsError); }); }); describe('Edge Cases and Input Validation', () => { test('should handle malformed API response gracefully', async () => { // Mock malformed response (missing required fields) const malformedPages = [ { id: 'invalid-id', // Should be number path: null, // Should be string url: undefined, // Should be string }, ]; mockWikiClient.listWikiPages.mockResolvedValue(malformedPages as any); const result = await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }); // Should still return the data as-is (transformation happens in client) expect(result).toEqual(malformedPages); }); test('should handle null/undefined response from client', async () => { mockWikiClient.listWikiPages.mockResolvedValue(null as any); await expect( listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }), ).rejects.toThrow(AzureDevOpsError); }); test('should handle very large page collections', async () => { // Create a large mock dataset const largeMockPages: WikiPageSummary[] = Array.from( { length: 10000 }, (_, i) => ({ id: i + 1, path: `/page-${i + 1}`, url: `https://dev.azure.com/org/project/_wiki/wikis/wiki1/${i + 1}`, order: i + 1, }), ); mockWikiClient.listWikiPages.mockResolvedValue(largeMockPages); const result = await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'large-wiki', }); expect(result).toEqual(largeMockPages); expect(result.length).toBe(10000); }); test('should handle pages with special characters in paths', async () => { const specialCharPages: WikiPageSummary[] = [ { id: 1, path: '/页面-中文', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/1', order: 1, }, { id: 2, path: '/página-español', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/2', order: 2, }, { id: 3, path: '/page with spaces & symbols!@#$%', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/3', order: 3, }, ]; mockWikiClient.listWikiPages.mockResolvedValue(specialCharPages); const result = await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'special-wiki', }); expect(result).toEqual(specialCharPages); }); test('should handle pages with missing optional order field', async () => { const pagesWithoutOrder: WikiPageSummary[] = [ { id: 1, path: '/page-1', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/1', // order field is optional and missing } as WikiPageSummary, { id: 2, path: '/page-2', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/2', order: 5, }, ]; mockWikiClient.listWikiPages.mockResolvedValue(pagesWithoutOrder); const result = await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }); expect(result).toEqual(pagesWithoutOrder); expect(result[0].order).toBeUndefined(); expect(result[1].order).toBe(5); }); }); describe('Parameter Validation Edge Cases', () => { test('should handle basic parameter validation', async () => { const mockPages: WikiPageSummary[] = []; mockWikiClient.listWikiPages.mockResolvedValue(mockPages); await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }); expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith( 'test-project', 'test-wiki', ); await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }); expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith( 'test-project', 'test-wiki', ); }); test('should handle empty string parameters', async () => { const mockPages: WikiPageSummary[] = []; mockWikiClient.listWikiPages.mockResolvedValue(mockPages); await listWikiPages({ organizationId: '', projectId: '', wikiId: 'test-wiki', }); expect(mockGetWikiClient).toHaveBeenCalledWith({ organizationId: 'azure-devops-mcp-testing', // Empty string gets overridden by default }); expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith( 'eShopOnWeb', // Empty string gets overridden by default project 'test-wiki', ); }); }); describe('Data Transformation and Mapping', () => { test('should preserve all WikiPageSummary fields correctly', async () => { const mockPages: WikiPageSummary[] = [ { id: 42, path: '/test-page', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/42', order: 10, }, ]; mockWikiClient.listWikiPages.mockResolvedValue(mockPages); const result = await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }); expect(result[0]).toEqual({ id: 42, path: '/test-page', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/42', order: 10, }); }); test('should handle mixed data types in response', async () => { const mixedPages = [ { id: 1, path: '/normal-page', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/1', order: 1, }, { id: 2, path: '/page-without-order', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/2', // order is undefined }, { id: 3, path: '/page-with-zero-order', url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/3', order: 0, }, ]; mockWikiClient.listWikiPages.mockResolvedValue( mixedPages as WikiPageSummary[], ); const result = await listWikiPages({ organizationId: 'test-org', projectId: 'test-project', wikiId: 'test-wiki', }); expect(result).toEqual(mixedPages); expect(result[1].order).toBeUndefined(); expect(result[2].order).toBe(0); }); }); }); ``` -------------------------------------------------------------------------------- /src/features/wikis/list-wiki-pages/feature.spec.int.ts: -------------------------------------------------------------------------------- ```typescript import { listWikiPages, WikiPageSummary } from './feature'; import { getWikis } from '../get-wikis/feature'; import { getTestConnection, shouldSkipIntegrationTest, } from '@/shared/test/test-helpers'; import { getOrgNameFromUrl } from '@/utils/environment'; import { AzureDevOpsError } from '@/shared/errors/azure-devops-errors'; // Ensure environment variables are set for testing process.env.AZURE_DEVOPS_DEFAULT_PROJECT = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'default-project'; describe('listWikiPages integration', () => { let projectName: string; let orgUrl: string; let organizationId: string; beforeAll(async () => { // Mock the required environment variable for testing process.env.AZURE_DEVOPS_ORG_URL = process.env.AZURE_DEVOPS_ORG_URL || 'https://example.visualstudio.com'; // Get and validate required environment variables const envProjectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT; if (!envProjectName) { throw new Error( 'AZURE_DEVOPS_DEFAULT_PROJECT environment variable is required', ); } projectName = envProjectName; const envOrgUrl = process.env.AZURE_DEVOPS_ORG_URL; if (!envOrgUrl) { throw new Error('AZURE_DEVOPS_ORG_URL environment variable is required'); } orgUrl = envOrgUrl; organizationId = getOrgNameFromUrl(orgUrl); }); describe('Happy Path Tests', () => { test('should list pages in real test wiki', async () => { // Skip if no connection available if (shouldSkipIntegrationTest()) { return; } // Get a real connection using environment variables const connection = await getTestConnection(); if (!connection) { throw new Error( 'Connection should be available when test is not skipped', ); } // First get available wikis const wikis = await getWikis(connection, { projectId: projectName }); // Skip if no wikis are available if (wikis.length === 0) { console.log('Skipping test: No wikis available in the project'); return; } // Use the first available wiki const wiki = wikis[0]; if (!wiki.name) { throw new Error('Wiki name is undefined'); } // List wiki pages const result = await listWikiPages({ organizationId, projectId: projectName, wikiId: wiki.name, }); // Verify the result structure expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); // If pages exist, verify their structure matches WikiPageSummary interface if (result.length > 0) { const page = result[0]; expect(page).toHaveProperty('id'); expect(page).toHaveProperty('path'); expect(page).toHaveProperty('url'); expect(typeof page.id).toBe('number'); expect(typeof page.path).toBe('string'); // url and order are optional if (page.url !== undefined) { expect(typeof page.url).toBe('string'); } if (page.order !== undefined) { expect(typeof page.order).toBe('number'); } } }); test('should handle wiki listing for different wiki structures', async () => { // Skip if integration tests are disabled or no connection available if (shouldSkipIntegrationTest()) { return; } // Get a real connection using environment variables const connection = await getTestConnection(); if (!connection) { throw new Error( 'Connection should be available when test is not skipped', ); } // First get available wikis const wikis = await getWikis(connection, { projectId: projectName }); // Skip if no wikis are available if (wikis.length === 0) { console.log('Skipping test: No wikis available in the project'); return; } // Use the first available wiki const wiki = wikis[0]; if (!wiki.name) { throw new Error('Wiki name is undefined'); } // Get all pages for different wiki structures const allPages = await listWikiPages({ organizationId, projectId: projectName, wikiId: wiki.name, }); expect(Array.isArray(allPages)).toBe(true); // If we have pages, verify they have expected structure if (allPages.length > 0) { const firstPage = allPages[0]; expect(firstPage).toHaveProperty('id'); expect(firstPage).toHaveProperty('path'); expect(firstPage).toHaveProperty('url'); // Verify nested pages if they exist const nestedPages = allPages.filter( (page) => page.path.includes('/') && page.path !== '/', ); console.log( `Found ${nestedPages.length} nested pages out of ${allPages.length} total pages`, ); } }); test('should handle basic wiki page listing consistently', async () => { // Skip if integration tests are disabled or no connection available if (shouldSkipIntegrationTest()) { return; } // Get a real connection using environment variables const connection = await getTestConnection(); if (!connection) { throw new Error( 'Connection should be available when test is not skipped', ); } // First get available wikis const wikis = await getWikis(connection, { projectId: projectName }); // Skip if no wikis are available if (wikis.length === 0) { console.log('Skipping test: No wikis available in the project'); return; } // Use the first available wiki const wiki = wikis[0]; if (!wiki.name) { throw new Error('Wiki name is undefined'); } // Test basic page listing const firstResult = await listWikiPages({ organizationId, projectId: projectName, wikiId: wiki.name, }); expect(Array.isArray(firstResult)).toBe(true); // Test again to ensure consistency const secondResult = await listWikiPages({ organizationId, projectId: projectName, wikiId: wiki.name, }); expect(Array.isArray(secondResult)).toBe(true); // Results should be consistent expect(secondResult.length).toBe(firstResult.length); }); }); describe('Error Scenarios', () => { test('should handle invalid wikiId (expect 404 error)', async () => { // Skip if integration tests are disabled or no connection available if (shouldSkipIntegrationTest()) { return; } const invalidWikiId = 'non-existent-wiki-id-12345'; await expect( listWikiPages({ organizationId, projectId: projectName, wikiId: invalidWikiId, }), ).rejects.toThrow(AzureDevOpsError); }); test('should handle invalid projectId', async () => { // Skip if integration tests are disabled or no connection available if (shouldSkipIntegrationTest()) { return; } const invalidProjectId = 'non-existent-project-12345'; await expect( listWikiPages({ organizationId, projectId: invalidProjectId, wikiId: 'any-wiki', }), ).rejects.toThrow(AzureDevOpsError); }); test('should handle invalid organizationId', async () => { // Skip if integration tests are disabled or no connection available if (shouldSkipIntegrationTest()) { return; } const invalidOrgId = 'non-existent-org-12345'; await expect( listWikiPages({ organizationId: invalidOrgId, projectId: projectName, wikiId: 'any-wiki', }), ).rejects.toThrow(AzureDevOpsError); }); }); describe('Edge Cases', () => { test('should handle empty wikis gracefully', async () => { // Skip if integration tests are disabled or no connection available if (shouldSkipIntegrationTest()) { return; } // Get a real connection using environment variables const connection = await getTestConnection(); if (!connection) { throw new Error( 'Connection should be available when test is not skipped', ); } // First get available wikis const wikis = await getWikis(connection, { projectId: projectName }); // Skip if no wikis are available if (wikis.length === 0) { console.log('Skipping test: No wikis available in the project'); return; } // Use the first available wiki const wiki = wikis[0]; if (!wiki.name) { throw new Error('Wiki name is undefined'); } // Test with a path that likely doesn't exist const result = await listWikiPages({ organizationId, projectId: projectName, wikiId: wiki.name, }); // Should return an array (may be empty or contain all pages depending on API behavior) expect(Array.isArray(result)).toBe(true); // Note: Azure DevOps API may return all pages when path doesn't match console.log(`Path filter test returned ${result.length} pages`); }); test('should handle deeply nested paths', async () => { // Skip if integration tests are disabled or no connection available if (shouldSkipIntegrationTest()) { return; } // Get a real connection using environment variables const connection = await getTestConnection(); if (!connection) { throw new Error( 'Connection should be available when test is not skipped', ); } // First get available wikis const wikis = await getWikis(connection, { projectId: projectName }); // Skip if no wikis are available if (wikis.length === 0) { console.log('Skipping test: No wikis available in the project'); return; } // Use the first available wiki const wiki = wikis[0]; if (!wiki.name) { throw new Error('Wiki name is undefined'); } // Test with default parameters const result = await listWikiPages({ organizationId, projectId: projectName, wikiId: wiki.name, }); expect(Array.isArray(result)).toBe(true); // Should not throw error with basic parameters }); test('should handle boundary recursionLevel values', async () => { // Skip if integration tests are disabled or no connection available if (shouldSkipIntegrationTest()) { return; } // Get a real connection using environment variables const connection = await getTestConnection(); if (!connection) { throw new Error( 'Connection should be available when test is not skipped', ); } // First get available wikis const wikis = await getWikis(connection, { projectId: projectName }); // Skip if no wikis are available if (wikis.length === 0) { console.log('Skipping test: No wikis available in the project'); return; } // Use the first available wiki const wiki = wikis[0]; if (!wiki.name) { throw new Error('Wiki name is undefined'); } // Test basic page listing const firstResult = await listWikiPages({ organizationId, projectId: projectName, wikiId: wiki.name, }); expect(Array.isArray(firstResult)).toBe(true); // Test again for consistency const secondResult = await listWikiPages({ organizationId, projectId: projectName, wikiId: wiki.name, }); expect(Array.isArray(secondResult)).toBe(true); }); }); describe('Data Structure Validation', () => { test('should verify returned data structure matches WikiPageSummary interface', async () => { // Skip if integration tests are disabled or no connection available if (shouldSkipIntegrationTest()) { return; } // Get a real connection using environment variables const connection = await getTestConnection(); if (!connection) { throw new Error( 'Connection should be available when test is not skipped', ); } // First get available wikis const wikis = await getWikis(connection, { projectId: projectName }); // Skip if no wikis are available if (wikis.length === 0) { console.log('Skipping test: No wikis available in the project'); return; } // Use the first available wiki const wiki = wikis[0]; if (!wiki.name) { throw new Error('Wiki name is undefined'); } const result = await listWikiPages({ organizationId, projectId: projectName, wikiId: wiki.name, }); expect(Array.isArray(result)).toBe(true); // Validate each page in the result result.forEach((page: WikiPageSummary) => { // Required fields expect(page).toHaveProperty('id'); expect(page).toHaveProperty('path'); expect(page).toHaveProperty('url'); expect(typeof page.id).toBe('number'); expect(typeof page.path).toBe('string'); // Optional fields if (page.url !== undefined) { expect(typeof page.url).toBe('string'); } if (page.order !== undefined) { expect(typeof page.order).toBe('number'); } // Validate URL format (if present) if (page.url !== undefined) { expect(page.url).toMatch(/^https?:\/\//); } // Validate path format (should start with /) expect(page.path).toMatch(/^\//); }); }); }); describe('Performance and Pagination', () => { test('should handle large wiki structures efficiently', async () => { // Skip if integration tests are disabled or no connection available if (shouldSkipIntegrationTest()) { return; } // Get a real connection using environment variables const connection = await getTestConnection(); if (!connection) { throw new Error( 'Connection should be available when test is not skipped', ); } // First get available wikis const wikis = await getWikis(connection, { projectId: projectName }); // Skip if no wikis are available if (wikis.length === 0) { console.log('Skipping test: No wikis available in the project'); return; } // Use the first available wiki const wiki = wikis[0]; if (!wiki.name) { throw new Error('Wiki name is undefined'); } const startTime = Date.now(); const result = await listWikiPages({ organizationId, projectId: projectName, wikiId: wiki.name, }); const endTime = Date.now(); const duration = endTime - startTime; expect(Array.isArray(result)).toBe(true); // Performance check - should complete within reasonable time (30 seconds) expect(duration).toBeLessThan(30000); console.log(`Retrieved ${result.length} pages in ${duration}ms`); }); }); }); ``` -------------------------------------------------------------------------------- /tasks.json: -------------------------------------------------------------------------------- ```json { "tasks": [ { "id": "42e6533a-f407-4286-be04-4d76fdfd8734", "name": "Create list-wiki-pages directory structure and schema", "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.", "status": "completed", "dependencies": [], "createdAt": "2025-05-26T16:18:03.641Z", "updatedAt": "2025-05-26T16:18:03.641Z", "relatedFiles": [ { "path": "src/features/wikis/list-wiki-pages/schema.ts", "type": "CREATE", "description": "Zod schema for list wiki pages parameters" }, { "path": "src/features/wikis/list-wiki-pages/index.ts", "type": "CREATE", "description": "Export file for list wiki pages feature" }, { "path": "src/features/wikis/get-wikis/schema.ts", "type": "REFERENCE", "description": "Reference pattern for schema structure" }, { "path": "src/utils/environment.ts", "type": "REFERENCE", "description": "Default organization and project utilities" } ], "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", "verificationCriteria": "Schema compiles without errors, exports are properly defined, follows existing naming conventions, includes proper TypeScript types and Zod validation", "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." }, { "id": "6b895c15-b337-444b-908a-e50a5ae07da3", "name": "Extend WikiClient with listWikiPages method", "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.", "status": "completed", "dependencies": [], "createdAt": "2025-05-26T16:18:03.641Z", "updatedAt": "2025-05-26T21:57:06.473Z", "relatedFiles": [ { "path": "src/clients/azure-devops.ts", "type": "TO_MODIFY", "description": "Add listWikiPages method to WikiClient class", "lineStart": 45, "lineEnd": 532 } ], "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", "verificationCriteria": "Method compiles without errors, implements proper pagination loop, handles all error cases, returns correctly typed results, follows existing WikiClient method patterns", "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.", "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.", "completedAt": "2025-05-26T21:57:06.472Z" }, { "id": "6f042d63-fa61-42c9-b7b0-820495aec9ba", "name": "Implement list-wiki-pages feature function", "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.", "status": "completed", "dependencies": [ { "taskId": "42e6533a-f407-4286-be04-4d76fdfd8734" }, { "taskId": "6b895c15-b337-444b-908a-e50a5ae07da3" } ], "createdAt": "2025-05-26T16:18:03.641Z", "updatedAt": "2025-05-26T22:44:06.001Z", "relatedFiles": [ { "path": "src/features/wikis/list-wiki-pages/feature.ts", "type": "CREATE", "description": "Main feature implementation" } ], "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", "verificationCriteria": "Feature function compiles and exports correctly, proper error handling, type safety maintained, follows existing feature patterns, integrates properly with WikiClient", "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.", "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.", "completedAt": "2025-05-26T22:44:06.000Z" }, { "id": "29f527f5-a069-4c2d-900b-eb3c7ac478d2", "name": "Add tool definition and update wikis module exports", "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.", "status": "completed", "dependencies": [ { "taskId": "42e6533a-f407-4286-be04-4d76fdfd8734" }, { "taskId": "6f042d63-fa61-42c9-b7b0-820495aec9ba" } ], "createdAt": "2025-05-26T16:18:03.641Z", "updatedAt": "2025-05-26T22:52:55.297Z", "relatedFiles": [ { "path": "src/features/wikis/tool-definitions.ts", "type": "TO_MODIFY", "description": "Add list_wiki_pages tool definition" }, { "path": "src/features/wikis/index.ts", "type": "TO_MODIFY", "description": "Add exports and request handler case" } ], "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", "verificationCriteria": "Tool definition is properly added, exports are correct, request handler case works, follows existing patterns for tool registration and handling", "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.", "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.", "completedAt": "2025-05-26T22:52:55.296Z" }, { "id": "457c0d1b-3635-49d7-916e-0e9aeb4f370f", "name": "Implement comprehensive integration tests", "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.", "status": "completed", "dependencies": [ { "taskId": "6f042d63-fa61-42c9-b7b0-820495aec9ba" } ], "createdAt": "2025-05-26T16:18:03.641Z", "updatedAt": "2025-05-26T23:24:09.973Z", "relatedFiles": [ { "path": "src/features/wikis/list-wiki-pages/feature.spec.int.ts", "type": "CREATE", "description": "Integration tests for list wiki pages feature" } ], "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", "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", "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.", "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.", "completedAt": "2025-05-26T23:24:09.973Z" }, { "id": "68804833-8bdd-4dda-ab6b-dc22f540a0e3", "name": "Implement unit tests for coverage gaps", "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.", "status": "completed", "dependencies": [ { "taskId": "457c0d1b-3635-49d7-916e-0e9aeb4f370f" } ], "createdAt": "2025-05-26T16:18:03.641Z", "updatedAt": "2025-05-26T23:31:32.356Z", "relatedFiles": [ { "path": "src/features/wikis/list-wiki-pages/feature.spec.unit.ts", "type": "CREATE", "description": "Unit tests for list wiki pages feature" } ], "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", "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", "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.", "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.", "completedAt": "2025-05-26T23:31:32.355Z" } ] } ``` -------------------------------------------------------------------------------- /docs/tools/repositories.md: -------------------------------------------------------------------------------- ```markdown # Azure DevOps Repositories Tools This document describes the tools available for working with Azure DevOps Git repositories. ## get_repository_details Gets detailed information about a specific Git repository, including optional branch statistics and refs. ### Description 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. ### Parameters ```json { "projectId": "MyProject", // Required: The ID or name of the project "repositoryId": "MyRepo", // Required: The ID or name of the repository "includeStatistics": true, // Optional: Whether to include branch statistics (default: false) "includeRefs": true, // Optional: Whether to include repository refs (default: false) "refFilter": "heads/", // Optional: Filter for refs (e.g., "heads/" or "tags/") "branchName": "main" // Optional: Name of specific branch to get statistics for } ``` | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | The ID or name of the project containing the repository | | `repositoryId` | string | Yes | The ID or name of the repository to get details for | | `includeStatistics` | boolean | No | Whether to include branch statistics (default: false) | | `includeRefs` | boolean | No | Whether to include repository refs (default: false) | | `refFilter` | string | No | Optional filter for refs (e.g., "heads/" or "tags/") | | `branchName` | string | No | Name of specific branch to get statistics for (if includeStatistics is true) | ### Response The tool returns a `RepositoryDetails` object containing: - `repository`: The basic repository information (same as returned by `get_repository`) - `statistics` (optional): Branch statistics if requested - `refs` (optional): Repository refs if requested Example response: ```json { "repository": { "id": "repo-guid", "name": "MyRepository", "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/MyRepository", "project": { "id": "project-guid", "name": "MyProject", "url": "https://dev.azure.com/organization/_apis/projects/project-guid" }, "defaultBranch": "refs/heads/main", "size": 25478, "remoteUrl": "https://dev.azure.com/organization/MyProject/_git/MyRepository", "sshUrl": "[email protected]:v3/organization/MyProject/MyRepository", "webUrl": "https://dev.azure.com/organization/MyProject/_git/MyRepository" }, "statistics": { "branches": [ { "name": "refs/heads/main", "aheadCount": 0, "behindCount": 0, "isBaseVersion": true, "commit": { "commitId": "commit-guid", "author": { "name": "John Doe", "email": "[email protected]", "date": "2023-01-01T12:00:00Z" }, "committer": { "name": "John Doe", "email": "[email protected]", "date": "2023-01-01T12:00:00Z" }, "comment": "Initial commit" } } ] }, "refs": { "value": [ { "name": "refs/heads/main", "objectId": "commit-guid", "creator": { "displayName": "John Doe", "id": "user-guid" }, "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/repo-guid/refs/heads/main" } ], "count": 1 } } ``` ### Error Handling The tool may throw the following errors: - General errors: If the API call fails or other unexpected errors occur - Authentication errors: If the authentication credentials are invalid or expired - Permission errors: If the authenticated user doesn't have permission to access the repository - ResourceNotFound errors: If the specified project or repository doesn't exist Error messages will be formatted as text and provide details about what went wrong. ### Example Usage ```typescript // Basic example - just repository info const repoDetails = await mcpClient.callTool('get_repository_details', { projectId: 'MyProject', repositoryId: 'MyRepo' }); console.log(repoDetails); // Example with branch statistics const repoWithStats = await mcpClient.callTool('get_repository_details', { projectId: 'MyProject', repositoryId: 'MyRepo', includeStatistics: true }); console.log(repoWithStats); // Example with refs filtered to branches const repoWithBranches = await mcpClient.callTool('get_repository_details', { projectId: 'MyProject', repositoryId: 'MyRepo', includeRefs: true, refFilter: 'heads/' }); console.log(repoWithBranches); // Example with all options const fullRepoDetails = await mcpClient.callTool('get_repository_details', { projectId: 'MyProject', repositoryId: 'MyRepo', includeStatistics: true, includeRefs: true, refFilter: 'heads/', branchName: 'main' }); console.log(fullRepoDetails); ``` ### Implementation Details This tool uses the Azure DevOps Node API's Git API to retrieve repository details: 1. It gets a connection to the Azure DevOps WebApi client 2. It calls the `getGitApi()` method to get a handle to the Git API 3. It retrieves the basic repository information using `getRepository()` 4. If requested, it retrieves branch statistics using `getBranches()` 5. If requested, it retrieves repository refs using `getRefs()` 6. The combined results are returned to the caller ## list_repositories Lists all Git repositories in a specific project. ### Description 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. This tool uses the Azure DevOps WebApi client to interact with the Git API. ### Parameters ```json { "projectId": "MyProject", // Required: The ID or name of the project "includeLinks": true // Optional: Whether to include reference links } ``` | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------ | | `projectId` | string | Yes | The ID or name of the project containing the repositories | | `includeLinks` | boolean | No | Whether to include reference links in the repository objects | ### Response The tool returns an array of `GitRepository` objects, each containing: - `id`: The unique identifier of the repository - `name`: The name of the repository - `url`: The URL of the repository - `project`: Object containing basic project information - `defaultBranch`: The default branch of the repository (e.g., "refs/heads/main") - `size`: The size of the repository - `remoteUrl`: The remote URL for cloning the repository - `sshUrl`: The SSH URL for cloning the repository - `webUrl`: The web URL for browsing the repository in browser - ... and potentially other repository properties Example response: ```json [ { "id": "repo-guid-1", "name": "FirstRepository", "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/FirstRepository", "project": { "id": "project-guid", "name": "MyProject", "url": "https://dev.azure.com/organization/_apis/projects/project-guid" }, "defaultBranch": "refs/heads/main", "size": 25478, "remoteUrl": "https://dev.azure.com/organization/MyProject/_git/FirstRepository", "sshUrl": "[email protected]:v3/organization/MyProject/FirstRepository", "webUrl": "https://dev.azure.com/organization/MyProject/_git/FirstRepository" }, { "id": "repo-guid-2", "name": "SecondRepository", "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/SecondRepository", "project": { "id": "project-guid", "name": "MyProject", "url": "https://dev.azure.com/organization/_apis/projects/project-guid" }, "defaultBranch": "refs/heads/main", "size": 15789, "remoteUrl": "https://dev.azure.com/organization/MyProject/_git/SecondRepository", "sshUrl": "[email protected]:v3/organization/MyProject/SecondRepository", "webUrl": "https://dev.azure.com/organization/MyProject/_git/SecondRepository" } ] ``` ### Error Handling The tool may throw the following errors: - General errors: If the API call fails or other unexpected errors occur - Authentication errors: If the authentication credentials are invalid or expired - Permission errors: If the authenticated user doesn't have permission to list repositories - ResourceNotFound errors: If the specified project doesn't exist Error messages will be formatted as text and provide details about what went wrong. ### Example Usage ```typescript // Basic example const repositories = await mcpClient.callTool('list_repositories', { projectId: 'MyProject', }); console.log(repositories); // Example with includeLinks parameter const repositoriesWithLinks = await mcpClient.callTool('list_repositories', { projectId: 'MyProject', includeLinks: true, }); console.log(repositoriesWithLinks); ``` ### Implementation Details This tool uses the Azure DevOps Node API's Git API to retrieve repositories: 1. It gets a connection to the Azure DevOps WebApi client 2. It calls the `getGitApi()` method to get a handle to the Git API 3. It then calls `getRepositories()` with the specified project ID and optional include links parameter 4. The results are returned directly to the caller ### Related Tools - `get_repository`: Get details of a specific repository - `get_repository_details`: Get detailed information about a repository including statistics and refs - `list_projects`: List all projects in the organization (to find project IDs) ## get_file_content Retrieves the content of a file or directory from a Git repository. ### Description 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. ### Parameters ```json { "projectId": "MyProject", // Required: The ID or name of the project "repositoryId": "MyRepo", // Required: The ID or name of the repository "path": "/src/index.ts", // Required: The path to the file or directory "versionType": "branch", // Optional: The type of version (branch, tag, or commit) "version": "main" // Optional: The name of the branch/tag, or commit ID } ``` | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | The ID or name of the project containing the repository | | `repositoryId` | string | Yes | The ID or name of the repository | | `path` | string | Yes | The path to the file or directory (starting with "/") | | `versionType` | enum | No | The type of version: "branch", "tag", or "commit" (GitVersionType) | | `version` | string | No | The name of the branch/tag, or the commit ID | ### Response The tool returns a `FileContentResponse` object containing: - `content`: The content of the file as a string, or a JSON string of items for directories - `isDirectory`: Boolean indicating whether the path refers to a directory Example response for a file: ```json { "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", "isDirectory": false } ``` Example response for a directory: ```json { "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\"}}]", "isDirectory": true } ``` ### Error Handling The tool may throw the following errors: - General errors: If the API call fails or other unexpected errors occur - Authentication errors: If the authentication credentials are invalid or expired - Permission errors: If the authenticated user doesn't have permission to access the repository - ResourceNotFound errors: If the specified project, repository, or path doesn't exist Error messages will be formatted as text and provide details about what went wrong. ### Example Usage ```typescript // Basic example - get file from default branch const fileContent = await mcpClient.callTool('get_file_content', { projectId: 'MyProject', repositoryId: 'MyRepo', path: '/src/index.ts' }); console.log(fileContent.content); // Get directory content const directoryContent = await mcpClient.callTool('get_file_content', { projectId: 'MyProject', repositoryId: 'MyRepo', path: '/src' }); if (directoryContent.isDirectory) { const items = JSON.parse(directoryContent.content); console.log(`Directory contains ${items.length} items`); } // Get file from specific branch const branchFileContent = await mcpClient.callTool('get_file_content', { projectId: 'MyProject', repositoryId: 'MyRepo', path: '/src/index.ts', versionType: 'branch', version: 'feature/new-ui' }); console.log(branchFileContent.content); // Get file from specific commit const commitFileContent = await mcpClient.callTool('get_file_content', { projectId: 'MyProject', repositoryId: 'MyRepo', path: '/src/index.ts', versionType: 'commit', version: 'a1b2c3d4e5f6g7h8i9j0' }); console.log(commitFileContent.content); ``` ### Implementation Details This tool uses the Azure DevOps Node API's Git API to retrieve file or directory content: 1. It gets a connection to the Azure DevOps WebApi client 2. It calls the `getGitApi()` method to get a handle to the Git API 3. It determines if the path is a file or directory by attempting to fetch items 4. For directories, it returns the list of items as a JSON string 5. For files, it fetches the file content and returns it as a string 6. The results are wrapped in a `FileContentResponse` object with the appropriate `isDirectory` flag ### Resource URI Access In addition to using this tool, file content can also be accessed via resource URIs with the following patterns: - Default branch: `ado://{organization}/{project}/{repo}/contents/{path}` - Specific branch: `ado://{organization}/{project}/{repo}/branches/{branch}/contents/{path}` - Specific commit: `ado://{organization}/{project}/{repo}/commits/{commit}/contents/{path}` - Specific tag: `ado://{organization}/{project}/{repo}/tags/{tag}/contents/{path}` - Pull request: `ado://{organization}/{project}/{repo}/pullrequests/{prId}/contents/{path}` ### Related Tools - `list_repositories`: List all repositories in a project - `get_repository`: Get details of a specific repository - `get_repository_details`: Get detailed information about a repository including statistics and refs - `search_code`: Search for code across repositories in a project ## get_all_repositories_tree Displays a hierarchical tree view of files and directories across multiple Azure DevOps repositories within a project, based on their default branches. ### Description 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. Key features: - Views multiple repositories at once - Filter repositories by name pattern - Filter files by pattern - Control depth to balance performance and detail - Shows directories and files in a hierarchical view - Provides statistics (count of files and directories) - Works with the default branch of each repository - Handles errors gracefully ### Parameters ```json { "organizationId": "MyOrg", "projectId": "MyProject", "repositoryPattern": "API*", "depth": 0, "pattern": "*.yaml" } ``` - `organizationId` (string, required): The ID or name of the Azure DevOps organization. - `projectId` (string, required): The ID or name of the project containing the repositories. - `repositoryPattern` (string, optional): Pattern to filter repositories by name (PowerShell wildcard). - `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. - `pattern` (string, optional): Pattern to filter files by name (PowerShell wildcard). Note: Directories are always shown regardless of this filter. ### Response The response is a formatted ASCII tree showing the file and directory structure of each repository: ``` Repo-API-1/ |-- src/ | |-- config.yaml | `-- utils/ `-- deploy.yaml 1 directory, 2 files Repo-API-Gateway/ |-- charts/ | `-- values.yaml `-- README.md 1 directory, 2 files Repo-Data-Service/ (Repository is empty or default branch not found) 0 directories, 0 files ``` ### Examples #### Basic Example - View All Repositories with Maximum Depth ```javascript const result = await mcpClient.callTool('get_all_repositories_tree', { organizationId: 'MyOrg', projectId: 'MyProject' }); console.log(result); ``` #### Filter Repositories by Name Pattern ```javascript const result = await mcpClient.callTool('get_all_repositories_tree', { organizationId: 'MyOrg', projectId: 'MyProject', repositoryPattern: 'API*' }); console.log(result); ``` #### Limited Depth and File Pattern Filter ```javascript const result = await mcpClient.callTool('get_all_repositories_tree', { organizationId: 'MyOrg', projectId: 'MyProject', depth: 1, // Only one level deep pattern: '*.yaml' }); console.log(result); ``` ### Performance Considerations - For maximum depth (depth=0), the tool uses server-side recursion (VersionControlRecursionType.Full) which is more efficient for retrieving deep directory structures. - For limited depth (depth=1 to 10), the tool uses client-side recursion which is better for controlled exploration. - When viewing very large repositories, consider using a limited depth or file pattern to reduce response time. ### Related Tools - `list_repositories`: Lists all repositories in a project (summary only) - `get_repository_details`: Gets detailed info about a single repository - `get_repository_tree`: Explores structure within a single repository (more detailed) - `get_file_content`: Gets content of a specific file ``` -------------------------------------------------------------------------------- /shrimp-rules.md: -------------------------------------------------------------------------------- ```markdown # Development Guidelines for AI Agents - mcp-server-azure-devops **This document is exclusively for AI Agent operational use. DO NOT include general development knowledge.** ## 1. Project Overview ### Purpose - This project, `@tiberriver256/mcp-server-azure-devops`, is an MCP (Model Context Protocol) server. - Its primary function is to provide tools for interacting with Azure DevOps services. ### Technology Stack - **Core**: TypeScript, Node.js - **Key Libraries**: - `@modelcontextprotocol/sdk`: For MCP server and type definitions. - `azure-devops-node-api`: For interacting with Azure DevOps. - `@azure/identity`: For Azure authentication. - `zod`: For schema definition and validation. - `zod-to-json-schema`: For converting Zod schemas to JSON schemas for MCP tools. - **Testing**: Jest (for unit, integration, and e2e tests). - **Linting/Formatting**: ESLint, Prettier. - **Environment Management**: `dotenv`. ### Core Functionality - Provides MCP tools to interact with Azure DevOps features including, but not limited to: - Organizations - Projects (list, get, get details) - Repositories (list, get, get content, get tree) - Work Items (list, get, create, update, manage links) - Pull Requests (list, get, create, update, add/get comments) - Pipelines (list, trigger) - Search (code, wiki, work items) - Users (get current user) - Wikis (list, get page, create, update page) ## 2. Project Architecture ### Main Directory Structure - **`./` (Root)**: - [`package.json`](package.json:0): Project metadata, dependencies, and NPM scripts. **REFER** to this for available commands and dependencies. - [`tsconfig.json`](tsconfig.json:0): TypeScript compiler configuration. **ADHERE** to its settings. - [`.eslintrc.json`](.eslintrc.json:0): ESLint configuration for code linting. **ADHERE** to its rules. - [`README.md`](README.md:0): General project information. - `setup_env.sh`: Shell script for environment setup. - `CHANGELOG.md` (if present): Tracks changes between versions. - **`src/`**: Contains all TypeScript source code. - **`src/features/`**: Core application logic. Each subdirectory represents a distinct Azure DevOps feature set (e.g., `projects`, `repositories`). - `src/features/[feature-name]/`: Contains all files related to a specific feature. - `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. - `src/features/[feature-name]/schemas.ts`: Defines Zod input/output schemas for all tools within this feature. **DEFINE** new schemas here. - `src/features/[feature-name]/tool-definitions.ts`: Defines MCP tools for the feature using `@modelcontextprotocol/sdk` and `zodToJsonSchema`. **ADD** new tool definitions here. - `src/features/[feature-name]/types.ts`: Contains TypeScript type definitions specific to this feature. **DEFINE** feature-specific types here. - `src/features/[feature-name]/[tool-name]/`: Subdirectory for a specific tool/action within the feature. - `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. - `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. - `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. - `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. - **`src/shared/`**: Contains shared modules and utilities used across features. - `src/shared/api/`: Azure DevOps API client setup (e.g., `client.ts`). - `src/shared/auth/`: Authentication logic for Azure DevOps (e.g., `auth-factory.ts`, `client-factory.ts`). **USE** these factories; DO NOT implement custom auth. - `src/shared/config/`: Configuration management (e.g., `version.ts`). - `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. - `src/shared/types/`: Global TypeScript type definitions (e.g., `config.ts`, `request-handler.ts`, `tool-definition.ts`). - **`src/utils/`**: General utility functions. - `src/utils/environment.ts`: Provides default values for environment variables (e.g., `defaultProject`, `defaultOrg`). - [`src/index.ts`](src/index.ts:1): Main application entry point. Handles environment variable loading and server initialization. **Exports** server components. - [`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). - **`docs/`**: Currently empty. If documentation is added, **MAINTAIN** it in sync with code changes. - **`project-management/`**: Contains project planning and design documents. **REFER** to `architecture-guide.md` for high-level design. - **`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`). ## 3. Code Standards ### Naming Conventions - **Files and Directories**: USE kebab-case (e.g., `my-feature`, `get-project-details.ts`). - **Variables and Functions**: USE camelCase (e.g., `projectId`, `listProjects`). - **Classes, Interfaces, Enums, Types**: USE PascalCase (e.g., `AzureDevOpsClient`, `TeamProject`, `AuthenticationMethod`). - **Test Files**: - Unit tests: `[filename].spec.unit.ts` (e.g., [`get-project.spec.unit.ts`](src/features/projects/get-project/feature.spec.unit.ts:0)). - Integration tests: `[filename].spec.int.ts` (e.g., [`get-project.spec.int.ts`](src/features/projects/get-project/feature.spec.int.ts:0)). - E2E tests: `[filename].spec.e2e.ts` (e.g., [`server.spec.e2e.ts`](src/server.spec.e2e.ts:0)). - **Feature Modules**: Place under `src/features/[feature-name]/`. - **Tool Logic**: Place in `src/features/[feature-name]/[tool-name]/feature.ts`. - **Schemas**: Define in `src/features/[feature-name]/schemas.ts`. - **Tool Definitions (MCP)**: Define in `src/features/[feature-name]/tool-definitions.ts`. - **Types**: Feature-specific types in `src/features/[feature-name]/types.ts`; global types in `src/shared/types/`. ### Formatting - **Prettier**: Enforced via ESLint and lint-staged. - **Rule**: ADHERE to formatting rules defined by Prettier (implicitly via [`.eslintrc.json`](.eslintrc.json:1) which extends `prettier`). - **Action**: ALWAYS run `npm run format` (or rely on lint-staged) before committing. ### Linting - **ESLint**: Configuration in [`.eslintrc.json`](.eslintrc.json:1). - **Rule**: ADHERE to linting rules. - **Action**: ALWAYS run `npm run lint` (or `npm run lint:fix`) and RESOLVE all errors/warnings before committing. - **Key Lint Rules (from [`.eslintrc.json`](.eslintrc.json:1))**: - `prettier/prettier: "error"` (Prettier violations are ESLint errors). - `@typescript-eslint/no-explicit-any: "warn"` (Avoid `any` where possible; it's "off" for `*.spec.unit.ts` and `tests/**/*.ts`). - `@typescript-eslint/no-unused-vars: ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }]` (No unused variables, allowing `_` prefix for ignored ones). ### Comments - **TSDoc**: USE TSDoc for documenting public functions, classes, interfaces, and types (e.g., `/** ... */`). - **Inline Comments**: For complex logic blocks, ADD inline comments (`// ...`) explaining the purpose. ### TypeScript Specifics (from [`tsconfig.json`](tsconfig.json:1)) - `strict: true`: ADHERE to strict mode. - `noImplicitAny: true`: DO NOT use implicit `any`. Explicitly type all entities. - `noUnusedLocals: true`, `noUnusedParameters: true`: ENSURE no unused local variables or parameters. - `moduleResolution: "Node16"`: Be aware of Node.js ESM module resolution specifics. - `paths: { "@/*": ["src/*"] }`: USE path alias `@/*` for imports from `src/`. ## 4. Functionality Implementation Standards ### Adding a New Tool/Functionality to an Existing Feature 1. **Identify Feature**: Determine the relevant feature directory in `src/features/[feature-name]/`. 2. **Create Tool Directory**: Inside the feature directory, CREATE a new subdirectory for your tool, e.g., `src/features/[feature-name]/[new-tool-name]/`. 3. **Implement Logic**: CREATE `[new-tool-name]/feature.ts`. Implement the core Azure DevOps interaction logic here. - USE `getClient()` from `src/shared/api/client.ts` or `getConnection()` from [`src/server.ts`](src/server.ts:1) to get `WebApi`. - USE error handling from `src/shared/errors/`. 4. **Define Schema**: - ADD Zod schema for the tool's input to `src/features/[feature-name]/schemas.ts`. - EXPORT it. - If needed, CREATE `[new-tool-name]/schema.ts` and re-export the specific schema from the feature-level `schemas.ts`. 5. **Define MCP Tool**: - ADD tool definition to `src/features/[feature-name]/tool-definitions.ts`. - Import the Zod schema and use `zodToJsonSchema` for `inputSchema`. - Ensure `name` matches the intended tool name for MCP. 6. **Update Feature Index**: - In `src/features/[feature-name]/index.ts`: - EXPORT your new tool's logic function (from `[new-tool-name]/feature.ts` or its `index.ts`). - ADD your new tool's name to the `includes()` check in `isFeatureRequest` function. - ADD a `case` for your new tool in the `handleFeatureRequest` function to call your logic. Parse arguments using the Zod schema. 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. 8. **Add Tests**: CREATE `[new-tool-name]/feature.spec.unit.ts` and `[new-tool-name]/feature.spec.int.ts`. ### Adding a New Feature Module (e.g., for a new Azure DevOps Service Area) 1. **Create Feature Directory**: CREATE `src/features/[new-feature-module-name]/`. 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`. 3. **Create Feature Index**: CREATE `src/features/[new-feature-module-name]/index.ts`. - EXPORT all schemas, types, tool logic functions. - EXPORT the `[new-feature-module-name]Tools` array from `tool-definitions.ts`. - CREATE and EXPORT `is[NewFeatureModuleName]Request` (e.g., `isMyNewFeatureRequest`) type guard. - CREATE and EXPORT `handle[NewFeatureModuleName]Request` (e.g., `handleMyNewFeatureRequest`) request handler function. 4. **Register Feature in Server**: - In [`src/server.ts`](src/server.ts:1): - IMPORT `[new-feature-module-name]Tools`, `is[NewFeatureModuleName]Request`, and `handle[NewFeatureModuleName]Request` from your new feature's `index.ts`. - ADD `...[new-feature-module-name]Tools` to the `tools` array in the `ListToolsRequestSchema` handler. - ADD an `if (is[NewFeatureModuleName]Request(request)) { return await handle[NewFeatureModuleName]Request(connection, request); }` block in the `CallToolRequestSchema` handler. 5. **Add Tests**: Ensure comprehensive tests for the new feature module. ## 5. Framework/Plugin/Third-party Library Usage Standards - **`@modelcontextprotocol/sdk`**: - USE `Server` class from `@modelcontextprotocol/sdk/server/index.js` to create the MCP server ([`src/server.ts`](src/server.ts:1)). - USE `StdioServerTransport` for transport ([`src/index.ts`](src/index.ts:1)). - USE schema types like `CallToolRequestSchema` from `@modelcontextprotocol/sdk/types.js`. - DEFINE tools as `ToolDefinition[]` (see `src/shared/types/tool-definition.ts` and feature `tool-definitions.ts` files). - **`azure-devops-node-api`**: - This is the primary library for Azure DevOps interactions. - OBTAIN `WebApi` connection object via `getConnection()` from [`src/server.ts`](src/server.ts:1) or `AzureDevOpsClient` from `src/shared/auth/client-factory.ts`. - USE specific APIs from the connection (e.g., `connection.getCoreApi()`, `connection.getWorkItemTrackingApi()`). - **`@azure/identity`**: - Used for Azure authentication (e.g., `DefaultAzureCredential`). - Primarily abstracted via `AzureDevOpsClient` in `src/shared/auth/`. PREFER using this abstraction. - **`zod`**: - USE for all input/output schema definition and validation. - DEFINE schemas in `src/features/[feature-name]/schemas.ts`. - USE `z.object({...})`, `z.string()`, `z.boolean()`, etc. - USE `.optional()`, `.default()`, `.describe()` for schema fields. - **`zod-to-json-schema`**: - USE to convert Zod schemas to JSON schemas for MCP `inputSchema` in `tool-definitions.ts`. - **`dotenv`**: - Used in [`src/index.ts`](src/index.ts:1) to load environment variables from a `.env` file. - **Jest**: - Test files co-located with source files or in feature-specific `__test__` directories. - Configuration in `jest.unit.config.js`, `jest.int.config.js`, `jest.e2e.config.js`. - **ESLint/Prettier**: See "Code Standards". ## 6. Workflow Standards ### Development Workflow 1. **Branch**: CREATE or CHECKOUT a feature/bugfix branch from `main` (or relevant development branch). 2. **Implement**: WRITE code and corresponding tests. 3. **Test**: - RUN unit tests: `npm run test:unit`. - RUN integration tests: `npm run test:int`. - RUN E2E tests: `npm run test:e2e`. - Or run all tests: `npm test`. - ENSURE all tests pass. 4. **Lint & Format**: - RUN `npm run lint` (or `npm run lint:fix`). RESOLVE all issues. - RUN `npm run format`. 5. **Commit**: - USE Conventional Commits specification (e.g., `feat: ...`, `fix: ...`). - RECOMMENDED: Use `npm run commit` (uses `cz-conventional-changelog`) for guided commit messages. 6. **Pull Request**: PUSH branch and CREATE Pull Request against `main` (or relevant development branch). ### NPM Scripts (from [`package.json`](package.json:1)) - `build`: `tsc` (Compiles TypeScript to `dist/`). - `dev`: `ts-node-dev --respawn --transpile-only src/index.ts` (Runs server in development with auto-restart). - `start`: `node dist/index.js` (Runs compiled server). - `inspector`: `npm run build && npx @modelcontextprotocol/inspector node dist/index.js` (Runs server with MCP Inspector). - `test:unit`, `test:int`, `test:e2e`, `test`: Run respective test suites. - `lint`, `lint:fix`: Run ESLint. - `format`: Run Prettier. - `prepare`: `husky install` (Sets up Git hooks). - `commit`: `cz` (Interactive commitizen). ### CI/CD - 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. ## 7. Key File Interaction Standards - **Adding/Modifying a Tool**: - TOUCH `src/features/[feature-name]/[tool-name]/feature.ts` (logic). - TOUCH `src/features/[feature-name]/schemas.ts` (Zod schema). - TOUCH `src/features/[feature-name]/tool-definitions.ts` (MCP tool definition). - TOUCH `src/features/[feature-name]/index.ts` (export logic, update request handler and guard). - TOUCH corresponding `*.spec.unit.ts` and `*.spec.int.ts` files. - **Adding a New Feature Module**: - CREATE files within `src/features/[new-feature-module-name]/` as per "Functionality Implementation Standards". - MODIFY [`src/server.ts`](src/server.ts:1) to import and register the new feature module's tools and handlers. - **Configuration Changes**: - Environment variables: Managed via `.env` file (loaded by `dotenv` in [`src/index.ts`](src/index.ts:1)). - TypeScript config: [`tsconfig.json`](tsconfig.json:1). - Linting config: [`.eslintrc.json`](.eslintrc.json:1). - **Dependency Management**: - MODIFY [`package.json`](package.json:1) to add/update dependencies. - RUN `npm install` or `npm ci`. - **Documentation**: - `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. - [`README.md`](README.md:0): UPDATE for significant high-level changes. ## 8. AI Decision-making Standards ### When Adding a New Azure DevOps API Interaction: 1. **Goal**: To expose a new Azure DevOps API endpoint as an MCP tool. 2. **Decision: New or Existing Feature?** - 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. - ELSE (e.g., interacting with Azure DevOps Audit Logs, a new service area), CREATE a new feature module. (See "Functionality Implementation Standards"). 3. **Pattern Adherence**: - FOLLOW the established pattern: - `src/features/[feature]/[tool]/feature.ts` for logic. - `src/features/[feature]/schemas.ts` for Zod schemas. - `src/features/[feature]/tool-definitions.ts` for MCP tool definitions. - `src/features/[feature]/index.ts` for feature-level exports, request guard (`isFeatureRequest`), and request handler (`handleFeatureRequest`). - **Example**: To add `get_pipeline_run_logs` to `pipelines` feature: - CREATE `src/features/pipelines/get-pipeline-run-logs/feature.ts`. - ADD `GetPipelineRunLogsSchema` to `src/features/pipelines/schemas.ts`. - ADD `get_pipeline_run_logs` definition to `src/features/pipelines/tool-definitions.ts`. - UPDATE `src/features/pipelines/index.ts` to export the new function, add to `isPipelinesRequest`, and handle in `handlePipelinesRequest`. 4. **Error Handling**: - ALWAYS use custom error classes from `src/shared/errors/azure-devops-errors.ts` (e.g., `AzureDevOpsResourceNotFoundError`). - WRAP external API calls in try/catch blocks. - 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. 5. **Testing**: - ALWAYS write unit tests for the new logic in `[tool-name]/feature.spec.unit.ts`. - ALWAYS write integration tests (NEVER mocking anything) in `[tool-name]/feature.spec.int.ts`. Prefer integration tests over unit tests. ### When Modifying Existing Functionality: 1. **Identify Impact**: DETERMINE all files affected by the change (logic, schemas, tool definitions, tests, potentially documentation). 2. **Maintain Consistency**: ENSURE changes are consistent with existing patterns within that feature module. 3. **Update Tests**: MODIFY existing tests or ADD new ones to cover the changes. ENSURE all tests pass. 4. **Version Bumping**: For significant changes, consider if a version bump in [`package.json`](package.json:1) is warranted (usually handled by `release-please`). ## 9. Prohibited Actions - **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. - **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. - **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`. - **DO NOT** bypass linting (`npm run lint`) or formatting (`npm run format`) checks. Code MUST adhere to these standards. - **DO NOT** commit code that fails tests (`npm test`). - **DO NOT** implement custom Azure DevOps authentication logic. USE the provided `AzureDevOpsClient` from `src/shared/auth/`. - **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`). - **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/`). - **DO NOT** modify files in `dist/` directory directly. This directory is auto-generated by `npm run build`. - **DO NOT** ignore the `project-management/` directory for understanding architectural guidelines, but DO NOT replicate its content here. - **DO NOT** use mocks within integration tests. ``` -------------------------------------------------------------------------------- /src/clients/azure-devops.ts: -------------------------------------------------------------------------------- ```typescript import axios, { AxiosError } from 'axios'; import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity'; import { AzureDevOpsError, AzureDevOpsResourceNotFoundError, AzureDevOpsValidationError, AzureDevOpsPermissionError, } from '../shared/errors'; import { defaultOrg, defaultProject } from '../utils/environment'; interface AzureDevOpsApiErrorResponse { message?: string; typeKey?: string; errorCode?: number; eventId?: number; } interface ClientOptions { organizationId?: string; } interface WikiCreateParameters { name: string; projectId: string; type: 'projectWiki' | 'codeWiki'; repositoryId?: string; mappedPath?: string; version?: { version: string; versionType?: 'branch' | 'tag' | 'commit'; }; } interface WikiPageContent { content: string; } export interface WikiPageSummary { id: number; path: string; url?: string; order?: number; } interface WikiPagesBatchRequest { top: number; continuationToken?: string; } interface WikiPagesBatchResponse { value: WikiPageSummary[]; continuationToken?: string; } interface PageUpdateOptions { comment?: string; versionDescriptor?: { version?: string; }; } export class WikiClient { private baseUrl: string; private organizationId: string; constructor(organizationId: string) { this.organizationId = organizationId || defaultOrg; this.baseUrl = `https://dev.azure.com/${this.organizationId}`; } /** * Gets a project's ID from its name or verifies a project ID * @param projectNameOrId - Project name or ID * @returns The project ID */ private async getProjectId(projectNameOrId: string): Promise<string> { try { // Try to get project details using the provided name or ID const url = `${this.baseUrl}/_apis/projects/${projectNameOrId}`; const authHeader = await getAuthorizationHeader(); const response = await axios.get(url, { params: { 'api-version': '7.1', }, headers: { Authorization: authHeader, 'Content-Type': 'application/json', }, }); // Return the project ID from the response return response.data.id; } catch (error) { const axiosError = error as AxiosError; if (axiosError.response) { const status = axiosError.response.status; const errorMessage = typeof axiosError.response.data === 'object' && axiosError.response.data ? (axiosError.response.data as AzureDevOpsApiErrorResponse) .message || axiosError.message : axiosError.message; if (status === 404) { throw new AzureDevOpsResourceNotFoundError( `Project not found: ${projectNameOrId}`, ); } if (status === 401 || status === 403) { throw new AzureDevOpsPermissionError( `Permission denied to access project: ${projectNameOrId}`, ); } throw new AzureDevOpsError( `Failed to get project details: ${errorMessage}`, ); } throw new AzureDevOpsError( `Network error when getting project details: ${axiosError.message}`, ); } } /** * Creates a new wiki in Azure DevOps * @param projectId - Project ID or name * @param params - Parameters for creating the wiki * @returns The created wiki */ async createWiki(projectId: string, params: WikiCreateParameters) { // Use the default project if not provided const project = projectId || defaultProject; try { // Get the actual project ID (whether the input was a name or ID) const actualProjectId = await this.getProjectId(project); // Construct the URL to create the wiki const url = `${this.baseUrl}/${project}/_apis/wiki/wikis`; // Get authorization header const authHeader = await getAuthorizationHeader(); // Make the API request const response = await axios.post( url, { name: params.name, type: params.type, projectId: actualProjectId, ...(params.type === 'codeWiki' && { repositoryId: params.repositoryId, mappedPath: params.mappedPath, version: params.version, }), }, { params: { 'api-version': '7.1', }, headers: { Authorization: authHeader, 'Content-Type': 'application/json', }, }, ); return response.data; } catch (error) { const axiosError = error as AxiosError; // Handle specific error cases if (axiosError.response) { const status = axiosError.response.status; const errorMessage = typeof axiosError.response.data === 'object' && axiosError.response.data ? (axiosError.response.data as AzureDevOpsApiErrorResponse) .message || axiosError.message : axiosError.message; // Handle 404 Not Found if (status === 404) { throw new AzureDevOpsResourceNotFoundError( `Project not found: ${projectId}`, ); } // Handle 401 Unauthorized or 403 Forbidden if (status === 401 || status === 403) { throw new AzureDevOpsPermissionError( `Permission denied to create wiki in project: ${projectId}`, ); } // Handle validation errors if (status === 400) { throw new AzureDevOpsValidationError( `Invalid wiki creation parameters: ${errorMessage}`, ); } // Handle other error statuses throw new AzureDevOpsError(`Failed to create wiki: ${errorMessage}`); } // Handle network errors throw new AzureDevOpsError( `Network error when creating wiki: ${axiosError.message}`, ); } } /** * Gets a wiki page's content * @param projectId - Project ID or name * @param wikiId - Wiki ID or name * @param pagePath - Path of the wiki page * @param options - Additional options like version * @returns The wiki page content and ETag */ async getPage(projectId: string, wikiId: string, pagePath: string) { // Use the default project if not provided const project = projectId || defaultProject; // Ensure pagePath starts with a forward slash const normalizedPath = pagePath.startsWith('/') ? pagePath : `/${pagePath}`; // Construct the URL to get the wiki page const url = `${this.baseUrl}/${project}/_apis/wiki/wikis/${wikiId}/pages`; const params: Record<string, string> = { 'api-version': '7.1', path: normalizedPath, }; try { // Get authorization header const authHeader = await getAuthorizationHeader(); // Make the API request for plain text content const response = await axios.get(url, { params, headers: { Authorization: authHeader, Accept: 'text/plain', 'Content-Type': 'application/json', }, responseType: 'text', }); // Return both the content and the ETag return { content: response.data, eTag: response.headers.etag?.replace(/"/g, ''), // Remove quotes from ETag }; } catch (error) { const axiosError = error as AxiosError; // Handle specific error cases if (axiosError.response) { const status = axiosError.response.status; const errorMessage = typeof axiosError.response.data === 'object' && axiosError.response.data ? (axiosError.response.data as AzureDevOpsApiErrorResponse) .message || axiosError.message : axiosError.message; // Handle 404 Not Found if (status === 404) { throw new AzureDevOpsResourceNotFoundError( `Wiki page not found: ${pagePath} in wiki ${wikiId}`, ); } // Handle 401 Unauthorized or 403 Forbidden if (status === 401 || status === 403) { throw new AzureDevOpsPermissionError( `Permission denied to access wiki page: ${pagePath}`, ); } // Handle other error statuses throw new AzureDevOpsError( `Failed to get wiki page: ${errorMessage} ${axiosError.response?.data}`, ); } // Handle network errors throw new AzureDevOpsError( `Network error when getting wiki page: ${axiosError.message}`, ); } } /** * Creates a new wiki page with the provided content * @param content - Content for the new wiki page * @param projectId - Project ID or name * @param wikiId - Wiki ID or name * @param pagePath - Path of the wiki page to create * @param options - Additional options like comment * @returns The created wiki page */ async createPage( content: string, projectId: string, wikiId: string, pagePath: string, options?: { comment?: string }, ) { // Use the default project if not provided const project = projectId || defaultProject; // Encode the page path, handling forward slashes properly const encodedPagePath = encodeURIComponent(pagePath).replace(/%2F/g, '/'); // Construct the URL to create the wiki page const url = `${this.baseUrl}/${project}/_apis/wiki/wikis/${wikiId}/pages`; const params: Record<string, string> = { 'api-version': '7.1', path: encodedPagePath, }; // Prepare the request payload const payload: Record<string, string> = { content, }; // Add comment if provided if (options?.comment) { payload.comment = options.comment; } try { // Get authorization header const authHeader = await getAuthorizationHeader(); // Make the API request const response = await axios.put(url, payload, { params, headers: { Authorization: authHeader, 'Content-Type': 'application/json', Accept: 'application/json', }, }); // The ETag header contains the version const eTag = response.headers.etag; // Return the page content along with metadata return { ...response.data, version: eTag ? eTag.replace(/"/g, '') : undefined, // Remove quotes from ETag }; } catch (error) { const axiosError = error as AxiosError; // Handle specific error cases if (axiosError.response) { const status = axiosError.response.status; const errorMessage = typeof axiosError.response.data === 'object' && axiosError.response.data ? (axiosError.response.data as AzureDevOpsApiErrorResponse) .message || axiosError.message : axiosError.message; // Handle 404 Not Found - usually means the parent path doesn't exist if (status === 404) { throw new AzureDevOpsResourceNotFoundError( `Cannot create wiki page: parent path for ${pagePath} does not exist`, ); } // Handle 401 Unauthorized or 403 Forbidden if (status === 401 || status === 403) { throw new AzureDevOpsPermissionError( `Permission denied to create wiki page: ${pagePath}`, ); } // Handle 412 Precondition Failed - page might already exist if (status === 412) { throw new AzureDevOpsValidationError( `Wiki page already exists: ${pagePath}`, ); } // Handle 400 Bad Request - usually validation errors if (status === 400) { throw new AzureDevOpsValidationError( `Invalid request when creating wiki page: ${errorMessage}`, ); } // Handle other error statuses throw new AzureDevOpsError( `Failed to create wiki page: ${errorMessage}`, ); } // Handle network errors throw new AzureDevOpsError( `Network error when creating wiki page: ${axiosError.message}`, ); } } /** * Updates a wiki page with the provided content * @param content - Content for the wiki page * @param projectId - Project ID or name * @param wikiId - Wiki ID or name * @param pagePath - Path of the wiki page * @param options - Additional options like comment and version * @returns The updated wiki page */ async updatePage( content: WikiPageContent, projectId: string, wikiId: string, pagePath: string, options?: PageUpdateOptions, ) { // Use the default project if not provided const project = projectId || defaultProject; // First get the current page version let currentETag; try { const currentPage = await this.getPage(project, wikiId, pagePath); currentETag = currentPage.eTag; } catch (error) { if (error instanceof AzureDevOpsResourceNotFoundError) { // If page doesn't exist, we'll create it (no If-Match header needed) currentETag = undefined; } else { throw error; } } // Encode the page path, handling forward slashes properly const encodedPagePath = encodeURIComponent(pagePath).replace(/%2F/g, '/'); // Construct the URL to update the wiki page const url = `${this.baseUrl}/${project}/_apis/wiki/wikis/${wikiId}/pages`; const params: Record<string, string> = { 'api-version': '7.1', path: encodedPagePath, }; // Add optional comment parameter if provided if (options?.comment) { params.comment = options.comment; } try { // Get authorization header const authHeader = await getAuthorizationHeader(); // Prepare request headers const headers: Record<string, string> = { Authorization: authHeader, 'Content-Type': 'application/json', }; // Add If-Match header if we have an ETag (for updates) if (currentETag) { headers['If-Match'] = `"${currentETag}"`; // Wrap in quotes as required by API } // Create a properly typed payload const payload: Record<string, string> = { content: content.content, }; // Make the API request const response = await axios.put(url, payload, { params, headers, }); // The ETag header contains the version const eTag = response.headers.etag; // Return the page content along with metadata return { ...response.data, version: eTag ? eTag.replace(/"/g, '') : undefined, // Remove quotes from ETag message: response.status === 201 ? 'Page created successfully' : 'Page updated successfully', }; } catch (error) { const axiosError = error as AxiosError; // Handle specific error cases if (axiosError.response) { const status = axiosError.response.status; const errorMessage = typeof axiosError.response.data === 'object' && axiosError.response.data ? (axiosError.response.data as AzureDevOpsApiErrorResponse) .message || axiosError.message : axiosError.message; // Handle 404 Not Found if (status === 404) { throw new AzureDevOpsResourceNotFoundError( `Wiki page not found: ${pagePath} in wiki ${wikiId}`, ); } // Handle 401 Unauthorized or 403 Forbidden if (status === 401 || status === 403) { throw new AzureDevOpsPermissionError( `Permission denied to update wiki page: ${pagePath}`, ); } // Handle 412 Precondition Failed (version conflict) if (status === 412) { throw new AzureDevOpsValidationError( `Version conflict: The wiki page has been modified since you retrieved it. Please get the latest version and try again.`, ); } // Handle other error statuses throw new AzureDevOpsError( `Failed to update wiki page: ${errorMessage}`, ); } // Handle network errors throw new AzureDevOpsError( `Network error when updating wiki page: ${axiosError.message}`, ); } } /** * Lists wiki pages from a wiki using the Pages Batch API * @param projectId - Project ID or name * @param wikiId - Wiki ID or name * @returns Array of wiki page summaries sorted by order then path */ async listWikiPages( projectId: string, wikiId: string, ): Promise<WikiPageSummary[]> { // Use the default project if not provided const project = projectId || defaultProject; // Construct the URL for the Pages Batch API const url = `${this.baseUrl}/${project}/_apis/wiki/wikis/${wikiId}/pagesbatch`; const allPages: WikiPageSummary[] = []; let continuationToken: string | undefined; try { // Get authorization header const authHeader = await getAuthorizationHeader(); do { // Prepare the request body const requestBody: WikiPagesBatchRequest = { top: 100, ...(continuationToken && { continuationToken }), }; // Make the API request const response = await axios.post<WikiPagesBatchResponse>( url, requestBody, { params: { 'api-version': '7.1', }, headers: { Authorization: authHeader, 'Content-Type': 'application/json', }, }, ); // Add the pages from this batch to our collection if (response.data.value && Array.isArray(response.data.value)) { allPages.push(...response.data.value); } // Update continuation token for next iteration continuationToken = response.data.continuationToken; } while (continuationToken); // Sort results by order then path return allPages.sort((a, b) => { // Handle optional order field const aOrder = a.order ?? Number.MAX_SAFE_INTEGER; const bOrder = b.order ?? Number.MAX_SAFE_INTEGER; if (aOrder !== bOrder) { return aOrder - bOrder; } return a.path.localeCompare(b.path); }); } catch (error) { const axiosError = error as AxiosError; // Handle specific error cases if (axiosError.response) { const status = axiosError.response.status; const errorMessage = typeof axiosError.response.data === 'object' && axiosError.response.data ? (axiosError.response.data as AzureDevOpsApiErrorResponse) .message || axiosError.message : axiosError.message; // Handle 404 Not Found if (status === 404) { throw new AzureDevOpsResourceNotFoundError( `Wiki not found: ${wikiId} in project ${projectId}`, ); } // Handle 401 Unauthorized or 403 Forbidden if (status === 401 || status === 403) { throw new AzureDevOpsPermissionError( `Permission denied to list wiki pages in wiki: ${wikiId}`, ); } // Handle other error statuses throw new AzureDevOpsError( `Failed to list wiki pages: ${errorMessage}`, ); } // Handle network errors throw new AzureDevOpsError( `Network error when listing wiki pages: ${axiosError.message}`, ); } } } /** * Creates a Wiki client for Azure DevOps operations * @param options - Options for creating the client * @returns A Wiki client instance */ export async function getWikiClient( options: ClientOptions, ): Promise<WikiClient> { const { organizationId } = options; return new WikiClient(organizationId || defaultOrg); } /** * Get the authorization header for Azure DevOps API requests * @returns The authorization header */ export async function getAuthorizationHeader(): Promise<string> { try { // For PAT authentication, we can construct the header directly if ( process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'pat' && process.env.AZURE_DEVOPS_PAT ) { // For PAT auth, we can construct the Basic auth header directly const token = process.env.AZURE_DEVOPS_PAT; const base64Token = Buffer.from(`:${token}`).toString('base64'); return `Basic ${base64Token}`; } // For Azure Identity / Azure CLI auth, we need to get a token // using the Azure DevOps resource ID // Choose the appropriate credential based on auth method const credential = process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-cli' ? new AzureCliCredential() : new DefaultAzureCredential(); // Azure DevOps resource ID for token acquisition const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798'; // Get token for Azure DevOps const token = await credential.getToken( `${AZURE_DEVOPS_RESOURCE_ID}/.default`, ); if (!token || !token.token) { throw new Error('Failed to acquire token for Azure DevOps'); } return `Bearer ${token.token}`; } catch (error) { throw new AzureDevOpsValidationError( `Failed to get authorization header: ${error instanceof Error ? error.message : String(error)}`, ); } } ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown # Changelog 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. ## [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) ### Features * implement human-readable string enums for Azure DevOps API responses ([8168bcb](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/8168bcbe8e4957e9632927f57ecbe9632c911735)) ## [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) ### Features * **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)) ### Bug Fixes * **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)) ## [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) ### Bug Fixes * simplify listWikiPages API by removing unused parameters ([fff7238](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/fff72384f69433942ee8439de0dda90d7fc85c38)) ## [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) ### Features * add listWikiPages functionality to Azure DevOps wiki client ([bb9ddc0](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/bb9ddc077e80be0caeda106a7e75dc336a62c9ae)) * 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)) ### Bug Fixes * enhanced the get-pull-request-comments tool to include path and line number ([f6017e5](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/f6017e5dd352b63b189e761c9cf27d103dd24b9d)) * remove uuid() validator to resolve unknown format error ([b251252](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/b251252c7c455ee11d8076380b70a546ebf40a6e)) ## [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) ### Bug Fixes * improve org name extraction from url ([496abc7](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/496abc7a9c5fd0867cf8484f8c33c47a3cc42edf)) ## [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) ### Bug Fixes * get_me support for visualstudio.com urls ([ffd3c8a](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ffd3c8a34bcee1856911e5eed7b719d524c25fef)) ## [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) ### Bug Fixes * 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)) ## [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) ### Bug Fixes * update creatorId and reviewerId to require UUIDs instead of allowing emails ([09e82ef](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/09e82ef5e7dfdcd07d1851450ea8c488ea8bb82a)) * 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)) ## [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) ### Features * add update-pull-request tool to tool-definitions and implement reviewer management ([b7b5398](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/b7b539813baadb84e15022fdb93d24f440491d94)) ## [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) ### Bug Fixes * 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) ## [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) ### Features * add get_pull_request_comments ([2b7fb3a](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2b7fb3a885466c633d2d2dfdd8906cc9573483d9)) * add_pull_request_comment ([1df6161](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/1df616161835e11c0039bc344ccdc57742f79507)) ## [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) ### Features * **pull-requests:** implement list-pull-requests functionality ([3f8cac4](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/3f8cac448e2adacaddeb069bc0116b8526577624)) * **wikis:** add create and update wiki functionalities ([27edd6d](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/27edd6d7786748548f4a0123ff19be43b30265c4)) ### Bug Fixes * **pull-requests:** update repository name environment variable ([d2fde5f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/d2fde5f94280f08056f94b613a673d4bbe9c0192)) ## [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) ### Features * **wikis:** implement `get_wiki_page` tool ([7ba5fd7](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/7ba5fd7830fefa17c014aa2b80f0bad04d8fcbf7)) * **wikis:** implement `get_wikis` tool ([3120479](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/3120479b5c31bfaeb50a507791056066f33b6534)) ## [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) ### Features * **pipelines:** implement trigger-pipeline functionality ([e9ba71b](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/e9ba71bfeb2c3a2dc0e1e314698a453d5995d099)) ## [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) ### Features * **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)) ## [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) ### Features * **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) ## [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) ### Features * **getWorkItem:** enhance get_work_item to include all available fields ([3810660](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/38106600f04842a44e5e5b6e824716ebb6f69e61)) * support default project and organization in all tools ([5beca06](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/5beca063057bdbc2dd869c865fb01e0d311c8917)) ### Bug Fixes * return actual field information from get_project_details tool ([64a030a](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/64a030a8c14fd1f9e7f871ae409f0dded23dbe98)) ## [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) ### Features * create pull request ([ab9c255](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ab9c2554ea82a497dead8131a6479ba6fe7c5ba8)) ## [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) ### Bug Fixes * add missing minimatch module ([ee1ffa3](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ee1ffa34afb0da9cdac31da140c17dbd9c589c2b)) ## [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) ### Features * **repositories:** add get_all_repositories_tree tool for viewing multi-repository file structure ([adbe206](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/adbe206300d55ba06063c675492b3a8153b688f7)) * support default project and organization in all tools ([96d61bd](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/96d61bd1098146dfafd1faf7dade1a37725cd7b7)) ## [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) ### Bug Fixes * allow parameterless tools to be called without arguments ([9ce88c3](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/9ce88c3afd4454b8a65392a98e7e2ffb45192584)) ## [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) ### Features * add get-file-content feature to access repository content ([a282f75](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/a282f75383ffc362e5b2d1ecbccebb0047e21571)) * restore get_file_content tool and update documentation ([f71013a](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/f71013a962fb5fbe5d121eaf7f1901e58cf70482)) ## [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) ### Bug Fixes * add explicit permissions to workflow file ([ae85b95](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ae85b953d2467538e42d8c6853b93e1af3c8ed51)) * refine WIQL query in integration test ([eb32e43](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/eb32e43a9064485d29661bcae99a987e3b863464)) * remove schema validation for parameterless tools ([031a71d](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/031a71d71083649216e5b67eb6d67c18c78702bd)) ## [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) ### Bug Fixes * package.json & package-lock.json to reduce vulnerabilities ([2fb1e72](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2fb1e725120edc75c9897bc81f57381c20ad880a)) ## [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) ### Bug Fixes * getMe profile bug ([ceca909](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ceca909beaa74b0dd150ce1688a498281fd0b9e8)) ## [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) ### Features * implement get_me tool ([2a3849d](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2a3849da063f6ce0877dd672992a8bc19f88230e)) ## [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) ### Features * limit search results to 10 when includeContent is true ([827e4e6](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/827e4e65be353125f5ae595b7e68d80f614f8c07)) * make projectId optional in search features for organization-wide search ([1ca1e0e](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/1ca1e0e146bf880d367078b02a2ddaebf6f54a2a)) ### Bug Fixes * correct [Object Object] display in search_code includeContent ([bdabd6b](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/bdabd6bbeb3f60347c37499bdcb621f5c206dfe0)) * resolve parameter conflict in getItemContent function ([38d624c](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/38d624c10dcfad26bab6d04a9290ad05097f5052)) * simplify content handling in search_code to properly process ReadableStream ([136a90a](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/136a90a94f446e2c4227d87286b8d71ef8223212)) ### Performance Improvements * optimize git hooks with lint-staged ([ba953d8](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/ba953d84706893d56a82573c8d9e8ecdf3b09591)) ## [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) ### Bug Fixes * search_work_items authentication with Azure Identity ([cdb2e72](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/cdb2e722ee3abf6be465adcad7dc294f7c623103)) ## [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) ### Bug Fixes * 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)) ## [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) ### Features * add 'expand' option to get_work_item ([6bee365](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6bee365d9b37f7e197eaff03065e713ab0ee1c5f)) * Add npm publish to release.yml ([50d0368](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/50d0368c090adc39a9b3ece67d198cabcd18c6ce)) * add pre-commit hook for prettier and eslint ([1b4ddff](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/1b4ddff90e3c3ab9954d041398d224f03c632f63)) * enhance GitHub release notes with changelog content ([2fb275d](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2fb275d38acbc9c092584573a549466ccd5482bc)) * implement automated release workflow ([9e5a5df](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/9e5a5dfacdd87ca933ed02efbd0aa8035239332d)) * 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) * implement get_repository_details core functionality ([dcef80b](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/dcef80b922ef338f6d3704ab30f59c1b126c70ee)) * implement manage work item link handler ([72cd641](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/72cd6419cf804eb0d72d5ba7763ad5b46bc35650)) * implement search_wiki handler with tests ([286598c](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/286598c47052ade3b6a524938046b3e3b9341b3a)) * implement search_work_items handler with tests ([e244658](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/e2446587e6f82fb7e2dbfe47d2d034ecfdfc3189)) * **search:** add code search functionality for Azure DevOps repos ([0680102](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/068010236b10d8ed444ec01bd6820b27c5c9dcdc)) ### Bug Fixes * add bin field to make package executable with npx ([2d3d5fa](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2d3d5fa31a9ba741c4a85d7ef21d72ff46270695)) * add build step to workflow and ensure dist files are included in package ([6e12d3c](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6e12d3ca666937c7b24c7c5d8b161fbb8e34798c)) * add parent-child relationship support for createWorkItem ([31d5efe](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/31d5efef49c162772e64eabd1e4012d8143dc270)) * add tag_name parameter to GitHub release action ([68cfa43](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/68cfa43839c5975cdf9c2ec8a5348ace6138d1c2)) * improve cross-platform CLI compatibility for Windows ([0f6ed3f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/0f6ed3fe7c72ba63ec5485047ce52e06278457ab)) * make AZURE_DEVOPS_AUTH_METHOD parameter case-insensitive ([9bbf53f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/9bbf53ffcc1a9170e6ba038fee182da0621be777)) * only request max 200 by default ([296de35](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/296de3584346bd05c14dec3b39dff9a5ec0036a5)) * resolve npm publish authentication and package content issues ([96e91d0](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/96e91d04ec620ad77fc35fea31c2b7795fb73d9e)) * restore tests/setup.ts to fix test suite ([5e23eab](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/5e23eab1228f3949c431f1b8509ad5fbf829e528)) * revert to direct execution of index.js to fix main module detection ([82efa90](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/82efa90852f56db3a0b028ec50eb5230072da88a)) * Typo in release.yaml workflow ([e0de15f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/e0de15fd220ef2141466cf0530383921ed99253d)) ## [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) ### Features * add 'expand' option to get_work_item ([6bee365](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6bee365d9b37f7e197eaff03065e713ab0ee1c5f)) * Add npm publish to release.yml ([50d0368](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/50d0368c090adc39a9b3ece67d198cabcd18c6ce)) * add pre-commit hook for prettier and eslint ([1b4ddff](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/1b4ddff90e3c3ab9954d041398d224f03c632f63)) * enhance GitHub release notes with changelog content ([2fb275d](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2fb275d38acbc9c092584573a549466ccd5482bc)) * implement automated release workflow ([9e5a5df](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/9e5a5dfacdd87ca933ed02efbd0aa8035239332d)) * 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) * implement get_repository_details core functionality ([dcef80b](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/dcef80b922ef338f6d3704ab30f59c1b126c70ee)) * implement manage work item link handler ([72cd641](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/72cd6419cf804eb0d72d5ba7763ad5b46bc35650)) * implement search_wiki handler with tests ([286598c](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/286598c47052ade3b6a524938046b3e3b9341b3a)) * implement search_work_items handler with tests ([e244658](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/e2446587e6f82fb7e2dbfe47d2d034ecfdfc3189)) * **search:** add code search functionality for Azure DevOps repos ([0680102](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/068010236b10d8ed444ec01bd6820b27c5c9dcdc)) ### Bug Fixes * add bin field to make package executable with npx ([2d3d5fa](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/2d3d5fa31a9ba741c4a85d7ef21d72ff46270695)) * add build step to workflow and ensure dist files are included in package ([6e12d3c](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6e12d3ca666937c7b24c7c5d8b161fbb8e34798c)) * add parent-child relationship support for createWorkItem ([31d5efe](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/31d5efef49c162772e64eabd1e4012d8143dc270)) * add tag_name parameter to GitHub release action ([68cfa43](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/68cfa43839c5975cdf9c2ec8a5348ace6138d1c2)) * improve cross-platform CLI compatibility for Windows ([0f6ed3f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/0f6ed3fe7c72ba63ec5485047ce52e06278457ab)) * make AZURE_DEVOPS_AUTH_METHOD parameter case-insensitive ([9bbf53f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/9bbf53ffcc1a9170e6ba038fee182da0621be777)) * only request max 200 by default ([296de35](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/296de3584346bd05c14dec3b39dff9a5ec0036a5)) * resolve npm publish authentication and package content issues ([96e91d0](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/96e91d04ec620ad77fc35fea31c2b7795fb73d9e)) * restore tests/setup.ts to fix test suite ([5e23eab](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/5e23eab1228f3949c431f1b8509ad5fbf829e528)) * revert to direct execution of index.js to fix main module detection ([82efa90](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/82efa90852f56db3a0b028ec50eb5230072da88a)) * Typo in release.yaml workflow ([e0de15f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/e0de15fd220ef2141466cf0530383921ed99253d)) ### [0.1.11](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/v0.1.10...v0.1.11) (2025-04-01) ### Features * **search:** add code search functionality for Azure DevOps repos ([0680102](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/068010236b10d8ed444ec01bd6820b27c5c9dcdc)) ### [0.1.10](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/v0.1.9...v0.1.10) (2025-04-01) ### Features * add 'expand' option to get_work_item ([6bee365](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/6bee365d9b37f7e197eaff03065e713ab0ee1c5f)) ### Bug Fixes * only request max 200 by default ([296de35](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/296de3584346bd05c14dec3b39dff9a5ec0036a5)) ### [0.1.9](https://github.com/Tiberriver256/mcp-server-azure-devops/compare/v0.1.8...v0.1.9) (2025-03-31) ### Features * add pre-commit hook for prettier and eslint ([1b4ddff](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/1b4ddff90e3c3ab9954d041398d224f03c632f63)) * implement manage work item link handler ([72cd641](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/72cd6419cf804eb0d72d5ba7763ad5b46bc35650)) ### Bug Fixes * add parent-child relationship support for createWorkItem ([31d5efe](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/31d5efef49c162772e64eabd1e4012d8143dc270)) * make AZURE_DEVOPS_AUTH_METHOD parameter case-insensitive ([9bbf53f](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/9bbf53ffcc1a9170e6ba038fee182da0621be777)) * restore tests/setup.ts to fix test suite ([5e23eab](https://github.com/Tiberriver256/mcp-server-azure-devops/commit/5e23eab1228f3949c431f1b8509ad5fbf829e528)) ### [0.1.8](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.7...v0.1.8) (2025-03-26) ### Bug Fixes * revert to direct execution of index.js to fix main module detection ([82efa90](https://github.com/Tiberriver256/azure-devops-mcp/commit/82efa90852f56db3a0b028ec50eb5230072da88a)) ### [0.1.7](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.6...v0.1.7) (2025-03-26) ### Bug Fixes * add build step to workflow and ensure dist files are included in package ([6e12d3c](https://github.com/Tiberriver256/azure-devops-mcp/commit/6e12d3ca666937c7b24c7c5d8b161fbb8e34798c)) ### [0.1.6](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.5...v0.1.6) (2025-03-26) ### Bug Fixes * improve cross-platform CLI compatibility for Windows ([0f6ed3f](https://github.com/Tiberriver256/azure-devops-mcp/commit/0f6ed3fe7c72ba63ec5485047ce52e06278457ab)) ### [0.1.5](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.4...v0.1.5) (2025-03-26) ### Bug Fixes * add bin field to make package executable with npx ([2d3d5fa](https://github.com/Tiberriver256/azure-devops-mcp/commit/2d3d5fa31a9ba741c4a85d7ef21d72ff46270695)) ### [0.1.4](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.3...v0.1.4) (2025-03-26) ### Bug Fixes * resolve npm publish authentication and package content issues ([96e91d0](https://github.com/Tiberriver256/azure-devops-mcp/commit/96e91d04ec620ad77fc35fea31c2b7795fb73d9e)) ### [0.1.3](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.2...v0.1.3) (2025-03-26) ### Features * Add npm publish to release.yml ([50d0368](https://github.com/Tiberriver256/azure-devops-mcp/commit/50d0368c090adc39a9b3ece67d198cabcd18c6ce)) ### Bug Fixes * Typo in release.yaml workflow ([e0de15f](https://github.com/Tiberriver256/azure-devops-mcp/commit/e0de15fd220ef2141466cf0530383921ed99253d)) ### [0.1.2](https://github.com/Tiberriver256/azure-devops-mcp/compare/v0.1.1...v0.1.2) (2025-03-26) ### Bug Fixes * add tag_name parameter to GitHub release action ([68cfa43](https://github.com/Tiberriver256/azure-devops-mcp/commit/68cfa43839c5975cdf9c2ec8a5348ace6138d1c2)) ### 0.1.1 (2025-03-26) ### Features * enhance GitHub release notes with changelog content ([2fb275d](https://github.com/Tiberriver256/azure-devops-mcp/commit/2fb275d38acbc9c092584573a549466ccd5482bc)) * implement automated release workflow ([9e5a5df](https://github.com/Tiberriver256/azure-devops-mcp/commit/9e5a5dfacdd87ca933ed02efbd0aa8035239332d)) ## 0.1.0 (2025-03-26) ### Features * enhance GitHub release notes with changelog content ([dcaf554](https://github.com/Tiberriver256/azure-devops-mcp/commit/dcaf5542fc08cbb9bd665623d305ae7879758f4e)) * implement automated release workflow ([6fbf41e](https://github.com/Tiberriver256/azure-devops-mcp/commit/6fbf41e5a52c4db054355d4aced33744f6b1a6eb)) ```