@tiberriver256/mcp-server-azure-devops
Version:
Azure DevOps reference server for the Model Context Protocol (MCP)
491 lines • 22.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WikiClient = void 0;
exports.getWikiClient = getWikiClient;
exports.getAuthorizationHeader = getAuthorizationHeader;
const axios_1 = __importDefault(require("axios"));
const identity_1 = require("@azure/identity");
const errors_1 = require("../shared/errors");
const environment_1 = require("../utils/environment");
class WikiClient {
baseUrl;
organizationId;
constructor(organizationId) {
this.organizationId = organizationId || environment_1.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
*/
async getProjectId(projectNameOrId) {
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_1.default.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;
if (axiosError.response) {
const status = axiosError.response.status;
const errorMessage = typeof axiosError.response.data === 'object' &&
axiosError.response.data
? axiosError.response.data
.message || axiosError.message
: axiosError.message;
if (status === 404) {
throw new errors_1.AzureDevOpsResourceNotFoundError(`Project not found: ${projectNameOrId}`);
}
if (status === 401 || status === 403) {
throw new errors_1.AzureDevOpsPermissionError(`Permission denied to access project: ${projectNameOrId}`);
}
throw new errors_1.AzureDevOpsError(`Failed to get project details: ${errorMessage}`);
}
throw new errors_1.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, params) {
// Use the default project if not provided
const project = projectId || environment_1.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_1.default.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;
// 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
.message || axiosError.message
: axiosError.message;
// Handle 404 Not Found
if (status === 404) {
throw new errors_1.AzureDevOpsResourceNotFoundError(`Project not found: ${projectId}`);
}
// Handle 401 Unauthorized or 403 Forbidden
if (status === 401 || status === 403) {
throw new errors_1.AzureDevOpsPermissionError(`Permission denied to create wiki in project: ${projectId}`);
}
// Handle validation errors
if (status === 400) {
throw new errors_1.AzureDevOpsValidationError(`Invalid wiki creation parameters: ${errorMessage}`);
}
// Handle other error statuses
throw new errors_1.AzureDevOpsError(`Failed to create wiki: ${errorMessage}`);
}
// Handle network errors
throw new errors_1.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, wikiId, pagePath) {
// Use the default project if not provided
const project = projectId || environment_1.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 = {
'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_1.default.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;
// 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
.message || axiosError.message
: axiosError.message;
// Handle 404 Not Found
if (status === 404) {
throw new errors_1.AzureDevOpsResourceNotFoundError(`Wiki page not found: ${pagePath} in wiki ${wikiId}`);
}
// Handle 401 Unauthorized or 403 Forbidden
if (status === 401 || status === 403) {
throw new errors_1.AzureDevOpsPermissionError(`Permission denied to access wiki page: ${pagePath}`);
}
// Handle other error statuses
throw new errors_1.AzureDevOpsError(`Failed to get wiki page: ${errorMessage} ${axiosError.response?.data}`);
}
// Handle network errors
throw new errors_1.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, projectId, wikiId, pagePath, options) {
// Use the default project if not provided
const project = projectId || environment_1.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 = {
'api-version': '7.1',
path: encodedPagePath,
};
// Prepare the request payload
const payload = {
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_1.default.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;
// 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
.message || axiosError.message
: axiosError.message;
// Handle 404 Not Found - usually means the parent path doesn't exist
if (status === 404) {
throw new errors_1.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 errors_1.AzureDevOpsPermissionError(`Permission denied to create wiki page: ${pagePath}`);
}
// Handle 412 Precondition Failed - page might already exist
if (status === 412) {
throw new errors_1.AzureDevOpsValidationError(`Wiki page already exists: ${pagePath}`);
}
// Handle 400 Bad Request - usually validation errors
if (status === 400) {
throw new errors_1.AzureDevOpsValidationError(`Invalid request when creating wiki page: ${errorMessage}`);
}
// Handle other error statuses
throw new errors_1.AzureDevOpsError(`Failed to create wiki page: ${errorMessage}`);
}
// Handle network errors
throw new errors_1.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, projectId, wikiId, pagePath, options) {
// Use the default project if not provided
const project = projectId || environment_1.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 errors_1.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 = {
'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 = {
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 = {
content: content.content,
};
// Make the API request
const response = await axios_1.default.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;
// 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
.message || axiosError.message
: axiosError.message;
// Handle 404 Not Found
if (status === 404) {
throw new errors_1.AzureDevOpsResourceNotFoundError(`Wiki page not found: ${pagePath} in wiki ${wikiId}`);
}
// Handle 401 Unauthorized or 403 Forbidden
if (status === 401 || status === 403) {
throw new errors_1.AzureDevOpsPermissionError(`Permission denied to update wiki page: ${pagePath}`);
}
// Handle 412 Precondition Failed (version conflict)
if (status === 412) {
throw new errors_1.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 errors_1.AzureDevOpsError(`Failed to update wiki page: ${errorMessage}`);
}
// Handle network errors
throw new errors_1.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, wikiId) {
// Use the default project if not provided
const project = projectId || environment_1.defaultProject;
// Construct the URL for the Pages Batch API
const url = `${this.baseUrl}/${project}/_apis/wiki/wikis/${wikiId}/pagesbatch`;
const allPages = [];
let continuationToken;
try {
// Get authorization header
const authHeader = await getAuthorizationHeader();
do {
// Prepare the request body
const requestBody = {
top: 100,
...(continuationToken && { continuationToken }),
};
// Make the API request
const response = await axios_1.default.post(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;
// 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
.message || axiosError.message
: axiosError.message;
// Handle 404 Not Found
if (status === 404) {
throw new errors_1.AzureDevOpsResourceNotFoundError(`Wiki not found: ${wikiId} in project ${projectId}`);
}
// Handle 401 Unauthorized or 403 Forbidden
if (status === 401 || status === 403) {
throw new errors_1.AzureDevOpsPermissionError(`Permission denied to list wiki pages in wiki: ${wikiId}`);
}
// Handle other error statuses
throw new errors_1.AzureDevOpsError(`Failed to list wiki pages: ${errorMessage}`);
}
// Handle network errors
throw new errors_1.AzureDevOpsError(`Network error when listing wiki pages: ${axiosError.message}`);
}
}
}
exports.WikiClient = WikiClient;
/**
* Creates a Wiki client for Azure DevOps operations
* @param options - Options for creating the client
* @returns A Wiki client instance
*/
async function getWikiClient(options) {
const { organizationId } = options;
return new WikiClient(organizationId || environment_1.defaultOrg);
}
/**
* Get the authorization header for Azure DevOps API requests
* @returns The authorization header
*/
async function getAuthorizationHeader() {
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 identity_1.AzureCliCredential()
: new identity_1.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 errors_1.AzureDevOpsValidationError(`Failed to get authorization header: ${error instanceof Error ? error.message : String(error)}`);
}
}
//# sourceMappingURL=azure-devops.js.map