@kitsy/coop-mcp 0.0.1 → 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,92 @@
1
+ # @kitsy/coop-mcp
2
+
3
+ MCP server for COOP.
4
+
5
+ This package exposes local COOP workspace state and operations to MCP-compatible clients over `stdio`.
6
+
7
+ ## Exposed Tools
8
+
9
+ - `coop_workspace_info()`
10
+ - `coop_list_tasks(filters?)`
11
+ - `coop_show_task(id)`
12
+ - `coop_graph_next(track?)`
13
+ - `coop_plan_delivery(name)`
14
+ - `coop_transition_task(id, status)`
15
+ - `coop_create_task(fields)`
16
+
17
+ ## Exposed Resources
18
+
19
+ - `coop://workspace`
20
+ - `coop://tasks`
21
+ - `coop://tasks/{id}`
22
+ - `coop://deliveries`
23
+ - `coop://graph`
24
+
25
+ ## Local Usage
26
+
27
+ From the repo root:
28
+
29
+ ```bash
30
+ pnpm --filter @kitsy/coop-mcp build
31
+ node packages/mcp/dist/index.js --repo C:/path/to/your/repo
32
+ ```
33
+
34
+ Or, after installation/build:
35
+
36
+ ```bash
37
+ coop-mcp --repo C:/path/to/your/repo
38
+ ```
39
+
40
+ If `--repo` is omitted, the server resolves the nearest parent directory containing `.coop/`.
41
+
42
+ ## Claude Code Setup
43
+
44
+ Example MCP configuration:
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "coop": {
50
+ "command": "node",
51
+ "args": [
52
+ "C:/Users/pkvsi/Wks/kitsy/coop/packages/mcp/dist/index.js",
53
+ "--repo",
54
+ "C:/path/to/your/workspace"
55
+ ]
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ If you prefer `pnpm` during local development:
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "coop": {
67
+ "command": "pnpm",
68
+ "args": [
69
+ "--dir",
70
+ "C:/Users/pkvsi/Wks/kitsy/coop",
71
+ "--filter",
72
+ "@kitsy/coop-mcp",
73
+ "exec",
74
+ "coop-mcp",
75
+ "--repo",
76
+ "C:/path/to/your/workspace"
77
+ ]
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## Other MCP-Compatible Tools
84
+
85
+ Any MCP client that supports `stdio` transports can launch this server with the same command/args pattern.
86
+
87
+ ## Notes
88
+
89
+ - The server reads and writes the local `.coop/` workspace directly.
90
+ - Tool mutations go through shared core rules for task writing, planning, and state transitions.
91
+ - Workspace identity is exposed from `.coop/config.yml -> project` for orchestrators and multi-repo agents.
92
+ - This phase intentionally keeps transport simple: local `stdio`, no remote hosting, no auth layer.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,194 @@
1
- type CoopMcpPlaceholder = {
2
- readonly package: "@kitsy/coop-mcp";
3
- readonly status: "planned";
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import * as _kitsy_coop_core from '@kitsy/coop-core';
4
+ import { TaskGraph, TaskType, TaskStatus } from '@kitsy/coop-core';
5
+
6
+ type TaskListFilters = {
7
+ status?: string;
8
+ track?: string;
9
+ assignee?: string;
10
+ delivery?: string;
11
+ limit?: number;
12
+ };
13
+ type GraphNextFilters = {
14
+ track?: string;
15
+ limit?: number;
16
+ today?: string;
17
+ };
18
+ type TransitionTaskInput = {
19
+ id: string;
20
+ status: string;
21
+ actor?: string;
22
+ };
23
+ type CreateTaskInput = {
24
+ id?: string;
25
+ title: string;
26
+ type?: string;
27
+ status?: string;
28
+ track?: string;
29
+ priority?: string;
30
+ assignee?: string;
31
+ delivery?: string;
32
+ tags?: string[];
33
+ depends_on?: string[];
34
+ body?: string;
35
+ };
36
+ declare class CoopMcpService {
37
+ readonly repoRoot: string;
38
+ readonly coopDir: string;
39
+ constructor(repoRoot?: string);
40
+ loadGraph(): TaskGraph;
41
+ workspaceInfo(): {
42
+ instance: {
43
+ id: string;
44
+ name: string;
45
+ aliases: string[];
46
+ };
47
+ repo: {
48
+ name: string;
49
+ root: string;
50
+ };
51
+ };
52
+ listTasks(filters?: TaskListFilters): {
53
+ id: string;
54
+ title: string;
55
+ type: TaskType;
56
+ status: TaskStatus;
57
+ priority: _kitsy_coop_core.TaskPriority | null;
58
+ track: string | null;
59
+ assignee: string | null;
60
+ delivery: string | null;
61
+ depends_on: string[];
62
+ blocks: string[];
63
+ tags: string[];
64
+ }[];
65
+ showTask(id: string): {
66
+ created: string;
67
+ updated: string;
68
+ body: string;
69
+ raw: Record<string, unknown>;
70
+ execution: {
71
+ executor: _kitsy_coop_core.ExecutorType;
72
+ agent?: string;
73
+ context?: {
74
+ paths?: string[];
75
+ files?: string[];
76
+ tasks?: string[];
77
+ };
78
+ runbook?: _kitsy_coop_core.RunbookStep[];
79
+ permissions?: _kitsy_coop_core.ExecutionPermissions;
80
+ constraints?: _kitsy_coop_core.ExecutionConstraints;
81
+ } | null;
82
+ estimate: _kitsy_coop_core.TaskEstimate | null;
83
+ risk: {
84
+ level?: _kitsy_coop_core.RiskLevel;
85
+ probability?: number;
86
+ impact_hours?: number;
87
+ mitigation?: string;
88
+ contingency?: string;
89
+ } | null;
90
+ resources: _kitsy_coop_core.TaskResources | null;
91
+ file_path: string;
92
+ id: string;
93
+ title: string;
94
+ type: TaskType;
95
+ status: TaskStatus;
96
+ priority: _kitsy_coop_core.TaskPriority | null;
97
+ track: string | null;
98
+ assignee: string | null;
99
+ delivery: string | null;
100
+ depends_on: string[];
101
+ blocks: string[];
102
+ tags: string[];
103
+ };
104
+ graphNext(filters?: GraphNextFilters): {
105
+ id: string;
106
+ title: string;
107
+ score: number;
108
+ track: string | null;
109
+ priority: _kitsy_coop_core.TaskPriority | null;
110
+ fits_capacity: boolean;
111
+ fits_wip: boolean;
112
+ }[];
113
+ planDelivery(nameOrId: string, today?: string): {
114
+ delivery: {
115
+ id: string;
116
+ name: string;
117
+ status: _kitsy_coop_core.DeliveryStatus;
118
+ target_date: string | null;
119
+ };
120
+ result: _kitsy_coop_core.FeasibilityResult;
121
+ };
122
+ transitionTask(input: TransitionTaskInput): {
123
+ id: string;
124
+ from: TaskStatus;
125
+ to: TaskStatus;
126
+ updated: string;
127
+ };
128
+ createTask(input: CreateTaskInput): {
129
+ file_path: string;
130
+ id: string;
131
+ title: string;
132
+ type: TaskType;
133
+ status: TaskStatus;
134
+ priority: _kitsy_coop_core.TaskPriority | null;
135
+ track: string | null;
136
+ assignee: string | null;
137
+ delivery: string | null;
138
+ depends_on: string[];
139
+ blocks: string[];
140
+ tags: string[];
141
+ };
142
+ listDeliveries(): {
143
+ id: string;
144
+ name: string;
145
+ status: _kitsy_coop_core.DeliveryStatus;
146
+ target_date: string | null;
147
+ scope: string[];
148
+ }[];
149
+ graphResource(): {
150
+ nodes: {
151
+ id: string;
152
+ title: string;
153
+ type: TaskType;
154
+ status: TaskStatus;
155
+ priority: _kitsy_coop_core.TaskPriority | null;
156
+ track: string | null;
157
+ assignee: string | null;
158
+ delivery: string | null;
159
+ depends_on: string[];
160
+ blocks: string[];
161
+ tags: string[];
162
+ }[];
163
+ edges: {
164
+ from: string;
165
+ to: string;
166
+ }[];
167
+ deliveries: {
168
+ id: string;
169
+ name: string;
170
+ status: _kitsy_coop_core.DeliveryStatus;
171
+ target_date: string | null;
172
+ scope: string[];
173
+ }[];
174
+ };
175
+ listTaskResourceUris(): {
176
+ uri: string;
177
+ name: string;
178
+ mimeType: string;
179
+ description: string;
180
+ }[];
181
+ readResource(uri: string): string;
182
+ }
183
+
184
+ type CoopMcpServerOptions = {
185
+ repoRoot?: string;
186
+ };
187
+ declare function createCoopMcpServer(options?: CoopMcpServerOptions): {
188
+ server: McpServer;
189
+ service: CoopMcpService;
190
+ repoRoot: string;
4
191
  };
5
- declare const coopMcp: CoopMcpPlaceholder;
192
+ declare function startCoopMcpServer(options?: CoopMcpServerOptions): Promise<McpServer>;
6
193
 
7
- export { type CoopMcpPlaceholder, coopMcp };
194
+ export { type CoopMcpServerOptions, createCoopMcpServer, startCoopMcpServer };
package/dist/index.js CHANGED
@@ -1,8 +1,667 @@
1
+ #!/usr/bin/env node
2
+
1
3
  // src/index.ts
2
- var coopMcp = {
3
- package: "@kitsy/coop-mcp",
4
- status: "planned"
4
+ import path2 from "path";
5
+ import { pathToFileURL } from "url";
6
+ import * as z from "zod/v4";
7
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+
10
+ // src/service.ts
11
+ import fs from "fs";
12
+ import os from "os";
13
+ import path from "path";
14
+ import crypto from "crypto";
15
+ import { spawnSync } from "child_process";
16
+ import {
17
+ TaskStatus,
18
+ TaskType,
19
+ analyze_feasibility,
20
+ load_graph,
21
+ parseTaskFile,
22
+ parseYamlFile,
23
+ schedule_next,
24
+ transition,
25
+ validateStructural,
26
+ writeTask
27
+ } from "@kitsy/coop-core";
28
+ function toIsoDate(value = /* @__PURE__ */ new Date()) {
29
+ return value.toISOString().slice(0, 10);
30
+ }
31
+ function sanitizeIdPart(input, fallback, maxLength = 16) {
32
+ const normalized = input.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
33
+ if (!normalized) return fallback;
34
+ return normalized.slice(0, maxLength);
35
+ }
36
+ function inferActor(root) {
37
+ const envValue = process.env.COOP_ID_NAMESPACE || process.env.GIT_AUTHOR_NAME || process.env.GIT_COMMITTER_NAME || process.env.GIT_AUTHOR_EMAIL || process.env.GIT_COMMITTER_EMAIL || process.env.USERNAME || process.env.USER;
38
+ if (envValue) {
39
+ return envValue.split("@")[0] ?? envValue;
40
+ }
41
+ const email = spawnSync("git", ["config", "--get", "user.email"], {
42
+ cwd: root,
43
+ encoding: "utf8",
44
+ windowsHide: true
45
+ }).stdout.trim();
46
+ if (email) {
47
+ return email.split("@")[0] ?? email;
48
+ }
49
+ const name = spawnSync("git", ["config", "--get", "user.name"], {
50
+ cwd: root,
51
+ encoding: "utf8",
52
+ windowsHide: true
53
+ }).stdout.trim();
54
+ if (name) return name;
55
+ return os.userInfo().username;
56
+ }
57
+ function shortDateToken(now = /* @__PURE__ */ new Date()) {
58
+ return now.toISOString().slice(2, 10).replace(/-/g, "");
59
+ }
60
+ function randomToken() {
61
+ return crypto.randomBytes(4).toString("hex").toUpperCase();
62
+ }
63
+ function readConfig(root) {
64
+ const configPath = path.join(root, ".coop", "config.yml");
65
+ if (!fs.existsSync(configPath)) {
66
+ return {};
67
+ }
68
+ return parseYamlFile(configPath);
69
+ }
70
+ function sequenceForPattern(existingIds, prefix, suffix) {
71
+ const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
72
+ const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
73
+ const regex = new RegExp(`^${escapedPrefix}(\\d+)${escapedSuffix}$`);
74
+ let max = 0;
75
+ for (const id of existingIds) {
76
+ const match = regex.exec(id);
77
+ if (!match) continue;
78
+ const next = Number(match[1]);
79
+ if (Number.isInteger(next) && next > max) {
80
+ max = next;
81
+ }
82
+ }
83
+ return max + 1;
84
+ }
85
+ function renderIdTemplate(root, existingIds, input) {
86
+ const config = readConfig(root);
87
+ const prefix = sanitizeIdPart(config.id_prefixes?.task ?? "PM", "PM", 12);
88
+ const actor = sanitizeIdPart(inferActor(root), "USER", 16);
89
+ const title = sanitizeIdPart(input.title, "TASK", 24);
90
+ const track = sanitizeIdPart(input.track ?? "UNASSIGNED", "UNASSIGNED", 16);
91
+ const status = sanitizeIdPart(input.status ?? "TODO", "TODO", 16);
92
+ const type = sanitizeIdPart(input.type ?? "TASK", "TASK", 16);
93
+ const template = config.id?.naming?.trim() || "<TYPE>-<USER>-<YYMMDD>-<RAND>";
94
+ const seqPadding = Number.isInteger(config.id?.seq_padding) ? Number(config.id?.seq_padding) : 0;
95
+ const base = template.replace(/<([^>]+)>/g, (_, rawToken) => {
96
+ const token = rawToken.toUpperCase();
97
+ if (token === "PREFIX") return prefix;
98
+ if (token === "TYPE" || token === "ENTITY") return type;
99
+ if (token === "USER") return actor;
100
+ if (token === "YYMMDD") return shortDateToken();
101
+ if (token === "RAND") return randomToken();
102
+ if (token === "TITLE") return title;
103
+ if (token === "TRACK") return track;
104
+ if (token === "STATUS") return status;
105
+ if (token === "SEQ") return "__SEQ__";
106
+ return sanitizeIdPart(token, token);
107
+ });
108
+ const normalized = sanitizeIdPart(base, "PM-TASK", 64);
109
+ if (!normalized.includes("__SEQ__")) {
110
+ return normalized;
111
+ }
112
+ const upperExisting = existingIds.map((id) => id.toUpperCase());
113
+ const [head, tail] = normalized.split("__SEQ__");
114
+ const next = sequenceForPattern(upperExisting, head ?? "", tail ?? "");
115
+ const sequence = seqPadding > 0 ? String(next).padStart(seqPadding, "0") : String(next);
116
+ return `${head ?? ""}${sequence}${tail ?? ""}`;
117
+ }
118
+ function walkFiles(dirPath) {
119
+ if (!fs.existsSync(dirPath)) return [];
120
+ const out = [];
121
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
122
+ const fullPath = path.join(dirPath, entry.name);
123
+ if (entry.isDirectory()) {
124
+ out.push(...walkFiles(fullPath));
125
+ continue;
126
+ }
127
+ if (entry.isFile() && fullPath.toLowerCase().endsWith(".md")) {
128
+ out.push(fullPath);
129
+ }
130
+ }
131
+ return out.sort((a, b) => a.localeCompare(b));
132
+ }
133
+ function taskFileById(root, id) {
134
+ const taskDir = path.join(root, ".coop", "tasks");
135
+ const wanted = `${id}.md`.toLowerCase();
136
+ const match = walkFiles(taskDir).find((filePath) => path.basename(filePath).toLowerCase() === wanted);
137
+ if (!match) {
138
+ throw new Error(`Task '${id}' not found.`);
139
+ }
140
+ return match;
141
+ }
142
+ function resolveDelivery(graph, nameOrId) {
143
+ const direct = graph.deliveries.get(nameOrId);
144
+ if (direct) return direct;
145
+ const match = Array.from(graph.deliveries.values()).find(
146
+ (delivery) => delivery.name.toLowerCase() === nameOrId.toLowerCase()
147
+ );
148
+ if (match) return match;
149
+ throw new Error(`Delivery '${nameOrId}' not found.`);
150
+ }
151
+ function deliveryScopeIds(delivery) {
152
+ const include = new Set(delivery.scope.include);
153
+ for (const excluded of delivery.scope.exclude) {
154
+ include.delete(excluded);
155
+ }
156
+ return include;
157
+ }
158
+ function taskSummary(graph, task) {
159
+ return {
160
+ id: task.id,
161
+ title: task.title,
162
+ type: task.type,
163
+ status: task.status,
164
+ priority: task.priority ?? null,
165
+ track: task.track ?? null,
166
+ assignee: task.assignee ?? null,
167
+ delivery: task.delivery ?? null,
168
+ depends_on: task.depends_on ?? [],
169
+ blocks: Array.from(graph.reverse.get(task.id) ?? /* @__PURE__ */ new Set()).sort((a, b) => a.localeCompare(b)),
170
+ tags: task.tags ?? []
171
+ };
172
+ }
173
+ function textJson(value) {
174
+ return JSON.stringify(value, null, 2);
175
+ }
176
+ function resolveRepoRoot(start = process.cwd()) {
177
+ let current = path.resolve(start);
178
+ while (true) {
179
+ if (fs.existsSync(path.join(current, ".coop"))) {
180
+ return current;
181
+ }
182
+ const parent = path.dirname(current);
183
+ if (parent === current) {
184
+ return path.resolve(start);
185
+ }
186
+ current = parent;
187
+ }
188
+ }
189
+ var CoopMcpService = class {
190
+ repoRoot;
191
+ coopDir;
192
+ constructor(repoRoot = resolveRepoRoot(process.env.COOP_REPO_ROOT ?? process.cwd())) {
193
+ this.repoRoot = path.resolve(repoRoot);
194
+ this.coopDir = path.join(this.repoRoot, ".coop");
195
+ if (!fs.existsSync(this.coopDir)) {
196
+ throw new Error(`Missing .coop directory at ${this.coopDir}.`);
197
+ }
198
+ }
199
+ loadGraph() {
200
+ return load_graph(this.coopDir);
201
+ }
202
+ workspaceInfo() {
203
+ const config = readConfig(this.repoRoot);
204
+ const repoName = path.basename(this.repoRoot);
205
+ return {
206
+ instance: {
207
+ id: config.project?.id?.trim() || repoName,
208
+ name: config.project?.name?.trim() || repoName,
209
+ aliases: (config.project?.aliases ?? []).filter((entry) => typeof entry === "string" && entry.trim().length > 0)
210
+ },
211
+ repo: {
212
+ name: repoName,
213
+ root: this.repoRoot
214
+ }
215
+ };
216
+ }
217
+ listTasks(filters = {}) {
218
+ const graph = this.loadGraph();
219
+ let tasks = Array.from(graph.nodes.values());
220
+ if (filters.delivery) {
221
+ const delivery = resolveDelivery(graph, filters.delivery);
222
+ const scopeIds = deliveryScopeIds(delivery);
223
+ tasks = tasks.filter((task) => scopeIds.has(task.id));
224
+ }
225
+ if (filters.status) {
226
+ tasks = tasks.filter((task) => task.status === filters.status);
227
+ }
228
+ if (filters.track) {
229
+ tasks = tasks.filter((task) => (task.track ?? "unassigned") === filters.track);
230
+ }
231
+ if (filters.assignee) {
232
+ tasks = tasks.filter((task) => (task.assignee ?? "") === filters.assignee);
233
+ }
234
+ tasks.sort((a, b) => a.id.localeCompare(b.id));
235
+ const limit = typeof filters.limit === "number" && filters.limit > 0 ? filters.limit : void 0;
236
+ const limited = typeof limit === "number" ? tasks.slice(0, limit) : tasks;
237
+ return limited.map((task) => taskSummary(graph, task));
238
+ }
239
+ showTask(id) {
240
+ const graph = this.loadGraph();
241
+ const filePath = taskFileById(this.repoRoot, id);
242
+ const parsed = parseTaskFile(filePath);
243
+ const task = graph.nodes.get(parsed.task.id) ?? parsed.task;
244
+ return {
245
+ ...taskSummary(graph, task),
246
+ created: task.created,
247
+ updated: task.updated,
248
+ body: parsed.body,
249
+ raw: parsed.raw,
250
+ execution: task.execution ?? null,
251
+ estimate: task.estimate ?? null,
252
+ risk: task.risk ?? null,
253
+ resources: task.resources ?? null,
254
+ file_path: path.relative(this.repoRoot, filePath).replace(/\\/g, "/")
255
+ };
256
+ }
257
+ graphNext(filters = {}) {
258
+ const graph = this.loadGraph();
259
+ return schedule_next(graph, {
260
+ track: filters.track,
261
+ today: filters.today,
262
+ limit: filters.limit
263
+ }).map((entry) => ({
264
+ id: entry.task.id,
265
+ title: entry.task.title,
266
+ score: Number(entry.score.toFixed(2)),
267
+ track: entry.task.track ?? null,
268
+ priority: entry.task.priority ?? null,
269
+ fits_capacity: entry.fits_capacity,
270
+ fits_wip: entry.fits_wip
271
+ }));
272
+ }
273
+ planDelivery(nameOrId, today) {
274
+ const graph = this.loadGraph();
275
+ const delivery = resolveDelivery(graph, nameOrId);
276
+ const result = analyze_feasibility(delivery.id, graph, today ?? /* @__PURE__ */ new Date());
277
+ return {
278
+ delivery: {
279
+ id: delivery.id,
280
+ name: delivery.name,
281
+ status: delivery.status,
282
+ target_date: delivery.target_date
283
+ },
284
+ result
285
+ };
286
+ }
287
+ transitionTask(input) {
288
+ const target = input.status.toLowerCase();
289
+ if (!Object.values(TaskStatus).includes(target)) {
290
+ throw new Error(`Invalid target status '${input.status}'.`);
291
+ }
292
+ const graph = this.loadGraph();
293
+ const filePath = taskFileById(this.repoRoot, input.id);
294
+ const parsed = parseTaskFile(filePath);
295
+ const dependencyStatuses = /* @__PURE__ */ new Map();
296
+ for (const depId of parsed.task.depends_on ?? []) {
297
+ const depTask = graph.nodes.get(depId);
298
+ if (depTask) {
299
+ dependencyStatuses.set(depId, depTask.status);
300
+ }
301
+ }
302
+ const result = transition(parsed.task, target, {
303
+ actor: input.actor ?? inferActor(this.repoRoot),
304
+ dependencyStatuses
305
+ });
306
+ if (!result.success) {
307
+ throw new Error(result.error ?? "Transition failed.");
308
+ }
309
+ const structural = validateStructural(result.task, { filePath });
310
+ if (structural.length > 0) {
311
+ throw new Error(structural.map((issue) => issue.message).join(" | "));
312
+ }
313
+ writeTask(result.task, {
314
+ body: parsed.body,
315
+ raw: parsed.raw,
316
+ filePath
317
+ });
318
+ return {
319
+ id: result.task.id,
320
+ from: parsed.task.status,
321
+ to: result.task.status,
322
+ updated: result.task.updated
323
+ };
324
+ }
325
+ createTask(input) {
326
+ if (!input.title?.trim()) {
327
+ throw new Error("Field 'title' is required.");
328
+ }
329
+ const graph = this.loadGraph();
330
+ const type = (input.type ?? "feature").toLowerCase();
331
+ if (!Object.values(TaskType).includes(type)) {
332
+ throw new Error(`Invalid task type '${String(input.type)}'.`);
333
+ }
334
+ const status = (input.status ?? "todo").toLowerCase();
335
+ if (!Object.values(TaskStatus).includes(status)) {
336
+ throw new Error(`Invalid task status '${String(input.status)}'.`);
337
+ }
338
+ const id = input.id?.trim().toUpperCase() || renderIdTemplate(this.repoRoot, Array.from(graph.nodes.keys()), input);
339
+ if (graph.nodes.has(id)) {
340
+ throw new Error(`Task '${id}' already exists.`);
341
+ }
342
+ const filePath = path.join(this.coopDir, "tasks", `${id}.md`);
343
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
344
+ const task = {
345
+ id,
346
+ title: input.title.trim(),
347
+ type,
348
+ status,
349
+ created: toIsoDate(),
350
+ updated: toIsoDate(),
351
+ priority: input.priority ? input.priority.toLowerCase() : "p2",
352
+ track: input.track?.trim() || "unassigned",
353
+ assignee: input.assignee?.trim() || null,
354
+ delivery: input.delivery?.trim() || null,
355
+ tags: input.tags?.filter(Boolean) ?? [],
356
+ depends_on: input.depends_on?.filter(Boolean) ?? []
357
+ };
358
+ const structural = validateStructural(task, { filePath });
359
+ if (structural.length > 0) {
360
+ throw new Error(structural.map((issue) => issue.message).join(" | "));
361
+ }
362
+ writeTask(task, {
363
+ body: input.body ?? "",
364
+ filePath
365
+ });
366
+ return {
367
+ ...taskSummary(this.loadGraph(), task),
368
+ file_path: path.relative(this.repoRoot, filePath).replace(/\\/g, "/")
369
+ };
370
+ }
371
+ listDeliveries() {
372
+ const graph = this.loadGraph();
373
+ return Array.from(graph.deliveries.values()).sort((a, b) => a.id.localeCompare(b.id)).map((delivery) => ({
374
+ id: delivery.id,
375
+ name: delivery.name,
376
+ status: delivery.status,
377
+ target_date: delivery.target_date,
378
+ scope: Array.from(deliveryScopeIds(delivery)).sort((a, b) => a.localeCompare(b))
379
+ }));
380
+ }
381
+ graphResource() {
382
+ const graph = this.loadGraph();
383
+ return {
384
+ nodes: Array.from(graph.nodes.values()).sort((a, b) => a.id.localeCompare(b.id)).map((task) => taskSummary(graph, task)),
385
+ edges: Array.from(graph.forward.entries()).flatMap(([taskId, deps]) => Array.from(deps).map((depId) => ({ from: depId, to: taskId }))).sort((a, b) => `${a.from}:${a.to}`.localeCompare(`${b.from}:${b.to}`)),
386
+ deliveries: this.listDeliveries()
387
+ };
388
+ }
389
+ listTaskResourceUris() {
390
+ return this.listTasks().map((task) => ({
391
+ uri: `coop://tasks/${task.id}`,
392
+ name: task.id,
393
+ mimeType: "application/json",
394
+ description: task.title
395
+ }));
396
+ }
397
+ readResource(uri) {
398
+ const parsed = new URL(uri);
399
+ if (parsed.protocol !== "coop:") {
400
+ throw new Error(`Unsupported resource URI '${uri}'.`);
401
+ }
402
+ if (parsed.host === "tasks" && (parsed.pathname === "" || parsed.pathname === "/")) {
403
+ return textJson(this.listTasks());
404
+ }
405
+ if (parsed.host === "tasks" && parsed.pathname.length > 1) {
406
+ const id = decodeURIComponent(parsed.pathname.slice(1));
407
+ return textJson(this.showTask(id));
408
+ }
409
+ if (parsed.host === "deliveries") {
410
+ return textJson(this.listDeliveries());
411
+ }
412
+ if (parsed.host === "graph") {
413
+ return textJson(this.graphResource());
414
+ }
415
+ if (parsed.host === "workspace") {
416
+ return textJson(this.workspaceInfo());
417
+ }
418
+ throw new Error(`Unsupported resource URI '${uri}'.`);
419
+ }
5
420
  };
421
+ function toolTextResult(value) {
422
+ return {
423
+ content: [
424
+ {
425
+ type: "text",
426
+ text: textJson(value)
427
+ }
428
+ ]
429
+ };
430
+ }
431
+ function resourceTextResult(uri, value) {
432
+ return {
433
+ contents: [
434
+ {
435
+ uri,
436
+ mimeType: "application/json",
437
+ text: textJson(value)
438
+ }
439
+ ]
440
+ };
441
+ }
442
+
443
+ // src/index.ts
444
+ function parseArgs(argv) {
445
+ let repoRoot;
446
+ for (let index = 2; index < argv.length; index += 1) {
447
+ const current = argv[index];
448
+ if (current === "--repo") {
449
+ repoRoot = argv[index + 1];
450
+ index += 1;
451
+ }
452
+ }
453
+ return { repoRoot };
454
+ }
455
+ function createCoopMcpServer(options = {}) {
456
+ const repoRoot = resolveRepoRoot(options.repoRoot ?? process.env.COOP_REPO_ROOT ?? process.cwd());
457
+ const service = new CoopMcpService(repoRoot);
458
+ const server = new McpServer(
459
+ {
460
+ name: "@kitsy/coop-mcp",
461
+ version: "1.0.0"
462
+ },
463
+ {
464
+ capabilities: {
465
+ logging: {}
466
+ },
467
+ instructions: "COOP MCP exposes local Git-native project state and operations from .coop for AI coding agents."
468
+ }
469
+ );
470
+ server.registerTool(
471
+ "coop_workspace_info",
472
+ {
473
+ description: "Show COOP workspace identity and repository metadata.",
474
+ inputSchema: {},
475
+ annotations: {
476
+ title: "COOP Workspace Info",
477
+ readOnlyHint: true
478
+ }
479
+ },
480
+ async () => toolTextResult(service.workspaceInfo())
481
+ );
482
+ server.registerTool(
483
+ "coop_list_tasks",
484
+ {
485
+ description: "List COOP task summaries with optional filters.",
486
+ inputSchema: {
487
+ status: z.string().optional(),
488
+ track: z.string().optional(),
489
+ assignee: z.string().optional(),
490
+ delivery: z.string().optional(),
491
+ limit: z.number().int().positive().optional()
492
+ },
493
+ annotations: {
494
+ title: "List COOP Tasks",
495
+ readOnlyHint: true
496
+ }
497
+ },
498
+ async (args) => toolTextResult(service.listTasks(args))
499
+ );
500
+ server.registerTool(
501
+ "coop_show_task",
502
+ {
503
+ description: "Show full COOP task details, including markdown body and computed blockers.",
504
+ inputSchema: {
505
+ id: z.string()
506
+ },
507
+ annotations: {
508
+ title: "Show COOP Task",
509
+ readOnlyHint: true
510
+ }
511
+ },
512
+ async ({ id }) => toolTextResult(service.showTask(id))
513
+ );
514
+ server.registerTool(
515
+ "coop_graph_next",
516
+ {
517
+ description: "Return recommended next tasks from the COOP scheduler.",
518
+ inputSchema: {
519
+ track: z.string().optional(),
520
+ limit: z.number().int().positive().optional(),
521
+ today: z.string().optional()
522
+ },
523
+ annotations: {
524
+ title: "COOP Graph Next",
525
+ readOnlyHint: true
526
+ }
527
+ },
528
+ async (args) => toolTextResult(service.graphNext(args))
529
+ );
530
+ server.registerTool(
531
+ "coop_plan_delivery",
532
+ {
533
+ description: "Run delivery feasibility planning for a COOP delivery.",
534
+ inputSchema: {
535
+ name: z.string(),
536
+ today: z.string().optional()
537
+ },
538
+ annotations: {
539
+ title: "Plan COOP Delivery",
540
+ readOnlyHint: true
541
+ }
542
+ },
543
+ async ({ name, today }) => toolTextResult(service.planDelivery(name, today))
544
+ );
545
+ server.registerTool(
546
+ "coop_transition_task",
547
+ {
548
+ description: "Transition a COOP task to a new status and persist the change.",
549
+ inputSchema: {
550
+ id: z.string(),
551
+ status: z.string(),
552
+ actor: z.string().optional()
553
+ },
554
+ annotations: {
555
+ title: "Transition COOP Task",
556
+ destructiveHint: true
557
+ }
558
+ },
559
+ async (args) => toolTextResult(service.transitionTask(args))
560
+ );
561
+ server.registerTool(
562
+ "coop_create_task",
563
+ {
564
+ description: "Create a new COOP task and persist it to .coop/tasks.",
565
+ inputSchema: {
566
+ id: z.string().optional(),
567
+ title: z.string(),
568
+ type: z.string().optional(),
569
+ status: z.string().optional(),
570
+ track: z.string().optional(),
571
+ priority: z.string().optional(),
572
+ assignee: z.string().optional(),
573
+ delivery: z.string().optional(),
574
+ tags: z.array(z.string()).optional(),
575
+ depends_on: z.array(z.string()).optional(),
576
+ body: z.string().optional()
577
+ },
578
+ annotations: {
579
+ title: "Create COOP Task",
580
+ destructiveHint: true
581
+ }
582
+ },
583
+ async (args) => toolTextResult(service.createTask(args))
584
+ );
585
+ server.registerResource(
586
+ "workspace",
587
+ "coop://workspace",
588
+ {
589
+ title: "COOP Workspace",
590
+ description: "Workspace identity, aliases, and repository metadata.",
591
+ mimeType: "application/json"
592
+ },
593
+ async () => resourceTextResult("coop://workspace", service.workspaceInfo())
594
+ );
595
+ server.registerResource(
596
+ "tasks",
597
+ "coop://tasks",
598
+ {
599
+ title: "COOP Tasks",
600
+ description: "All task summaries from the current COOP workspace.",
601
+ mimeType: "application/json"
602
+ },
603
+ async () => resourceTextResult("coop://tasks", service.listTasks())
604
+ );
605
+ server.registerResource(
606
+ "deliveries",
607
+ "coop://deliveries",
608
+ {
609
+ title: "COOP Deliveries",
610
+ description: "All deliveries from the current COOP workspace.",
611
+ mimeType: "application/json"
612
+ },
613
+ async () => resourceTextResult("coop://deliveries", service.listDeliveries())
614
+ );
615
+ server.registerResource(
616
+ "graph",
617
+ "coop://graph",
618
+ {
619
+ title: "COOP Graph",
620
+ description: "The current task graph, deliveries, and dependency edges.",
621
+ mimeType: "application/json"
622
+ },
623
+ async () => resourceTextResult("coop://graph", service.graphResource())
624
+ );
625
+ server.registerResource(
626
+ "task",
627
+ new ResourceTemplate("coop://tasks/{id}", {
628
+ list: async () => ({
629
+ resources: service.listTaskResourceUris()
630
+ }),
631
+ complete: {
632
+ id: async (value) => service.listTasks().map((task) => task.id).filter((id) => id.toLowerCase().startsWith(value.toLowerCase()))
633
+ }
634
+ }),
635
+ {
636
+ title: "COOP Task Resource",
637
+ description: "An individual COOP task by id.",
638
+ mimeType: "application/json"
639
+ },
640
+ async (uri) => resourceTextResult(uri.toString(), service.showTask(decodeURIComponent(uri.pathname.slice(1))))
641
+ );
642
+ return { server, service, repoRoot };
643
+ }
644
+ async function startCoopMcpServer(options = {}) {
645
+ const { server } = createCoopMcpServer(options);
646
+ const transport = new StdioServerTransport();
647
+ await server.connect(transport);
648
+ return server;
649
+ }
650
+ function isMainModule() {
651
+ const entryPath = process.argv[1];
652
+ if (!entryPath) {
653
+ return false;
654
+ }
655
+ return import.meta.url === pathToFileURL(path2.resolve(entryPath)).href;
656
+ }
657
+ if (isMainModule()) {
658
+ const options = parseArgs(process.argv);
659
+ startCoopMcpServer(options).catch((error) => {
660
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
661
+ process.exitCode = 1;
662
+ });
663
+ }
6
664
  export {
7
- coopMcp
665
+ createCoopMcpServer,
666
+ startCoopMcpServer
8
667
  };
package/package.json CHANGED
@@ -1,17 +1,26 @@
1
1
  {
2
2
  "name": "@kitsy/coop-mcp",
3
- "version": "0.0.1",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
- "main": "src/index.ts",
6
- "types": "src/index.ts",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "coop-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
7
14
  "exports": {
8
15
  ".": {
9
- "types": "./src/index.ts",
10
- "import": "./src/index.ts"
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
11
18
  }
12
19
  },
13
20
  "dependencies": {
14
- "@kitsy/coop-core": "0.0.1"
21
+ "@modelcontextprotocol/sdk": "^1.27.1",
22
+ "zod": "^4.3.6",
23
+ "@kitsy/coop-core": "1.0.0"
15
24
  },
16
25
  "devDependencies": {
17
26
  "@types/node": "^24.12.0",
@@ -19,7 +28,7 @@
19
28
  "typescript": "^5.9.3"
20
29
  },
21
30
  "scripts": {
22
- "build": "tsup src/index.ts --format esm,cjs --dts --clean",
31
+ "build": "tsup src/index.ts --format esm --dts --clean",
23
32
  "typecheck": "tsc -p tsconfig.json --noEmit"
24
33
  }
25
34
  }
package/dist/index.cjs DELETED
@@ -1,33 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- coopMcp: () => coopMcp
24
- });
25
- module.exports = __toCommonJS(index_exports);
26
- var coopMcp = {
27
- package: "@kitsy/coop-mcp",
28
- status: "planned"
29
- };
30
- // Annotate the CommonJS export names for ESM import in node:
31
- 0 && (module.exports = {
32
- coopMcp
33
- });
package/dist/index.d.cts DELETED
@@ -1,7 +0,0 @@
1
- type CoopMcpPlaceholder = {
2
- readonly package: "@kitsy/coop-mcp";
3
- readonly status: "planned";
4
- };
5
- declare const coopMcp: CoopMcpPlaceholder;
6
-
7
- export { type CoopMcpPlaceholder, coopMcp };
package/src/index.ts DELETED
@@ -1,9 +0,0 @@
1
- export type CoopMcpPlaceholder = {
2
- readonly package: "@kitsy/coop-mcp";
3
- readonly status: "planned";
4
- };
5
-
6
- export const coopMcp: CoopMcpPlaceholder = {
7
- package: "@kitsy/coop-mcp",
8
- status: "planned",
9
- };
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist"
5
- },
6
- "include": ["src/**/*.ts"]
7
- }