@projitive/mcp 1.0.2 → 1.0.4

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.
Files changed (40) hide show
  1. package/README.md +47 -23
  2. package/output/hooks.js +1 -14
  3. package/output/hooks.test.js +7 -18
  4. package/output/index.js +23 -5
  5. package/output/package.json +36 -0
  6. package/output/projitive.js +21 -2
  7. package/output/projitive.test.js +1 -0
  8. package/output/source/designs.js +38 -0
  9. package/output/source/helpers/artifacts/artifacts.js +10 -0
  10. package/output/source/helpers/artifacts/artifacts.test.js +18 -0
  11. package/output/source/helpers/artifacts/index.js +1 -0
  12. package/output/source/helpers/catch/catch.js +48 -0
  13. package/output/source/helpers/catch/catch.test.js +43 -0
  14. package/output/source/helpers/catch/index.js +1 -0
  15. package/output/source/helpers/files/files.js +62 -0
  16. package/output/source/helpers/files/files.test.js +32 -0
  17. package/output/source/helpers/files/index.js +1 -0
  18. package/output/source/helpers/index.js +6 -0
  19. package/output/source/helpers/linter/codes.js +25 -0
  20. package/output/source/helpers/linter/index.js +2 -0
  21. package/output/source/helpers/linter/linter.js +6 -0
  22. package/output/source/helpers/linter/linter.test.js +16 -0
  23. package/output/source/helpers/markdown/index.js +1 -0
  24. package/output/source/helpers/markdown/markdown.js +33 -0
  25. package/output/source/helpers/markdown/markdown.test.js +36 -0
  26. package/output/source/helpers/response/index.js +1 -0
  27. package/output/source/helpers/response/response.js +73 -0
  28. package/output/source/helpers/response/response.test.js +50 -0
  29. package/output/source/index.js +215 -0
  30. package/output/source/projitive.js +497 -0
  31. package/output/source/projitive.test.js +75 -0
  32. package/output/source/readme.js +26 -0
  33. package/output/source/reports.js +36 -0
  34. package/output/source/roadmap.js +165 -0
  35. package/output/source/roadmap.test.js +11 -0
  36. package/output/source/tasks.js +762 -0
  37. package/output/source/tasks.test.js +152 -0
  38. package/output/tasks.js +100 -80
  39. package/output/tasks.test.js +32 -8
  40. package/package.json +1 -1
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderLintSuggestions } from "./linter.js";
3
+ describe("renderLintSuggestions", () => {
4
+ it("renders lint lines with code and message", () => {
5
+ const lines = renderLintSuggestions([
6
+ { code: "TASK_001", message: "Example lint" },
7
+ ]);
8
+ expect(lines).toEqual(["- [TASK_001] Example lint"]);
9
+ });
10
+ it("appends fixHint when provided", () => {
11
+ const lines = renderLintSuggestions([
12
+ { code: "TASK_002", message: "Missing field.", fixHint: "Set owner." },
13
+ ]);
14
+ expect(lines).toEqual(["- [TASK_002] Missing field. Set owner."]);
15
+ });
16
+ });
@@ -0,0 +1 @@
1
+ export * from './markdown.js';
@@ -0,0 +1,33 @@
1
+ import fs from "node:fs/promises";
2
+ export async function readMarkdownSections(filePath) {
3
+ const content = await fs.readFile(filePath, "utf-8");
4
+ const lines = content.split(/\r?\n/);
5
+ const headers = [];
6
+ lines.forEach((line, index) => {
7
+ const match = line.match(/^(#{1,6})\s+(.+)$/);
8
+ if (match) {
9
+ headers.push({ level: match[1].length, heading: match[2].trim(), startLine: index + 1 });
10
+ }
11
+ });
12
+ const sections = headers.map((header, index) => {
13
+ const next = headers[index + 1];
14
+ return {
15
+ heading: header.heading,
16
+ level: header.level,
17
+ startLine: header.startLine,
18
+ endLine: next ? next.startLine - 1 : lines.length,
19
+ };
20
+ });
21
+ return { filePath, lineCount: lines.length, sections };
22
+ }
23
+ export async function findTextReferences(filePath, needle) {
24
+ const content = await fs.readFile(filePath, "utf-8");
25
+ const lines = content.split(/\r?\n/);
26
+ const result = [];
27
+ lines.forEach((line, index) => {
28
+ if (line.includes(needle)) {
29
+ result.push({ filePath, line: index + 1, text: line.trim() });
30
+ }
31
+ });
32
+ return result;
33
+ }
@@ -0,0 +1,36 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { findTextReferences, readMarkdownSections } from "./markdown.js";
6
+ const tempPaths = [];
7
+ async function createTempDir() {
8
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
9
+ tempPaths.push(dir);
10
+ return dir;
11
+ }
12
+ afterEach(async () => {
13
+ await Promise.all(tempPaths.splice(0).map(async (dir) => {
14
+ await fs.rm(dir, { recursive: true, force: true });
15
+ }));
16
+ });
17
+ describe("markdown module", () => {
18
+ it("locates markdown sections with line ranges", async () => {
19
+ const root = await createTempDir();
20
+ const file = path.join(root, "tasks.md");
21
+ await fs.writeFile(file, ["# Tasks", "", "## TODO", "- TASK-0001", "## DONE", "- TASK-0002"].join("\n"), "utf-8");
22
+ const located = await readMarkdownSections(file);
23
+ expect(located.lineCount).toBe(6);
24
+ expect(located.sections[0].heading).toBe("Tasks");
25
+ expect(located.sections[1].heading).toBe("TODO");
26
+ expect(located.sections[1].startLine).toBe(3);
27
+ });
28
+ it("finds ID references with exact line number", async () => {
29
+ const root = await createTempDir();
30
+ const file = path.join(root, "reports.md");
31
+ await fs.writeFile(file, ["Task: TASK-0001", "Roadmap: ROADMAP-0001"].join("\n"), "utf-8");
32
+ const refs = await findTextReferences(file, "TASK-0001");
33
+ expect(refs).toHaveLength(1);
34
+ expect(refs[0].line).toBe(1);
35
+ });
36
+ });
@@ -0,0 +1 @@
1
+ export * from "./response.js";
@@ -0,0 +1,73 @@
1
+ export function asText(markdown) {
2
+ return {
3
+ content: [{ type: "text", text: markdown }],
4
+ };
5
+ }
6
+ function withFallback(lines) {
7
+ return lines.length > 0 ? lines : ["- (none)"];
8
+ }
9
+ function shouldKeepRawLine(trimmed) {
10
+ if (trimmed.length === 0) {
11
+ return true;
12
+ }
13
+ if (trimmed.startsWith("#") || trimmed.startsWith(">") || trimmed.startsWith("```")) {
14
+ return true;
15
+ }
16
+ if (/^[-*+]\s/.test(trimmed)) {
17
+ return true;
18
+ }
19
+ if (/^\d+\.\s/.test(trimmed)) {
20
+ return true;
21
+ }
22
+ return false;
23
+ }
24
+ function normalizeLine(line) {
25
+ const trimmed = line.trim();
26
+ if (shouldKeepRawLine(trimmed)) {
27
+ return line;
28
+ }
29
+ return `- ${trimmed}`;
30
+ }
31
+ function normalizeLines(lines) {
32
+ return lines.map((line) => normalizeLine(line));
33
+ }
34
+ export function section(title, lines) {
35
+ return { title, lines: normalizeLines(lines) };
36
+ }
37
+ export function summarySection(lines) {
38
+ return section("Summary", lines);
39
+ }
40
+ export function evidenceSection(lines) {
41
+ return section("Evidence", lines);
42
+ }
43
+ export function guidanceSection(lines) {
44
+ return section("Agent Guidance", lines);
45
+ }
46
+ export function lintSection(lines) {
47
+ return section("Lint Suggestions", lines);
48
+ }
49
+ export function nextCallSection(nextCall) {
50
+ return section("Next Call", nextCall ? [nextCall] : []);
51
+ }
52
+ export function renderToolResponseMarkdown(payload) {
53
+ const body = payload.sections.flatMap((section) => [
54
+ `## ${section.title}`,
55
+ ...withFallback(section.lines),
56
+ "",
57
+ ]);
58
+ return [
59
+ `# ${payload.toolName}`,
60
+ "",
61
+ ...body,
62
+ ].join("\n").trimEnd();
63
+ }
64
+ export function renderErrorMarkdown(toolName, cause, nextSteps, retryExample) {
65
+ return renderToolResponseMarkdown({
66
+ toolName,
67
+ sections: [
68
+ section("Error", [`cause: ${cause}`]),
69
+ section("Next Step", nextSteps),
70
+ section("Retry Example", [retryExample ?? "(none)"]),
71
+ ],
72
+ });
73
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { asText, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "./response.js";
3
+ describe("response helpers", () => {
4
+ it("wraps markdown text as MCP text content", () => {
5
+ const result = asText("# hello");
6
+ expect(result.content).toEqual([{ type: "text", text: "# hello" }]);
7
+ });
8
+ it("renders error markdown sections", () => {
9
+ const markdown = renderErrorMarkdown("taskContext", "bad id", ["retry"], "taskContext(...)");
10
+ expect(markdown).toContain("# taskContext");
11
+ expect(markdown).toContain("## Error");
12
+ expect(markdown).toContain("- cause: bad id");
13
+ expect(markdown).toContain("- retry");
14
+ expect(markdown).toContain("## Retry Example");
15
+ });
16
+ it("renders standard tool response sections with fallback", () => {
17
+ const markdown = renderToolResponseMarkdown({
18
+ toolName: "taskList",
19
+ sections: [
20
+ { title: "Summary", lines: ["- governanceDir: /tmp/.projitive"] },
21
+ { title: "Evidence", lines: [] },
22
+ ],
23
+ });
24
+ expect(markdown).toContain("# taskList");
25
+ expect(markdown).toContain("## Summary");
26
+ expect(markdown).toContain("## Evidence");
27
+ expect(markdown).toContain("- (none)");
28
+ });
29
+ it("auto-prefixes plain lines in section helpers", () => {
30
+ const markdown = renderToolResponseMarkdown({
31
+ toolName: "taskList",
32
+ sections: [
33
+ summarySection(["governanceDir: /tmp/.projitive"]),
34
+ ],
35
+ });
36
+ expect(markdown).toContain("- governanceDir: /tmp/.projitive");
37
+ });
38
+ it("nextCallSection accepts optional call and falls back when missing", () => {
39
+ const withCall = renderToolResponseMarkdown({
40
+ toolName: "taskList",
41
+ sections: [nextCallSection("taskContext(projectPath=\"/tmp\", taskId=\"TASK-0001\")")],
42
+ });
43
+ expect(withCall).toContain("- taskContext(projectPath=\"/tmp\", taskId=\"TASK-0001\")");
44
+ const withoutCall = renderToolResponseMarkdown({
45
+ toolName: "taskList",
46
+ sections: [nextCallSection(undefined)],
47
+ });
48
+ expect(withoutCall).toContain("- (none)");
49
+ });
50
+ });
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { z } from "zod";
9
+ import packageJson from "../package.json" with { type: "json" };
10
+ import { registerProjectTools } from "./projitive.js";
11
+ import { registerTaskTools } from "./tasks.js";
12
+ import { registerRoadmapTools } from "./roadmap.js";
13
+ const PROJITIVE_SPEC_VERSION = "1.0.0";
14
+ const currentFilePath = fileURLToPath(import.meta.url);
15
+ const sourceDir = path.dirname(currentFilePath);
16
+ const repoRoot = path.resolve(sourceDir, "..", "..", "..");
17
+ const MCP_RUNTIME_VERSION = typeof packageJson.version === "string" && packageJson.version.trim().length > 0
18
+ ? packageJson.version.trim()
19
+ : PROJITIVE_SPEC_VERSION;
20
+ const server = new McpServer({
21
+ name: "projitive",
22
+ version: MCP_RUNTIME_VERSION,
23
+ description: "Semantic Projitive MCP for project/task discovery and agent guidance with markdown-first outputs",
24
+ });
25
+ function resolveRepoFile(relativePath) {
26
+ return path.join(repoRoot, relativePath);
27
+ }
28
+ async function readMarkdownOrFallback(relativePath, fallbackTitle) {
29
+ const absolutePath = resolveRepoFile(relativePath);
30
+ const content = await fs.readFile(absolutePath, "utf-8").catch(() => undefined);
31
+ if (typeof content === "string" && content.trim().length > 0) {
32
+ return content;
33
+ }
34
+ return [
35
+ `# ${fallbackTitle}`,
36
+ "",
37
+ `- file: ${relativePath}`,
38
+ "- status: missing-or-empty",
39
+ "- next: create this file or ensure it has readable markdown content",
40
+ ].join("\n");
41
+ }
42
+ function renderMethodCatalogMarkdown() {
43
+ return [
44
+ "# MCP Method Catalog",
45
+ "",
46
+ "## Core Pattern",
47
+ "- Prefer List/Context for primary discovery/detail flows.",
48
+ "- Use Next/Scan/Locate for acceleration and bootstrapping.",
49
+ "",
50
+ "## Methods",
51
+ "| Group | Method | Role |",
52
+ "|---|---|---|",
53
+ "| Project | projectInit | initialize governance directory structure |",
54
+ "| Project | projectScan | discover governance projects by marker |",
55
+ "| Project | projectNext | rank actionable projects |",
56
+ "| Project | projectLocate | resolve nearest governance root |",
57
+ "| Project | projectContext | summarize project governance context |",
58
+ "| Task | taskList | list tasks with optional filters |",
59
+ "| Task | taskNext | select top actionable task |",
60
+ "| Task | taskContext | inspect one task with references |",
61
+ "| Roadmap | roadmapList | list roadmap IDs and linked tasks |",
62
+ "| Roadmap | roadmapContext | inspect one roadmap with references |",
63
+ ].join("\n");
64
+ }
65
+ function registerGovernanceResources() {
66
+ server.registerResource("governanceWorkspace", "projitive://governance/workspace", {
67
+ title: "Governance Workspace",
68
+ description: "Primary governance README under .projitive",
69
+ mimeType: "text/markdown",
70
+ }, async () => ({
71
+ contents: [
72
+ {
73
+ uri: "projitive://governance/workspace",
74
+ text: await readMarkdownOrFallback(".projitive/README.md", "Governance Workspace"),
75
+ },
76
+ ],
77
+ }));
78
+ server.registerResource("governanceTasks", "projitive://governance/tasks", {
79
+ title: "Governance Tasks",
80
+ description: "Current task pool and status under .projitive/tasks.md",
81
+ mimeType: "text/markdown",
82
+ }, async () => ({
83
+ contents: [
84
+ {
85
+ uri: "projitive://governance/tasks",
86
+ text: await readMarkdownOrFallback(".projitive/tasks.md", "Governance Tasks"),
87
+ },
88
+ ],
89
+ }));
90
+ server.registerResource("governanceRoadmap", "projitive://governance/roadmap", {
91
+ title: "Governance Roadmap",
92
+ description: "Current roadmap under .projitive/roadmap.md",
93
+ mimeType: "text/markdown",
94
+ }, async () => ({
95
+ contents: [
96
+ {
97
+ uri: "projitive://governance/roadmap",
98
+ text: await readMarkdownOrFallback(".projitive/roadmap.md", "Governance Roadmap"),
99
+ },
100
+ ],
101
+ }));
102
+ server.registerResource("mcpMethodCatalog", "projitive://mcp/method-catalog", {
103
+ title: "MCP Method Catalog",
104
+ description: "Method naming and purpose map for agent routing",
105
+ mimeType: "text/markdown",
106
+ }, async () => ({
107
+ contents: [
108
+ {
109
+ uri: "projitive://mcp/method-catalog",
110
+ text: renderMethodCatalogMarkdown(),
111
+ },
112
+ ],
113
+ }));
114
+ }
115
+ function asUserPrompt(text) {
116
+ return {
117
+ messages: [
118
+ {
119
+ role: "user",
120
+ content: {
121
+ type: "text",
122
+ text,
123
+ },
124
+ },
125
+ ],
126
+ };
127
+ }
128
+ function registerGovernancePrompts() {
129
+ server.registerPrompt("executeTaskWorkflow", {
130
+ title: "Execute Task Workflow",
131
+ description: "Guide an agent through taskNext -> taskContext -> artifact update -> verification",
132
+ argsSchema: {
133
+ rootPath: z.string().optional(),
134
+ projectPath: z.string().optional(),
135
+ taskId: z.string().optional(),
136
+ },
137
+ }, async ({ rootPath, projectPath, taskId }) => {
138
+ const text = [
139
+ "You are executing Projitive governance workflow.",
140
+ "",
141
+ "Execution order:",
142
+ taskId && projectPath
143
+ ? `1) Run taskContext(projectPath=\"${projectPath}\", taskId=\"${taskId}\").`
144
+ : `1) Run taskNext(${rootPath ? `rootPath=\"${rootPath}\"` : ""}).`,
145
+ "2) Read Suggested Read Order and collect blocking gaps.",
146
+ "3) Update markdown artifacts only (tasks/designs/reports/roadmap as needed).",
147
+ "4) Re-run taskContext for the selected task and verify references are consistent.",
148
+ "",
149
+ "Hard rules:",
150
+ "- Keep TASK/ROADMAP IDs immutable.",
151
+ "- Every status transition must have report evidence.",
152
+ "- Do not introduce non-governance file edits unless task scope requires.",
153
+ ].join("\n");
154
+ return asUserPrompt(text);
155
+ });
156
+ server.registerPrompt("updateTaskStatusWithEvidence", {
157
+ title: "Update Task Status With Evidence",
158
+ description: "Template for safe task status transitions and evidence alignment",
159
+ argsSchema: {
160
+ projectPath: z.string(),
161
+ taskId: z.string(),
162
+ targetStatus: z.enum(["TODO", "IN_PROGRESS", "BLOCKED", "DONE"]),
163
+ },
164
+ }, async ({ projectPath, taskId, targetStatus }) => {
165
+ const text = [
166
+ "Perform a safe task status update using Projitive rules.",
167
+ "",
168
+ `1) Run taskContext(projectPath=\"${projectPath}\", taskId=\"${taskId}\").`,
169
+ `2) Plan status transition toward ${targetStatus}.`,
170
+ "3) Update tasks.md status and updatedAt.",
171
+ "4) Add or update a report under reports/ with concrete evidence.",
172
+ "5) Re-run taskContext and confirm status/evidence/reference consistency.",
173
+ "",
174
+ "Checklist:",
175
+ "- Transition is valid per status machine.",
176
+ "- links/roadmapRefs remain parseable and consistent.",
177
+ "- Only `hooks/task_no_actionable.md` is used as global background hook for no-task discovery.",
178
+ ].join("\n");
179
+ return asUserPrompt(text);
180
+ });
181
+ server.registerPrompt("triageProjectGovernance", {
182
+ title: "Triage Project Governance",
183
+ description: "Template to inspect a project and select next actionable governance task",
184
+ argsSchema: {
185
+ rootPath: z.string().optional(),
186
+ },
187
+ }, async ({ rootPath }) => {
188
+ const text = [
189
+ "Triage governance across projects and pick execution target.",
190
+ "",
191
+ `1) Run projectNext(${rootPath ? `rootPath=\"${rootPath}\"` : ""}).`,
192
+ "2) Select top ranked project.",
193
+ "3) Run projectContext(projectPath=<selectedProject>).",
194
+ "4) Run taskList(projectPath=<selectedProject>, status=IN_PROGRESS).",
195
+ "5) If none, run taskNext to select TODO/IN_PROGRESS candidate.",
196
+ "6) Continue with taskContext for detailed evidence mapping.",
197
+ ].join("\n");
198
+ return asUserPrompt(text);
199
+ });
200
+ }
201
+ registerTaskTools(server);
202
+ registerProjectTools(server);
203
+ registerRoadmapTools(server);
204
+ registerGovernanceResources();
205
+ registerGovernancePrompts();
206
+ async function main() {
207
+ console.error(`[projitive-mcp] starting server`);
208
+ console.error(`[projitive-mcp] version=${MCP_RUNTIME_VERSION} spec=${PROJITIVE_SPEC_VERSION} transport=stdio pid=${process.pid}`);
209
+ const transport = new StdioServerTransport();
210
+ await server.connect(transport);
211
+ }
212
+ void main().catch((error) => {
213
+ console.error("Server error:", error);
214
+ process.exit(1);
215
+ });