@projitive/mcp 2.0.3 → 2.1.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.
Files changed (52) hide show
  1. package/output/package.json +8 -2
  2. package/output/source/common/artifacts.js +1 -1
  3. package/output/source/common/artifacts.test.js +11 -11
  4. package/output/source/common/errors.js +19 -19
  5. package/output/source/common/errors.test.js +59 -0
  6. package/output/source/common/files.js +30 -19
  7. package/output/source/common/files.test.js +14 -14
  8. package/output/source/common/index.js +11 -10
  9. package/output/source/common/linter.js +29 -27
  10. package/output/source/common/linter.test.js +9 -9
  11. package/output/source/common/markdown.js +3 -3
  12. package/output/source/common/markdown.test.js +15 -15
  13. package/output/source/common/response.js +91 -107
  14. package/output/source/common/response.test.js +30 -30
  15. package/output/source/common/store.js +40 -40
  16. package/output/source/common/store.test.js +72 -72
  17. package/output/source/common/tool.js +43 -0
  18. package/output/source/common/types.js +3 -3
  19. package/output/source/common/utils.js +8 -8
  20. package/output/source/common/utils.test.js +48 -0
  21. package/output/source/index.js +16 -16
  22. package/output/source/index.runtime.test.js +57 -0
  23. package/output/source/index.test.js +64 -64
  24. package/output/source/prompts/index.js +3 -3
  25. package/output/source/prompts/index.test.js +23 -0
  26. package/output/source/prompts/quickStart.js +96 -96
  27. package/output/source/prompts/quickStart.test.js +24 -0
  28. package/output/source/prompts/taskDiscovery.js +184 -184
  29. package/output/source/prompts/taskDiscovery.test.js +24 -0
  30. package/output/source/prompts/taskExecution.js +164 -148
  31. package/output/source/prompts/taskExecution.test.js +27 -0
  32. package/output/source/resources/designs.js +26 -26
  33. package/output/source/resources/designs.resources.test.js +52 -0
  34. package/output/source/resources/designs.test.js +88 -88
  35. package/output/source/resources/governance.js +19 -19
  36. package/output/source/resources/governance.test.js +35 -0
  37. package/output/source/resources/index.js +2 -2
  38. package/output/source/resources/index.test.js +18 -0
  39. package/output/source/resources/readme.js +7 -7
  40. package/output/source/resources/readme.test.js +113 -113
  41. package/output/source/resources/reports.js +10 -10
  42. package/output/source/resources/reports.test.js +83 -83
  43. package/output/source/tools/index.js +3 -3
  44. package/output/source/tools/index.test.js +23 -0
  45. package/output/source/tools/project.js +330 -377
  46. package/output/source/tools/project.test.js +308 -175
  47. package/output/source/tools/roadmap.js +236 -255
  48. package/output/source/tools/roadmap.test.js +241 -46
  49. package/output/source/tools/task.js +770 -652
  50. package/output/source/tools/task.test.js +433 -105
  51. package/output/source/types.js +28 -22
  52. package/package.json +8 -2
@@ -1,26 +1,26 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { afterEach, describe, expect, it } from "vitest";
4
- import { ensureStore, getMarkdownViewState, getStoreVersion, loadActionableTasksFromStore, loadRoadmapIdsFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, markMarkdownViewBuilt, markMarkdownViewDirty, replaceRoadmapsInStore, replaceTasksInStore, upsertRoadmapInStore, upsertTaskInStore, } from "./store.js";
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { afterEach, describe, expect, it } from 'vitest';
4
+ import { ensureStore, getMarkdownViewState, getStoreVersion, loadActionableTasksFromStore, loadRoadmapIdsFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, markMarkdownViewBuilt, markMarkdownViewDirty, replaceRoadmapsInStore, replaceTasksInStore, upsertRoadmapInStore, upsertTaskInStore, } from './store.js';
5
5
  const tempPaths = [];
6
6
  async function createTempDbPath() {
7
- const sandboxRoot = path.join(process.cwd(), ".tmp", "store-tests");
7
+ const sandboxRoot = path.join(process.cwd(), '.tmp', 'store-tests');
8
8
  await fs.mkdir(sandboxRoot, { recursive: true });
9
- const dir = await fs.mkdtemp(path.join(sandboxRoot, "case-"));
9
+ const dir = await fs.mkdtemp(path.join(sandboxRoot, 'case-'));
10
10
  tempPaths.push(dir);
11
- return path.join(dir, ".projitive");
11
+ return path.join(dir, '.projitive');
12
12
  }
13
13
  async function readRawStore(dbPath) {
14
- const content = await fs.readFile(dbPath, "utf8");
14
+ const content = await fs.readFile(dbPath, 'utf8');
15
15
  return JSON.parse(content);
16
16
  }
17
17
  function task(input) {
18
18
  return {
19
19
  id: input.id,
20
20
  title: input.title,
21
- status: input.status ?? "TODO",
22
- owner: input.owner ?? "",
23
- summary: input.summary ?? "",
21
+ status: input.status ?? 'TODO',
22
+ owner: input.owner ?? '',
23
+ summary: input.summary ?? '',
24
24
  updatedAt: input.updatedAt ?? new Date().toISOString(),
25
25
  links: input.links ?? [],
26
26
  roadmapRefs: input.roadmapRefs ?? [],
@@ -32,7 +32,7 @@ function milestone(input) {
32
32
  return {
33
33
  id: input.id,
34
34
  title: input.title,
35
- status: input.status ?? "active",
35
+ status: input.status ?? 'active',
36
36
  time: input.time,
37
37
  updatedAt: input.updatedAt ?? new Date().toISOString(),
38
38
  };
@@ -40,12 +40,12 @@ function milestone(input) {
40
40
  afterEach(async () => {
41
41
  await Promise.all(tempPaths.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
42
42
  });
43
- describe("store", () => {
44
- it("initializes JSON store with default meta/view state", async () => {
43
+ describe('store', () => {
44
+ it('initializes JSON store with default meta/view state', async () => {
45
45
  const dbPath = await createTempDbPath();
46
46
  await ensureStore(dbPath);
47
47
  const store = await readRawStore(dbPath);
48
- expect(store.schema).toBe("projitive-json-store");
48
+ expect(store.schema).toBe('projitive-json-store');
49
49
  expect(store.meta.store_schema_version).toBe(3);
50
50
  expect(store.meta.tasks_version).toBe(0);
51
51
  expect(store.meta.roadmaps_version).toBe(0);
@@ -55,65 +55,65 @@ describe("store", () => {
55
55
  expect(store.view_state.roadmaps_markdown.lastSourceVersion).toBe(0);
56
56
  expect(Array.isArray(store.migration_history)).toBe(true);
57
57
  });
58
- it("tracks view state dirty/build transitions", async () => {
58
+ it('tracks view state dirty/build transitions', async () => {
59
59
  const dbPath = await createTempDbPath();
60
60
  await ensureStore(dbPath);
61
- const initial = await getMarkdownViewState(dbPath, "tasks_markdown");
61
+ const initial = await getMarkdownViewState(dbPath, 'tasks_markdown');
62
62
  expect(initial.dirty).toBe(true);
63
- await markMarkdownViewBuilt(dbPath, "tasks_markdown", 7, "2026-03-13T00:00:00.000Z");
64
- const built = await getMarkdownViewState(dbPath, "tasks_markdown");
63
+ await markMarkdownViewBuilt(dbPath, 'tasks_markdown', 7, '2026-03-13T00:00:00.000Z');
64
+ const built = await getMarkdownViewState(dbPath, 'tasks_markdown');
65
65
  expect(built.dirty).toBe(false);
66
66
  expect(built.lastSourceVersion).toBe(7);
67
- expect(built.lastBuiltAt).toBe("2026-03-13T00:00:00.000Z");
68
- await markMarkdownViewDirty(dbPath, "tasks_markdown");
69
- const dirtyAgain = await getMarkdownViewState(dbPath, "tasks_markdown");
67
+ expect(built.lastBuiltAt).toBe('2026-03-13T00:00:00.000Z');
68
+ await markMarkdownViewDirty(dbPath, 'tasks_markdown');
69
+ const dirtyAgain = await getMarkdownViewState(dbPath, 'tasks_markdown');
70
70
  expect(dirtyAgain.dirty).toBe(true);
71
71
  expect(dirtyAgain.lastSourceVersion).toBe(7);
72
72
  });
73
- it("upserts tasks and updates source/view versions", async () => {
73
+ it('upserts tasks and updates source/view versions', async () => {
74
74
  const dbPath = await createTempDbPath();
75
75
  await ensureStore(dbPath);
76
76
  await upsertTaskInStore(dbPath, task({
77
- id: "TASK-0001",
78
- title: "First",
79
- status: "TODO",
80
- owner: "alice",
81
- summary: "first",
82
- updatedAt: "2026-03-13T00:00:00.000Z",
83
- roadmapRefs: ["ROADMAP-0001"],
77
+ id: 'TASK-0001',
78
+ title: 'First',
79
+ status: 'TODO',
80
+ owner: 'alice',
81
+ summary: 'first',
82
+ updatedAt: '2026-03-13T00:00:00.000Z',
83
+ roadmapRefs: ['ROADMAP-0001'],
84
84
  }));
85
85
  await upsertTaskInStore(dbPath, task({
86
- id: "TASK-0001",
87
- title: "First Updated",
88
- status: "IN_PROGRESS",
89
- owner: "alice",
90
- summary: "updated",
91
- updatedAt: "2026-03-13T01:00:00.000Z",
92
- roadmapRefs: ["ROADMAP-0001"],
93
- links: ["reports/r1.md"],
86
+ id: 'TASK-0001',
87
+ title: 'First Updated',
88
+ status: 'IN_PROGRESS',
89
+ owner: 'alice',
90
+ summary: 'updated',
91
+ updatedAt: '2026-03-13T01:00:00.000Z',
92
+ roadmapRefs: ['ROADMAP-0001'],
93
+ links: ['reports/r1.md'],
94
94
  }));
95
95
  const tasks = await loadTasksFromStore(dbPath);
96
96
  expect(tasks).toHaveLength(1);
97
- expect(tasks[0].title).toBe("First Updated");
98
- expect(tasks[0].status).toBe("IN_PROGRESS");
99
- expect(tasks[0].links).toEqual(["reports/r1.md"]);
100
- const tasksVersion = await getStoreVersion(dbPath, "tasks");
97
+ expect(tasks[0].title).toBe('First Updated');
98
+ expect(tasks[0].status).toBe('IN_PROGRESS');
99
+ expect(tasks[0].links).toEqual(['reports/r1.md']);
100
+ const tasksVersion = await getStoreVersion(dbPath, 'tasks');
101
101
  expect(tasksVersion).toBe(2);
102
- const viewState = await getMarkdownViewState(dbPath, "tasks_markdown");
102
+ const viewState = await getMarkdownViewState(dbPath, 'tasks_markdown');
103
103
  expect(viewState.dirty).toBe(true);
104
104
  const store = await readRawStore(dbPath);
105
- const updated = store.tasks.find((item) => item.id === "TASK-0001");
105
+ const updated = store.tasks.find((item) => item.id === 'TASK-0001');
106
106
  expect(updated?.recordVersion).toBe(2);
107
107
  });
108
- it("replaces task set and computes actionable ranking/stats", async () => {
108
+ it('replaces task set and computes actionable ranking/stats', async () => {
109
109
  const dbPath = await createTempDbPath();
110
110
  await ensureStore(dbPath);
111
111
  await replaceTasksInStore(dbPath, [
112
- task({ id: "TASK-0001", title: "Todo older", status: "TODO", updatedAt: "2026-03-10T00:00:00.000Z" }),
113
- task({ id: "TASK-0002", title: "In progress", status: "IN_PROGRESS", updatedAt: "2026-03-12T00:00:00.000Z" }),
114
- task({ id: "TASK-0003", title: "Todo newer", status: "TODO", updatedAt: "2026-03-13T00:00:00.000Z" }),
115
- task({ id: "TASK-0004", title: "Blocked", status: "BLOCKED", updatedAt: "2026-03-11T00:00:00.000Z" }),
116
- task({ id: "TASK-0005", title: "Done", status: "DONE", updatedAt: "2026-03-09T00:00:00.000Z" }),
112
+ task({ id: 'TASK-0001', title: 'Todo older', status: 'TODO', updatedAt: '2026-03-10T00:00:00.000Z' }),
113
+ task({ id: 'TASK-0002', title: 'In progress', status: 'IN_PROGRESS', updatedAt: '2026-03-12T00:00:00.000Z' }),
114
+ task({ id: 'TASK-0003', title: 'Todo newer', status: 'TODO', updatedAt: '2026-03-13T00:00:00.000Z' }),
115
+ task({ id: 'TASK-0004', title: 'Blocked', status: 'BLOCKED', updatedAt: '2026-03-11T00:00:00.000Z' }),
116
+ task({ id: 'TASK-0005', title: 'Done', status: 'DONE', updatedAt: '2026-03-09T00:00:00.000Z' }),
117
117
  ]);
118
118
  const stats = await loadTaskStatusStatsFromStore(dbPath);
119
119
  expect(stats.todo).toBe(2);
@@ -121,44 +121,44 @@ describe("store", () => {
121
121
  expect(stats.blocked).toBe(1);
122
122
  expect(stats.done).toBe(1);
123
123
  expect(stats.total).toBe(5);
124
- expect(stats.latestUpdatedAt).toBe("2026-03-13T00:00:00.000Z");
124
+ expect(stats.latestUpdatedAt).toBe('2026-03-13T00:00:00.000Z');
125
125
  const actionable = await loadActionableTasksFromStore(dbPath, 2);
126
126
  expect(actionable).toHaveLength(2);
127
- expect(actionable[0].id).toBe("TASK-0002");
128
- expect(actionable[1].id).toBe("TASK-0003");
129
- const tasksVersion = await getStoreVersion(dbPath, "tasks");
127
+ expect(actionable[0].id).toBe('TASK-0002');
128
+ expect(actionable[1].id).toBe('TASK-0003');
129
+ const tasksVersion = await getStoreVersion(dbPath, 'tasks');
130
130
  expect(tasksVersion).toBe(1);
131
131
  });
132
- it("upserts/replaces roadmaps and updates versions", async () => {
132
+ it('upserts/replaces roadmaps and updates versions', async () => {
133
133
  const dbPath = await createTempDbPath();
134
134
  await ensureStore(dbPath);
135
135
  await upsertRoadmapInStore(dbPath, milestone({
136
- id: "ROADMAP-0001",
137
- title: "Phase 1",
138
- status: "active",
139
- updatedAt: "2026-03-10T00:00:00.000Z",
136
+ id: 'ROADMAP-0001',
137
+ title: 'Phase 1',
138
+ status: 'active',
139
+ updatedAt: '2026-03-10T00:00:00.000Z',
140
140
  }));
141
141
  await upsertRoadmapInStore(dbPath, milestone({
142
- id: "ROADMAP-0001",
143
- title: "Phase 1 done",
144
- status: "done",
145
- time: "2026-Q1",
146
- updatedAt: "2026-03-11T00:00:00.000Z",
142
+ id: 'ROADMAP-0001',
143
+ title: 'Phase 1 done',
144
+ status: 'done',
145
+ time: '2026-Q1',
146
+ updatedAt: '2026-03-11T00:00:00.000Z',
147
147
  }));
148
148
  const list1 = await loadRoadmapsFromStore(dbPath);
149
149
  expect(list1).toHaveLength(1);
150
- expect(list1[0].title).toBe("Phase 1 done");
151
- expect(list1[0].status).toBe("done");
152
- expect(list1[0].time).toBe("2026-Q1");
150
+ expect(list1[0].title).toBe('Phase 1 done');
151
+ expect(list1[0].status).toBe('done');
152
+ expect(list1[0].time).toBe('2026-Q1');
153
153
  await replaceRoadmapsInStore(dbPath, [
154
- milestone({ id: "ROADMAP-0002", title: "Phase 2", status: "active", updatedAt: "2026-03-12T00:00:00.000Z" }),
155
- milestone({ id: "ROADMAP-0003", title: "Phase 3", status: "done", updatedAt: "2026-03-13T00:00:00.000Z" }),
154
+ milestone({ id: 'ROADMAP-0002', title: 'Phase 2', status: 'active', updatedAt: '2026-03-12T00:00:00.000Z' }),
155
+ milestone({ id: 'ROADMAP-0003', title: 'Phase 3', status: 'done', updatedAt: '2026-03-13T00:00:00.000Z' }),
156
156
  ]);
157
157
  const ids = await loadRoadmapIdsFromStore(dbPath);
158
- expect(ids).toEqual(["ROADMAP-0003", "ROADMAP-0002"]);
159
- const roadmapsVersion = await getStoreVersion(dbPath, "roadmaps");
158
+ expect(ids).toEqual(['ROADMAP-0003', 'ROADMAP-0002']);
159
+ const roadmapsVersion = await getStoreVersion(dbPath, 'roadmaps');
160
160
  expect(roadmapsVersion).toBe(3);
161
- const viewState = await getMarkdownViewState(dbPath, "roadmaps_markdown");
161
+ const viewState = await getMarkdownViewState(dbPath, 'roadmaps_markdown');
162
162
  expect(viewState.dirty).toBe(true);
163
163
  });
164
164
  });
@@ -0,0 +1,43 @@
1
+ import { asText, renderToolResponseMarkdown, renderErrorMarkdown, evidenceSection, guidanceSection, lintSection, nextCallSection, summarySection, } from './response.js';
2
+ export class ToolExecutionError extends Error {
3
+ nextSteps;
4
+ retryExample;
5
+ constructor(message, nextSteps = [], retryExample) {
6
+ super(message);
7
+ this.nextSteps = nextSteps;
8
+ this.retryExample = retryExample;
9
+ this.name = 'ToolExecutionError';
10
+ }
11
+ }
12
+ export function createGovernedTool(spec) {
13
+ const cb = async (input) => {
14
+ try {
15
+ const ctx = { now: new Date().toISOString() };
16
+ const data = await spec.execute(input, ctx);
17
+ const markdown = renderToolResponseMarkdown({
18
+ toolName: spec.name,
19
+ sections: [
20
+ summarySection(await spec.summary(data, ctx)),
21
+ evidenceSection(await (spec.evidence?.(data, ctx) ?? [])),
22
+ guidanceSection(await spec.guidance(data, ctx)),
23
+ lintSection(await (spec.suggestions?.(data, ctx) ?? [])),
24
+ nextCallSection(await spec.nextCall(data, ctx)),
25
+ ],
26
+ });
27
+ return asText(markdown);
28
+ }
29
+ catch (error) {
30
+ if (error instanceof ToolExecutionError) {
31
+ return {
32
+ ...asText(renderErrorMarkdown(spec.name, error.message, error.nextSteps, error.retryExample)),
33
+ isError: true,
34
+ };
35
+ }
36
+ return {
37
+ ...asText(renderErrorMarkdown(spec.name, String(error), [])),
38
+ isError: true,
39
+ };
40
+ }
41
+ };
42
+ return [spec.name, { title: spec.title, description: spec.description, inputSchema: spec.inputSchema }, cb];
43
+ }
@@ -1,7 +1,7 @@
1
1
  // Common type definitions (simplified)
2
2
  export const VALID_STATUS_TRANSITIONS = {
3
- TODO: ["IN_PROGRESS"],
4
- IN_PROGRESS: ["TODO", "BLOCKED", "DONE"],
5
- BLOCKED: ["IN_PROGRESS"],
3
+ TODO: ['IN_PROGRESS'],
4
+ IN_PROGRESS: ['TODO', 'BLOCKED', 'DONE'],
5
+ BLOCKED: ['IN_PROGRESS'],
6
6
  DONE: [],
7
7
  };
@@ -1,29 +1,29 @@
1
1
  // Common utility functions
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
4
  /**
5
5
  * Safely read Markdown file content, return fallback if file doesn't exist or is empty
6
6
  */
7
7
  export async function readMarkdownOrFallback(relativePath, fallbackTitle, repoRoot = process.cwd()) {
8
8
  const absolutePath = path.resolve(repoRoot, relativePath);
9
9
  try {
10
- const content = await fs.readFile(absolutePath, "utf-8");
10
+ const content = await fs.readFile(absolutePath, 'utf-8');
11
11
  if (content.trim().length > 0) {
12
12
  return content;
13
13
  }
14
14
  }
15
15
  catch (error) {
16
- if (error.code !== "ENOENT") {
16
+ if (!(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')) {
17
17
  console.error(`Failed to read file: ${absolutePath}`, error);
18
18
  }
19
19
  }
20
20
  return [
21
21
  `# ${fallbackTitle}`,
22
- "",
22
+ '',
23
23
  `- file: ${relativePath}`,
24
- "- status: missing-or-empty",
25
- "- next: create this file or ensure it has readable markdown content",
26
- ].join("\n");
24
+ '- status: missing-or-empty',
25
+ '- next: create this file or ensure it has readable markdown content',
26
+ ].join('\n');
27
27
  }
28
28
  /**
29
29
  * Capitalize first letter
@@ -0,0 +1,48 @@
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, vi } from 'vitest';
5
+ import { capitalizeFirstLetter, formatTitle, readMarkdownOrFallback } from './utils.js';
6
+ const tempPaths = [];
7
+ async function createTempDir() {
8
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-utils-test-'));
9
+ tempPaths.push(dir);
10
+ return dir;
11
+ }
12
+ afterEach(async () => {
13
+ vi.restoreAllMocks();
14
+ await Promise.all(tempPaths.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
15
+ });
16
+ describe('utils module', () => {
17
+ it('reads markdown content when file exists and is non-empty', async () => {
18
+ const root = await createTempDir();
19
+ const filePath = path.join(root, 'README.md');
20
+ await fs.writeFile(filePath, '# Hello\n', 'utf-8');
21
+ const content = await readMarkdownOrFallback('README.md', 'Fallback', root);
22
+ expect(content).toBe('# Hello\n');
23
+ });
24
+ it('returns fallback markdown for missing or empty files', async () => {
25
+ const root = await createTempDir();
26
+ await fs.writeFile(path.join(root, 'empty.md'), ' ', 'utf-8');
27
+ const missing = await readMarkdownOrFallback('missing.md', 'Missing Title', root);
28
+ const empty = await readMarkdownOrFallback('empty.md', 'Empty Title', root);
29
+ expect(missing).toContain('# Missing Title');
30
+ expect(missing).toContain('- status: missing-or-empty');
31
+ expect(empty).toContain('# Empty Title');
32
+ expect(empty).toContain('- file: empty.md');
33
+ });
34
+ it('logs non-ENOENT read errors before falling back', async () => {
35
+ const root = await createTempDir();
36
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
37
+ vi.spyOn(fs, 'readFile').mockRejectedValueOnce({ code: 'EACCES' });
38
+ const result = await readMarkdownOrFallback('secret.md', 'Secret', root);
39
+ expect(result).toContain('# Secret');
40
+ expect(errorSpy).toHaveBeenCalledOnce();
41
+ });
42
+ it('formats title helpers consistently', () => {
43
+ expect(capitalizeFirstLetter('task_execution')).toBe('TaskExecution');
44
+ expect(capitalizeFirstLetter('roadmap-next')).toBe('RoadmapNext');
45
+ expect(formatTitle('task_execution')).toBe('Task Execution');
46
+ expect(formatTitle('roadmap-next')).toBe('Roadmap Next');
47
+ });
48
+ });
@@ -1,36 +1,36 @@
1
1
  #!/usr/bin/env node
2
- import path from "node:path";
3
- import process from "node:process";
4
- import { fileURLToPath } from "node:url";
5
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import packageJson from "../package.json" with { type: "json" };
8
- import { registerTools } from "./tools/index.js";
9
- import { registerPrompts } from "./prompts/index.js";
10
- import { registerResources } from "./resources/index.js";
11
- const PROJITIVE_SPEC_VERSION = "1.1.0";
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import packageJson from '../package.json' with { type: 'json' };
8
+ import { registerTools } from './tools/index.js';
9
+ import { registerPrompts } from './prompts/index.js';
10
+ import { registerResources } from './resources/index.js';
11
+ const PROJITIVE_SPEC_VERSION = '1.1.0';
12
12
  const currentFilePath = fileURLToPath(import.meta.url);
13
13
  const sourceDir = path.dirname(currentFilePath);
14
- const repoRoot = path.resolve(sourceDir, "..", "..", "..");
15
- const MCP_RUNTIME_VERSION = typeof packageJson.version === "string" && packageJson.version.trim().length > 0
14
+ const repoRoot = path.resolve(sourceDir, '..', '..', '..');
15
+ const MCP_RUNTIME_VERSION = typeof packageJson.version === 'string' && packageJson.version.trim().length > 0
16
16
  ? packageJson.version.trim()
17
17
  : PROJITIVE_SPEC_VERSION;
18
18
  const server = new McpServer({
19
- name: "projitive",
19
+ name: 'projitive',
20
20
  version: MCP_RUNTIME_VERSION,
21
- description: "Semantic Projitive MCP for project/task discovery and agent guidance with governance-store-first outputs",
21
+ description: 'Semantic Projitive MCP for project/task discovery and agent guidance with governance-store-first outputs',
22
22
  });
23
23
  // 注册所有模块
24
24
  registerTools(server);
25
25
  registerPrompts(server);
26
26
  registerResources(server, repoRoot);
27
27
  async function main() {
28
- console.error(`[projitive-mcp] starting server`);
28
+ console.error('[projitive-mcp] starting server');
29
29
  console.error(`[projitive-mcp] version=${MCP_RUNTIME_VERSION} spec=${PROJITIVE_SPEC_VERSION} transport=stdio pid=${process.pid}`);
30
30
  const transport = new StdioServerTransport();
31
31
  await server.connect(transport);
32
32
  }
33
33
  void main().catch((error) => {
34
- console.error("Server error:", error);
34
+ console.error('Server error:', error);
35
35
  process.exit(1);
36
36
  });
@@ -0,0 +1,57 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ const hoisted = vi.hoisted(() => {
3
+ const connectMock = vi.fn().mockResolvedValue(undefined);
4
+ const serverInstance = { connect: connectMock };
5
+ return {
6
+ connectMock,
7
+ serverInstance,
8
+ registerToolsMock: vi.fn(),
9
+ registerPromptsMock: vi.fn(),
10
+ registerResourcesMock: vi.fn(),
11
+ mcpServerMock: vi.fn().mockImplementation(() => serverInstance),
12
+ stdioTransportMock: vi.fn().mockImplementation(() => ({ kind: 'stdio' })),
13
+ };
14
+ });
15
+ vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
16
+ McpServer: hoisted.mcpServerMock,
17
+ }));
18
+ vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
19
+ StdioServerTransport: hoisted.stdioTransportMock,
20
+ }));
21
+ vi.mock('./tools/index.js', () => ({
22
+ registerTools: hoisted.registerToolsMock,
23
+ }));
24
+ vi.mock('./prompts/index.js', () => ({
25
+ registerPrompts: hoisted.registerPromptsMock,
26
+ }));
27
+ vi.mock('./resources/index.js', () => ({
28
+ registerResources: hoisted.registerResourcesMock,
29
+ }));
30
+ describe('index runtime module', () => {
31
+ afterEach(() => {
32
+ vi.restoreAllMocks();
33
+ vi.resetModules();
34
+ hoisted.connectMock.mockClear();
35
+ hoisted.registerToolsMock.mockClear();
36
+ hoisted.registerPromptsMock.mockClear();
37
+ hoisted.registerResourcesMock.mockClear();
38
+ hoisted.mcpServerMock.mockClear();
39
+ hoisted.stdioTransportMock.mockClear();
40
+ });
41
+ it('boots the MCP server, registers modules, and connects stdio transport', async () => {
42
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
43
+ await import('./index.js');
44
+ await Promise.resolve();
45
+ expect(hoisted.mcpServerMock).toHaveBeenCalledWith(expect.objectContaining({
46
+ name: 'projitive',
47
+ description: expect.stringContaining('governance-store-first outputs'),
48
+ }));
49
+ expect(hoisted.registerToolsMock).toHaveBeenCalledWith(hoisted.serverInstance);
50
+ expect(hoisted.registerPromptsMock).toHaveBeenCalledWith(hoisted.serverInstance);
51
+ expect(hoisted.registerResourcesMock).toHaveBeenCalledWith(hoisted.serverInstance, expect.any(String));
52
+ expect(hoisted.stdioTransportMock).toHaveBeenCalledOnce();
53
+ expect(hoisted.connectMock).toHaveBeenCalledOnce();
54
+ expect(errorSpy).toHaveBeenCalledWith('[projitive-mcp] starting server');
55
+ expect(errorSpy.mock.calls.some((call) => String(call[0]).includes('transport=stdio'))).toBe(true);
56
+ });
57
+ });