@project-tracker/mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @project-tracker/mcp
2
+
3
+ MCP server for [Project Tracker](https://app.project-tracker.ai). Lets [Claude Code](https://claude.com/claude-code) and other MCP-aware AI clients read and update your tracker via tool calls — `list_projects`, `create_item`, `move_item`, `update_item`, etc.
4
+
5
+ If your AI client doesn't support MCP, use the [curl-prompt path](https://app.project-tracker.ai/docs/curl-prompt.md) instead — same data, different shape.
6
+
7
+ ## Install
8
+
9
+ Add to your Claude Code MCP config (`~/.claude/mcp.json` or the equivalent UI):
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "project-tracker": {
15
+ "command": "npx",
16
+ "args": ["-y", "@project-tracker/mcp"],
17
+ "env": {
18
+ "API_BASE": "https://app.project-tracker.ai",
19
+ "API_KEY": "pt_your_token_here"
20
+ }
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ Restart Claude Code. The `project-tracker` server will appear in your MCP panel with its tool catalogue.
27
+
28
+ ### Where the API key comes from
29
+
30
+ Log in to https://app.project-tracker.ai → **Integrations** (top right) → **Generate key** → name it (e.g. `Claude Code on my laptop`) → copy the `pt_...` token. Keys are shown once; store it in a password manager.
31
+
32
+ The key inherits your user's permissions — owner / team-member / collaborator access to each project. If a tool call returns `Permission denied: requires X on project Y`, it's not a bug, it's your project role.
33
+
34
+ ## Configuration
35
+
36
+ | Env var | Required | Default | Meaning |
37
+ |---|---|---|---|
38
+ | `API_KEY` | yes (in practice) | — | Your `pt_...` API key. Calls go through unauthenticated if unset, which means most operations 401. |
39
+ | `API_BASE` | no | `http://127.0.0.1:3001` | Origin of the Project Tracker API. Use `https://app.project-tracker.ai` for the hosted service, or your own host for a self-hosted PT. Note: this is the origin only — the server appends `/api/...` internally. |
40
+
41
+ ## Tools
42
+
43
+ - **Projects**: `list_projects`, `create_project`, `update_project`
44
+ - **Items**: `list_items`, `get_item`, `get_item_context`, `create_item`, `update_item`, `delete_item`, `restore_item`, `move_item`, `bulk_update_items`
45
+ - **Views**: `get_board`, `get_matrix`
46
+
47
+ Full schema for each tool surfaces in your MCP client. Argument shapes mirror the underlying REST API — full reference at https://app.project-tracker.ai/api/docs.
48
+
49
+ ## AI behaviour prompt
50
+
51
+ Drop the contents of https://app.project-tracker.ai/docs/mcp-prompt.md into your project's `CLAUDE.md` (or equivalent system prompt) so the AI knows how to use the tools — work-claiming conventions, multi-AI safety, etc. The MCP server checks the prompt version on session start and flags via `_meta.promptUpdateAvailable` if your bundled copy is stale.
52
+
53
+ ## Requirements
54
+
55
+ Node.js ≥ 18. The package ships pre-built JavaScript; `npx -y @project-tracker/mcp` installs and runs without a separate build step.
56
+
57
+ ## Source
58
+
59
+ This package lives in the [`mcp-server/`](https://github.com/DQ-ProjectTracker/project-tracker/tree/main/mcp-server) subdirectory of the Project Tracker repo. Issues + PRs welcome at https://github.com/DQ-ProjectTracker/project-tracker.
60
+
61
+ ## License
62
+
63
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ // PT-89: shebang is preserved by tsc into dist/index.js so the `bin` entry
3
+ // in package.json runs the file directly under node when invoked via npx.
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ const API_BASE = process.env.API_BASE || "http://127.0.0.1:3001";
8
+ const API_KEY = process.env.API_KEY;
9
+ // Bundled prompt version. Bump this together with the front-matter version
10
+ // in project-planning-prompt-mcp.md whenever AI behaviour changes.
11
+ const BUNDLED_PROMPT_VERSION = "1.4.0";
12
+ let promptUpdate = null;
13
+ let versionCheckPromise = null;
14
+ function compareVersions(a, b) {
15
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
16
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
17
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
18
+ const d = (pa[i] || 0) - (pb[i] || 0);
19
+ if (d !== 0)
20
+ return d;
21
+ }
22
+ return 0;
23
+ }
24
+ async function checkPromptVersion() {
25
+ try {
26
+ const res = await fetch(`${API_BASE}/api/prompts/version`);
27
+ if (!res.ok)
28
+ return;
29
+ const data = (await res.json());
30
+ if (compareVersions(data.mcp, BUNDLED_PROMPT_VERSION) > 0) {
31
+ promptUpdate = {
32
+ current: BUNDLED_PROMPT_VERSION,
33
+ latest: data.mcp,
34
+ changelogUrl: data.changelog,
35
+ };
36
+ console.error(`⚠ Project Tracker prompt v${data.mcp} available (you have v${BUNDLED_PROMPT_VERSION}). Changelog: ${data.changelog}`);
37
+ }
38
+ }
39
+ catch {
40
+ // Network failure during version check is non-fatal — tool calls still work.
41
+ }
42
+ }
43
+ function ensureVersionChecked() {
44
+ if (!versionCheckPromise)
45
+ versionCheckPromise = checkPromptVersion();
46
+ return versionCheckPromise;
47
+ }
48
+ // Coercion helpers — MCP clients may pass numbers/arrays as strings
49
+ const coerceNumber = z.coerce.number();
50
+ const coerceLabels = z.preprocess((v) => (typeof v === "string" ? JSON.parse(v) : v), z.array(z.string()));
51
+ async function apiRequest(path, options) {
52
+ // Trigger version check on first API call (fire-and-forget after first call).
53
+ ensureVersionChecked();
54
+ const headers = {
55
+ "Content-Type": "application/json",
56
+ ...options?.headers,
57
+ };
58
+ if (API_KEY)
59
+ headers["Authorization"] = `Bearer ${API_KEY}`;
60
+ const res = await fetch(`${API_BASE}${path}`, {
61
+ ...options,
62
+ headers,
63
+ });
64
+ if (!res.ok) {
65
+ const body = await res.json().catch(() => ({}));
66
+ throw new Error(body.error || `API error: ${res.status}`);
67
+ }
68
+ return res.json();
69
+ }
70
+ function textResult(data) {
71
+ const result = {
72
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
73
+ };
74
+ if (promptUpdate) {
75
+ result._meta = { promptUpdateAvailable: promptUpdate };
76
+ }
77
+ return result;
78
+ }
79
+ const server = new McpServer({
80
+ name: "project-tracker",
81
+ version: "1.0.0",
82
+ });
83
+ // --- Projects ---
84
+ server.tool("list_projects", "List all projects", {}, async () => {
85
+ const projects = await apiRequest("/api/projects");
86
+ return textResult(projects);
87
+ });
88
+ server.tool("create_project", "Create a new project", {
89
+ key: z.string().describe("Short slug, e.g. WEB, API"),
90
+ name: z.string().describe("Display name"),
91
+ color: z.string().optional().describe("Hex color, e.g. #3B82F6"),
92
+ repoPath: z.string().optional().describe("Default working directory for this project"),
93
+ claudeMdPath: z.string().optional().describe("Path to the project's claude.md"),
94
+ branch: z.string().optional().describe("Default branch name"),
95
+ notes: z.string().optional().describe("Context notes for Claude"),
96
+ }, async (params) => {
97
+ const project = await apiRequest("/api/projects", {
98
+ method: "POST",
99
+ body: JSON.stringify(params),
100
+ });
101
+ return textResult(project);
102
+ });
103
+ server.tool("update_project", "Update an existing project's fields", {
104
+ key: z.string().describe("Project key"),
105
+ name: z.string().optional(),
106
+ color: z.string().optional(),
107
+ status: z.enum(["active", "archived"]).optional().describe("Set to 'archived' to hide project from default views"),
108
+ repoPath: z.string().optional(),
109
+ claudeMdPath: z.string().optional(),
110
+ branch: z.string().optional(),
111
+ notes: z.string().optional(),
112
+ }, async (params) => {
113
+ const { key, ...updates } = params;
114
+ const project = await apiRequest(`/api/projects/${key}`, {
115
+ method: "PATCH",
116
+ body: JSON.stringify(updates),
117
+ });
118
+ return textResult(project);
119
+ });
120
+ // --- Items ---
121
+ server.tool("list_items", "List project items with optional filters. Set deleted=true to list soft-deleted (trashed) items instead.", {
122
+ status: z.enum(["backlog", "todo", "blocked", "in_progress", "review", "done"]).optional(),
123
+ type: z.enum(["story", "task"]).optional(),
124
+ label: z.string().optional(),
125
+ search: z.string().optional(),
126
+ project: z.string().optional().describe("Filter by project key"),
127
+ deleted: z.boolean().optional().describe("If true, returns soft-deleted items only (the trash list)."),
128
+ }, async (params) => {
129
+ const qs = new URLSearchParams();
130
+ if (params.status)
131
+ qs.set("status", params.status);
132
+ if (params.type)
133
+ qs.set("type", params.type);
134
+ if (params.label)
135
+ qs.set("label", params.label);
136
+ if (params.search)
137
+ qs.set("search", params.search);
138
+ if (params.project)
139
+ qs.set("project", params.project);
140
+ if (params.deleted)
141
+ qs.set("deleted", "true");
142
+ const qstr = qs.toString();
143
+ const items = await apiRequest(`/api/items${qstr ? "?" + qstr : ""}`);
144
+ return textResult(items);
145
+ });
146
+ server.tool("get_item", "Get a single item by its key (e.g. PT-1)", {
147
+ key: z.string().describe("Item key like PT-1"),
148
+ }, async (params) => {
149
+ const item = await apiRequest(`/api/items/${params.key}`);
150
+ return textResult(item);
151
+ });
152
+ server.tool("get_item_context", "Get resolved context for an item (merged project defaults + item overrides). Call this before starting work on any item to know the repo path, claude.md location, branch, and notes.", {
153
+ key: z.string().describe("Item key like PT-1"),
154
+ }, async (params) => {
155
+ const context = await apiRequest(`/api/items/${params.key}/context`);
156
+ return textResult(context);
157
+ });
158
+ server.tool("create_item", "Create a new story or task", {
159
+ type: z.enum(["story", "task"]),
160
+ title: z.string(),
161
+ description: z.string().optional(),
162
+ status: z.enum(["backlog", "todo", "blocked", "in_progress", "review", "done"]).optional(),
163
+ priority: z.enum(["low", "medium", "high", "critical"]).optional(),
164
+ labels: coerceLabels.optional(),
165
+ assignee: z.string().optional(),
166
+ parentKey: z.string().optional(),
167
+ project: z.string().optional().describe("Project key (defaults to PT)"),
168
+ value: coerceNumber.min(1).max(5).optional().describe("Business value 1-5 (XS-XL)"),
169
+ effort: coerceNumber.min(1).max(5).optional().describe("Effort estimate 1-5 (XS-XL)"),
170
+ desirability: coerceNumber.min(1).max(5).optional().describe("Need/desirability 1-5 (XS-XL)"),
171
+ dueDate: z.string().optional().describe("Due date in ISO format (YYYY-MM-DD)"),
172
+ blockedBy: coerceLabels.optional().describe("Array of item keys that block this item"),
173
+ blockedReason: z.string().optional().describe("Free-text reason for the block — useful when blocked by external things not in the tracker"),
174
+ milestone: z.string().optional().describe("Milestone or sprint name (e.g. v2, Sprint 3)"),
175
+ repoPath: z.string().optional().describe("Override project repo path"),
176
+ claudeMdPath: z.string().optional().describe("Override project claude.md path"),
177
+ branch: z.string().optional().describe("Override project branch"),
178
+ notes: z.string().optional().describe("Working notes for this item"),
179
+ }, async (params) => {
180
+ const item = await apiRequest("/api/items", {
181
+ method: "POST",
182
+ body: JSON.stringify(params),
183
+ });
184
+ return textResult(item);
185
+ });
186
+ server.tool("update_item", "Update an existing item's fields", {
187
+ key: z.string().describe("Item key like PT-1"),
188
+ title: z.string().optional(),
189
+ description: z.string().optional(),
190
+ status: z.enum(["backlog", "todo", "blocked", "in_progress", "review", "done"]).optional(),
191
+ priority: z.enum(["low", "medium", "high", "critical"]).optional(),
192
+ type: z.enum(["story", "task"]).optional(),
193
+ labels: coerceLabels.optional(),
194
+ assignee: z.string().optional(),
195
+ parentKey: z.string().optional(),
196
+ value: coerceNumber.min(1).max(5).optional(),
197
+ effort: coerceNumber.min(1).max(5).optional(),
198
+ desirability: coerceNumber.min(1).max(5).optional(),
199
+ dueDate: z.string().optional().describe("Due date in ISO format (YYYY-MM-DD)"),
200
+ blockedBy: coerceLabels.optional().describe("Array of item keys that block this item"),
201
+ blockedReason: z.string().optional().describe("Free-text reason for the block — useful when blocked by external things not in the tracker"),
202
+ milestone: z.string().optional().describe("Milestone or sprint name"),
203
+ changeLog: z.string().optional().describe("What changed — appended to the item's change log"),
204
+ repoPath: z.string().optional(),
205
+ claudeMdPath: z.string().optional(),
206
+ branch: z.string().optional(),
207
+ notes: z.string().optional(),
208
+ expectedVersion: coerceNumber.optional().describe("Optional optimistic concurrency token. Pass the item's __v from your last get_item; on mismatch the server returns 409 and the call fails with the latest version. Omit for last-write-wins."),
209
+ }, async (params) => {
210
+ const { key, ...updates } = params;
211
+ const item = await apiRequest(`/api/items/${key}`, {
212
+ method: "PATCH",
213
+ body: JSON.stringify(updates),
214
+ });
215
+ return textResult(item);
216
+ });
217
+ server.tool("delete_item", "Soft-delete an item by key. The item is marked deletedAt and disappears from board/list/matrix/timeline, but the row stays in the DB and any active personal schedule claims are released. Restore via restore_item. Idempotent.", {
218
+ key: z.string().describe("Item key like PT-1"),
219
+ }, async (params) => {
220
+ const result = await apiRequest(`/api/items/${params.key}`, { method: "DELETE" });
221
+ return textResult(result);
222
+ });
223
+ server.tool("restore_item", "Restore a soft-deleted item by key — clears deletedAt and brings it back into normal views. Does not re-add to anyone's schedule (the user re-plans manually).", {
224
+ key: z.string().describe("Item key like PT-1"),
225
+ }, async (params) => {
226
+ const result = await apiRequest(`/api/items/${params.key}/restore`, { method: "POST" });
227
+ return textResult(result);
228
+ });
229
+ server.tool("move_item", "Move an item to a different status column and/or reorder it. When moving to in_progress, pass claimedBy with your worker identifier (e.g. 'claude-opus-codex') so the server can detect conflicts. If another worker already holds it you'll get a 409 — pass force=true to take over deliberately.", {
230
+ key: z.string().describe("Item key like PT-1"),
231
+ status: z.enum(["backlog", "todo", "blocked", "in_progress", "review", "done"]),
232
+ order: coerceNumber.optional(),
233
+ claimedBy: z.string().optional().describe("Your worker identifier; recorded as assignee when claiming in_progress."),
234
+ force: z.boolean().optional().describe("Override an existing in_progress claim by another worker."),
235
+ }, async (params) => {
236
+ const item = await apiRequest(`/api/items/${params.key}/move`, {
237
+ method: "PATCH",
238
+ body: JSON.stringify({
239
+ status: params.status,
240
+ order: params.order ?? 0,
241
+ claimedBy: params.claimedBy,
242
+ force: params.force,
243
+ }),
244
+ });
245
+ return textResult(item);
246
+ });
247
+ server.tool("get_board", "Get the full Kanban board with items grouped by status", {
248
+ project: z.string().optional().describe("Filter by project key"),
249
+ }, async (params) => {
250
+ const qs = params.project ? `?project=${params.project}` : "";
251
+ const board = await apiRequest(`/api/items/board${qs}`);
252
+ return textResult(board);
253
+ });
254
+ server.tool("get_matrix", "Get the value/effort matrix (plan view) with items grouped into a 5x5 grid", {
255
+ project: z.string().optional().describe("Filter by project key"),
256
+ }, async (params) => {
257
+ const qs = params.project ? `?project=${params.project}` : "";
258
+ const matrix = await apiRequest(`/api/items/board/matrix${qs}`);
259
+ return textResult(matrix);
260
+ });
261
+ server.tool("bulk_update_items", "Update multiple items at once. Pass an array of updates, each with a key and fields to update.", {
262
+ updates: z.preprocess((v) => (typeof v === "string" ? JSON.parse(v) : v), z.array(z.object({
263
+ key: z.string(),
264
+ status: z.enum(["backlog", "todo", "blocked", "in_progress", "review", "done"]).optional(),
265
+ priority: z.enum(["low", "medium", "high", "critical"]).optional(),
266
+ labels: z.array(z.string()).optional(),
267
+ value: z.number().min(1).max(5).optional(),
268
+ effort: z.number().min(1).max(5).optional(),
269
+ desirability: z.number().min(1).max(5).optional(),
270
+ changeLog: z.string().optional(),
271
+ }))).describe("Array of { key, ...fields } objects"),
272
+ }, async (params) => {
273
+ const result = await apiRequest("/api/items/bulk", {
274
+ method: "PATCH",
275
+ body: JSON.stringify({ updates: params.updates }),
276
+ });
277
+ return textResult(result);
278
+ });
279
+ async function main() {
280
+ const transport = new StdioServerTransport();
281
+ await server.connect(transport);
282
+ console.error("Project Tracker MCP server running");
283
+ }
284
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@project-tracker/mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Project Tracker (app.project-tracker.ai) — lets Claude Code and other MCP-aware AI clients read and update your tracker via tool calls.",
5
+ "type": "module",
6
+ "bin": {
7
+ "project-tracker-mcp": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "start": "node dist/index.js",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "project-tracker",
24
+ "claude",
25
+ "kanban",
26
+ "ai-tools"
27
+ ],
28
+ "author": "Jason Gould",
29
+ "license": "MIT",
30
+ "homepage": "https://app.project-tracker.ai",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/DQ-ProjectTracker/project-tracker.git",
34
+ "directory": "mcp-server"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/DQ-ProjectTracker/project-tracker/issues"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.9.0",
50
+ "typescript": "^5.6.0"
51
+ }
52
+ }