@tiberriver256/mcp-server-azure-devops
Version:
Azure DevOps reference server for the Model Context Protocol (MCP)
355 lines • 15.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAzureDevOpsServer = createAzureDevOpsServer;
exports.getConnection = getConnection;
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const GitInterfaces_1 = require("azure-devops-node-api/interfaces/GitInterfaces");
const config_1 = require("./shared/config");
const errors_1 = require("./shared/errors");
const handle_request_error_1 = require("./shared/errors/handle-request-error");
const auth_1 = require("./shared/auth");
// Import environment defaults when needed in feature handlers
// Import feature modules with request handlers and tool definitions
const work_items_1 = require("./features/work-items");
const projects_1 = require("./features/projects");
const repositories_1 = require("./features/repositories");
const organizations_1 = require("./features/organizations");
const search_1 = require("./features/search");
const users_1 = require("./features/users");
const pull_requests_1 = require("./features/pull-requests");
const pipelines_1 = require("./features/pipelines");
const wikis_1 = require("./features/wikis");
// Create a safe console logging function that won't interfere with MCP protocol
function safeLog(message) {
process.stderr.write(`${message}\n`);
}
/**
* Create an Azure DevOps MCP Server
*
* @param config The Azure DevOps configuration
* @returns A configured MCP server instance
*/
function createAzureDevOpsServer(config) {
// Validate the configuration
validateConfig(config);
// Initialize the MCP server
const server = new index_js_1.Server({
name: 'azure-devops-mcp',
version: config_1.VERSION,
}, {
capabilities: {
tools: {},
resources: {},
},
});
// Register the ListTools request handler
server.setRequestHandler(types_js_1.ListToolsRequestSchema, () => {
// Combine tools from all features
const tools = [
...users_1.usersTools,
...organizations_1.organizationsTools,
...projects_1.projectsTools,
...repositories_1.repositoriesTools,
...work_items_1.workItemsTools,
...search_1.searchTools,
...pull_requests_1.pullRequestsTools,
...pipelines_1.pipelinesTools,
...wikis_1.wikisTools,
];
return { tools };
});
// Register the resource handlers
// ListResources - register available resource templates
server.setRequestHandler(types_js_1.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(types_js_1.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 errors_1.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;
let version;
if (segments[3] === 'branches' && segments.length >= 5) {
versionType = GitInterfaces_1.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 = GitInterfaces_1.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 = GitInterfaces_1.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 errors_1.AzureDevOpsError) {
throw error;
}
throw new errors_1.AzureDevOpsResourceNotFoundError(`Failed to read resource: ${error instanceof Error ? error.message : String(error)}`);
}
});
// Register the CallTool request handler
server.setRequestHandler(types_js_1.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 ((0, work_items_1.isWorkItemsRequest)(request)) {
return await (0, work_items_1.handleWorkItemsRequest)(connection, request);
}
if ((0, projects_1.isProjectsRequest)(request)) {
return await (0, projects_1.handleProjectsRequest)(connection, request);
}
if ((0, repositories_1.isRepositoriesRequest)(request)) {
return await (0, repositories_1.handleRepositoriesRequest)(connection, request);
}
if ((0, organizations_1.isOrganizationsRequest)(request)) {
// Organizations feature doesn't need the config object anymore
return await (0, organizations_1.handleOrganizationsRequest)(connection, request);
}
if ((0, search_1.isSearchRequest)(request)) {
return await (0, search_1.handleSearchRequest)(connection, request);
}
if ((0, users_1.isUsersRequest)(request)) {
return await (0, users_1.handleUsersRequest)(connection, request);
}
if ((0, pull_requests_1.isPullRequestsRequest)(request)) {
return await (0, pull_requests_1.handlePullRequestsRequest)(connection, request);
}
if ((0, pipelines_1.isPipelinesRequest)(request)) {
return await (0, pipelines_1.handlePipelinesRequest)(connection, request);
}
if ((0, wikis_1.isWikisRequest)(request)) {
return await (0, wikis_1.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 (0, handle_request_error_1.handleResponseError)(error);
}
});
return server;
}
/**
* Get a mime type based on file extension
*
* @param path File path
* @returns Mime type string
*/
function getMimeType(path) {
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) {
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 errors_1.AzureDevOpsValidationError('Organization URL is required');
}
// Set default authentication method if not specified
if (!config.authMethod) {
config.authMethod = auth_1.AuthenticationMethod.AzureIdentity;
}
// Validate PAT if using PAT authentication
if (config.authMethod === auth_1.AuthenticationMethod.PersonalAccessToken &&
!config.personalAccessToken) {
throw new errors_1.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
*/
async function getConnection(config) {
try {
// Create a client with the appropriate authentication method
const client = new auth_1.AzureDevOpsClient({
method: config.authMethod || auth_1.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 errors_1.AzureDevOpsAuthenticationError(`Failed to connect to Azure DevOps: ${error instanceof Error ? error.message : String(error)}`);
}
}
//# sourceMappingURL=server.js.map