npm package: @joaodotwork/plantas-github-projects-mcp
Description: Plantas GitHub Projects v2 MCP server (unofficial) - automation for creating projects, milestones, issues, and iterations
Author: João Doria de Souza
Latest version: 1.5.3

# GitHub Projects MCP Server

A Model Context Protocol (MCP) server for automating GitHub Projects v2 workflows - create projects, milestones, issues, and iterations programmatically.

**Works with:** Claude Desktop (desktop app) and Claude Code (VS Code extension)

## Features

- ✅ **Create Projects**: Set up new GitHub Projects v2 boards
- ✅ **Create Milestones**: Organize work into milestones
- ✅ **Create Issues**: Bulk create issues with milestones and labels
- ✅ **Add to Projects**: Automatically add issues to project boards
- ✅ **Iteration Fields**: Create weekly/sprint iteration fields
- ✅ **Add/Update Iterations**: Add new iterations to existing fields or update existing ones
- ✅ **Assign Iterations**: Distribute issues across sprints
- ✅ **Sub-issue Management**: Add, remove, and reprioritize sub-issues
- ✅ **Update Status**: Change project item status with human-readable values
- ✅ **Project Status Updates**: Add and retrieve project-level status updates (On Track, At Risk, etc.)
- ✅ **Update Project**: Modify project settings like title, description, and visibility
- ✅ **Get Info**: Retrieve repository and project metadata

## Installation

### Prerequisites

- Node.js 20+
- **Option A:** GitHub Personal Access Token (PAT) with `repo` and `project` scopes
- **Option B:** GitHub OAuth App with Device Flow enabled (auto-refreshing tokens)

### Quick Start (Recommended)

Use `npx` to run the server directly without installation:

```bash
npx -y @joaodotwork/plantas-github-projects-mcp
```

### Install as global npm package

```bash
npm install -g @joaodotwork/plantas-github-projects-mcp
```

### Build from source (Development)

```bash
git clone https://github.com/joaodotwork/plantas-github-projects-mcp.git
cd plants-github-projects-mcp
npm install
npm run build
```

## Configuration

### Authentication

The server supports two authentication methods:

#### Option A: Personal Access Token (simplest)

```bash
export GITHUB_TOKEN=ghp_your_token_here
```

#### Option B: OAuth Device Flow (recommended)

Uses a GitHub OAuth App with auto-refreshing tokens stored in encrypted local storage. No need to manually manage tokens.

```bash
export GITHUB_CLIENT_ID=your_oauth_app_client_id
export GITHUB_CLIENT_SECRET=your_oauth_app_client_secret
```

On first tool call, the server will prompt you to authorize via browser. Authentication is deferred until needed, so the server starts instantly — no risk of timeouts in Claude Code or other MCP hosts. Tokens are stored encrypted at `~/.config/github-projects-mcp/credentials.enc` and refresh automatically.

### Configure Claude

**For Claude Code (CLI):**
```bash
# With PAT
claude mcp add github-projects --env GITHUB_TOKEN=ghp_your_token_here -- npx -y @joaodotwork/plantas-github-projects-mcp

# With OAuth
claude mcp add github-projects \
  --env GITHUB_CLIENT_ID=your_client_id \
  --env GITHUB_CLIENT_SECRET=your_client_secret \
  -- npx -y @joaodotwork/plantas-github-projects-mcp
```

**For Gemini CLI:**
```bash
gemini mcp add github-projects npx -e GITHUB_TOKEN=ghp_your_token_here -- -y @joaodotwork/plantas-github-projects-mcp
```

**For Claude Desktop:** Add to `~/Library/Application Support/Claude/claude_desktop_config.json`

See [INSTALL.md](INSTALL.md) for detailed platform-specific instructions.

```json
{
  "mcpServers": {
    "github-projects": {
      "command": "npx",
      "args": ["-y", "@joaodotwork/plantas-github-projects-mcp"],
      "env": {
        "GITHUB_TOKEN": "ghp_your_token_here"
      }
    }
  }
}
```

### 3. Restart

**Claude Desktop:** Completely quit and reopen the app

**Claude Code:** Reload VS Code window (Cmd+Shift+P → "Developer: Reload Window")

## Available Tools

### `create_project`

Create a new GitHub Projects v2 board.

**Parameters:**
- `owner` (string, required): GitHub username or organization
- `title` (string, required): Project title
- `description` (string, optional): Project description

**Example:**
```typescript
{
  "owner": "joaodotwork",
  "title": "v1.0 Production Release",
  "description": "Sprint to ship v1.0"
}
```

**Returns:**
```json
{
  "id": "PVT_kwHOAwJiCM4BNC20",
  "number": 7,
  "title": "v1.0 Production Release",
  "url": "https://github.com/users/joaodotwork/projects/7"
}
```

---

### `create_milestone`

Create a milestone in a repository.

**Parameters:**
- `owner` (string, required): Repository owner
- `repo` (string, required): Repository name
- `title` (string, required): Milestone title
- `description` (string, optional): Milestone description
- `dueOn` (string, optional): Due date in ISO 8601 format

**Example:**
```typescript
{
  "owner": "joaodotwork",
  "repo": "dpds-arkiv",
  "title": "Epic 1: GitHub Metadata Workflow",
  "description": "Automate GitHub Projects sync (1 issue)"
}
```

**Returns:**
```json
{
  "id": "MI_kwDOPxqaGM4A3o8i",
  "number": 4,
  "title": "Epic 1: GitHub Metadata Workflow",
  "url": "https://github.com/joaodotwork/dpds-arkiv/milestone/4"
}
```

---

### `create_issue`

Create an issue with optional milestone, labels, and assignees.

**Parameters:**
- `owner` (string, required): Repository owner
- `repo` (string, required): Repository name
- `title` (string, required): Issue title
- `body` (string, required): Issue body (markdown)
- `milestoneNumber` (number, optional): Milestone number
- `labelIds` (string[], optional): Array of label IDs
- `assignees` (string[], optional): Array of usernames

**Example:**
```typescript
{
  "owner": "joaodotwork",
  "repo": "dpds-arkiv",
  "title": "Implement GitHub Projects Sync Workflow",
  "body": "**Epic:** GitHub Metadata Workflow\n...",
  "milestoneNumber": 4,
  "labelIds": ["LA_kwDOPxqaGM8AAAACVcj5iQ"],
  "assignees": ["joaodotwork"]
}
```

**Returns:**
```json
{
  "id": "I_kwDOPxqaGM6RkGzw",
  "number": 80,
  "title": "Implement GitHub Projects Sync Workflow",
  "url": "https://github.com/joaodotwork/dpds-arkiv/issues/80"
}
```

---

### `add_issue_to_project`

Add an issue to a Projects v2 board.

**Parameters:**
- `projectId` (string, required): Project node ID
- `issueId` (string, required): Issue node ID

**Example:**
```typescript
{
  "projectId": "PVT_kwHOAwJiCM4BNC20",
  "issueId": "I_kwDOPxqaGM6RkGzw"
}
```

**Returns:**
```json
{
  "id": "PVTI_lAHOAwJiCM4BNC20zgXYZ..."
}
```

---

### `create_iteration_field`

Create an iteration field with weekly sprints.

**Parameters:**
- `projectId` (string, required): Project node ID
- `fieldName` (string, required): Field name (e.g., "Sprint")
- `duration` (number, required): Duration in days (typically 7)
- `startDate` (string, required): Start date (YYYY-MM-DD)
- `iterations` (array, required): Array of iteration definitions

**Example:**
```typescript
{
  "projectId": "PVT_kwHOAwJiCM4BNC20",
  "fieldName": "Sprint",
  "duration": 7,
  "startDate": "2026-01-20",
  "iterations": [
    { "title": "Week 1", "startDate": "2026-01-20", "duration": 7 },
    { "title": "Week 2", "startDate": "2026-01-27", "duration": 7 },
    { "title": "Week 3", "startDate": "2026-02-03", "duration": 7 }
  ]
}
```

**Returns:**
```json
{
  "id": "PVTIF_lAHOAwJiCM4BNC20zg8J544",
  "name": "Sprint",
  "configuration": {
    "iterations": [
      {
        "id": "bab3ba50",
        "title": "Week 1",
        "startDate": "2026-01-20",
        "duration": 7
      },
      ...
    ]
  }
}
```

---

### `assign_issue_to_iteration`

Assign an issue to a specific iteration.

**Parameters:**
- `owner` (string, required): Repository owner
- `repo` (string, required): Repository name
- `projectNumber` (number, required): Project number
- `issueNumber` (number, required): Issue number
- `fieldId` (string, required): Iteration field ID
- `iterationId` (string, required): Iteration ID

**Example:**
```typescript
{
  "owner": "joaodotwork",
  "repo": "dpds-arkiv",
  "projectNumber": 7,
  "issueNumber": 80,
  "fieldId": "PVTIF_lAHOAwJiCM4BNC20zg8J544",
  "iterationId": "bab3ba50"
}
```

---

### `add_iteration`

Add a new iteration to an existing iteration field.

**Parameters:**
- `projectId` (string, required): Project node ID
- `fieldId` (string, required): Iteration field ID
- `title` (string, required): Iteration title (e.g., "Sprint 5")
- `startDate` (string, required): Start date (YYYY-MM-DD)
- `duration` (number, required): Duration in days (typically 7 or 14)

**Example:**
```typescript
{
  "projectId": "PVT_kwHOAwJiCM4BNC20",
  "fieldId": "PVTIF_lAHOAwJiCM4BNC20zg8J544",
  "title": "Phase 4: Production hardening",
  "startDate": "2026-02-10",
  "duration": 14
}
```

---

### `update_iteration`

Update an existing iteration's title, start date, or duration.

**Parameters:**
- `projectId` (string, required): Project node ID
- `fieldId` (string, required): Iteration field ID
- `iterationId` (string, required): Iteration ID to update
- `title` (string, optional): New title
- `startDate` (string, optional): New start date (YYYY-MM-DD)
- `duration` (number, optional): New duration in days

**Example:**
```typescript
{
  "projectId": "PVT_kwHOAwJiCM4BNC20",
  "fieldId": "PVTIF_lAHOAwJiCM4BNC20zg8J544",
  "iterationId": "bab3ba50",
  "title": "Sprint 3 (extended)",
  "duration": 14
}
```

---

### `add_subissue`

Add a sub-issue to a parent issue.

**Parameters:**
- `issueId` (string, required): Node ID of the parent issue
- `subIssueId` (string, optional): Node ID of the sub-issue
- `subIssueUrl` (string, optional): URL of the sub-issue
- `replaceParent` (boolean, optional): Replace parent issue if one already exists

**Example:**
```typescript
{
  "issueId": "I_kwDOPxqaGM6RkGzw",
  "subIssueId": "I_kwDOPxqaGM6RkHAB"
}
```

**Returns:**
```json
{
  "success": true,
  "message": "Sub-issue added successfully"
}
```

---

### `remove_subissue`

Remove a sub-issue from a parent issue.

**Parameters:**
- `issueId` (string, required): Node ID of the parent issue
- `subIssueId` (string, required): Node ID of the sub-issue to remove

**Example:**
```typescript
{
  "issueId": "I_kwDOPxqaGM6RkGzw",
  "subIssueId": "I_kwDOPxqaGM6RkHAB"
}
```

**Returns:**
```json
{
  "success": true,
  "message": "Sub-issue removed successfully"
}
```

---

### `reprioritize_subissue`

Reprioritize a sub-issue within a parent issue.

**Parameters:**
- `issueId` (string, required): Node ID of the parent issue
- `subIssueId` (string, required): Node ID of the sub-issue to reprioritize
- `afterId` (string, optional): ID of the sub-issue to be prioritized after
- `beforeId` (string, optional): ID of the sub-issue to be prioritized before

**Note:** Specify either `afterId` OR `beforeId`, not both.

**Example:**
```typescript
{
  "issueId": "I_kwDOPxqaGM6RkGzw",
  "subIssueId": "I_kwDOPxqaGM6RkHAB",
  "afterId": "I_kwDOPxqaGM6RkHCD"
}
```

**Returns:**
```json
{
  "success": true,
  "message": "Sub-issue reprioritized successfully"
}
```

---

### `update_item_status`

Update the status of a project item using human-readable status values.

**Parameters:**
- `projectId` (string, required): Project node ID
- `itemId` (string, required): Project item node ID
- `status` (string, required): Human-readable status (e.g., "Todo", "In Progress", "Done")

**Note:** The tool automatically finds the Status field and matches the status name (case-insensitive). It will show available options if the status is not found.

**Example:**
```typescript
{
  "projectId": "PVT_kwHOAwJiCM4BNC20",
  "itemId": "PVTI_lAHOAwJiCM4BNC20zgXYZ...",
  "status": "In Progress"
}
```

**Returns:**
```json
{
  "success": true,
  "message": "Status updated to 'In Progress'",
  "itemId": "PVTI_lAHOAwJiCM4BNC20zgXYZ..."
}
```

---

### `update_project_settings`

Update project settings like title, description, README, or visibility.

**Parameters:**
- `projectId` (string, required): Project node ID
- `title` (string, optional): New project title
- `shortDescription` (string, optional): New short description
- `readme` (string, optional): New README content (markdown)
- `public` (boolean, optional): Set project visibility (true = public, false = private)

**Example:**
```typescript
{
  "projectId": "PVT_kwHOAwJiCM4BNC20",
  "title": "Q1 2025 Product Launch",
  "shortDescription": "Sprint planning for v2.0 release",
  "public": true
}
```

**Returns:**
```json
{
  "success": true,
  "message": "Project settings updated successfully",
  "project": {
    "id": "PVT_kwHOAwJiCM4BNC20",
    "title": "Q1 2025 Product Launch",
    "shortDescription": "Sprint planning for v2.0 release",
    "public": true,
    "url": "https://github.com/users/joaodotwork/projects/7"
  }
}
```

---

### `get_repository_info`

Get repository ID, labels, and milestones.

**Parameters:**
- `owner` (string, required): Repository owner
- `repo` (string, required): Repository name

**Example:**
```typescript
{
  "owner": "joaodotwork",
  "repo": "dpds-arkiv"
}
```

**Returns:**
```json
{
  "id": "R_kgDOPxqaGA",
  "name": "dpds-arkiv",
  "labels": {
    "nodes": [
      { "id": "LA_kwDOPxqaGM8...", "name": "priority:high" },
      ...
    ]
  },
  "milestones": {
    "nodes": [
      { "id": "MI_kwDOPxqaGM4...", "number": 4, "title": "Epic 1..." },
      ...
    ]
  }
}
```

---

### `get_project_info`

Get project ID, fields, and iteration IDs.

**Parameters:**
- `owner` (string, required): Project owner
- `projectNumber` (number, required): Project number

**Example:**
```typescript
{
  "owner": "joaodotwork",
  "projectNumber": 7
}
```

**Returns:**
```json
{
  "id": "PVT_kwHOAwJiCM4BNC20",
  "title": "v1.0 Production Release",
  "number": 7,
  "fields": {
    "nodes": [
      {
        "id": "PVTIF_lAHOAwJiCM4BNC20zg8J544",
        "name": "Sprint",
        "dataType": "ITERATION",
        "configuration": {
          "iterations": [...]
        }
      },
      ...
    ]
  }
}
```

### `create_project_status_update`

Create a status update for a project board.

**Parameters:**
- `projectId` (string, required): Project node ID
- `status` (string, required): The status level (`INACTIVE`, `ON_TRACK`, `AT_RISK`, `OFF_TRACK`, `COMPLETE`)
- `body` (string, optional): Status update body (markdown)
- `startDate` (string, optional): Start date (YYYY-MM-DD)
- `targetDate` (string, optional): Target date (YYYY-MM-DD)

**Example:**
```typescript
{
  "projectId": "PVT_kwHOAwJiCM4BNC20",
  "status": "ON_TRACK",
  "body": "Project is proceeding as planned. All milestones for this week are met."
}
```

---

### `get_project_status_updates`

Get recent status updates for a project.

**Parameters:**
- `projectId` (string, required): Project node ID
- `limit` (number, optional): Number of updates to retrieve (default: 5)

**Example:**
```typescript
{
  "projectId": "PVT_kwHOAwJiCM4BNC20",
  "limit": 3
}
```

## Usage Examples

### Example 1: Create Complete Sprint Setup

```typescript
// 1. Create project
const project = await create_project({
  owner: "joaodotwork",
  title: "v1.0 Production Release",
  description: "Sprint to ship v1.0"
});

// 2. Create milestones
const milestone1 = await create_milestone({
  owner: "joaodotwork",
  repo: "dpds-arkiv",
  title: "Epic 1: GitHub Metadata Workflow",
  description: "Automate GitHub Projects sync (1 issue)"
});

// 3. Get repository info (for label IDs)
const repoInfo = await get_repository_info({
  owner: "joaodotwork",
  repo: "dpds-arkiv"
});

// 4. Create issue
const issue = await create_issue({
  owner: "joaodotwork",
  repo: "dpds-arkiv",
  title: "Implement GitHub Projects Sync Workflow",
  body: "...",
  milestoneNumber: milestone1.number,
  labelIds: [repoInfo.labels.nodes[0].id],
  assignees: ["joaodotwork"]
});

// 5. Add issue to project
await add_issue_to_project({
  projectId: project.id,
  issueId: issue.id
});

// 6. Create iteration field
const iterationField = await create_iteration_field({
  projectId: project.id,
  fieldName: "Sprint",
  duration: 7,
  startDate: "2026-01-20",
  iterations: [
    { title: "Week 1", startDate: "2026-01-20", duration: 7 },
    { title: "Week 2", startDate: "2026-01-27", duration: 7 },
    { title: "Week 3", startDate: "2026-02-03", duration: 7 }
  ]
});

// 7. Assign issue to iteration
await assign_issue_to_iteration({
  owner: "joaodotwork",
  repo: "dpds-arkiv",
  projectNumber: project.number,
  issueNumber: issue.number,
  fieldId: iterationField.id,
  iterationId: iterationField.configuration.iterations[0].id
});
```

### Example 2: Bulk Create Issues

```typescript
const issues = [
  {
    title: "Issue 1",
    body: "Description...",
    milestoneNumber: 4
  },
  {
    title: "Issue 2",
    body: "Description...",
    milestoneNumber: 5
  }
];

for (const issueData of issues) {
  const issue = await create_issue({
    owner: "joaodotwork",
    repo: "dpds-arkiv",
    ...issueData
  });

  await add_issue_to_project({
    projectId: "PVT_kwHOAwJiCM4BNC20",
    issueId: issue.id
  });
}
```

## Troubleshooting

### "401 Unauthorized" Error

- **PAT users:** Check that `GITHUB_TOKEN` is set correctly and has `repo` and `project` scopes
- **OAuth users:** Your token may have expired. Restart the MCP server to trigger a refresh or re-authentication
- The server validates tokens on first tool call and provides actionable error messages

### "Issue not found in project" Error

- Ensure the issue has been added to the project first using `add_issue_to_project`
- Verify the `projectNumber` is correct

### MCP Server Not Loading

- Check Claude Desktop config file path
- Verify JSON syntax in config file
- Restart Claude Desktop completely
- Check logs: `~/Library/Logs/Claude/mcp*.log`

## Development

```bash
# Install dependencies
npm install

# Build
npm run build

# Watch mode
npm run dev

# Test locally
node dist/index.js
```

## License

MIT

## Author

João Doria de Souza ([@joaodotwork](https://github.com/joaodotwork))

---

**Built with the MCP SDK** - Model Context Protocol for Claude Desktop