@projitive/mcp 1.0.6 → 1.0.7

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Language: English | [简体中文](README_CN.md)
4
4
 
5
- **Current Spec Version: projitive-spec v1.0.0 | MCP Version: 1.0.6**
5
+ **Current Spec Version: projitive-spec v1.0.0 | MCP Version: 1.0.7**
6
6
 
7
7
  Projitive MCP server (semantic interface edition) helps agents discover projects, select tasks, locate evidence, and execute under governance workflows.
8
8
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projitive/mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Projitive MCP Server for project and task discovery/update",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -133,6 +133,41 @@ function parentDir(dirPath) {
133
133
  const parent = path.dirname(dirPath);
134
134
  return parent === dirPath ? null : parent;
135
135
  }
136
+ export function toProjectPath(governanceDir) {
137
+ return path.dirname(governanceDir);
138
+ }
139
+ async function listChildGovernanceDirs(parentPath) {
140
+ const entriesResult = await catchIt(fs.readdir(parentPath, { withFileTypes: true }));
141
+ if (entriesResult.isError()) {
142
+ return [];
143
+ }
144
+ const folders = entriesResult.value
145
+ .filter((entry) => entry.isDirectory())
146
+ .map((entry) => path.join(parentPath, entry.name));
147
+ const markerChecks = await Promise.all(folders.map(async (folderPath) => ({
148
+ folderPath,
149
+ hasMarker: await hasProjectMarker(folderPath),
150
+ })));
151
+ const candidates = markerChecks
152
+ .filter((item) => item.hasMarker)
153
+ .map((item) => item.folderPath)
154
+ .sort((a, b) => a.localeCompare(b));
155
+ return candidates;
156
+ }
157
+ async function resolveChildGovernanceDir(parentPath) {
158
+ const candidates = await listChildGovernanceDirs(parentPath);
159
+ if (candidates.length === 0) {
160
+ return undefined;
161
+ }
162
+ const defaultCandidate = path.join(parentPath, DEFAULT_GOVERNANCE_DIR);
163
+ if (candidates.includes(defaultCandidate)) {
164
+ return defaultCandidate;
165
+ }
166
+ if (candidates.length === 1) {
167
+ return candidates[0];
168
+ }
169
+ throw new Error(`Multiple governance roots found under path: ${parentPath}. Use projectPath/governanceDir explicitly.`);
170
+ }
136
171
  export async function resolveGovernanceDir(inputPath) {
137
172
  const absolutePath = path.resolve(inputPath);
138
173
  const statResult = await catchIt(fs.stat(absolutePath));
@@ -145,6 +180,10 @@ export async function resolveGovernanceDir(inputPath) {
145
180
  if (await hasProjectMarker(cursor)) {
146
181
  return cursor;
147
182
  }
183
+ const childGovernanceDir = await resolveChildGovernanceDir(cursor);
184
+ if (childGovernanceDir) {
185
+ return childGovernanceDir;
186
+ }
148
187
  cursor = parentDir(cursor);
149
188
  }
150
189
  throw new Error(`No ${PROJECT_MARKER} marker found from path: ${absolutePath}`);
@@ -158,6 +197,8 @@ export async function discoverProjects(rootPath, maxDepth) {
158
197
  if (await hasProjectMarker(currentPath)) {
159
198
  results.push(currentPath);
160
199
  }
200
+ const childGovernanceDirs = await listChildGovernanceDirs(currentPath);
201
+ results.push(...childGovernanceDirs);
161
202
  const entriesResult = await catchIt(fs.readdir(currentPath, { withFileTypes: true }));
162
203
  if (entriesResult.isError()) {
163
204
  return;
@@ -274,7 +315,7 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
274
315
  export function registerProjectTools(server) {
275
316
  server.registerTool("projectInit", {
276
317
  title: "Project Init",
277
- description: "Bootstrap governance files when a project has no .projitive yet",
318
+ description: "Bootstrap governance files when a project has no .projitive yet (requires projectPath)",
278
319
  inputSchema: {
279
320
  projectPath: z.string(),
280
321
  governanceDir: z.string().optional(),
@@ -313,7 +354,7 @@ export function registerProjectTools(server) {
313
354
  "- After init, fill owner/roadmapRefs/links in tasks.md before marking DONE.",
314
355
  "- Keep task source-of-truth inside marker block only.",
315
356
  ]),
316
- nextCallSection(`projectContext(projectPath=\"${initialized.governanceDir}\")`),
357
+ nextCallSection(`projectContext(projectPath=\"${initialized.projectPath}\")`),
317
358
  ],
318
359
  });
319
360
  return asText(markdown);
@@ -408,13 +449,13 @@ export function registerProjectTools(server) {
408
449
  ...ranked.map((item, index) => `${index + 1}. ${item.governanceDir} | actionable=${item.actionable} | in_progress=${item.inProgress} | todo=${item.todo} | blocked=${item.blocked} | done=${item.done} | latest=${item.latestUpdatedAt} | tasksPath=${item.tasksPath}${item.tasksExists ? "" : " (missing)"}`),
409
450
  ]),
410
451
  guidanceSection([
411
- "- Pick top 1 project and call `projectContext` with its governanceDir.",
452
+ "- Pick top 1 project and call `projectContext` with its projectPath.",
412
453
  "- Then call `taskList` and `taskContext` to continue execution.",
413
454
  "- If `tasksPath` is missing, create tasks.md using project convention before task-level operations.",
414
455
  ]),
415
456
  lintSection(ranked[0]?.lintSuggestions ?? []),
416
457
  nextCallSection(ranked[0]
417
- ? `projectContext(projectPath=\"${ranked[0].governanceDir}\")`
458
+ ? `projectContext(projectPath=\"${toProjectPath(ranked[0].governanceDir)}\")`
418
459
  : undefined),
419
460
  ],
420
461
  });
@@ -429,18 +470,20 @@ export function registerProjectTools(server) {
429
470
  }, async ({ inputPath }) => {
430
471
  const resolvedFrom = normalizePath(inputPath);
431
472
  const governanceDir = await resolveGovernanceDir(resolvedFrom);
473
+ const projectPath = toProjectPath(governanceDir);
432
474
  const markerPath = path.join(governanceDir, ".projitive");
433
475
  const markdown = renderToolResponseMarkdown({
434
476
  toolName: "projectLocate",
435
477
  sections: [
436
478
  summarySection([
437
479
  `- resolvedFrom: ${resolvedFrom}`,
480
+ `- projectPath: ${projectPath}`,
438
481
  `- governanceDir: ${governanceDir}`,
439
482
  `- markerPath: ${markerPath}`,
440
483
  ]),
441
- guidanceSection(["- Call `projectContext` with this governanceDir to get task and roadmap summaries."]),
484
+ guidanceSection(["- Call `projectContext` with this projectPath to get task and roadmap summaries."]),
442
485
  lintSection(["- Run `projectContext` to get governance/module lint suggestions for this project."]),
443
- nextCallSection(`projectContext(projectPath=\"${governanceDir}\")`),
486
+ nextCallSection(`projectContext(projectPath=\"${projectPath}\")`),
444
487
  ],
445
488
  });
446
489
  return asText(markdown);
@@ -453,6 +496,7 @@ export function registerProjectTools(server) {
453
496
  },
454
497
  }, async ({ projectPath }) => {
455
498
  const governanceDir = await resolveGovernanceDir(projectPath);
499
+ const normalizedProjectPath = toProjectPath(governanceDir);
456
500
  const artifacts = await discoverGovernanceArtifacts(governanceDir);
457
501
  const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
458
502
  const roadmapIds = await readRoadmapIds(governanceDir);
@@ -468,6 +512,7 @@ export function registerProjectTools(server) {
468
512
  toolName: "projectContext",
469
513
  sections: [
470
514
  summarySection([
515
+ `- projectPath: ${normalizedProjectPath}`,
471
516
  `- governanceDir: ${governanceDir}`,
472
517
  `- tasksFile: ${tasksPath}`,
473
518
  `- roadmapIds: ${roadmapIds.length}`,
@@ -488,7 +533,7 @@ export function registerProjectTools(server) {
488
533
  "- Then call `taskContext` with a task ID to retrieve evidence locations and reading order.",
489
534
  ]),
490
535
  lintSection(lintSuggestions),
491
- nextCallSection(`taskList(projectPath=\"${governanceDir}\")`),
536
+ nextCallSection(`taskList(projectPath=\"${normalizedProjectPath}\")`),
492
537
  ],
493
538
  });
494
539
  return asText(markdown);
@@ -31,6 +31,24 @@ describe("projitive module", () => {
31
31
  const resolved = await resolveGovernanceDir(deepDir);
32
32
  expect(resolved).toBe(governanceDir);
33
33
  });
34
+ it("resolves nested default governance dir when input path is project root", async () => {
35
+ const root = await createTempDir();
36
+ const projectRoot = path.join(root, "repo");
37
+ const governanceDir = path.join(projectRoot, ".projitive");
38
+ await fs.mkdir(governanceDir, { recursive: true });
39
+ await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
40
+ const resolved = await resolveGovernanceDir(projectRoot);
41
+ expect(resolved).toBe(governanceDir);
42
+ });
43
+ it("resolves nested custom governance dir when input path is project root", async () => {
44
+ const root = await createTempDir();
45
+ const projectRoot = path.join(root, "repo");
46
+ const governanceDir = path.join(projectRoot, "governance");
47
+ await fs.mkdir(governanceDir, { recursive: true });
48
+ await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
49
+ const resolved = await resolveGovernanceDir(projectRoot);
50
+ expect(resolved).toBe(governanceDir);
51
+ });
34
52
  it("discovers projects by marker file", async () => {
35
53
  const root = await createTempDir();
36
54
  const p1 = path.join(root, "a");
@@ -43,6 +61,24 @@ describe("projitive module", () => {
43
61
  expect(projects).toContain(p1);
44
62
  expect(projects).toContain(p2);
45
63
  });
64
+ it("discovers nested default governance directory under project root", async () => {
65
+ const root = await createTempDir();
66
+ const projectRoot = path.join(root, "app");
67
+ const governanceDir = path.join(projectRoot, ".projitive");
68
+ await fs.mkdir(governanceDir, { recursive: true });
69
+ await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
70
+ const projects = await discoverProjects(root, 3);
71
+ expect(projects).toContain(governanceDir);
72
+ });
73
+ it("discovers nested custom governance directory under project root", async () => {
74
+ const root = await createTempDir();
75
+ const projectRoot = path.join(root, "app");
76
+ const governanceDir = path.join(projectRoot, "governance");
77
+ await fs.mkdir(governanceDir, { recursive: true });
78
+ await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
79
+ const projects = await discoverProjects(root, 3);
80
+ expect(projects).toContain(governanceDir);
81
+ });
46
82
  it("initializes governance structure under default .projitive directory", async () => {
47
83
  const root = await createTempDir();
48
84
  const initialized = await initializeProjectStructure(root);
@@ -6,7 +6,7 @@ import { discoverGovernanceArtifacts } from "./helpers/files/index.js";
6
6
  import { ROADMAP_LINT_CODES, renderLintSuggestions } from "./helpers/linter/index.js";
7
7
  import { findTextReferences } from "./helpers/markdown/index.js";
8
8
  import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "./helpers/response/index.js";
9
- import { resolveGovernanceDir } from "./projitive.js";
9
+ import { resolveGovernanceDir, toProjectPath } from "./projitive.js";
10
10
  import { loadTasks } from "./tasks.js";
11
11
  export const ROADMAP_ID_REGEX = /^ROADMAP-\d{4}$/;
12
12
  function collectRoadmapLintSuggestionItems(roadmapIds, tasks) {
@@ -99,7 +99,7 @@ export function registerRoadmapTools(server) {
99
99
  guidanceSection(["- Pick one roadmap ID and call `roadmapContext`."]),
100
100
  lintSection(lintSuggestions),
101
101
  nextCallSection(roadmapIds[0]
102
- ? `roadmapContext(projectPath=\"${governanceDir}\", roadmapId=\"${roadmapIds[0]}\")`
102
+ ? `roadmapContext(projectPath=\"${toProjectPath(governanceDir)}\", roadmapId=\"${roadmapIds[0]}\")`
103
103
  : undefined),
104
104
  ],
105
105
  });
@@ -157,7 +157,7 @@ export function registerRoadmapTools(server) {
157
157
  "- Re-run `roadmapContext` after edits to confirm references remain consistent.",
158
158
  ]),
159
159
  lintSection(lintSuggestions),
160
- nextCallSection(`roadmapContext(projectPath=\"${governanceDir}\", roadmapId=\"${roadmapId}\")`),
160
+ nextCallSection(`roadmapContext(projectPath=\"${toProjectPath(governanceDir)}\", roadmapId=\"${roadmapId}\")`),
161
161
  ],
162
162
  });
163
163
  return asText(markdown);
@@ -7,7 +7,7 @@ import { findTextReferences } from "./helpers/markdown/index.js";
7
7
  import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "./helpers/response/index.js";
8
8
  import { catchIt } from "./helpers/catch/index.js";
9
9
  import { TASK_LINT_CODES, renderLintSuggestions } from "./helpers/linter/index.js";
10
- import { resolveGovernanceDir, resolveScanDepth, resolveScanRoot, discoverProjects } from "./projitive.js";
10
+ import { resolveGovernanceDir, resolveScanDepth, resolveScanRoot, discoverProjects, toProjectPath } from "./projitive.js";
11
11
  import { isValidRoadmapId } from "./roadmap.js";
12
12
  export const TASKS_START = "<!-- PROJITIVE:TASKS:START -->";
13
13
  export const TASKS_END = "<!-- PROJITIVE:TASKS:END -->";
@@ -535,7 +535,7 @@ export function registerTaskTools(server) {
535
535
  guidanceSection(["- Pick one task ID and call `taskContext`."]),
536
536
  lintSection(lintSuggestions),
537
537
  nextCallSection(nextTaskId
538
- ? `taskContext(projectPath=\"${governanceDir}\", taskId=\"${nextTaskId}\")`
538
+ ? `taskContext(projectPath=\"${toProjectPath(governanceDir)}\", taskId=\"${nextTaskId}\")`
539
539
  : undefined),
540
540
  ],
541
541
  });
@@ -609,7 +609,7 @@ export function registerTaskTools(server) {
609
609
  "- Ensure each new task has stable TASK-xxxx ID and at least one roadmapRefs item.",
610
610
  ]),
611
611
  nextCallSection(preferredProject
612
- ? `projectContext(projectPath=\"${preferredProject.governanceDir}\")`
612
+ ? `projectContext(projectPath=\"${toProjectPath(preferredProject.governanceDir)}\")`
613
613
  : "projectScan()"),
614
614
  ],
615
615
  });
@@ -672,7 +672,7 @@ export function registerTaskTools(server) {
672
672
  "- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.",
673
673
  ]),
674
674
  lintSection(lintSuggestions),
675
- nextCallSection(`taskContext(projectPath=\"${selected.governanceDir}\", taskId=\"${selected.task.id}\")`),
675
+ nextCallSection(`taskContext(projectPath=\"${toProjectPath(selected.governanceDir)}\", taskId=\"${selected.task.id}\")`),
676
676
  ],
677
677
  });
678
678
  return asText(markdown);
@@ -696,7 +696,7 @@ export function registerTaskTools(server) {
696
696
  const task = tasks.find((item) => item.id === taskId);
697
697
  if (!task) {
698
698
  return {
699
- ...asText(renderErrorMarkdown("taskContext", `Task not found: ${taskId}`, ["run `taskList` to discover available IDs", "retry with an existing task ID"], `taskList(projectPath=\"${governanceDir}\")`)),
699
+ ...asText(renderErrorMarkdown("taskContext", `Task not found: ${taskId}`, ["run `taskList` to discover available IDs", "retry with an existing task ID"], `taskList(projectPath=\"${toProjectPath(governanceDir)}\")`)),
700
700
  isError: true,
701
701
  };
702
702
  }
@@ -753,7 +753,7 @@ export function registerTaskTools(server) {
753
753
  "- After editing, re-run `taskContext` to verify references and context consistency.",
754
754
  ]),
755
755
  lintSection(lintSuggestions),
756
- nextCallSection(`taskContext(projectPath=\"${governanceDir}\", taskId=\"${task.id}\")`),
756
+ nextCallSection(`taskContext(projectPath=\"${toProjectPath(governanceDir)}\", taskId=\"${task.id}\")`),
757
757
  ],
758
758
  });
759
759
  return asText(coreMarkdown);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projitive/mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Projitive MCP Server for project and task discovery/update",
5
5
  "license": "ISC",
6
6
  "author": "",