Closedloop.ai
MCP Server

Project Management

Create, retrieve, update, and reorder projects — the top-level containers for documents

Overview

Projects (PRO-*) are the top-level containers in Closedloop. Every document — PRD, implementation plan, or feature — belongs to a project. Projects are typically weekly or thematic (e.g. a sprint week or an "Enterprise" theme).

All project tools accept either a UUID or a human-readable slug (e.g. PRO-7). Pass the slug verbatim — the API resolves it server-side. Newly created projects return an assigned PRO-* slug, which is the preferred handle for follow-up calls.

FieldValuesDescription
priorityLOW, MEDIUM, HIGH, URGENT, nullProject priority. null means the project has none — the same field an artifact carries. A project created without one reads back as null.
statusNOT_STARTED, IN_PROGRESS, COMPLETED, ARCHIVEDProject lifecycle status

Listing Projects

Use list-projects to retrieve the projects available to the authenticated user. The returned slug values (PRO-*) are the preferred user-facing handles for follow-up calls.

Tool: list-projects Underlying API: GET /projects

list-projects(
  limit?: number,   // 1–100, default 25
  offset?: number   // Pagination offset, default 0
)

The response is a paginated envelope. Each item includes id, name, slug, status, and updatedAt:

{
  "total": 12,
  "offset": 0,
  "limit": 25,
  "returned": 12,
  "hasMore": false,
  "nextOffset": null,
  "items": [
    {
      "id": "...",
      "name": "5/11–15",
      "slug": "PRO-25",
      "status": "IN_PROGRESS",
      "updatedAt": "2025-01-15T10:00:00Z"
    }
  ]
}

Fetching a Single Project

Use get-project to read one project by UUID or slug. When the user references a project by its slug (e.g. "tell me about PRO-7"), pass that slug as projectId directly.

Tool: get-project Underlying API: GET /projects/:projectId

get-project(
  projectId: string   // PRO-* slug or UUID
)

Listing Teams

Every project belongs to a team, so list-teams is where the teamId that create-project requires comes from. Teams are identified by UUID only — there is no slug form.

Tool: list-teams Underlying API: GET /teams

list-teams(
  limit?: number,   // 1–100, default 25
  offset?: number   // Pagination offset, default 0
)

The response is the same paginated envelope list-projects returns. Each item includes id, name, slug, memberCount, projectCount, and updatedAt:

{
  "total": 2,
  "offset": 0,
  "limit": 25,
  "returned": 2,
  "hasMore": false,
  "nextOffset": null,
  "items": [
    {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "name": "Platform",
      "slug": "platform",
      "memberCount": 6,
      "projectCount": 12,
      "updatedAt": "2025-01-15T10:00:00Z"
    }
  ]
}

The organization is resolved from the connection you authenticated with, so a caller only ever sees its own organization's teams.


Creating a Project

Use create-project to create a new top-level container for documents. The assigned slug (PRO-*) is returned in the response and is the preferred handle for future calls.

A project must belong to a team. Pass teamId. A project with no team has no URL in the web app, so a human cannot open it or see the documents inside it.

Tool: create-project Underlying API: POST /projects

create-project(
  name: string,
  teamId?: string,     // UUID; required in practice — see below
  description?: string,
  priority?: "LOW" | "MEDIUM" | "HIGH" | "URGENT",
  status?: "NOT_STARTED" | "IN_PROGRESS" | "COMPLETED" | "ARCHIVED"
)

Omit priority to create the project with no priority. null is not accepted here — on create there is no existing value to clear, so omission already says it.

teamId is optional in the tool schema so that a call without one still reaches the server, where it is refused with a 400 that lists the teams in this organization — so a wrong call self-corrects without a prior lookup. Treat that roster as best-effort rather than guaranteed. It is attached only when the server can read it, and a failed read returns the refusal sentence on its own rather than turning an invalid request into a 500. It is also bounded by a character budget; when an organization has more teams than fit, the error says how many were omitted and points at list-teams, which is the complete set. If a retry still names no teams, the id has to come from list-teams (or GET /teams) on a read-scoped connection, or from a human in the web app.

Example workflow:

1. create-project(
     name: "Enterprise Onboarding",
     teamId: "7c9e6679-7425-40de-944b-e07fc1f90ae7",
     priority: "HIGH"
   )
   → returns { slug: "PRO-26", ... }

   // Don't know the team id? Call list-teams, or call without it and
   // read the 400: "At least one team is required. Teams in this
   // organization: Platform (7c9e6679-...), Growth (2b1f...)." Then retry.

2. create-document(
     title: "Onboarding Revamp",
     type: "FEATURE",
     projectId: "PRO-26",
     content: "..."
   )

Updating a Project

Use update-project to change a project's metadata, status, or owning teams by UUID or slug. Only the fields you pass are modified.

Tool: update-project Underlying API: PUT /projects/:projectId

update-project(
  projectId: string,   // PRO-* slug or UUID
  name?: string,
  teamIds?: string[],  // Team UUIDs from list-teams — replaces the current set
  description?: string,
  priority?: "LOW" | "MEDIUM" | "HIGH" | "URGENT" | null,
  status?: "NOT_STARTED" | "IN_PROGRESS" | "COMPLETED" | "ARCHIVED"
)

priority has three states. Omit it to leave the project's priority alone, pass a member to set it, or pass null to CLEAR it. Those are three different requests: omission is not the same as null.

teamIds replaces the project's teams rather than adding to them. The server deletes every existing team link and recreates exactly the set you send, so pass every team the project should belong to — not just the new one. Omitting teamIds leaves the teams untouched. An empty array is refused rather than applied, because clearing the last team is what produces the state below.

Setting teamIds is how a teamless project is repaired. A project created before a team was required has no team link, which leaves it with no URL in the web app: it does not appear in the project list, and a search hit for it has no page to open. Attaching a team makes it reachable again.

As with create-project, a team id that does not resolve comes back as a 400 naming the teams in this organization — on the same best-effort terms described there. A write-scoped connection is not offered list-teams, which is read-scoped, so sending any id and reading the roster off the refusal is that caller's only discovery path, and it has no fallback of its own when the roster is absent: retry once, and if the refusal still names no teams, the id has to come from a read-scoped connection or a human.


Reordering Project Artifacts

Use move-artifact to reorder a root artifact within its project's stack rank — the human-curated priority order shown on the project page. This is a write-scope operation.

Tool: move-artifact Underlying API: POST /projects/:projectId/artifacts/move

move-artifact(
  projectId: string,            // PRO-* slug or UUID
  artifactId: string,           // Artifact to move (PRD-*, PLN-*, FEA-*, or UUID)
  position: "top" | "bottom" | "before" | "after",
  referenceArtifactId?: string  // Required for "before"/"after"; omit for "top"/"bottom"
)

Stack rank is a project-wide, root-level ordering: only top-level documents and features participate, not nested children. Use top / bottom to move to the ends, or before / after with referenceArtifactId to position relative to another artifact. Read sortOrder from list-documents / get-document to see the current order (lower sorts first).

The response echoes the move and reports the new sort order:

{
  "moved": true,
  "artifactId": "FEA-42",
  "position": "before",
  "referenceArtifactId": "FEA-40",
  "newSortOrder": 3
}

On this page