Skip to main content
Glama

MCP Harbor

by nomagicln
harbor.service.ts16.1 kB
import { HarborClient } from "@hapic/harbor"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { HarborArtifact, HarborArtifactTag, HarborChart, HarborChartVersion, HarborRepository, ProjectData, ResourceError, ValidationError, DeleteResponse, TOOL_NAMES, HarborMetadata, } from "../types/index.js"; // Internal type definitions to match Harbor API types interface BaseResource { name: string; creation_time?: string; update_time?: string; } interface Project extends BaseResource { project_id: number; owner_id: number; owner_name: string; repo_count: number; metadata: { public: boolean; auto_scan?: string; severity?: string; }; } interface Repository extends BaseResource { artifact_count?: number; } interface ResourceCollection<T> { data: T[]; meta?: { total?: number; }; } export class HarborService { private client: HarborClient; constructor(apiUrl: string, auth: { username: string; password: string }) { 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(): Promise<HarborRepository[]> { try { const response = (await this.client.project.getMany({ query: {}, })) as ResourceCollection<Project>; 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: unknown) { throw this.handleError(error, "Failed to get projects"); } } async getProject(projectId: string): Promise<HarborRepository> { try { if (!projectId) throw new ValidationError("Project ID is required"); const response = !isNaN(Number(projectId)) ? ((await this.client.project.getOne(Number(projectId))) as Project) : ((await this.client.project.getOne(projectId, true)) as Project); 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: unknown) { throw this.handleError(error, `Failed to get project ${projectId}`); } } async createProject(projectData: ProjectData): Promise<HarborRepository> { 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: unknown) { throw this.handleError(error, "Failed to create project"); } } async deleteProject(projectId: string): Promise<void> { 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: unknown) { throw this.handleError(error, `Failed to delete project ${projectId}`); } } // Repository/Image operations async getRepositories(projectId: string): Promise<HarborRepository[]> { try { if (!projectId) throw new ValidationError("Project ID is required"); const response = (await this.client.projectRepository.getMany({ projectName: projectId, query: {}, })) as ResourceCollection<Repository>; 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: unknown) { throw this.handleError( error, `Failed to get repositories for project ${projectId}` ); } } async deleteRepository( projectId: string, repositoryName: string ): Promise<void> { 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: unknown) { throw this.handleError( error, `Failed to delete repository ${repositoryName}` ); } } async getTags( projectId: string, repositoryName: string ): Promise<HarborArtifact[]> { 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): HarborArtifact => ({ digest: artifact.digest, tags: artifact.tags?.map( (tag): HarborArtifactTag => ({ 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: unknown) { throw this.handleError( error, `Failed to get tags for repository ${repositoryName}` ); } } async deleteTag( projectId: string, repositoryName: string, tagName: string ): Promise<DeleteResponse> { 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: unknown) { throw this.handleError(error, `Failed to delete tag ${tagName}`); } } // Helm Chart operations async getCharts(projectId: string): Promise<HarborChart[]> { try { if (!projectId) throw new ValidationError("Project ID is required"); const response = (await this.client.projectRepository.getMany({ projectName: projectId, query: {}, })) as ResourceCollection<Repository>; 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: unknown) { throw this.handleError( error, `Failed to get charts for project ${projectId}` ); } } async getChartVersions( projectId: string, chartName: string ): Promise<HarborChartVersion[]> { 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: unknown) { throw this.handleError( error, `Failed to get versions for chart ${chartName}` ); } } async deleteChart( projectId: string, chartName: string, version: string ): Promise<DeleteResponse> { 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: unknown) { throw this.handleError( error, `Failed to delete chart ${chartName} version ${version}` ); } } private handleError(error: unknown, defaultMessage: string): never { 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: string, args: Record<string, unknown> ): Promise<{ content: { type: string; text: string }[] }> { const validateParam = (param: unknown, name: string): string => { if (!param) { throw new McpError(ErrorCode.InvalidParams, `${name} is required`); } return param as string; }; 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 as HarborMetadata | undefined; const projectData: 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}` ); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nomagicln/mcp-harbor'

If you have feedback or need assistance with the MCP directory API, please join our Discord server