UNPKG

mcp-harbor

Version:

A Node.js application for connecting to Harbor and providing operations capabilities.

408 lines 16.6 kB
import { HarborClient } from "@hapic/harbor"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { ResourceError, ValidationError, TOOL_NAMES, } from "../types/index.js"; export class HarborService { client; constructor(apiUrl, auth) { if (!apiUrl) throw new ValidationError("API URL is required"); if (!auth.username) throw new ValidationError("Username is required"); if (!auth.password) throw new ValidationError("Password is required"); this.client = new HarborClient({ request: { credentials: "include", }, connectionOptions: { host: apiUrl, user: auth.username, password: auth.password, }, }); } // Project operations async getProjects() { try { const response = (await this.client.project.getMany({ query: {}, })); return (response?.data || []).map((project) => ({ name: project.name, project_id: project.project_id, creation_time: project.creation_time, update_time: project.update_time, })); } catch (error) { throw this.handleError(error, "Failed to get projects"); } } async getProject(projectId) { try { if (!projectId) throw new ValidationError("Project ID is required"); const response = !isNaN(Number(projectId)) ? (await this.client.project.getOne(Number(projectId))) : (await this.client.project.getOne(projectId, true)); if (!response) { throw new ResourceError(`Project ${projectId} not found`); } return { name: response.name, project_id: response.project_id, creation_time: response.creation_time, update_time: response.update_time, }; } catch (error) { throw this.handleError(error, `Failed to get project ${projectId}`); } } async createProject(projectData) { try { if (!projectData.project_name) { throw new ValidationError("Project name is required"); } const response = await this.client.project.create({ project_name: projectData.project_name, ...(projectData.metadata && { metadata: projectData.metadata }), }); // Since create only returns ID, fetch the full project details if (response.id) { return this.getProject(response.id.toString()); } throw new Error("Failed to create project: No project ID returned"); } catch (error) { throw this.handleError(error, "Failed to create project"); } } async deleteProject(projectId) { try { if (!projectId) throw new ValidationError("Project ID is required"); if (!isNaN(Number(projectId))) { await this.client.project.delete(Number(projectId)); } else { await this.client.project.delete(projectId, true); } } catch (error) { throw this.handleError(error, `Failed to delete project ${projectId}`); } } // Repository/Image operations async getRepositories(projectId) { try { if (!projectId) throw new ValidationError("Project ID is required"); const response = (await this.client.projectRepository.getMany({ projectName: projectId, query: {}, })); return (response?.data || []).map((repo) => ({ name: repo.name, artifact_count: repo.artifact_count, creation_time: repo.creation_time, update_time: repo.update_time, })); } catch (error) { throw this.handleError(error, `Failed to get repositories for project ${projectId}`); } } async deleteRepository(projectId, repositoryName) { try { if (!projectId) throw new ValidationError("Project ID is required"); if (!repositoryName) throw new ValidationError("Repository name is required"); const fullRepoName = `${projectId}/${repositoryName}`; await this.client.projectRepository.delete(fullRepoName); } catch (error) { throw this.handleError(error, `Failed to delete repository ${repositoryName}`); } } async getTags(projectId, repositoryName) { try { if (!projectId) throw new ValidationError("Project ID is required"); if (!repositoryName) throw new ValidationError("Repository name is required"); const artifacts = await this.client.projectRepositoryArtifact.getMany({ projectName: projectId, repositoryName: repositoryName, query: {}, }); return (artifacts || []).map((artifact) => ({ digest: artifact.digest, tags: artifact.tags?.map((tag) => ({ id: tag.id, name: tag.name, push_time: tag.push_time, pull_time: tag.pull_time, immutable: tag.immutable, repository_id: tag.repository_id, artifact_id: tag.artifact_id, signed: tag.signed, })), size: artifact.size, push_time: artifact.push_time, pull_time: artifact.pull_time, type: artifact.type, project_id: artifact.project_id, repository_id: artifact.repository_id, id: artifact.id, })); } catch (error) { throw this.handleError(error, `Failed to get tags for repository ${repositoryName}`); } } async deleteTag(projectId, repositoryName, tagName) { try { if (!projectId) throw new ValidationError("Project ID is required"); if (!repositoryName) throw new ValidationError("Repository name is required"); if (!tagName) throw new ValidationError("Tag name is required"); const artifacts = await this.client.projectRepositoryArtifact.getMany({ projectName: projectId, repositoryName: repositoryName, query: {}, }); const artifact = (artifacts || []).find((a) => a.tags?.some((tag) => tag.name === tagName)); if (!artifact) { throw new ResourceError(`Tag ${tagName} not found in repository ${repositoryName}`); } await this.client.projectRepositoryArtifact.delete({ projectName: projectId, repositoryName: repositoryName, tagOrDigest: artifact.digest, }); return { success: true, message: `Tag ${tagName} deleted successfully`, }; } catch (error) { throw this.handleError(error, `Failed to delete tag ${tagName}`); } } // Helm Chart operations async getCharts(projectId) { try { if (!projectId) throw new ValidationError("Project ID is required"); const response = (await this.client.projectRepository.getMany({ projectName: projectId, query: {}, })); const chartRepos = (response?.data || []).filter((repo) => repo.name && repo.name.includes("/charts/")); return chartRepos.map((repo) => ({ name: repo.name.split("/").pop() || "", total_versions: repo.artifact_count || 0, latest_version: "", created: repo.creation_time || "", updated: repo.update_time || "", })); } catch (error) { throw this.handleError(error, `Failed to get charts for project ${projectId}`); } } async getChartVersions(projectId, chartName) { try { if (!projectId) throw new ValidationError("Project ID is required"); if (!chartName) throw new ValidationError("Chart name is required"); const artifacts = await this.client.projectRepositoryArtifact.getMany({ projectName: projectId, repositoryName: `charts/${chartName}`, query: {}, }); return (artifacts || []).map((artifact) => ({ name: artifact.digest, version: artifact.tags?.[0]?.name || "", created: artifact.push_time || "", updated: artifact.push_time || "", // Using push_time as update_time is not available })); } catch (error) { throw this.handleError(error, `Failed to get versions for chart ${chartName}`); } } async deleteChart(projectId, chartName, version) { try { if (!projectId) throw new ValidationError("Project ID is required"); if (!chartName) throw new ValidationError("Chart name is required"); if (!version) throw new ValidationError("Version is required"); const artifacts = await this.client.projectRepositoryArtifact.getMany({ projectName: projectId, repositoryName: `charts/${chartName}`, query: {}, }); const artifact = (artifacts || []).find((a) => a.tags?.some((tag) => tag.name === version)); if (!artifact) { throw new ResourceError(`Chart version ${version} not found for chart ${chartName}`); } await this.client.projectRepositoryArtifact.delete({ projectName: projectId, repositoryName: `charts/${chartName}`, tagOrDigest: artifact.digest, }); return { success: true, message: `Chart ${chartName} version ${version} deleted successfully`, }; } catch (error) { throw this.handleError(error, `Failed to delete chart ${chartName} version ${version}`); } } handleError(error, defaultMessage) { if (error instanceof Error) { if (error instanceof ValidationError || error instanceof ResourceError) { throw error; } throw new Error(error.message || defaultMessage); } throw new Error(defaultMessage); } async handleToolRequest(toolName, args) { const validateParam = (param, name) => { if (!param) { throw new McpError(ErrorCode.InvalidParams, `${name} is required`); } return param; }; switch (toolName) { case TOOL_NAMES.LIST_PROJECTS: return { content: [ { type: "text", text: JSON.stringify(await this.getProjects(), null, 2), }, ], }; case TOOL_NAMES.GET_PROJECT: { const projectId = validateParam(args.projectId, "projectId"); return { content: [ { type: "text", text: JSON.stringify(await this.getProject(projectId), null, 2), }, ], }; } case TOOL_NAMES.CREATE_PROJECT: { const project_name = validateParam(args.project_name, "project_name"); const metadata = args.metadata; const projectData = { project_name, ...(metadata && { metadata }), }; return { content: [ { type: "text", text: JSON.stringify(await this.createProject(projectData), null, 2), }, ], }; } case TOOL_NAMES.DELETE_PROJECT: { const projectId = validateParam(args.projectId, "projectId"); await this.deleteProject(projectId); return { content: [{ type: "text", text: "Project deleted successfully" }], }; } case TOOL_NAMES.LIST_REPOSITORIES: { const projectId = validateParam(args.projectId, "projectId"); return { content: [ { type: "text", text: JSON.stringify(await this.getRepositories(projectId), null, 2), }, ], }; } case TOOL_NAMES.DELETE_REPOSITORY: { const projectId = validateParam(args.projectId, "projectId"); const repositoryName = validateParam(args.repositoryName, "repositoryName"); await this.deleteRepository(projectId, repositoryName); return { content: [{ type: "text", text: "Repository deleted successfully" }], }; } case TOOL_NAMES.LIST_TAGS: { const projectId = validateParam(args.projectId, "projectId"); const repositoryName = validateParam(args.repositoryName, "repositoryName"); return { content: [ { type: "text", text: JSON.stringify(await this.getTags(projectId, repositoryName), null, 2), }, ], }; } case TOOL_NAMES.DELETE_TAG: { const projectId = validateParam(args.projectId, "projectId"); const repositoryName = validateParam(args.repositoryName, "repositoryName"); const tag = validateParam(args.tag, "tag"); await this.deleteTag(projectId, repositoryName, tag); return { content: [{ type: "text", text: "Tag deleted successfully" }], }; } case TOOL_NAMES.LIST_CHARTS: { const projectId = validateParam(args.projectId, "projectId"); return { content: [ { type: "text", text: JSON.stringify(await this.getCharts(projectId), null, 2), }, ], }; } case TOOL_NAMES.LIST_CHART_VERSIONS: { const projectId = validateParam(args.projectId, "projectId"); const chartName = validateParam(args.chartName, "chartName"); return { content: [ { type: "text", text: JSON.stringify(await this.getChartVersions(projectId, chartName), null, 2), }, ], }; } case TOOL_NAMES.DELETE_CHART: { const projectId = validateParam(args.projectId, "projectId"); const chartName = validateParam(args.chartName, "chartName"); const version = validateParam(args.version, "version"); await this.deleteChart(projectId, chartName, version); return { content: [{ type: "text", text: "Chart deleted successfully" }], }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`); } } } //# sourceMappingURL=harbor.service.js.map