projects.ts•8.97 kB
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Config, Resources } from '../../utils/mcp-helpers.js';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import fetch from 'cross-fetch';
import { projectsListSchema, projectSchema } from '../../schemas/jira.js';
import { getProjects as getProjectsApi, getProject as getProjectApi } from '../../utils/jira-resource-api.js';
const logger = Logger.getLogger('JiraResource:Projects');
/**
 * Create basic headers for Atlassian API with Basic Authentication
 */
function createBasicHeaders(email: string, apiToken: string) {
  const auth = Buffer.from(`${email}:${apiToken}`).toString('base64');
  return {
    'Authorization': `Basic ${auth}`,
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'User-Agent': 'MCP-Atlassian-Server/1.0.0'
  };
}
/**
 * Helper function to get the list of projects
 */
async function getProjects(config: AtlassianConfig): Promise<any[]> {
  return await getProjectsApi(config);
}
/**
 * Helper function to get project details
 */
async function getProject(config: AtlassianConfig, projectKey: string): Promise<any> {
  return await getProjectApi(config, projectKey);
}
/**
 * Register resources related to Jira projects
 * @param server MCP Server instance
 */
export function registerProjectResources(server: McpServer) {
  // Resource: List all projects
  server.resource(
    'jira-projects-list',
    new ResourceTemplate('jira://projects', {
      list: async (_extra) => {
        return {
          resources: [
            {
              uri: 'jira://projects',
              name: 'Jira Projects',
              description: 'List and search all Jira projects',
              mimeType: 'application/json'
            }
          ]
        };
      }
    }),
    async (uri, _params, _extra) => {
      logger.info('Getting list of Jira projects');
      try {
        // Get config from environment
        const config = Config.getAtlassianConfigFromEnv();
        
        // Get the list of projects from Jira API
        const projects = await getProjects(config);
        // Convert response to a more friendly format
        const formattedProjects = projects.map((project: any) => ({
          id: project.id,
          key: project.key,
          name: project.name,
          projectType: project.projectTypeKey,
          url: `${config.baseUrl}/browse/${project.key}`,
          lead: project.lead?.displayName || 'Unknown'
        }));
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        // Return standardized resource with metadata and schema
        return Resources.createStandardResource(
          uriString,
          formattedProjects,
          'projects',
          projectsListSchema,
          formattedProjects.length,
          formattedProjects.length,
          0,
          `${config.baseUrl}/jira/projects`
        );
      } catch (error) {
        logger.error('Error getting Jira projects:', error);
        throw error;
      }
    }
  );
  
  // Resource: Project details
  server.resource(
    'jira-project-details',
    new ResourceTemplate('jira://projects/{projectKey}', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://projects/{projectKey}',
            name: 'Jira Project Details',
            description: 'Get details for a specific Jira project by key. Replace {projectKey} with the project key.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      try {
        // Get config from environment
        const config = Config.getAtlassianConfigFromEnv();
        
        // Get projectKey from URI pattern
        let normalizedProjectKey = '';
        if (params && 'projectKey' in params) {
          normalizedProjectKey = Array.isArray(params.projectKey) ? params.projectKey[0] : params.projectKey;
        }
        
        if (!normalizedProjectKey) {
          throw new ApiError(
            ApiErrorType.VALIDATION_ERROR,
            'Project key not provided',
            400,
            new Error('Missing project key parameter')
          );
        }
        logger.info(`Getting details for Jira project: ${normalizedProjectKey}`);
        
        // Get project info from Jira API
        const project = await getProject(config, normalizedProjectKey);
        // Convert response to a more friendly format
        const formattedProject = {
          id: project.id,
          key: project.key,
          name: project.name,
          description: project.description || 'No description',
          lead: project.lead?.displayName || 'Unknown',
          url: `${config.baseUrl}/browse/${project.key}`,
          projectCategory: project.projectCategory?.name || 'Uncategorized',
          projectType: project.projectTypeKey
        };
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        // Chuẩn hóa metadata/schema
        return Resources.createStandardResource(
          uriString,
          [formattedProject],
          'project',
          projectSchema,
          1,
          1,
          0,
          `${config.baseUrl}/browse/${project.key}`
        );
      } catch (error) {
        logger.error(`Error getting Jira project details:`, error);
        throw error;
      }
    }
  );
  // Resource: List roles of a project
  server.resource(
    'jira-project-roles',
    new ResourceTemplate('jira://projects/{projectKey}/roles', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://projects/{projectKey}/roles',
            name: 'Jira Project Roles',
            description: 'List roles for a Jira project. Replace {projectKey} with the project key.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      try {
        // Get config from environment
        const config = Config.getAtlassianConfigFromEnv();
        
        let normalizedProjectKey = '';
        if (params && 'projectKey' in params) {
          normalizedProjectKey = Array.isArray(params.projectKey) ? params.projectKey[0] : params.projectKey;
        }
        
        if (!normalizedProjectKey) {
          throw new Error('Missing projectKey');
        }
        logger.info(`Getting roles for Jira project: ${normalizedProjectKey}`);
        
        const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
        const headers = {
          'Authorization': `Basic ${auth}`,
          'Content-Type': 'application/json',
          'Accept': 'application/json',
          'User-Agent': 'MCP-Atlassian-Server/1.0.0'
        };
        let baseUrl = config.baseUrl;
        if (!baseUrl.startsWith('https://')) baseUrl = `https://${baseUrl}`;
        const url = `${baseUrl}/rest/api/3/project/${encodeURIComponent(normalizedProjectKey)}/role`;
        logger.debug(`Calling Jira API: ${url}`);
        const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
        if (!response.ok) {
          const statusCode = response.status;
          const responseText = await response.text();
          logger.error(`Jira API error (${statusCode}):`, responseText);
          throw new Error(`Jira API error: ${responseText}`);
        }
        const data = await response.json();
        // data is an object: key is roleName, value is URL containing roleId
        const roles = Object.entries(data).map(([roleName, url]) => {
          const urlStr = String(url);
          const match = urlStr.match(/\/role\/(\d+)$/);
          return {
            roleName,
            roleId: match ? match[1] : '',
            url: urlStr
          };
        });
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        // Chuẩn hóa metadata/schema (dùng array of role object, schema tự tạo inline)
        const rolesListSchema = {
          type: "array",
          items: {
            type: "object",
            properties: {
              roleName: { type: "string" },
              roleId: { type: "string" },
              url: { type: "string" }
            },
            required: ["roleName", "roleId", "url"]
          }
        };
        return Resources.createStandardResource(
          uriString,
          roles,
          'roles',
          rolesListSchema,
          roles.length,
          roles.length,
          0,
          `${config.baseUrl}/browse/${normalizedProjectKey}/project-roles`
        );
      } catch (error) {
        logger.error(`Error getting roles for Jira project:`, error);
        throw error;
      }
    }
  );
  
  logger.info('Jira project resources registered successfully');
}