Skip to main content
Glama
mshegolev

harbor-registry-mcp

harbor_delete_old_artifacts

Destructive

Delete old artifacts from a Harbor repository, keeping only the N newest. Use dry run to preview deletions before executing.

Instructions

Keep the N newest artifacts in a repository, delete the rest.

DESTRUCTIVE. dry_run=True is the default — the agent must explicitly set dry_run=False to actually delete. Each entry in to_delete carries a deleted field (True/False after real run, None in dry-run).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_nameYesHarbor project name.
repository_nameYesRepository name within the project.
keep_countNoNumber of newest artifacts to keep.
dry_runNoIf True (default) — only report what would be deleted, do not delete.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
projectYes
repositoryYes
dry_runYes
keepingYes
to_delete_countYes
freed_sizeYes
freed_bytesYes
to_deleteYes
hintYes
messageYes

Implementation Reference

  • The main handler function for harbor_delete_old_artifacts. It sorts all artifacts by push_time, keeps the N newest (keep_count), and deletes the rest. Supports dry_run mode (default true). Iterates over artifacts to delete, calling Harbor API to delete each one when dry_run=False, tracking freed size.
    def harbor_delete_old_artifacts(
        project_name: Annotated[str, Field(min_length=1, max_length=255, description="Harbor project name.")],
        repository_name: Annotated[
            str, Field(min_length=1, max_length=255, description="Repository name within the project.")
        ],
        keep_count: Annotated[int, Field(default=1, ge=0, le=1000, description="Number of newest artifacts to keep.")] = 1,
        dry_run: Annotated[
            bool, Field(default=True, description="If True (default) — only report what would be deleted, do not delete.")
        ] = True,
    ) -> DeleteOldOutput:
        """Keep the N newest artifacts in a repository, delete the rest.
    
        **DESTRUCTIVE.** ``dry_run=True`` is the default — the agent must
        explicitly set ``dry_run=False`` to actually delete. Each entry in
        ``to_delete`` carries a ``deleted`` field (``True``/``False`` after
        real run, ``None`` in dry-run).
        """
        try:
            client = get_client()
            artifacts = _list_artifacts(client, project_name, repository_name)
            sorted_arts = sorted(artifacts, key=lambda a: a.get("push_time") or "", reverse=True)
            to_keep_raw = sorted_arts[:keep_count]
            to_delete_raw = sorted_arts[keep_count:]
    
            if not to_delete_raw:
                empty: DeleteOldOutput = {
                    "project": project_name,
                    "repository": repository_name,
                    "dry_run": dry_run,
                    "keeping": [
                        {
                            "tags": [t["name"] for t in (a.get("tags") or [])],
                            "digest": a.get("digest", "")[:19],
                            "push_time": _short(a.get("push_time")),
                        }
                        for a in to_keep_raw
                    ],
                    "to_delete_count": 0,
                    "freed_size": "0 B",
                    "freed_bytes": 0,
                    "to_delete": [],
                    "hint": None,
                    "message": f"Nothing to delete — repository has {len(sorted_arts)} artifacts, keep_count={keep_count}.",
                }
                return output.ok(empty, empty["message"] or "")  # type: ignore[return-value]
    
            to_delete: list[ToDeleteArtifact] = []
            kept: list[KeptArtifact] = [
                {
                    "tags": [t["name"] for t in (a.get("tags") or [])],
                    "digest": a.get("digest", "")[:19],
                    "push_time": _short(a.get("push_time")),
                }
                for a in to_keep_raw
            ]
            total_freed = 0
    
            for art in to_delete_raw:
                digest = art.get("digest", "")
                tags = [t["name"] for t in (art.get("tags") or [])]
                size = int(art.get("size", 0) or 0)
                entry: ToDeleteArtifact = {
                    "tags": tags,
                    "digest": digest[:19],
                    "size": size_human(size),
                    "push_time": _short(art.get("push_time")),
                    "deleted": None,
                }
                if not dry_run:
                    try:
                        client.delete(
                            f"/projects/{project_name}/repositories/{encode_repo(repository_name)}/artifacts/{encode_repo(digest)}"
                        )
                        entry["deleted"] = True
                        total_freed += size
                    except Exception as e:
                        entry["deleted"] = False
                        entry["tags"].append(f"(error: {str(e)[:80]})")
                else:
                    total_freed += size
                to_delete.append(entry)
    
            result: DeleteOldOutput = {
                "project": project_name,
                "repository": repository_name,
                "dry_run": dry_run,
                "keeping": kept,
                "to_delete_count": len(to_delete),
                "freed_size": size_human(total_freed),
                "freed_bytes": total_freed,
                "to_delete": to_delete,
                "hint": "Re-run with dry_run=False to perform the deletion." if dry_run else None,
                "message": None,
            }
            prefix = "🔎 dry-run" if dry_run else "🗑 deleted"
            md = (
                f"## {prefix}: keep {keep_count} of {len(sorted_arts)} "
                f"in {project_name}/{repository_name} "
                f"({size_human(total_freed)} freed)"
            )
            return output.ok(result, md)  # type: ignore[return-value]
        except Exception as exc:
            output.fail(exc, f"deleting old artifacts in {project_name}/{repository_name}")
  • DeleteOldOutput TypedDict — output schema for harbor_delete_old_artifacts. Contains project, repository, dry_run, keeping (list of KeptArtifact), to_delete_count, freed_size, freed_bytes, to_delete (list of ToDeleteArtifact), hint, and message.
    class DeleteOldOutput(TypedDict):
        """Result of :func:`harbor_delete_old_artifacts`.
    
        ``dry_run=True`` means no artifacts were actually deleted — every item in
        ``to_delete`` has ``deleted=None`` and ``freed_*`` reflects hypothetical
        space reclaim. ``message`` is set when nothing matched.
        """
    
        project: str
        repository: str
        dry_run: bool
        keeping: list[KeptArtifact]
        to_delete_count: int
        freed_size: str
        freed_bytes: int
        to_delete: list[ToDeleteArtifact]
        hint: str | None
        message: str | None
  • ToDeleteArtifact TypedDict — schema for each artifact entry in the to_delete list. Fields: tags, digest, size, push_time, deleted (bool | None).
    class ToDeleteArtifact(TypedDict):
        tags: list[str]
        digest: str
        size: str
        push_time: str | None
        deleted: bool | None
  • KeptArtifact TypedDict — schema for artifacts that are kept. Fields: tags, digest, push_time.
    class KeptArtifact(TypedDict):
        tags: list[str]
        digest: str
        push_time: str | None
  • Registration of the tool using @mcp.tool() decorator with name='harbor_delete_old_artifacts', annotations including destructiveHint=True, and structured_output=True.
    @mcp.tool(
        name="harbor_delete_old_artifacts",
        annotations={
            "title": "Delete Old Artifacts (keep N)",
            "readOnlyHint": False,
            "destructiveHint": True,
            "idempotentHint": False,
            "openWorldHint": True,
        },
        structured_output=True,
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already set destructiveHint=true and readOnlyHint=false. The description adds key behavior: 'dry_run=True is the default' and that each to_delete entry has a 'deleted' field (True/False after real run, None in dry-run). This adds value beyond annotations without contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is very concise: two sentences plus a note. It front-loads the main purpose and essential safety information without any wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the output schema (mentioned in context signals) and good annotations, the description covers the key aspects: purpose, destructive nature, dry-run default, and a preview of the response. It is complete for a cleanup tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Input schema has 100% coverage with descriptions for all 4 parameters. The description adds extra context: the dry_run default behavior and the output field 'deleted' which is not in the input schema, providing additional meaning.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The title and description clearly state: 'Keep the N newest artifacts in a repository, delete the rest.' It uses a specific verb (delete) and resource (old artifacts in repository) and distinguishes from siblings like harbor_delete_artifact (single artifact) and harbor_delete_untagged (untagged only).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description warns 'DESTRUCTIVE' and explains the dry_run default, advising the agent to explicitly set dry_run=False for actual deletion. This provides clear usage guidance and safety context, though it doesn't explicitly list when not to use it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

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/mshegolev/harbor-registry-mcp'

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