@projitive/mcp 1.1.2 → 1.2.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
CHANGED
|
@@ -84,7 +84,7 @@ MCP client config example (`mcp.json`):
|
|
|
84
84
|
"command": "npx",
|
|
85
85
|
"args": ["-y", "@projitive/mcp"],
|
|
86
86
|
"env": {
|
|
87
|
-
"
|
|
87
|
+
"PROJITIVE_SCAN_ROOT_PATHS": "/workspace/a:/workspace/b",
|
|
88
88
|
"PROJITIVE_SCAN_MAX_DEPTH": "3"
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -94,7 +94,9 @@ MCP client config example (`mcp.json`):
|
|
|
94
94
|
|
|
95
95
|
Environment variables (required):
|
|
96
96
|
|
|
97
|
-
- `
|
|
97
|
+
- `PROJITIVE_SCAN_ROOT_PATHS`: required scan roots for discovery methods.
|
|
98
|
+
- Use platform-delimiter string (`:` on Linux/macOS, `;` on Windows), e.g. `/workspace/a:/workspace/b`.
|
|
99
|
+
- Fallback: if not set, legacy `PROJITIVE_SCAN_ROOT_PATH` is used.
|
|
98
100
|
- `PROJITIVE_SCAN_MAX_DEPTH`: required scan depth for discovery methods (integer `0-8`).
|
|
99
101
|
|
|
100
102
|
Local path startup is not the recommended usage mode in this README.
|
package/output/package.json
CHANGED
|
@@ -46,9 +46,43 @@ function requireEnvVar(name) {
|
|
|
46
46
|
}
|
|
47
47
|
return value.trim();
|
|
48
48
|
}
|
|
49
|
+
function normalizeScanRoots(rootPaths) {
|
|
50
|
+
const normalized = rootPaths
|
|
51
|
+
.map((entry) => entry.trim())
|
|
52
|
+
.filter((entry) => entry.length > 0)
|
|
53
|
+
.map((entry) => normalizePath(entry));
|
|
54
|
+
return Array.from(new Set(normalized));
|
|
55
|
+
}
|
|
56
|
+
function parseScanRoots(rawValue) {
|
|
57
|
+
const trimmed = rawValue.trim();
|
|
58
|
+
if (trimmed.length === 0) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return trimmed.split(path.delimiter);
|
|
62
|
+
}
|
|
63
|
+
export function resolveScanRoots(inputPaths) {
|
|
64
|
+
const normalizedInputPaths = normalizeScanRoots(inputPaths ?? []);
|
|
65
|
+
if (normalizedInputPaths.length > 0) {
|
|
66
|
+
return normalizedInputPaths;
|
|
67
|
+
}
|
|
68
|
+
const configuredRoots = process.env.PROJITIVE_SCAN_ROOT_PATHS;
|
|
69
|
+
const rootsFromMultiEnv = typeof configuredRoots === "string"
|
|
70
|
+
? normalizeScanRoots(parseScanRoots(configuredRoots))
|
|
71
|
+
: [];
|
|
72
|
+
if (rootsFromMultiEnv.length > 0) {
|
|
73
|
+
return rootsFromMultiEnv;
|
|
74
|
+
}
|
|
75
|
+
const legacyRoot = process.env.PROJITIVE_SCAN_ROOT_PATH;
|
|
76
|
+
const rootsFromLegacyEnv = typeof legacyRoot === "string"
|
|
77
|
+
? normalizeScanRoots([legacyRoot])
|
|
78
|
+
: [];
|
|
79
|
+
if (rootsFromLegacyEnv.length > 0) {
|
|
80
|
+
return rootsFromLegacyEnv;
|
|
81
|
+
}
|
|
82
|
+
throw new Error("Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS (or legacy PROJITIVE_SCAN_ROOT_PATH)");
|
|
83
|
+
}
|
|
49
84
|
export function resolveScanRoot(inputPath) {
|
|
50
|
-
|
|
51
|
-
return normalizePath(inputPath ?? configuredRoot);
|
|
85
|
+
return resolveScanRoots(inputPath ? [inputPath] : undefined)[0];
|
|
52
86
|
}
|
|
53
87
|
export function resolveScanDepth(inputDepth) {
|
|
54
88
|
const configuredDepthRaw = requireEnvVar("PROJITIVE_SCAN_MAX_DEPTH");
|
|
@@ -210,6 +244,10 @@ export async function discoverProjects(rootPath, maxDepth) {
|
|
|
210
244
|
await walk(rootPath, 0);
|
|
211
245
|
return Array.from(new Set(results)).sort();
|
|
212
246
|
}
|
|
247
|
+
export async function discoverProjectsAcrossRoots(rootPaths, maxDepth) {
|
|
248
|
+
const perRootResults = await Promise.all(rootPaths.map((rootPath) => discoverProjects(rootPath, maxDepth)));
|
|
249
|
+
return Array.from(new Set(perRootResults.flat())).sort();
|
|
250
|
+
}
|
|
213
251
|
async function pathExists(targetPath) {
|
|
214
252
|
const accessResult = await catchIt(fs.access(targetPath));
|
|
215
253
|
return !accessResult.isError();
|
|
@@ -362,14 +400,15 @@ export function registerProjectTools(server) {
|
|
|
362
400
|
description: "Start here when project path is unknown; discover all governance roots",
|
|
363
401
|
inputSchema: {},
|
|
364
402
|
}, async () => {
|
|
365
|
-
const
|
|
403
|
+
const roots = resolveScanRoots();
|
|
366
404
|
const depth = resolveScanDepth();
|
|
367
|
-
const projects = await
|
|
405
|
+
const projects = await discoverProjectsAcrossRoots(roots, depth);
|
|
368
406
|
const markdown = renderToolResponseMarkdown({
|
|
369
407
|
toolName: "projectScan",
|
|
370
408
|
sections: [
|
|
371
409
|
summarySection([
|
|
372
|
-
`-
|
|
410
|
+
`- rootPaths: ${roots.join(", ")}`,
|
|
411
|
+
`- rootCount: ${roots.length}`,
|
|
373
412
|
`- maxDepth: ${depth}`,
|
|
374
413
|
`- discoveredCount: ${projects.length}`,
|
|
375
414
|
]),
|
|
@@ -398,9 +437,9 @@ export function registerProjectTools(server) {
|
|
|
398
437
|
limit: z.number().int().min(1).max(50).optional(),
|
|
399
438
|
},
|
|
400
439
|
}, async ({ limit }) => {
|
|
401
|
-
const
|
|
440
|
+
const roots = resolveScanRoots();
|
|
402
441
|
const depth = resolveScanDepth();
|
|
403
|
-
const projects = await
|
|
442
|
+
const projects = await discoverProjectsAcrossRoots(roots, depth);
|
|
404
443
|
const snapshots = await Promise.all(projects.map(async (governanceDir) => {
|
|
405
444
|
const snapshot = await readTasksSnapshot(governanceDir);
|
|
406
445
|
const inProgress = snapshot.tasks.filter((task) => task.status === "IN_PROGRESS").length;
|
|
@@ -435,7 +474,8 @@ export function registerProjectTools(server) {
|
|
|
435
474
|
toolName: "projectNext",
|
|
436
475
|
sections: [
|
|
437
476
|
summarySection([
|
|
438
|
-
`-
|
|
477
|
+
`- rootPaths: ${roots.join(", ")}`,
|
|
478
|
+
`- rootCount: ${roots.length}`,
|
|
439
479
|
`- maxDepth: ${depth}`,
|
|
440
480
|
`- matchedProjects: ${projects.length}`,
|
|
441
481
|
`- actionableProjects: ${ranked.length}`,
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
import { discoverProjects, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir,
|
|
5
|
+
import { discoverProjects, discoverProjectsAcrossRoots, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir, resolveScanRoots, resolveScanDepth, toProjectPath, registerProjectTools } from "./project.js";
|
|
6
6
|
const tempPaths = [];
|
|
7
7
|
async function createTempDir() {
|
|
8
8
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
|
|
@@ -186,6 +186,15 @@ describe("projitive module", () => {
|
|
|
186
186
|
const projects = await discoverProjects(root, 3);
|
|
187
187
|
expect(projects).toEqual([]);
|
|
188
188
|
});
|
|
189
|
+
it("ignores non-existent roots when scanning across multiple roots", async () => {
|
|
190
|
+
const validRoot = await createTempDir();
|
|
191
|
+
const validProject = path.join(validRoot, "project-a");
|
|
192
|
+
const missingRoot = path.join(validRoot, "__missing_root__");
|
|
193
|
+
await fs.mkdir(validProject, { recursive: true });
|
|
194
|
+
await fs.writeFile(path.join(validProject, ".projitive"), "", "utf-8");
|
|
195
|
+
const projects = await discoverProjectsAcrossRoots([missingRoot, validRoot], 3);
|
|
196
|
+
expect(projects).toContain(validProject);
|
|
197
|
+
});
|
|
189
198
|
});
|
|
190
199
|
describe("initializeProjectStructure", () => {
|
|
191
200
|
it("initializes governance structure under default .projitive directory", async () => {
|
|
@@ -267,20 +276,30 @@ describe("projitive module", () => {
|
|
|
267
276
|
expect(toProjectPath("/a/b/c")).toBe("/a/b");
|
|
268
277
|
});
|
|
269
278
|
});
|
|
270
|
-
describe("
|
|
271
|
-
it("uses environment variable when no
|
|
279
|
+
describe("resolveScanRoots", () => {
|
|
280
|
+
it("uses legacy environment variable when no multi-root env is provided", () => {
|
|
272
281
|
vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
|
|
273
|
-
expect(
|
|
282
|
+
expect(resolveScanRoots()).toEqual(["/test/root"]);
|
|
274
283
|
vi.unstubAllEnvs();
|
|
275
284
|
});
|
|
276
|
-
it("uses input
|
|
285
|
+
it("uses input paths when provided", () => {
|
|
277
286
|
vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
|
|
278
|
-
expect(
|
|
287
|
+
expect(resolveScanRoots(["/custom/path", " /custom/path ", "/second/path"])).toEqual(["/custom/path", "/second/path"]);
|
|
288
|
+
vi.unstubAllEnvs();
|
|
289
|
+
});
|
|
290
|
+
it("uses PROJITIVE_SCAN_ROOT_PATHS with platform delimiter", () => {
|
|
291
|
+
vi.stubEnv("PROJITIVE_SCAN_ROOT_PATHS", ["/root/a", "/root/b", "", " /root/c "].join(path.delimiter));
|
|
292
|
+
expect(resolveScanRoots()).toEqual(["/root/a", "/root/b", "/root/c"]);
|
|
293
|
+
vi.unstubAllEnvs();
|
|
294
|
+
});
|
|
295
|
+
it("treats JSON-like string as plain delimiter input", () => {
|
|
296
|
+
vi.stubEnv("PROJITIVE_SCAN_ROOT_PATHS", JSON.stringify(["/json/a", "/json/b"]));
|
|
297
|
+
expect(resolveScanRoots()).toHaveLength(1);
|
|
279
298
|
vi.unstubAllEnvs();
|
|
280
299
|
});
|
|
281
|
-
it("throws error when
|
|
300
|
+
it("throws error when no root environment variables are configured", () => {
|
|
282
301
|
vi.unstubAllEnvs();
|
|
283
|
-
expect(() =>
|
|
302
|
+
expect(() => resolveScanRoots()).toThrow("Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS");
|
|
284
303
|
});
|
|
285
304
|
});
|
|
286
305
|
describe("resolveScanDepth", () => {
|
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, findTextReferences } from "../common/index.js";
|
|
5
5
|
import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "../common/index.js";
|
|
6
6
|
import { catchIt, TASK_LINT_CODES, renderLintSuggestions } from "../common/index.js";
|
|
7
|
-
import { resolveGovernanceDir, resolveScanDepth,
|
|
7
|
+
import { resolveGovernanceDir, resolveScanDepth, resolveScanRoots, discoverProjectsAcrossRoots, toProjectPath } from "./project.js";
|
|
8
8
|
import { isValidRoadmapId } from "./roadmap.js";
|
|
9
9
|
import { SUB_STATE_PHASES, BLOCKER_TYPES } from "../types.js";
|
|
10
10
|
export const TASKS_START = "<!-- PROJITIVE:TASKS:START -->";
|
|
@@ -820,9 +820,9 @@ export function registerTaskTools(server) {
|
|
|
820
820
|
limit: z.number().int().min(1).max(20).optional(),
|
|
821
821
|
},
|
|
822
822
|
}, async ({ limit }) => {
|
|
823
|
-
const
|
|
823
|
+
const roots = resolveScanRoots();
|
|
824
824
|
const depth = resolveScanDepth();
|
|
825
|
-
const projects = await
|
|
825
|
+
const projects = await discoverProjectsAcrossRoots(roots, depth);
|
|
826
826
|
const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
|
|
827
827
|
if (rankedCandidates.length === 0) {
|
|
828
828
|
const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
|
|
@@ -850,7 +850,8 @@ export function registerTaskTools(server) {
|
|
|
850
850
|
toolName: "taskNext",
|
|
851
851
|
sections: [
|
|
852
852
|
summarySection([
|
|
853
|
-
`-
|
|
853
|
+
`- rootPaths: ${roots.join(", ")}`,
|
|
854
|
+
`- rootCount: ${roots.length}`,
|
|
854
855
|
`- maxDepth: ${depth}`,
|
|
855
856
|
`- matchedProjects: ${projects.length}`,
|
|
856
857
|
"- actionableTasks: 0",
|
|
@@ -900,7 +901,8 @@ export function registerTaskTools(server) {
|
|
|
900
901
|
toolName: "taskNext",
|
|
901
902
|
sections: [
|
|
902
903
|
summarySection([
|
|
903
|
-
`-
|
|
904
|
+
`- rootPaths: ${roots.join(", ")}`,
|
|
905
|
+
`- rootCount: ${roots.length}`,
|
|
904
906
|
`- maxDepth: ${depth}`,
|
|
905
907
|
`- matchedProjects: ${projects.length}`,
|
|
906
908
|
`- actionableTasks: ${rankedCandidates.length}`,
|