This is page 4 of 13. Use http://codebase.md/tejpalvirk/contextmanager?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .gitattributes
├── .gitignore
├── build-all-domains.sh
├── developer
│ ├── .gitattributes
│ ├── developer_advancedcontext.txt
│ ├── developer_buildcontext.txt
│ ├── developer_deletecontext.txt
│ ├── developer_endsession_examples.txt
│ ├── developer_endsession.txt
│ ├── developer_loadcontext.txt
│ ├── developer_startsession.txt
│ ├── Dockerfile
│ ├── index.d.ts
│ ├── index.js
│ ├── index.ts
│ ├── package.json
│ ├── README.md
│ └── tsconfig.json
├── dist
│ ├── developer
│ │ ├── index.d.ts
│ │ └── index.js
│ ├── main
│ │ ├── descriptions
│ │ │ ├── common_advancedcontext.txt
│ │ │ ├── common_buildcontext.txt
│ │ │ ├── common_deletecontext.txt
│ │ │ ├── common_endsession.txt
│ │ │ ├── common_loadcontext.txt
│ │ │ ├── common_startsession.txt
│ │ │ ├── developer_advancedcontext.txt
│ │ │ ├── developer_buildcontext.txt
│ │ │ ├── developer_deletecontext.txt
│ │ │ ├── developer_endsession_examples.txt
│ │ │ ├── developer_endsession.txt
│ │ │ ├── developer_loadcontext.txt
│ │ │ ├── developer_startsession.txt
│ │ │ ├── project_advancedcontext.txt
│ │ │ ├── project_buildcontext.txt
│ │ │ ├── project_deletecontext.txt
│ │ │ ├── project_endsession_examples.txt
│ │ │ ├── project_endsession.txt
│ │ │ ├── project_loadcontext.txt
│ │ │ ├── project_startsession.txt
│ │ │ ├── qualitativeresearch_advancedcontext.txt
│ │ │ ├── qualitativeresearch_buildcontext.txt
│ │ │ ├── qualitativeresearch_deletecontext.txt
│ │ │ ├── qualitativeresearch_endsession_examples.txt
│ │ │ ├── qualitativeresearch_endsession.txt
│ │ │ ├── qualitativeresearch_loadcontext.txt
│ │ │ ├── qualitativeresearch_startsession.txt
│ │ │ ├── quantitativeresearch_advancedcontext.txt
│ │ │ ├── quantitativeresearch_buildcontext.txt
│ │ │ ├── quantitativeresearch_deletecontext.txt
│ │ │ ├── quantitativeresearch_endsession_examples.txt
│ │ │ ├── quantitativeresearch_endsession.txt
│ │ │ ├── quantitativeresearch_loadcontext.txt
│ │ │ ├── quantitativeresearch_startsession.txt
│ │ │ ├── student_advancedcontext.txt
│ │ │ ├── student_buildcontext.txt
│ │ │ ├── student_deletecontext.txt
│ │ │ ├── student_endsession_examples.txt
│ │ │ ├── student_endsession.txt
│ │ │ ├── student_loadcontext.txt
│ │ │ └── student_startsession.txt
│ │ ├── index.d.ts
│ │ ├── index.js
│ │ ├── mcp.d.ts
│ │ └── mcp.js
│ ├── project
│ │ ├── index.d.ts
│ │ └── index.js
│ ├── qualitativeresearch
│ │ ├── index.d.ts
│ │ └── index.js
│ ├── quantitativeresearch
│ │ ├── index.d.ts
│ │ └── index.js
│ └── student
│ ├── index.d.ts
│ └── index.js
├── main
│ ├── descriptions
│ │ ├── common_advancedcontext.txt
│ │ ├── common_buildcontext.txt
│ │ ├── common_deletecontext.txt
│ │ ├── common_endsession.txt
│ │ ├── common_loadcontext.txt
│ │ ├── common_startsession.txt
│ │ ├── developer_advancedcontext.txt
│ │ ├── developer_buildcontext.txt
│ │ ├── developer_deletecontext.txt
│ │ ├── developer_endsession_examples.txt
│ │ ├── developer_endsession.txt
│ │ ├── developer_loadcontext.txt
│ │ ├── developer_startsession.txt
│ │ ├── project_advancedcontext.txt
│ │ ├── project_buildcontext.txt
│ │ ├── project_deletecontext.txt
│ │ ├── project_endsession_examples.txt
│ │ ├── project_endsession.txt
│ │ ├── project_loadcontext.txt
│ │ ├── project_startsession.txt
│ │ ├── qualitativeresearch_advancedcontext.txt
│ │ ├── qualitativeresearch_buildcontext.txt
│ │ ├── qualitativeresearch_deletecontext.txt
│ │ ├── qualitativeresearch_endsession_examples.txt
│ │ ├── qualitativeresearch_endsession.txt
│ │ ├── qualitativeresearch_loadcontext.txt
│ │ ├── qualitativeresearch_startsession.txt
│ │ ├── quantitativeresearch_advancedcontext.txt
│ │ ├── quantitativeresearch_buildcontext.txt
│ │ ├── quantitativeresearch_deletecontext.txt
│ │ ├── quantitativeresearch_endsession_examples.txt
│ │ ├── quantitativeresearch_endsession.txt
│ │ ├── quantitativeresearch_loadcontext.txt
│ │ ├── quantitativeresearch_startsession.txt
│ │ ├── student_advancedcontext.txt
│ │ ├── student_buildcontext.txt
│ │ ├── student_deletecontext.txt
│ │ ├── student_endsession_examples.txt
│ │ ├── student_endsession.txt
│ │ ├── student_loadcontext.txt
│ │ └── student_startsession.txt
│ ├── index.js
│ ├── index.ts
│ ├── mcp.ts
│ ├── package.json
│ ├── README.md
│ └── tsconfig.json
├── package-lock.json
├── package.json
├── project
│ ├── .gitattributes
│ ├── Dockerfile
│ ├── index.d.ts
│ ├── index.js
│ ├── index.ts
│ ├── package.json
│ ├── project_advancedcontext.txt
│ ├── project_buildcontext.txt
│ ├── project_deletecontext.txt
│ ├── project_endsession_examples.txt
│ ├── project_endsession.txt
│ ├── project_loadcontext.txt
│ ├── project_startsession.txt
│ ├── README.md
│ └── tsconfig.json
├── qualitativeresearch
│ ├── .gitattributes
│ ├── Dockerfile
│ ├── index.d.ts
│ ├── index.js
│ ├── index.ts
│ ├── package.json
│ ├── qualitativeresearch_advancedcontext.txt
│ ├── qualitativeresearch_buildcontext.txt
│ ├── qualitativeresearch_deletecontext.txt
│ ├── qualitativeresearch_endsession_examples.txt
│ ├── qualitativeresearch_endsession.txt
│ ├── qualitativeresearch_loadcontext.txt
│ ├── qualitativeresearch_startsession.txt
│ ├── README.md
│ └── tsconfig.json
├── quantitativeresearch
│ ├── .gitattributes
│ ├── Dockerfile
│ ├── index.d.ts
│ ├── index.js
│ ├── index.ts
│ ├── package.json
│ ├── quantitativeresearch_advancedcontext.txt
│ ├── quantitativeresearch_buildcontext.txt
│ ├── quantitativeresearch_deletecontext.txt
│ ├── quantitativeresearch_endsession_examples.txt
│ ├── quantitativeresearch_endsession.txt
│ ├── quantitativeresearch_loadcontext.txt
│ ├── quantitativeresearch_startsession.txt
│ ├── README.md
│ └── tsconfig.json
├── README.md
├── student
│ ├── .gitattributes
│ ├── Dockerfile
│ ├── index.d.ts
│ ├── index.js
│ ├── index.ts
│ ├── package.json
│ ├── README.md
│ ├── student_advancedcontext.txt
│ ├── student_buildcontext.txt
│ ├── student_deletecontext.txt
│ ├── student_endsession_examples.txt
│ ├── student_endsession.txt
│ ├── student_loadcontext.txt
│ ├── student_startsession.txt
│ └── tsconfig.json
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/main/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { McpServer } from "./mcp.js";
4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6 | import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
7 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8 | import { z } from "zod";
9 | import path from "path";
10 | import { fileURLToPath } from "url";
11 | import { readFileSync, existsSync } from "fs";
12 |
13 | // Get the directory where the contextmanager is located
14 | const __filename = fileURLToPath(import.meta.url);
15 | const __dirname = path.dirname(__filename);
16 |
17 | // Domain interfaces
18 | interface DomainInfo {
19 | name: string;
20 | description: string;
21 | entityTypes: string[];
22 | host?: string;
23 | port?: number;
24 | path?: string;
25 | command?: string;
26 | args?: string[];
27 | }
28 |
29 | interface FlowInfo {
30 | id: string;
31 | domain: string;
32 | entityName?: string;
33 | entityType?: string;
34 | active: boolean;
35 | createdAt: number;
36 | }
37 |
38 | // Domain client class
39 | class DomainClient {
40 | public name: string;
41 | public client: Client | null = null;
42 | public transport: SSEClientTransport | StdioClientTransport | null = null;
43 | public connected: boolean = false;
44 | private process: any = null;
45 |
46 | constructor(public domain: DomainInfo) {
47 | this.name = domain.name;
48 | }
49 |
50 | async connect(): Promise<boolean> {
51 | if (this.connected) {
52 | return true;
53 | }
54 |
55 | try {
56 | // Create client
57 | this.client = new Client(
58 | {
59 | name: `contextmanager-${this.name}-client`,
60 | version: "1.0.0"
61 | },
62 | {
63 | capabilities: {
64 | resources: {},
65 | tools: {},
66 | prompts: {}
67 | }
68 | }
69 | );
70 |
71 | // Connect to domain server
72 | if (this.domain.host && this.domain.port) {
73 | // Connect via SSE
74 | const url = new URL(`http://${this.domain.host}:${this.domain.port}${this.domain.path || '/sse'}`);
75 | this.transport = new SSEClientTransport(url);
76 | } else if (this.domain.command) {
77 | // Connect via stdio
78 | this.transport = new StdioClientTransport({
79 | command: this.domain.command,
80 | args: this.domain.args || [],
81 | });
82 | } else {
83 | console.error(`Domain ${this.name} has no connection information`);
84 | return false;
85 | }
86 |
87 | await this.client.connect(this.transport);
88 | this.connected = true;
89 | return true;
90 | } catch (error) {
91 | console.error(`Failed to connect to domain ${this.name}:`, error);
92 | this.connected = false;
93 | return false;
94 | }
95 | }
96 |
97 | async disconnect(): Promise<void> {
98 | if (!this.connected) {
99 | return;
100 | }
101 |
102 | try {
103 | if (this.client) {
104 | // Manually close the transport as there's no official disconnect method
105 | if (this.transport) {
106 | if ('close' in this.transport) {
107 | await (this.transport as any).close();
108 | }
109 | }
110 | }
111 | this.connected = false;
112 | } catch (error) {
113 | console.error(`Error disconnecting from domain ${this.name}:`, error);
114 | }
115 | }
116 |
117 | async callTool(toolRequest: { name: string; arguments: any }): Promise<any> {
118 | if (!this.connected || !this.client) {
119 | const connected = await this.connect();
120 | if (!connected) {
121 | return {
122 | content: [{ type: "text", text: `Error: Not connected to domain ${this.name}` }],
123 | isError: true
124 | };
125 | }
126 | }
127 |
128 | try {
129 | if (!this.client) {
130 | throw new Error(`Client for domain ${this.name} is not initialized`);
131 | }
132 |
133 | const result = await this.client.callTool(toolRequest);
134 | return result;
135 | } catch (error) {
136 | console.error(`Error calling tool ${toolRequest.name} on domain ${this.name}:`, error);
137 | return {
138 | content: [{ type: "text", text: `Error calling tool ${toolRequest.name} on domain ${this.name}: ${error}` }],
139 | isError: true
140 | };
141 | }
142 | }
143 |
144 | async readResource(resourceRequest: { uri: string }): Promise<any> {
145 | if (!this.connected || !this.client) {
146 | const connected = await this.connect();
147 | if (!connected) {
148 | return {
149 | contents: [{
150 | uri: resourceRequest.uri,
151 | text: `Error: Not connected to domain ${this.name}`
152 | }]
153 | };
154 | }
155 | }
156 |
157 | try {
158 | if (!this.client) {
159 | throw new Error(`Client for domain ${this.name} is not initialized`);
160 | }
161 |
162 | return await this.client.readResource(resourceRequest);
163 | } catch (error) {
164 | console.error(`Error reading resource ${resourceRequest.uri} from domain ${this.name}:`, error);
165 | return {
166 | contents: [{
167 | uri: resourceRequest.uri,
168 | text: `Error reading resource: ${error}`
169 | }]
170 | };
171 | }
172 | }
173 | }
174 |
175 | // In-memory storage
176 | const domains: DomainInfo[] = [
177 | {
178 | name: "developer",
179 | description: "Software development context with entities like projects, components, and tasks",
180 | entityTypes: ["project", "component", "task", "issue", "commit"],
181 | command: "node",
182 | args: [path.resolve(__dirname, "../developer/index.js")]
183 | },
184 | {
185 | name: "project",
186 | description: "Project management context with entities like projects, tasks, and resources",
187 | entityTypes: ["project", "task", "resource", "milestone", "risk"],
188 | command: "node",
189 | args: [path.resolve(__dirname, "../project/index.js")]
190 | },
191 | {
192 | name: "student",
193 | description: "Educational context with entities like courses, assignments, and exams",
194 | entityTypes: ["course", "assignment", "exam", "note", "grade"],
195 | command: "node",
196 | args: [path.resolve(__dirname, "../student/index.js")]
197 | },
198 | {
199 | name: "qualitativeresearch",
200 | description: "Qualitative research context with entities like studies, participants, and interviews",
201 | entityTypes: ["study", "participant", "interview", "code", "theme"],
202 | command: "node",
203 | args: [path.resolve(__dirname, "../qualitativeresearch/index.js")]
204 | },
205 | {
206 | name: "quantitativeresearch",
207 | description: "Quantitative research context with entities like datasets, variables, and analyses",
208 | entityTypes: ["dataset", "variable", "analysis", "model", "result"],
209 | command: "node",
210 | args: [path.resolve(__dirname, "../quantitativeresearch/index.js")]
211 | }
212 | ];
213 |
214 | // Domain clients
215 | const domainClients: { [key: string]: DomainClient } = {};
216 |
217 | // Initialize domain clients
218 | for (const domain of domains) {
219 | domainClients[domain.name] = new DomainClient(domain);
220 | }
221 |
222 | // In-memory flow management
223 | const flows: FlowInfo[] = [];
224 | let activeDomain: string | null = null;
225 | let flowCounter = 0;
226 |
227 | // Function to get tool description based on active domain
228 | function getToolDescription(toolName: string, domain: string | null): string {
229 | if (!domain) {
230 | domain = "common";
231 | }
232 |
233 | const descriptionFilePath = path.resolve(
234 | __dirname,
235 | "descriptions",
236 | `${domain}_${toolName}.txt`
237 | );
238 |
239 | try {
240 | if (existsSync(descriptionFilePath)) {
241 | return readFileSync(descriptionFilePath, "utf8");
242 | } else {
243 | console.warn(`Description file not found: ${descriptionFilePath}`);
244 | return `${toolName} tool for ${domain} domain`;
245 | }
246 | } catch (error) {
247 | console.error(`Error reading description file for ${domain}_${toolName}:`, error);
248 | return `${toolName} tool for ${domain} domain`;
249 | }
250 | }
251 |
252 | // Create an MCP server
253 | const server = new McpServer({
254 | name: "Context Manager",
255 | version: "1.0.0"
256 | });
257 |
258 | // Store tool schemas and callbacks for re-registration when domain changes
259 | const toolDefinitions: {
260 | [key: string]: {
261 | schema: any;
262 | callback: (...args: any[]) => Promise<any>;
263 | }
264 | } = {};
265 |
266 | // Function to register or update a tool with domain-specific description
267 | function registerDomainTool(
268 | name: string,
269 | schema: any,
270 | callback: (...args: any[]) => Promise<any>
271 | ): void {
272 | // Store the tool definition for later re-registration
273 | toolDefinitions[name] = {
274 | schema,
275 | callback
276 | };
277 |
278 | // Register the tool with the current domain description
279 | server.tool(
280 | name,
281 | getToolDescription(name, activeDomain),
282 | schema,
283 | callback
284 | );
285 | }
286 |
287 | // Function to update all tool descriptions when domain changes
288 | function updateDomainToolDescriptions(): void {
289 | // Re-register all stored tools with new descriptions
290 | for (const [name, definition] of Object.entries(toolDefinitions)) {
291 | server.removeTool(name);
292 | server.tool(
293 | name,
294 | getToolDescription(name, activeDomain),
295 | definition.schema,
296 | definition.callback
297 | );
298 | }
299 | }
300 |
301 | // Domain management tools
302 | server.tool(
303 | "setActiveDomain",
304 | "Change the active domain for subsequent tool calls",
305 | { domain: z.string() },
306 | async ({ domain }) => {
307 | const foundDomain = domains.find(d => d.name.toLowerCase() === domain.toLowerCase());
308 | if (!foundDomain) {
309 | return {
310 | content: [{ type: "text", text: `Error: Domain '${domain}' not found. Available domains: ${domains.map(d => d.name).join(", ")}` }],
311 | isError: true
312 | };
313 | }
314 |
315 | // Connect to the domain server
316 | const domainClient = domainClients[foundDomain.name];
317 | const connected = await domainClient.connect();
318 |
319 | if (!connected) {
320 | return {
321 | content: [{ type: "text", text: `Error: Could not connect to domain server for '${domain}'` }],
322 | isError: true
323 | };
324 | }
325 |
326 | activeDomain = foundDomain.name;
327 |
328 | // Update tool descriptions for the new active domain
329 | updateDomainToolDescriptions();
330 |
331 | return {
332 | content: [{ type: "text", text: `Active domain set to: ${activeDomain}` }]
333 | };
334 | }
335 | );
336 |
337 | // Flow management tools
338 | registerDomainTool(
339 | "startsession",
340 | {
341 | domain: z.string()
342 | },
343 | async ({ domain }) => {
344 | const foundDomain = domains.find(d => d.name.toLowerCase() === domain.toLowerCase());
345 | if (!foundDomain) {
346 | return {
347 | content: [{ type: "text", text: `Error: Domain '${domain}' not found. Available domains: ${domains.map(d => d.name).join(", ")}` }],
348 | isError: true
349 | };
350 | }
351 |
352 | // Connect to the domain server
353 | const domainClient = domainClients[foundDomain.name];
354 | const connected = await domainClient.connect();
355 |
356 | if (!connected) {
357 | return {
358 | content: [{ type: "text", text: `Error: Could not connect to domain server for '${domain}'` }],
359 | isError: true
360 | };
361 | }
362 |
363 | activeDomain = foundDomain.name;
364 | flowCounter++;
365 |
366 | // Update tool descriptions for the new active domain since activeDomain changed
367 | updateDomainToolDescriptions();
368 |
369 | // Forward the startsession call to the domain server with a domain-specific session identifier
370 | try {
371 | const result = await domainClient.callTool({
372 | name: "startsession",
373 | arguments: {}
374 | });
375 |
376 | const lastWord = result.content[0].text.split(' ').pop();
377 | // Create a contextmanager flow ID from domain session ID (last word)
378 | const flowId = `flow_${lastWord}`;
379 |
380 | flows.push({
381 | id: flowId,
382 | domain: activeDomain,
383 | active: true,
384 | createdAt: Date.now()
385 | });
386 |
387 | return {
388 | content: [
389 | { type: "text", text: `${result.content[0].text}`}
390 | ],
391 | };
392 | } catch (error) {
393 | console.error(`Error starting session for domain ${activeDomain}:`, error);
394 | return {
395 | content: [{ type: "text", text: `Error starting session for domain ${activeDomain}: ${error}` }],
396 | isError: true
397 | };
398 | }
399 | }
400 | );
401 |
402 | registerDomainTool(
403 | "endsession",
404 | {
405 | sessionId: z.string(),
406 | stage: z.string(),
407 | stageNumber: z.number(),
408 | totalStages: z.number(),
409 | nextStageNeeded: z.boolean(),
410 | analysis: z.string().optional(),
411 | isRevision: z.boolean().optional(),
412 | revisesStage: z.number().optional(),
413 | stageData: z.record(z.string(), z.any()).optional()
414 | },
415 | async ({ sessionId, stage, stageNumber, totalStages, nextStageNeeded, analysis, isRevision, revisesStage, stageData }) => {
416 | const flowId = `flow_${sessionId}`;
417 | const flowIndex = flows.findIndex(s => s.id === flowId);
418 | if (flowIndex === -1) {
419 | return {
420 | content: [{ type: "text", text: `Error: Context Manager flow with ID '${flowId}' not found.` }],
421 | isError: true
422 | };
423 | }
424 |
425 | const flow = flows[flowIndex];
426 | const domainName = flow.domain;
427 | const domainClient = domainClients[domainName];
428 |
429 | if (!domainClient || !domainClient.connected) {
430 | const connected = await domainClient.connect();
431 | if (!connected) {
432 | return {
433 | content: [{ type: "text", text: `Error: Could not connect to domain server for '${domainName}'` }],
434 | isError: true
435 | };
436 | }
437 | }
438 |
439 | // Forward the endsession call to the domain server
440 | try {
441 | const result = await domainClient.callTool({
442 | name: "endsession",
443 | arguments: {
444 | sessionId: sessionId,
445 | stage,
446 | stageNumber,
447 | totalStages,
448 | nextStageNeeded,
449 | analysis,
450 | isRevision,
451 | revisesStage,
452 | stageData
453 | }
454 | });
455 |
456 | if (!nextStageNeeded) {
457 | flows[flowIndex].active = false;
458 | return {
459 | content: [{ type: "text", text: `${result.content[0].text}`}]
460 | }
461 | }
462 |
463 | return {
464 | content: [{ type: "text", text: `${result.content[0].text}` }]
465 | };
466 | } catch (error) {
467 | console.error(`Error ending session for domain ${domainName}:`, error);
468 | return {
469 | content: [{ type: "text", text: `Error ending session for domain ${domainName}: ${error}` }],
470 | isError: true
471 | };
472 | }
473 | }
474 | );
475 |
476 | // Context management tools
477 | registerDomainTool(
478 | "buildcontext",
479 | {
480 | type: z.enum(["entities", "relations", "observations"]),
481 | data: z.array(z.any())
482 | },
483 | async ({ type, data }) => {
484 | if (!activeDomain) {
485 | return {
486 | content: [{ type: "text", text: "Error: No active domain set. Use setActiveDomain or startsession tool first." }],
487 | isError: true
488 | };
489 | }
490 |
491 | const domainClient = domainClients[activeDomain];
492 |
493 | if (!domainClient || !domainClient.connected) {
494 | const connected = await domainClient.connect();
495 | if (!connected) {
496 | return {
497 | content: [{ type: "text", text: `Error: Could not connect to domain server for '${activeDomain}'` }],
498 | isError: true
499 | };
500 | }
501 | }
502 |
503 | // Forward the buildcontext call to the domain server
504 | try {
505 | const result = await domainClient.callTool({
506 | name: "buildcontext",
507 | arguments: {
508 | type,
509 | data
510 | }
511 | });
512 |
513 | return result;
514 | } catch (error) {
515 | console.error(`Error building context for domain ${activeDomain}:`, error);
516 | return {
517 | content: [{ type: "text", text: `Error building context for domain ${activeDomain}: ${error}` }],
518 | isError: true
519 | };
520 | }
521 | }
522 | );
523 |
524 | registerDomainTool(
525 | "deletecontext",
526 | {
527 | type: z.enum(["entities", "relations", "observations"]),
528 | data: z.array(z.any())
529 | },
530 | async ({ type, data }) => {
531 | if (!activeDomain) {
532 | return {
533 | content: [{ type: "text", text: "Error: No active domain set. Use setActiveDomain or startsession tool first." }],
534 | isError: true
535 | };
536 | }
537 |
538 | const domainClient = domainClients[activeDomain];
539 |
540 | if (!domainClient || !domainClient.connected) {
541 | const connected = await domainClient.connect();
542 | if (!connected) {
543 | return {
544 | content: [{ type: "text", text: `Error: Could not connect to domain server for '${activeDomain}'` }],
545 | isError: true
546 | };
547 | }
548 | }
549 |
550 | // Forward the deletecontext call to the domain server
551 | try {
552 | const result = await domainClient.callTool({
553 | name: "deletecontext",
554 | arguments: {
555 | type,
556 | data
557 | }
558 | });
559 |
560 | return result;
561 | } catch (error) {
562 | console.error(`Error deleting context for domain ${activeDomain}:`, error);
563 | return {
564 | content: [{ type: "text", text: `Error deleting context for domain ${activeDomain}: ${error}` }],
565 | isError: true
566 | };
567 | }
568 | }
569 | );
570 |
571 | registerDomainTool(
572 | "loadcontext",
573 | {
574 | entityName: z.string(),
575 | entityType: z.string().optional(),
576 | sessionId: z.string().optional()
577 | },
578 | async ({ entityName, entityType, sessionId }) => {
579 | if (!activeDomain) {
580 | return {
581 | content: [{ type: "text", text: "Error: No active domain set. Use setActiveDomain or startsession tool first." }],
582 | isError: true
583 | };
584 | }
585 |
586 | const flowId = `flow_${sessionId}`;
587 |
588 | // Find active contextmanager flow or use provided sessionId
589 | let targetFlow: FlowInfo | undefined;
590 | if (sessionId) {
591 | targetFlow = flows.find(s => s.id === flowId);
592 | } else {
593 | targetFlow = flows.find(s => s.domain === activeDomain && s.active);
594 | }
595 |
596 | if (!targetFlow) {
597 | return {
598 | content: [{ type: "text", text: "Error: No active Context Manager flow found. Start a flow first." }],
599 | isError: true
600 | };
601 | }
602 |
603 | const domainClient = domainClients[activeDomain];
604 |
605 | if (!domainClient || !domainClient.connected) {
606 | const connected = await domainClient.connect();
607 | if (!connected) {
608 | return {
609 | content: [{ type: "text", text: `Error: Could not connect to domain server for '${activeDomain}'` }],
610 | isError: true
611 | };
612 | }
613 | }
614 |
615 | // Update contextmanager flow with entity details
616 | targetFlow.entityName = entityName;
617 | targetFlow.entityType = entityType || "unknown";
618 |
619 | // Forward the loadcontext call to the domain server
620 | try {
621 | const result = await domainClient.callTool({
622 | name: "loadcontext",
623 | arguments: {
624 | entityName,
625 | entityType,
626 | sessionId: sessionId
627 | }
628 | });
629 |
630 | return {
631 | content: [{
632 | type: "text",
633 | text: `${result.content[0].text}`
634 | }]
635 | };
636 | } catch (error) {
637 | console.error(`Error loading context for domain ${activeDomain}:`, error);
638 | return {
639 | content: [{ type: "text", text: `Error loading context for domain ${activeDomain}: ${error}` }],
640 | isError: true
641 | };
642 | }
643 | }
644 | );
645 |
646 | registerDomainTool(
647 | "advancedcontext",
648 | {
649 | type: z.string(),
650 | params: z.record(z.string(), z.any())
651 | },
652 | async ({ type, params }) => {
653 | if (!activeDomain) {
654 | return {
655 | content: [{ type: "text", text: "Error: No active domain set. Use setActiveDomain or startsession tool first." }],
656 | isError: true
657 | };
658 | }
659 |
660 | const domainClient = domainClients[activeDomain];
661 |
662 | if (!domainClient || !domainClient.connected) {
663 | const connected = await domainClient.connect();
664 | if (!connected) {
665 | return {
666 | content: [{ type: "text", text: `Error: Could not connect to domain server for '${activeDomain}'` }],
667 | isError: true
668 | };
669 | }
670 | }
671 |
672 | // Forward the advancedcontext call to the domain server
673 | try {
674 | const result = await domainClient.callTool({
675 | name: "advancedcontext",
676 | arguments: {
677 | type,
678 | params
679 | }
680 | });
681 |
682 | return result;
683 | } catch (error) {
684 | console.error(`Error performing advanced context operation for domain ${activeDomain}:`, error);
685 | return {
686 | content: [{ type: "text", text: `Error performing advanced context operation for domain ${activeDomain}: ${error}` }],
687 | isError: true
688 | };
689 | }
690 | }
691 | );
692 |
693 | // // List all entities
694 | // server.tool(
695 | // "listAllEntities",
696 | // {
697 | // domain: z.string().optional()
698 | // },
699 | // async () => {
700 | // if (!activeDomain) {
701 | // return {
702 | // content: [{ type: "text", text: "Error: No active domain set. Use setActiveDomain or startsession tool first." }],
703 | // isError: true
704 | // };
705 | // }
706 |
707 | // const domainClient = domainClients[activeDomain];
708 |
709 | // if (!domainClient || !domainClient.connected) {
710 | // const connected = await domainClient.connect();
711 | // if (!connected) {
712 | // return {
713 | // content: [{ type: "text", text: `Error: Could not connect to domain server for '${activeDomain}'` }],
714 | // isError: true
715 | // };
716 | // }
717 | // }
718 |
719 | // // Forward the listAllEntities call to the domain server
720 | // try {
721 | // const result = await domainClient.callTool({
722 | // name: "listAllEntities",
723 | // arguments: {}
724 | // });
725 |
726 | // return result;
727 | // } catch (error) {
728 | // // If the domain server doesn't support listAllEntities, fall back to our domain registry
729 | // const domain = domains.find(d => d.name === activeDomain);
730 | // if (!domain) {
731 | // return {
732 | // content: [{ type: "text", text: `Error: Domain '${activeDomain}' not found.` }],
733 | // isError: true
734 | // };
735 | // }
736 |
737 | // return {
738 | // content: [{
739 | // type: "text",
740 | // text: `Available entity types in ${activeDomain} domain: ${domain.entityTypes.join(", ")}`
741 | // }]
742 | // };
743 | // }
744 | // }
745 | // );
746 |
747 | // Domain discovery resources
748 | server.resource(
749 | "domains",
750 | "domains://list",
751 | async (uri) => ({
752 | contents: [{
753 | uri: uri.href,
754 | text: JSON.stringify(domains, null, 2)
755 | }]
756 | })
757 | );
758 |
759 | // Cross-domain functionality
760 | server.tool(
761 | "relateCrossDomain",
762 | "Create connections between entities across different domains",
763 | {
764 | fromDomain: z.string(),
765 | fromEntity: z.string(),
766 | toDomain: z.string(),
767 | toEntity: z.string(),
768 | relationType: z.string()
769 | },
770 | async ({ fromDomain, fromEntity, toDomain, toEntity, relationType }) => {
771 | // Validate domains
772 | const fromDomainInfo = domains.find(d => d.name.toLowerCase() === fromDomain.toLowerCase());
773 | const toDomainInfo = domains.find(d => d.name.toLowerCase() === toDomain.toLowerCase());
774 |
775 | if (!fromDomainInfo) {
776 | return {
777 | content: [{ type: "text", text: `Error: Source domain '${fromDomain}' not found.` }],
778 | isError: true
779 | };
780 | }
781 |
782 | if (!toDomainInfo) {
783 | return {
784 | content: [{ type: "text", text: `Error: Target domain '${toDomain}' not found.` }],
785 | isError: true
786 | };
787 | }
788 |
789 | // Create cross-domain relation by adding observations to both entities
790 | try {
791 | // Connect to source domain
792 | const fromDomainClient = domainClients[fromDomainInfo.name];
793 | if (!fromDomainClient || !fromDomainClient.connected) {
794 | const connected = await fromDomainClient.connect();
795 | if (!connected) {
796 | return {
797 | content: [{ type: "text", text: `Error: Could not connect to domain server for '${fromDomain}'` }],
798 | isError: true
799 | };
800 | }
801 | }
802 |
803 | // Connect to target domain
804 | const toDomainClient = domainClients[toDomainInfo.name];
805 | if (!toDomainClient || !toDomainClient.connected) {
806 | const connected = await toDomainClient.connect();
807 | if (!connected) {
808 | return {
809 | content: [{ type: "text", text: `Error: Could not connect to domain server for '${toDomain}'` }],
810 | isError: true
811 | };
812 | }
813 | }
814 |
815 | // Add observation to source entity about relation to target entity
816 | await fromDomainClient.callTool({
817 | name: "buildcontext",
818 | arguments: {
819 | type: "observations",
820 | data: {
821 | observations: [
822 | {
823 | entityName: fromEntity,
824 | contents: [`Related to ${toEntity} (${toDomainInfo.name} domain) via ${relationType}`]
825 | }
826 | ]
827 | }
828 | }
829 | });
830 |
831 | // Add observation to target entity about relation from source entity
832 | await toDomainClient.callTool({
833 | name: "buildcontext",
834 | arguments: {
835 | type: "observations",
836 | data: {
837 | observations: [
838 | {
839 | entityName: toEntity,
840 | contents: [`Related from ${fromEntity} (${fromDomainInfo.name} domain) via ${relationType}`]
841 | }
842 | ]
843 | }
844 | }
845 | });
846 |
847 | return {
848 | content: [{
849 | type: "text",
850 | text: `Created cross-domain relation: ${fromEntity} (${fromDomain}) --[${relationType}]--> ${toEntity} (${toDomain})`
851 | }]
852 | };
853 | } catch (error) {
854 | console.error(`Error creating cross-domain relation:`, error);
855 | return {
856 | content: [{ type: "text", text: `Error creating cross-domain relation: ${error}` }],
857 | isError: true
858 | };
859 | }
860 | }
861 | );
862 |
863 | // Main function to start the server
864 | async function main() {
865 | // Start receiving messages on stdin and sending messages on stdout
866 | const transport = new StdioServerTransport();
867 | await server.connect(transport);
868 | }
869 |
870 | // Execute the main function
871 | main().catch(error => {
872 | console.error("Error in contextmanager:", error);
873 | process.exit(1);
874 | });
```
--------------------------------------------------------------------------------
/developer/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5 | import { promises as fs } from 'fs';
6 | import * as path from 'path';
7 | import { fileURLToPath } from 'url';
8 | import { z } from "zod";
9 | import { readFileSync, existsSync } from "fs";
10 |
11 | // Define memory file path using environment variable with fallback
12 | const parentPath = path.dirname(fileURLToPath(import.meta.url));
13 | const defaultMemoryPath = path.join(parentPath, 'memory.json');
14 | const defaultSessionsPath = path.join(parentPath, 'sessions.json');
15 |
16 | // Properly handle absolute and relative paths for MEMORY_FILE_PATH
17 | const MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH
18 | ? path.isAbsolute(process.env.MEMORY_FILE_PATH)
19 | ? process.env.MEMORY_FILE_PATH // Use absolute path as is
20 | : path.join(process.cwd(), process.env.MEMORY_FILE_PATH) // Relative to current working directory
21 | : defaultMemoryPath; // Default fallback
22 |
23 | // Properly handle absolute and relative paths for SESSIONS_FILE_PATH
24 | const SESSIONS_FILE_PATH = process.env.SESSIONS_FILE_PATH
25 | ? path.isAbsolute(process.env.SESSIONS_FILE_PATH)
26 | ? process.env.SESSIONS_FILE_PATH // Use absolute path as is
27 | : path.join(process.cwd(), process.env.SESSIONS_FILE_PATH) // Relative to current working directory
28 | : defaultSessionsPath; // Default fallback
29 |
30 | // Software Development specific entity types
31 | const VALID_ENTITY_TYPES = [
32 | 'project', // Overall software project
33 | 'component', // Module, service, or package within a project
34 | 'feature', // Specific functionality being developed
35 | 'issue', // Bug or problem to be fixed
36 | 'task', // Work item or activity needed for development
37 | 'technology', // Language, framework, or tool used
38 | 'decision', // Important technical or architectural decision
39 | 'milestone', // Key project deadline or phase
40 | 'environment', // Development, staging, production environments
41 | 'documentation', // Project documentation
42 | 'requirement', // Project requirement or specification
43 | 'status', // Entity status (inactive, active, or complete)
44 | 'priority' // Entity priority (low or high)
45 | ];
46 |
47 | // Software Development specific relation types
48 | const VALID_RELATION_TYPES = [
49 | 'depends_on', // Dependency relationship
50 | 'implements', // Component implements a feature
51 | 'blocked_by', // Task is blocked by an issue
52 | 'uses', // Component uses a technology
53 | 'part_of', // Component is part of a project
54 | 'contains', // Project contains a component
55 | 'related_to', // General relationship
56 | 'affects', // Issue affects a component
57 | 'resolves', // Task resolves an issue
58 | 'documented_in', // Component is documented in documentation
59 | 'decided_in', // Decision was made in a meeting
60 | 'required_by', // Feature is required by a requirement
61 | 'has_status', // Entity has a particular status
62 | 'has_priority', // Entity has a particular priority
63 | 'depends_on_milestone', // Task depends on reaching a milestone
64 | 'precedes', // Task precedes another task (for sequencing)
65 | 'tested_in' // Component is tested in an environment
66 | ];
67 |
68 | const __filename = fileURLToPath(import.meta.url);
69 | const __dirname = path.dirname(__filename);
70 |
71 | // Collect tool descriptions from text files
72 | const toolDescriptions: Record<string, string> = {
73 | 'startsession': '',
74 | 'loadcontext': '',
75 | 'deletecontext': '',
76 | 'buildcontext': '',
77 | 'advancedcontext': '',
78 | 'endsession': '',
79 | };
80 | for (const tool of Object.keys(toolDescriptions)) {
81 | try {
82 | const descriptionFilePath = path.resolve(
83 | __dirname,
84 | `developer_${tool}.txt`
85 | );
86 | if (existsSync(descriptionFilePath)) {
87 | toolDescriptions[tool] = readFileSync(descriptionFilePath, 'utf-8');
88 | }
89 | } catch (error) {
90 | console.error(`Error reading description file for tool '${tool}': ${error}`);
91 | }
92 | }
93 |
94 | // Session management functions
95 | async function loadSessionStates(): Promise<Map<string, any[]>> {
96 | try {
97 | const fileContent = await fs.readFile(SESSIONS_FILE_PATH, 'utf-8');
98 | const sessions = JSON.parse(fileContent);
99 | // Convert from object to Map
100 | const sessionsMap = new Map<string, any[]>();
101 | for (const [key, value] of Object.entries(sessions)) {
102 | sessionsMap.set(key, value as any[]);
103 | }
104 | return sessionsMap;
105 | } catch (error) {
106 | if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") {
107 | return new Map<string, any[]>();
108 | }
109 | throw error;
110 | }
111 | }
112 |
113 | async function saveSessionStates(sessionsMap: Map<string, any[]>): Promise<void> {
114 | // Convert from Map to object
115 | const sessions: Record<string, any[]> = {};
116 | for (const [key, value] of sessionsMap.entries()) {
117 | sessions[key] = value;
118 | }
119 | await fs.writeFile(SESSIONS_FILE_PATH, JSON.stringify(sessions, null, 2), 'utf-8');
120 | }
121 |
122 | // Generate a unique session ID
123 | function generateSessionId(): string {
124 | return `dev_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
125 | }
126 |
127 | // Basic validation functions
128 | function validateEntityType(entityType: string): boolean {
129 | return VALID_ENTITY_TYPES.includes(entityType);
130 | }
131 |
132 | function validateRelationType(relationType: string): boolean {
133 | return VALID_RELATION_TYPES.includes(relationType);
134 | }
135 |
136 | // We are storing our memory using entities, relations, and observations in a graph structure
137 | interface Entity {
138 | name: string;
139 | entityType: string;
140 | observations: string[];
141 | }
142 |
143 | interface Relation {
144 | from: string;
145 | to: string;
146 | relationType: string;
147 | }
148 |
149 | interface KnowledgeGraph {
150 | entities: Entity[];
151 | relations: Relation[];
152 | }
153 |
154 | // Define the valid status and priority values
155 | const VALID_STATUS_VALUES = ['inactive', 'active', 'complete'];
156 | const VALID_PRIORITY_VALUES = ['low', 'high'];
157 |
158 | // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
159 | class KnowledgeGraphManager {
160 | private async loadGraph(): Promise<KnowledgeGraph> {
161 | try {
162 | const fileContent = await fs.readFile(MEMORY_FILE_PATH, 'utf-8');
163 | return JSON.parse(fileContent);
164 | } catch (error) {
165 | if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") {
166 | return { entities: [], relations: [] };
167 | }
168 | throw error;
169 | }
170 | }
171 |
172 | private async saveGraph(graph: KnowledgeGraph): Promise<void> {
173 | await fs.writeFile(MEMORY_FILE_PATH, JSON.stringify(graph, null, 2), 'utf-8');
174 | }
175 |
176 | // Initialize status and priority entities
177 | async initializeStatusAndPriority(): Promise<void> {
178 | const graph = await this.loadGraph();
179 |
180 | // Create status entities if they don't exist
181 | for (const statusValue of VALID_STATUS_VALUES) {
182 | const statusName = `status:${statusValue}`;
183 | if (!graph.entities.some(e => e.name === statusName && e.entityType === 'status')) {
184 | graph.entities.push({
185 | name: statusName,
186 | entityType: 'status',
187 | observations: [`A ${statusValue} status value`]
188 | });
189 | }
190 | }
191 |
192 | // Create priority entities if they don't exist
193 | for (const priorityValue of VALID_PRIORITY_VALUES) {
194 | const priorityName = `priority:${priorityValue}`;
195 | if (!graph.entities.some(e => e.name === priorityName && e.entityType === 'priority')) {
196 | graph.entities.push({
197 | name: priorityName,
198 | entityType: 'priority',
199 | observations: [`A ${priorityValue} priority value`]
200 | });
201 | }
202 | }
203 |
204 | await this.saveGraph(graph);
205 | }
206 |
207 | // Helper method to get status of an entity
208 | async getEntityStatus(entityName: string): Promise<string | null> {
209 | const graph = await this.loadGraph();
210 |
211 | // Find status relation for this entity
212 | const statusRelation = graph.relations.find(r =>
213 | r.from === entityName &&
214 | r.relationType === 'has_status'
215 | );
216 |
217 | if (statusRelation) {
218 | // Extract status value from the status entity name (status:value)
219 | return statusRelation.to.split(':')[1];
220 | }
221 |
222 | return null;
223 | }
224 |
225 | // Helper method to get priority of an entity
226 | async getEntityPriority(entityName: string): Promise<string | null> {
227 | const graph = await this.loadGraph();
228 |
229 | // Find priority relation for this entity
230 | const priorityRelation = graph.relations.find(r =>
231 | r.from === entityName &&
232 | r.relationType === 'has_priority'
233 | );
234 |
235 | if (priorityRelation) {
236 | // Extract priority value from the priority entity name (priority:value)
237 | return priorityRelation.to.split(':')[1];
238 | }
239 |
240 | return null;
241 | }
242 |
243 | // Helper method to set status of an entity
244 | async setEntityStatus(entityName: string, statusValue: string): Promise<void> {
245 | if (!VALID_STATUS_VALUES.includes(statusValue)) {
246 | throw new Error(`Invalid status value: ${statusValue}. Valid values are: ${VALID_STATUS_VALUES.join(', ')}`);
247 | }
248 |
249 | const graph = await this.loadGraph();
250 |
251 | // Remove any existing status relations for this entity
252 | graph.relations = graph.relations.filter(r =>
253 | !(r.from === entityName && r.relationType === 'has_status')
254 | );
255 |
256 | // Add new status relation
257 | graph.relations.push({
258 | from: entityName,
259 | to: `status:${statusValue}`,
260 | relationType: 'has_status'
261 | });
262 |
263 | await this.saveGraph(graph);
264 | }
265 |
266 | // Helper method to set priority of an entity
267 | async setEntityPriority(entityName: string, priorityValue: string): Promise<void> {
268 | if (!VALID_PRIORITY_VALUES.includes(priorityValue)) {
269 | throw new Error(`Invalid priority value: ${priorityValue}. Valid values are: ${VALID_PRIORITY_VALUES.join(', ')}`);
270 | }
271 |
272 | const graph = await this.loadGraph();
273 |
274 | // Remove any existing priority relations for this entity
275 | graph.relations = graph.relations.filter(r =>
276 | !(r.from === entityName && r.relationType === 'has_priority')
277 | );
278 |
279 | // Add new priority relation
280 | graph.relations.push({
281 | from: entityName,
282 | to: `priority:${priorityValue}`,
283 | relationType: 'has_priority'
284 | });
285 |
286 | await this.saveGraph(graph);
287 | }
288 |
289 | async createEntities(entities: Entity[]): Promise<Entity[]> {
290 | // Validate entity types
291 | for (const entity of entities) {
292 | if (!validateEntityType(entity.entityType)) {
293 | throw new Error(`Invalid entity type: ${entity.entityType}. Valid types are: ${VALID_ENTITY_TYPES.join(', ')}`);
294 | }
295 | }
296 |
297 | const graph = await this.loadGraph();
298 | const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
299 | graph.entities.push(...newEntities);
300 | await this.saveGraph(graph);
301 | return newEntities;
302 | }
303 |
304 | async createRelations(relations: Relation[]): Promise<Relation[]> {
305 | // Validate relation types
306 | for (const relation of relations) {
307 | if (!validateRelationType(relation.relationType)) {
308 | throw new Error(`Invalid relation type: ${relation.relationType}. Valid types are: ${VALID_RELATION_TYPES.join(', ')}`);
309 | }
310 | }
311 |
312 | const graph = await this.loadGraph();
313 |
314 | // Check if entities exist
315 | for (const relation of relations) {
316 | const fromEntity = graph.entities.find(e => e.name === relation.from);
317 | const toEntity = graph.entities.find(e => e.name === relation.to);
318 |
319 | if (!fromEntity) {
320 | throw new Error(`Source entity '${relation.from}' does not exist. Please create it first.`);
321 | }
322 | if (!toEntity) {
323 | throw new Error(`Target entity '${relation.to}' does not exist. Please create it first.`);
324 | }
325 | }
326 |
327 | const newRelations = relations.filter(r => !graph.relations.some(existingRelation =>
328 | existingRelation.from === r.from &&
329 | existingRelation.to === r.to &&
330 | existingRelation.relationType === r.relationType
331 | ));
332 | graph.relations.push(...newRelations);
333 | await this.saveGraph(graph);
334 | return newRelations;
335 | }
336 |
337 | async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: string[] }[]> {
338 | const graph = await this.loadGraph();
339 | const results = observations.map(o => {
340 | const entity = graph.entities.find(e => e.name === o.entityName);
341 | if (!entity) {
342 | throw new Error(`Entity with name ${o.entityName} not found`);
343 | }
344 | const newObservations = o.contents.filter(content => !entity.observations.includes(content));
345 | entity.observations.push(...newObservations);
346 | return { entityName: o.entityName, addedObservations: newObservations };
347 | });
348 | await this.saveGraph(graph);
349 | return results;
350 | }
351 |
352 | async deleteEntities(entityNames: string[]): Promise<void> {
353 | const graph = await this.loadGraph();
354 | graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
355 | graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
356 | await this.saveGraph(graph);
357 | }
358 |
359 | async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<void> {
360 | const graph = await this.loadGraph();
361 | deletions.forEach(d => {
362 | const entity = graph.entities.find(e => e.name === d.entityName);
363 | if (entity) {
364 | entity.observations = entity.observations.filter(o => !d.observations.includes(o));
365 | }
366 | });
367 | await this.saveGraph(graph);
368 | }
369 |
370 | async deleteRelations(relations: Relation[]): Promise<void> {
371 | const graph = await this.loadGraph();
372 | graph.relations = graph.relations.filter(r => !relations.some(delRelation =>
373 | r.from === delRelation.from &&
374 | r.to === delRelation.to &&
375 | r.relationType === delRelation.relationType
376 | ));
377 | await this.saveGraph(graph);
378 | }
379 |
380 | async readGraph(): Promise<KnowledgeGraph> {
381 | return this.loadGraph();
382 | }
383 |
384 | // Basic search function
385 | async searchNodes(query: string): Promise<KnowledgeGraph> {
386 | const graph = await this.loadGraph();
387 |
388 | // Filter entities
389 | const filteredEntities = graph.entities.filter(e =>
390 | e.name.toLowerCase().includes(query.toLowerCase()) ||
391 | e.entityType.toLowerCase().includes(query.toLowerCase()) ||
392 | e.observations.some(o => o.toLowerCase().includes(query.toLowerCase()))
393 | );
394 |
395 | // Create a Set of filtered entity names for quick lookup
396 | const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
397 |
398 | // Filter relations to only include those between filtered entities
399 | const filteredRelations = graph.relations.filter(r =>
400 | filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
401 | );
402 |
403 | const filteredGraph: KnowledgeGraph = {
404 | entities: filteredEntities,
405 | relations: filteredRelations,
406 | };
407 |
408 | return filteredGraph;
409 | }
410 |
411 | async openNodes(names: string[]): Promise<KnowledgeGraph> {
412 | const graph = await this.loadGraph();
413 |
414 | // Filter entities
415 | const filteredEntities = graph.entities.filter(e => names.includes(e.name));
416 |
417 | // Create a Set of filtered entity names for quick lookup
418 | const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
419 |
420 | // Filter relations to only include those between filtered entities
421 | const filteredRelations = graph.relations.filter(r =>
422 | filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
423 | );
424 |
425 | const filteredGraph: KnowledgeGraph = {
426 | entities: filteredEntities,
427 | relations: filteredRelations,
428 | };
429 |
430 | return filteredGraph;
431 | }
432 |
433 | // Software Development specific functions
434 |
435 | // Get project overview including components, features, issues, etc.
436 | async getProjectStatus(projectName: string): Promise<any> {
437 | const graph = await this.loadGraph();
438 |
439 | // Find the project entity
440 | const project = graph.entities.find(e => e.name === projectName && e.entityType === 'project');
441 | if (!project) {
442 | throw new Error(`Project '${projectName}' not found`);
443 | }
444 |
445 | // Find components that are part of this project
446 | const components: Entity[] = [];
447 |
448 | // Find features, issues, tasks, milestones related to this project
449 | const features: Entity[] = [];
450 | const issues: Entity[] = [];
451 | const tasks: Entity[] = [];
452 | const milestones: Entity[] = [];
453 |
454 | // Find entities directly related to the project
455 | for (const relation of graph.relations) {
456 | if (relation.from === projectName || relation.to === projectName) {
457 | const relatedEntity = graph.entities.find(e =>
458 | (relation.from === projectName && e.name === relation.to) ||
459 | (relation.to === projectName && e.name === relation.from)
460 | );
461 |
462 | if (relatedEntity) {
463 | if (relatedEntity.entityType === 'component') components.push(relatedEntity);
464 | if (relatedEntity.entityType === 'feature') features.push(relatedEntity);
465 | if (relatedEntity.entityType === 'issue') issues.push(relatedEntity);
466 | if (relatedEntity.entityType === 'task') tasks.push(relatedEntity);
467 | if (relatedEntity.entityType === 'milestone') milestones.push(relatedEntity);
468 | }
469 | }
470 | }
471 |
472 | // Find entities related to components of the project
473 | for (const component of components) {
474 | for (const relation of graph.relations) {
475 | if (relation.from === component.name || relation.to === component.name) {
476 | const relatedEntity = graph.entities.find(e =>
477 | (relation.from === component.name && e.name === relation.to) ||
478 | (relation.to === component.name && e.name === relation.from)
479 | );
480 |
481 | if (relatedEntity) {
482 | if (relatedEntity.entityType === 'feature' && !features.some(f => f.name === relatedEntity.name)) {
483 | features.push(relatedEntity);
484 | }
485 | if (relatedEntity.entityType === 'issue' && !issues.some(i => i.name === relatedEntity.name)) {
486 | issues.push(relatedEntity);
487 | }
488 | if (relatedEntity.entityType === 'task' && !tasks.some(t => t.name === relatedEntity.name)) {
489 | tasks.push(relatedEntity);
490 | }
491 | }
492 | }
493 | }
494 | }
495 |
496 | // Get active tasks and issues
497 | const statuses: Record<string, string> = {};
498 | const priorities: Record<string, string> = {};
499 |
500 | // Load status and priority for tasks and issues
501 | for (const entity of [...tasks, ...issues, ...features, ...milestones]) {
502 | const status = await this.getEntityStatus(entity.name);
503 | if (status) {
504 | statuses[entity.name] = status;
505 | }
506 |
507 | const priority = await this.getEntityPriority(entity.name);
508 | if (priority) {
509 | priorities[entity.name] = priority;
510 | }
511 | }
512 |
513 | // Filter active tasks and issues based on status
514 | const activeTasks = tasks.filter(task => {
515 | const status = statuses[task.name];
516 | return status ? status === 'active' : true;
517 | });
518 |
519 | const activeIssues = issues.filter(issue => {
520 | const status = statuses[issue.name];
521 | return status ? status === 'active' : true;
522 | });
523 |
524 | // Find upcoming milestones
525 | const upcomingMilestones = milestones.filter(milestone => {
526 | const status = statuses[milestone.name];
527 | return status ? status === 'active' : true;
528 | });
529 |
530 | // Get decision history
531 | const decisions = graph.entities.filter(e =>
532 | e.entityType === 'decision' &&
533 | graph.relations.some(r =>
534 | (r.from === e.name && r.to === projectName) ||
535 | (r.to === e.name && r.from === projectName)
536 | )
537 | );
538 |
539 | // Find task sequencing
540 | const taskSequencing: Record<string, string[]> = {};
541 | for (const task of tasks) {
542 | const precedingTasks: string[] = [];
543 | const followingTasks: string[] = [];
544 |
545 | // Find tasks that this task precedes
546 | for (const relation of graph.relations) {
547 | if (relation.from === task.name && relation.relationType === 'precedes') {
548 | followingTasks.push(relation.to);
549 | }
550 | if (relation.to === task.name && relation.relationType === 'precedes') {
551 | precedingTasks.push(relation.from);
552 | }
553 | }
554 |
555 | if (precedingTasks.length > 0 || followingTasks.length > 0) {
556 | taskSequencing[task.name] = {
557 | precedingTasks,
558 | followingTasks
559 | } as any;
560 | }
561 | }
562 |
563 | return {
564 | project,
565 | components,
566 | activeFeatures: features.filter(f => {
567 | const status = statuses[f.name];
568 | return status ? status === 'active' : true;
569 | }),
570 | activeTasks,
571 | activeIssues,
572 | upcomingMilestones,
573 | allFeatures: features,
574 | allIssues: issues,
575 | allTasks: tasks,
576 | allMilestones: milestones,
577 | recentDecisions: decisions.slice(0, 5), // Limit to 5 most recent decisions
578 | statuses, // Include status mapping for reference
579 | priorities, // Include priority mapping for reference
580 | taskSequencing // Include task sequencing information
581 | };
582 | }
583 |
584 | // Get detailed context for a specific component
585 | async getComponentContext(componentName: string): Promise<any> {
586 | const graph = await this.loadGraph();
587 |
588 | // Find the component entity
589 | const component = graph.entities.find(e => e.name === componentName && e.entityType === 'component');
590 | if (!component) {
591 | throw new Error(`Component '${componentName}' not found`);
592 | }
593 |
594 | // Find projects this component is part of
595 | const projects: Entity[] = [];
596 |
597 | for (const relation of graph.relations) {
598 | if (relation.relationType === 'contains' && relation.to === componentName) {
599 | const project = graph.entities.find(e => e.name === relation.from && e.entityType === 'project');
600 | if (project) {
601 | projects.push(project);
602 | }
603 | }
604 | }
605 |
606 | // Find features implemented by this component
607 | const features: Entity[] = [];
608 |
609 | for (const relation of graph.relations) {
610 | if (relation.relationType === 'implements' && relation.from === componentName) {
611 | const feature = graph.entities.find(e => e.name === relation.to && e.entityType === 'feature');
612 | if (feature) {
613 | features.push(feature);
614 | }
615 | }
616 | }
617 |
618 | // Find technologies used by this component
619 | const technologies: Entity[] = [];
620 |
621 | for (const relation of graph.relations) {
622 | if (relation.relationType === 'uses' && relation.from === componentName) {
623 | const technology = graph.entities.find(e => e.name === relation.to && e.entityType === 'technology');
624 | if (technology) {
625 | technologies.push(technology);
626 | }
627 | }
628 | }
629 |
630 | // Find issues affecting this component
631 | const issues: Entity[] = [];
632 |
633 | for (const relation of graph.relations) {
634 | if (relation.relationType === 'affects' && relation.to === componentName) {
635 | const issue = graph.entities.find(e => e.name === relation.from && e.entityType === 'issue');
636 | if (issue) {
637 | issues.push(issue);
638 | }
639 | }
640 | }
641 |
642 | // Find tasks related to this component
643 | const tasks = [];
644 | for (const relation of graph.relations) {
645 | if ((relation.from === componentName || relation.to === componentName) &&
646 | graph.entities.some(e =>
647 | (e.name === relation.from || e.name === relation.to) &&
648 | e.name !== componentName &&
649 | e.entityType === 'task'
650 | )) {
651 | const task = graph.entities.find(e =>
652 | (e.name === relation.from || e.name === relation.to) &&
653 | e.name !== componentName &&
654 | e.entityType === 'task'
655 | );
656 | if (task) {
657 | tasks.push(task);
658 | }
659 | }
660 | }
661 |
662 | // Find documentation for this component
663 | const documentation = [];
664 | for (const relation of graph.relations) {
665 | if (relation.relationType === 'documented_in' && relation.from === componentName) {
666 | const doc = graph.entities.find(e => e.name === relation.to && e.entityType === 'documentation');
667 | if (doc) {
668 | documentation.push(doc);
669 | }
670 | }
671 | }
672 |
673 | // Find dependencies
674 | const dependencies = [];
675 | for (const relation of graph.relations) {
676 | if (relation.relationType === 'depends_on' && relation.from === componentName) {
677 | const dependency = graph.entities.find(e => e.name === relation.to);
678 | if (dependency) {
679 | dependencies.push(dependency);
680 | }
681 | }
682 | }
683 |
684 | // Get statuses and priorities for tasks and issues
685 | const statuses: Record<string, string> = {};
686 | const priorities: Record<string, string> = {};
687 |
688 | // Load status and priority for tasks and issues
689 | for (const entity of [...tasks, ...issues, ...features]) {
690 | const status = await this.getEntityStatus(entity.name);
691 | if (status) {
692 | statuses[entity.name] = status;
693 | }
694 |
695 | const priority = await this.getEntityPriority(entity.name);
696 | if (priority) {
697 | priorities[entity.name] = priority;
698 | }
699 | }
700 |
701 | return {
702 | component,
703 | projects,
704 | features,
705 | technologies,
706 | activeIssues: issues.filter(issue => {
707 | const status = statuses[issue.name];
708 | return status ? status === 'active' : true;
709 | }),
710 | activeTasks: tasks.filter(task => {
711 | const status = statuses[task.name];
712 | return status ? status === 'active' : true;
713 | }),
714 | documentation,
715 | dependencies,
716 | allIssues: issues,
717 | allTasks: tasks,
718 | statuses,
719 | priorities
720 | };
721 | }
722 |
723 | // Get all entities related to a specific entity
724 | async getRelatedEntities(entityName: string, relationTypes?: string[]): Promise<any> {
725 | const graph = await this.loadGraph();
726 |
727 | // Find the entity
728 | const entity = graph.entities.find(e => e.name === entityName);
729 | if (!entity) {
730 | throw new Error(`Entity '${entityName}' not found`);
731 | }
732 |
733 | // Find all relations involving this entity
734 | let relevantRelations = graph.relations.filter(r => r.from === entityName || r.to === entityName);
735 |
736 | // Filter by relation types if specified
737 | if (relationTypes && relationTypes.length > 0) {
738 | relevantRelations = relevantRelations.filter(r => relationTypes.includes(r.relationType));
739 | }
740 |
741 | // Get all related entities
742 | const related = {
743 | entity,
744 | incomingRelations: [] as { relation: Relation; source: Entity }[],
745 | outgoingRelations: [] as { relation: Relation; target: Entity }[],
746 | };
747 |
748 | for (const relation of relevantRelations) {
749 | if (relation.from === entityName) {
750 | const target = graph.entities.find(e => e.name === relation.to);
751 | if (target) {
752 | related.outgoingRelations.push({
753 | relation,
754 | target
755 | });
756 | }
757 | } else {
758 | const source = graph.entities.find(e => e.name === relation.from);
759 | if (source) {
760 | related.incomingRelations.push({
761 | relation,
762 | source
763 | });
764 | }
765 | }
766 | }
767 |
768 | return related;
769 | }
770 |
771 | // Get the history of decisions related to a project
772 | async getDecisionHistory(projectName: string): Promise<any> {
773 | const graph = await this.loadGraph();
774 |
775 | // Find the project
776 | const project = graph.entities.find(e => e.name === projectName && e.entityType === "project");
777 | if (!project) {
778 | throw new Error(`Project '${projectName}' not found`);
779 | }
780 |
781 | // Find all decision entities related to this project
782 | const decisions: Entity[] = [];
783 |
784 | // Direct decision relations to the project
785 | for (const relation of graph.relations) {
786 | if (relation.relationType === "related_to" && relation.to === projectName) {
787 | const decision = graph.entities.find(e => e.name === relation.from && e.entityType === "decision");
788 | if (decision) {
789 | decisions.push(decision);
790 | }
791 | }
792 | }
793 |
794 | // Decisions related to components of the project
795 | const components: Entity[] = [];
796 |
797 | for (const relation of graph.relations) {
798 | if (relation.relationType === "contains" && relation.from === projectName) {
799 | const component = graph.entities.find(e => e.name === relation.to && e.entityType === "component");
800 | if (component) {
801 | components.push(component);
802 | }
803 | }
804 | }
805 |
806 | for (const component of components) {
807 | for (const relation of graph.relations) {
808 | if (relation.relationType === "related_to" && relation.to === component.name) {
809 | const decision = graph.entities.find(e => e.name === relation.from && e.entityType === "decision");
810 | if (decision && !decisions.some(d => d.name === decision.name)) {
811 | decisions.push(decision);
812 | }
813 | }
814 | }
815 | }
816 |
817 | // Sort decisions chronologically if they have date observations
818 | const decisionsWithDates = decisions.map(decision => {
819 | const dateObs = decision.observations.find(o => o.startsWith('Date:'));
820 | return {
821 | decision,
822 | date: dateObs ? new Date(dateObs.split(':')[1].trim()) : new Date(0)
823 | };
824 | });
825 |
826 | decisionsWithDates.sort((a, b) => b.date.getTime() - a.date.getTime());
827 |
828 | return {
829 | project,
830 | decisions: decisionsWithDates.map(d => d.decision),
831 | };
832 | }
833 |
834 | // Get progress toward a milestone
835 | async getMilestoneProgress(milestoneName: string): Promise<any> {
836 | const graph = await this.loadGraph();
837 |
838 | // Find the milestone
839 | const milestone = graph.entities.find(e => e.name === milestoneName && e.entityType === "milestone");
840 | if (!milestone) {
841 | throw new Error(`Milestone '${milestoneName}' not found`);
842 | }
843 |
844 | // Find all tasks related to this milestone
845 | const tasks: Entity[] = [];
846 |
847 | for (const relation of graph.relations) {
848 | if (relation.relationType === "related_to" && relation.to === milestoneName) {
849 | const task = graph.entities.find(e => e.name === relation.from && e.entityType === "task");
850 | if (task) {
851 | tasks.push(task);
852 | }
853 | }
854 | }
855 |
856 | // Get statuses for all tasks
857 | const statuses: Record<string, string> = {};
858 |
859 | // Load status for tasks
860 | for (const task of tasks) {
861 | const status = await this.getEntityStatus(task.name);
862 | if (status) {
863 | statuses[task.name] = status;
864 | }
865 | }
866 |
867 | // Group tasks by status
868 | const completedTasks: Entity[] = [];
869 | const inProgressTasks: Entity[] = [];
870 | const notStartedTasks: Entity[] = [];
871 |
872 | for (const task of tasks) {
873 | const status = statuses[task.name] || 'inactive';
874 |
875 | if (status === 'complete') {
876 | completedTasks.push(task);
877 | } else if (status === 'active') {
878 | inProgressTasks.push(task);
879 | } else {
880 | notStartedTasks.push(task);
881 | }
882 | }
883 |
884 | // Calculate progress percentage
885 | const totalTasks = tasks.length;
886 | const progressPercentage = totalTasks > 0
887 | ? Math.round((completedTasks.length / totalTasks) * 100)
888 | : 0;
889 |
890 | // Find task sequencing
891 | const taskSequencing: Record<string, any> = {};
892 | for (const task of tasks) {
893 | const precedingTasks: string[] = [];
894 | const followingTasks: string[] = [];
895 |
896 | // Find tasks that this task precedes
897 | for (const relation of graph.relations) {
898 | if (relation.from === task.name && relation.relationType === 'precedes') {
899 | followingTasks.push(relation.to);
900 | }
901 | if (relation.to === task.name && relation.relationType === 'precedes') {
902 | precedingTasks.push(relation.from);
903 | }
904 | }
905 |
906 | if (precedingTasks.length > 0 || followingTasks.length > 0) {
907 | taskSequencing[task.name] = {
908 | precedingTasks,
909 | followingTasks
910 | };
911 | }
912 | }
913 |
914 | // Determine if milestone can be considered complete
915 | const milestoneComplete = tasks.length > 0 && tasks.every(task =>
916 | statuses[task.name] === 'complete'
917 | );
918 |
919 | return {
920 | milestone,
921 | progress: {
922 | totalTasks,
923 | completedTasks: completedTasks.length,
924 | inProgressTasks: inProgressTasks.length,
925 | notStartedTasks: notStartedTasks.length,
926 | percentage: progressPercentage,
927 | complete: milestoneComplete
928 | },
929 | tasks: {
930 | completed: completedTasks,
931 | inProgress: inProgressTasks,
932 | notStarted: notStartedTasks
933 | },
934 | taskSequencing,
935 | statuses
936 | };
937 | }
938 | }
939 |
940 | // Main function to set up the MCP server
941 | async function main() {
942 | try {
943 | const knowledgeGraphManager = new KnowledgeGraphManager();
944 |
945 | // Initialize status and priority entities
946 | await knowledgeGraphManager.initializeStatusAndPriority();
947 |
948 | // Initialize session states from persistent storage
949 | const sessionStates = await loadSessionStates();
950 |
951 | // Create the MCP server with a name and version
952 | const server = new McpServer({
953 | name: "Context Manager",
954 | version: "1.0.0"
955 | });
956 |
957 | // Define a resource that exposes the entire graph
958 | server.resource(
959 | "graph",
960 | "graph://developer",
961 | async (uri) => ({
962 | contents: [{
963 | uri: uri.href,
964 | text: JSON.stringify(await knowledgeGraphManager.readGraph(), null, 2)
965 | }]
966 | })
967 | );
968 |
969 | // Define tools using zod for parameter validation
970 |
971 | // CRUD operations - these are now consolidated into buildcontext, deletecontext, and advancedcontext tools
972 |
973 | /**
974 | * Create new entities, relations, and observations.
975 | */
976 | server.tool(
977 | "buildcontext",
978 | toolDescriptions["buildcontext"],
979 | {
980 | type: z.enum(["entities", "relations", "observations"]).describe("Type of creation operation: 'entities', 'relations', or 'observations'"),
981 | data: z.array(z.any()).describe("Data for the creation operation, structure varies by type but must be an array")
982 | },
983 | async ({ type, data }) => {
984 | try {
985 | let result;
986 |
987 | switch (type) {
988 | case "entities":
989 | // Ensure entities match the Entity interface
990 | const typedEntities: Entity[] = data.map((e: any) => ({
991 | name: e.name,
992 | entityType: e.entityType,
993 | observations: e.observations
994 | }));
995 | result = await knowledgeGraphManager.createEntities(typedEntities);
996 | return {
997 | content: [{
998 | type: "text",
999 | text: JSON.stringify({ success: true, created: result }, null, 2)
1000 | }]
1001 | };
1002 |
1003 | case "relations":
1004 | // Ensure relations match the Relation interface
1005 | const typedRelations: Relation[] = data.map((r: any) => ({
1006 | from: r.from,
1007 | to: r.to,
1008 | relationType: r.relationType
1009 | }));
1010 | result = await knowledgeGraphManager.createRelations(typedRelations);
1011 | return {
1012 | content: [{
1013 | type: "text",
1014 | text: JSON.stringify({ success: true, created: result }, null, 2)
1015 | }]
1016 | };
1017 |
1018 | case "observations":
1019 | // Ensure observations match the required interface
1020 | const typedObservations: { entityName: string; contents: string[] }[] = data.map((o: any) => ({
1021 | entityName: o.entityName,
1022 | contents: o.contents
1023 | }));
1024 | result = await knowledgeGraphManager.addObservations(typedObservations);
1025 | return {
1026 | content: [{
1027 | type: "text",
1028 | text: JSON.stringify({ success: true, added: result }, null, 2)
1029 | }]
1030 | };
1031 |
1032 | default:
1033 | throw new Error(`Invalid type: ${type}. Must be 'entities', 'relations', or 'observations'.`);
1034 | }
1035 | } catch (error) {
1036 | return {
1037 | content: [{
1038 | type: "text",
1039 | text: JSON.stringify({
1040 | success: false,
1041 | error: error instanceof Error ? error.message : String(error)
1042 | }, null, 2)
1043 | }]
1044 | };
1045 | }
1046 | }
1047 | );
1048 |
1049 | /**
1050 | * Delete entities, relations, and observations.
1051 | */
1052 | server.tool(
1053 | "deletecontext",
1054 | toolDescriptions["deletecontext"],
1055 | {
1056 | type: z.enum(["entities", "relations", "observations"]).describe("Type of deletion operation: 'entities', 'relations', or 'observations'"),
1057 | data: z.array(z.any()).describe("Data for the deletion operation, structure varies by type but must be an array")
1058 | },
1059 | async ({ type, data }) => {
1060 | try {
1061 | switch (type) {
1062 | case "entities":
1063 | await knowledgeGraphManager.deleteEntities(data);
1064 | return {
1065 | content: [{
1066 | type: "text",
1067 | text: JSON.stringify({ success: true, message: `Deleted ${data.length} entities` }, null, 2)
1068 | }]
1069 | };
1070 |
1071 | case "relations":
1072 | // Ensure relations match the Relation interface
1073 | const typedRelations: Relation[] = data.map((r: any) => ({
1074 | from: r.from,
1075 | to: r.to,
1076 | relationType: r.relationType
1077 | }));
1078 | await knowledgeGraphManager.deleteRelations(typedRelations);
1079 | return {
1080 | content: [{
1081 | type: "text",
1082 | text: JSON.stringify({ success: true, message: `Deleted ${data.length} relations` }, null, 2)
1083 | }]
1084 | };
1085 |
1086 | case "observations":
1087 | // Ensure deletions match the required interface
1088 | const typedDeletions: { entityName: string; observations: string[] }[] = data.map((d: any) => ({
1089 | entityName: d.entityName,
1090 | observations: d.observations
1091 | }));
1092 | await knowledgeGraphManager.deleteObservations(typedDeletions);
1093 | return {
1094 | content: [{
1095 | type: "text",
1096 | text: JSON.stringify({ success: true, message: `Deleted observations from ${data.length} entities` }, null, 2)
1097 | }]
1098 | };
1099 |
1100 | default:
1101 | throw new Error(`Invalid type: ${type}. Must be 'entities', 'relations', or 'observations'.`);
1102 | }
1103 | } catch (error) {
1104 | return {
1105 | content: [{
1106 | type: "text",
1107 | text: JSON.stringify({
1108 | success: false,
1109 | error: error instanceof Error ? error.message : String(error)
1110 | }, null, 2)
1111 | }]
1112 | };
1113 | }
1114 | }
1115 | );
1116 |
1117 | /**
1118 | * Get information about the graph, search for nodes, open nodes, get related entities, get decision history, and get milestone progress.
1119 | */
1120 | server.tool(
1121 | "advancedcontext",
1122 | toolDescriptions["advancedcontext"],
1123 | {
1124 | type: z.enum(["graph", "search", "nodes", "related", "decisions", "milestone"]).describe("Type of get operation: 'graph', 'search', 'nodes', 'related', 'decisions', or 'milestone'"),
1125 | params: z.record(z.string(), z.any()).describe("Parameters for the operation, structure varies by type")
1126 | },
1127 | async ({ type, params }) => {
1128 | try {
1129 | let result;
1130 |
1131 | switch (type) {
1132 | case "graph":
1133 | result = await knowledgeGraphManager.readGraph();
1134 | return {
1135 | content: [{
1136 | type: "text",
1137 | text: JSON.stringify({ success: true, graph: result }, null, 2)
1138 | }]
1139 | };
1140 |
1141 | case "search":
1142 | result = await knowledgeGraphManager.searchNodes(params.query);
1143 | return {
1144 | content: [{
1145 | type: "text",
1146 | text: JSON.stringify({ success: true, results: result }, null, 2)
1147 | }]
1148 | };
1149 |
1150 | case "nodes":
1151 | result = await knowledgeGraphManager.openNodes(params.names);
1152 | return {
1153 | content: [{
1154 | type: "text",
1155 | text: JSON.stringify({ success: true, nodes: result }, null, 2)
1156 | }]
1157 | };
1158 |
1159 | case "related":
1160 | result = await knowledgeGraphManager.getRelatedEntities(params.entityName, params.relationTypes);
1161 | return {
1162 | content: [{
1163 | type: "text",
1164 | text: JSON.stringify({ success: true, entities: result }, null, 2)
1165 | }]
1166 | };
1167 |
1168 | case "decisions":
1169 | result = await knowledgeGraphManager.getDecisionHistory(params.projectName);
1170 | return {
1171 | content: [{
1172 | type: "text",
1173 | text: JSON.stringify({ success: true, decisions: result }, null, 2)
1174 | }]
1175 | };
1176 |
1177 | case "milestone":
1178 | result = await knowledgeGraphManager.getMilestoneProgress(params.milestoneName);
1179 | return {
1180 | content: [{
1181 | type: "text",
1182 | text: JSON.stringify({ success: true, progress: result }, null, 2)
1183 | }]
1184 | };
1185 |
1186 | default:
1187 | throw new Error(`Invalid type: ${type}. Must be 'graph', 'search', 'nodes', 'related', 'decisions', or 'milestone'.`);
1188 | }
1189 | } catch (error) {
1190 | return {
1191 | content: [{
1192 | type: "text",
1193 | text: JSON.stringify({
1194 | success: false,
1195 | error: error instanceof Error ? error.message : String(error)
1196 | }, null, 2)
1197 | }]
1198 | };
1199 | }
1200 | }
1201 | );
1202 |
1203 | /**
1204 | * Start a new development session. Returns session ID, recent development sessions, active projects, high-priority tasks, and upcoming milestones.
1205 | * The output allows the user to easily choose what to focus on and which specific context to load.
1206 | */
1207 | server.tool(
1208 | "startsession",
1209 | toolDescriptions["startsession"],
1210 | {},
1211 | async () => {
1212 | try {
1213 | // Generate a unique session ID
1214 | const sessionId = generateSessionId();
1215 |
1216 | // Get recent sessions from persistent storage
1217 | const sessionStates = await loadSessionStates();
1218 |
1219 | // Initialize the session state
1220 | sessionStates.set(sessionId, []);
1221 | await saveSessionStates(sessionStates);
1222 |
1223 | // Convert sessions map to array, sort by date, and take most recent ones
1224 | const recentSessions = Array.from(sessionStates.entries())
1225 | .map(([id, stages]) => {
1226 | // Extract summary data from the first stage (if it exists)
1227 | const summaryStage = stages.find(s => s.stage === "summary");
1228 | return {
1229 | id,
1230 | project: summaryStage?.stageData?.project || "Unknown project",
1231 | focus: summaryStage?.stageData?.focus || "Unknown focus",
1232 | summary: summaryStage?.stageData?.summary || "No summary available"
1233 | };
1234 | })
1235 | .slice(0, 3); // Default to showing 3 recent sessions
1236 |
1237 | // Get active development projects
1238 | const graph = await knowledgeGraphManager.readGraph();
1239 | const activeProjects = [];
1240 |
1241 | // Find projects with active status
1242 | for (const entity of graph.entities) {
1243 | if (entity.entityType === 'project') {
1244 | const status = await knowledgeGraphManager.getEntityStatus(entity.name);
1245 | if (status === 'active') {
1246 | activeProjects.push(entity);
1247 | }
1248 | }
1249 | }
1250 |
1251 | // Get high-priority development tasks
1252 | const highPriorityTasks = [];
1253 |
1254 | // Find tasks with high priority and active status
1255 | for (const entity of graph.entities) {
1256 | if (entity.entityType === 'task') {
1257 | const status = await knowledgeGraphManager.getEntityStatus(entity.name);
1258 | const priority = await knowledgeGraphManager.getEntityPriority(entity.name);
1259 |
1260 | if (status === 'active' && priority === 'high') {
1261 | highPriorityTasks.push(entity);
1262 | }
1263 | }
1264 | }
1265 |
1266 | // Get upcoming milestones
1267 | const upcomingMilestones = [];
1268 |
1269 | // Find milestones with active status
1270 | for (const entity of graph.entities) {
1271 | if (entity.entityType === 'milestone') {
1272 | const status = await knowledgeGraphManager.getEntityStatus(entity.name);
1273 |
1274 | if (status === 'active') {
1275 | upcomingMilestones.push(entity);
1276 | }
1277 | }
1278 | }
1279 |
1280 | let sessionsText = "No recent sessions found.";
1281 | if (recentSessions.length > 0) {
1282 | sessionsText = recentSessions.map(session =>
1283 | `- ${session.project} - ${session.focus} - ${session.summary.substring(0, 100)}${session.summary.length > 100 ? '...' : ''}`
1284 | ).join('\n');
1285 | }
1286 |
1287 | let projectsText = "No active projects found.";
1288 | if (activeProjects.length > 0) {
1289 | projectsText = activeProjects.map(project => {
1290 | const obsPreview = project.observations.length > 0 ?
1291 | `: ${project.observations[0].substring(0, 60)}${project.observations[0].length > 60 ? '...' : ''}` : '';
1292 | return `- ${project.name}${obsPreview}`;
1293 | }).join('\n');
1294 | }
1295 |
1296 | let tasksText = "No high-priority tasks found.";
1297 | if (highPriorityTasks.length > 0) {
1298 | tasksText = highPriorityTasks.map(task => {
1299 | const obsPreview = task.observations.length > 0 ?
1300 | `: ${task.observations[0].substring(0, 60)}${task.observations[0].length > 60 ? '...' : ''}` : '';
1301 | return `- ${task.name}${obsPreview}`;
1302 | }).join('\n');
1303 | }
1304 |
1305 | let milestonesText = "No upcoming milestones found.";
1306 | if (upcomingMilestones.length > 0) {
1307 | milestonesText = upcomingMilestones.map(milestone => {
1308 | const obsPreview = milestone.observations.length > 0 ?
1309 | `: ${milestone.observations[0].substring(0, 60)}${milestone.observations[0].length > 60 ? '...' : ''}` : '';
1310 | return `- ${milestone.name}${obsPreview}`;
1311 | }).join('\n');
1312 | }
1313 |
1314 | return {
1315 | content: [{
1316 | type: "text",
1317 | text: `# Ask user to choose what to focus on in this session. Present the following options:
1318 |
1319 | ## Recent Development Sessions
1320 | ${sessionsText}
1321 |
1322 | ## Active Projects
1323 | ${projectsText}
1324 |
1325 | ## High-Priority Tasks
1326 | ${tasksText}
1327 |
1328 | ## Upcoming Milestones
1329 | ${milestonesText}
1330 |
1331 | To load specific context based on the user's choice, use the \`loadcontext\` tool with the entity name and developer session ID - ${sessionId}.`
1332 | }]
1333 | };
1334 | } catch (error) {
1335 | return {
1336 | content: [{
1337 | type: "text",
1338 | text: JSON.stringify({
1339 | success: false,
1340 | error: error instanceof Error ? error.message : String(error)
1341 | }, null, 2)
1342 | }]
1343 | };
1344 | }
1345 | }
1346 | );
1347 |
1348 | /**
1349 | * Load the context for a specific entity.
1350 | * Valid entity types are: project, component, task, issue, milestone, decision, feature, technology, documentation, dependency.
1351 | */
1352 | server.tool(
1353 | "loadcontext",
1354 | toolDescriptions["loadcontext"],
1355 | {
1356 | entityName: z.string(),
1357 | entityType: z.string().optional(),
1358 | sessionId: z.string().optional()
1359 | },
1360 | async ({ entityName, entityType = "project", sessionId }) => {
1361 | try {
1362 | // Validate session if ID is provided
1363 | if (sessionId) {
1364 | const sessionStates = await loadSessionStates();
1365 | if (!sessionStates.has(sessionId)) {
1366 | console.warn(`Warning: Session ${sessionId} not found, but proceeding with context load`);
1367 | // Initialize it anyway for more robustness
1368 | sessionStates.set(sessionId, []);
1369 | await saveSessionStates(sessionStates);
1370 | }
1371 |
1372 | // Track that this entity was loaded in this session
1373 | const sessionState = sessionStates.get(sessionId) || [];
1374 | const loadEvent = {
1375 | type: 'context_loaded',
1376 | timestamp: new Date().toISOString(),
1377 | entityName,
1378 | entityType
1379 | };
1380 | sessionState.push(loadEvent);
1381 | sessionStates.set(sessionId, sessionState);
1382 | await saveSessionStates(sessionStates);
1383 | }
1384 |
1385 | // Get entity
1386 | const entityGraph = await knowledgeGraphManager.searchNodes(entityName);
1387 | if (entityGraph.entities.length === 0) {
1388 | throw new Error(`Entity ${entityName} not found`);
1389 | }
1390 |
1391 | // Find the exact entity by name (case-sensitive match)
1392 | const entity = entityGraph.entities.find(e => e.name === entityName);
1393 | if (!entity) {
1394 | throw new Error(`Entity ${entityName} not found`);
1395 | }
1396 |
1397 | // Get status and priority
1398 | const status = await knowledgeGraphManager.getEntityStatus(entityName) || "unknown";
1399 | const priority = await knowledgeGraphManager.getEntityPriority(entityName);
1400 |
1401 | // Format observations for display (show all observations)
1402 | const observationsList = entity.observations.length > 0
1403 | ? entity.observations.map(obs => `- ${obs}`).join("\n")
1404 | : "No observations";
1405 |
1406 | // Different context loading based on entity type
1407 | let contextMessage = "";
1408 |
1409 | if (entityType === "project") {
1410 | // Get project status
1411 | const projectStatus = await knowledgeGraphManager.getProjectStatus(entityName);
1412 |
1413 | // Format project context
1414 | const componentsText = projectStatus.components?.map((component: Entity) => {
1415 | return `- **${component.name}**${component.observations.length > 0 ? `: ${component.observations[0]}` : ''}`;
1416 | }).join("\n") || "No components found";
1417 |
1418 | const featuresText = projectStatus.activeFeatures?.map((feature: Entity) => {
1419 | const featureStatus = projectStatus.statuses[feature.name] || "unknown";
1420 | return `- **${feature.name}** (${featureStatus})${feature.observations.length > 0 ? `: ${feature.observations.join(', ')}` : ''}`;
1421 | }).join("\n") || "No active features found";
1422 |
1423 | const tasksText = projectStatus.activeTasks?.map((task: Entity) => {
1424 | const taskStatus = projectStatus.statuses[task.name] || "unknown";
1425 | const taskPriority = projectStatus.priorities[task.name] || "normal";
1426 | return `- **${task.name}** (${taskStatus}, ${taskPriority} priority)${task.observations.length > 0 ? `: ${task.observations.join(', ')}` : ''}`;
1427 | }).join("\n") || "No active tasks found";
1428 |
1429 | const issuesText = projectStatus.activeIssues?.map((issue: Entity) => {
1430 | const issueStatus = projectStatus.statuses[issue.name] || "unknown";
1431 | return `- **${issue.name}** (${issueStatus})${issue.observations.length > 0 ? `: ${issue.observations.join(', ')}` : ''}`;
1432 | }).join("\n") || "No active issues found";
1433 |
1434 | const milestonesText = projectStatus.upcomingMilestones?.map((milestone: Entity) => {
1435 | const milestoneStatus = projectStatus.statuses[milestone.name] || "unknown";
1436 | return `- **${milestone.name}** (${milestoneStatus})${milestone.observations.length > 0 ? `: ${milestone.observations.join(', ')}` : ''}`;
1437 | }).join("\n") || "No upcoming milestones found";
1438 |
1439 | const decisionsText = projectStatus.recentDecisions?.map((decision: Entity) => {
1440 | return `- **${decision.name}**${decision.observations.length > 0 ? `: ${decision.observations.join(', ')}` : ''}`;
1441 | }).join("\n") || "No recent decisions";
1442 |
1443 | // Task sequencing information
1444 | const sequencingText = Object.keys(projectStatus.taskSequencing || {}).length > 0
1445 | ? Object.entries(projectStatus.taskSequencing).map(([taskName, sequence]: [string, any]) => {
1446 | return `- **${taskName}**:\n - Precedes: ${sequence.followingTasks.length > 0 ? sequence.followingTasks.join(', ') : 'None'}\n - Follows: ${sequence.precedingTasks.length > 0 ? sequence.precedingTasks.join(', ') : 'None'}`;
1447 | }).join("\n")
1448 | : "No task sequencing information available";
1449 |
1450 | contextMessage = `# Software Development Project Context: ${entityName}
1451 |
1452 | ## Project Overview
1453 | - **Status**: ${status}
1454 | - **Priority**: ${priority || "N/A"}
1455 |
1456 | ## Observations
1457 | ${observationsList}
1458 |
1459 | ## Components
1460 | ${componentsText}
1461 |
1462 | ## Active Features
1463 | ${featuresText}
1464 |
1465 | ## Active Tasks
1466 | ${tasksText}
1467 |
1468 | ## Active Issues
1469 | ${issuesText}
1470 |
1471 | ## Upcoming Milestones
1472 | ${milestonesText}
1473 |
1474 | ## Recent Decisions
1475 | ${decisionsText}
1476 |
1477 | ## Task Sequencing
1478 | ${sequencingText}`;
1479 | }
1480 | else if (entityType === "component") {
1481 | // Get component context
1482 | const componentContext = await knowledgeGraphManager.getComponentContext(entityName);
1483 |
1484 | const projectsText = componentContext.projects?.map((project: Entity) => {
1485 | return `- **${project.name}**${project.observations.length > 0 ? `: ${project.observations.join(', ')}` : ''}`;
1486 | }).join("\n") || "No parent projects found";
1487 |
1488 | const featuresText = componentContext.features?.map((feature: Entity) => {
1489 | const featureStatus = componentContext.statuses[feature.name] || "unknown";
1490 | return `- **${feature.name}** (${featureStatus})${feature.observations.length > 0 ? `: ${feature.observations.join(', ')}` : ''}`;
1491 | }).join("\n") || "No implemented features found";
1492 |
1493 | const technologiesText = componentContext.technologies?.map((tech: Entity) => {
1494 | return `- **${tech.name}**${tech.observations.length > 0 ? `: ${tech.observations.join(', ')}` : ''}`;
1495 | }).join("\n") || "No technologies specified";
1496 |
1497 | const issuesText = componentContext.activeIssues?.map((issue: Entity) => {
1498 | const issueStatus = componentContext.statuses[issue.name] || "unknown";
1499 | return `- **${issue.name}** (${issueStatus})${issue.observations.length > 0 ? `: ${issue.observations.join(', ')}` : ''}`;
1500 | }).join("\n") || "No active issues found";
1501 |
1502 | const dependenciesText = componentContext.dependencies?.map((dep: Entity) => {
1503 | return `- **${dep.name}** (${dep.entityType})${dep.observations.length > 0 ? `: ${dep.observations.join(', ')}` : ''}`;
1504 | }).join("\n") || "No dependencies found";
1505 |
1506 | const documentationText = componentContext.documentation?.map((doc: Entity) => {
1507 | return `- **${doc.name}**${doc.observations.length > 0 ? `: ${doc.observations.join(', ')}` : ''}`;
1508 | }).join("\n") || "No documentation found";
1509 |
1510 | contextMessage = `# Component Context: ${entityName}
1511 |
1512 | ## Overview
1513 | - **Status**: ${status}
1514 | - **Priority**: ${priority || "N/A"}
1515 |
1516 | ## Observations
1517 | ${observationsList}
1518 |
1519 | ## Part of Projects
1520 | ${projectsText}
1521 |
1522 | ## Technologies
1523 | ${technologiesText}
1524 |
1525 | ## Implemented Features
1526 | ${featuresText}
1527 |
1528 | ## Dependencies
1529 | ${dependenciesText}
1530 |
1531 | ## Active Issues
1532 | ${issuesText}
1533 |
1534 | ## Documentation
1535 | ${documentationText}`;
1536 | }
1537 | else if (entityType === "feature") {
1538 | // Get related entities
1539 | const relatedEntities = await knowledgeGraphManager.getRelatedEntities(entityName);
1540 |
1541 | // Find implementing components
1542 | const implementingComponents = relatedEntities.incomingRelations
1543 | .filter((rel: { relation: Relation; source: Entity }) => rel.relation.relationType === "implements")
1544 | .map((rel: { relation: Relation; source: Entity }) => rel.source);
1545 |
1546 | const componentsText = implementingComponents.map((component: Entity) => {
1547 | return `- **${component.name}**${component.observations.length > 0 ? `: ${component.observations.join(', ')}` : ''}`;
1548 | }).join("\n") || "No implementing components found";
1549 |
1550 | // Find related tasks
1551 | const relatedTasks = [...relatedEntities.incomingRelations, ...relatedEntities.outgoingRelations]
1552 | .filter((rel: { relation: Relation; source?: Entity; target?: Entity }) =>
1553 | rel.relation.relationType === "related_to" &&
1554 | (rel.source?.entityType === "task" || rel.target?.entityType === "task")
1555 | )
1556 | .map((rel: { relation: Relation; source?: Entity; target?: Entity }) =>
1557 | rel.source?.entityType === "task" ? rel.source : rel.target
1558 | )
1559 | .filter((entity): entity is Entity => entity !== undefined);
1560 |
1561 | // Get status for each task
1562 | const taskStatuses: Record<string, string> = {};
1563 | for (const task of relatedTasks) {
1564 | const taskStatus = await knowledgeGraphManager.getEntityStatus(task.name);
1565 | if (taskStatus) {
1566 | taskStatuses[task.name] = taskStatus;
1567 | }
1568 | }
1569 |
1570 | const tasksText = relatedTasks.map((task: Entity) => {
1571 | const taskStatus = taskStatuses[task.name] || "unknown";
1572 | return `- **${task.name}** (${taskStatus})${task.observations.length > 0 ? `: ${task.observations.join(', ')}` : ''}`;
1573 | }).join("\n") || "No related tasks found";
1574 |
1575 | // Find requirements
1576 | const requirements = relatedEntities.incomingRelations
1577 | .filter((rel: { relation: Relation; source: Entity }) => rel.relation.relationType === "required_by")
1578 | .map((rel: { relation: Relation; source: Entity }) => rel.source);
1579 |
1580 | const requirementsText = requirements.map((req: Entity) => {
1581 | return `- **${req.name}**${req.observations.length > 0 ? `: ${req.observations.join(', ')}` : ''}`;
1582 | }).join("\n") || "No requirements specified";
1583 |
1584 | contextMessage = `# Feature Context: ${entityName}
1585 |
1586 | ## Overview
1587 | - **Status**: ${status}
1588 | - **Priority**: ${priority || "normal"}
1589 |
1590 | ## Observations
1591 | ${observationsList}
1592 |
1593 | ## Requirements
1594 | ${requirementsText}
1595 |
1596 | ## Implementing Components
1597 | ${componentsText}
1598 |
1599 | ## Related Tasks
1600 | ${tasksText}`;
1601 | }
1602 | else if (entityType === "task") {
1603 | // Get related entities
1604 | const relatedEntities = await knowledgeGraphManager.getRelatedEntities(entityName);
1605 |
1606 | // Find related issues
1607 | const relatedIssues = relatedEntities.outgoingRelations
1608 | .filter((rel: { relation: Relation; target: Entity }) => rel.relation.relationType === "resolves")
1609 | .map((rel: { relation: Relation; target: Entity }) => rel.target);
1610 |
1611 | // Get status for each issue
1612 | const issueStatuses: Record<string, string> = {};
1613 | for (const issue of relatedIssues) {
1614 | const issueStatus = await knowledgeGraphManager.getEntityStatus(issue.name);
1615 | if (issueStatus) {
1616 | issueStatuses[issue.name] = issueStatus;
1617 | }
1618 | }
1619 |
1620 | const issuesText = relatedIssues.map((issue: Entity) => {
1621 | const issueStatus = issueStatuses[issue.name] || "unknown";
1622 | return `- **${issue.name}** (${issueStatus})${issue.observations.length > 0 ? `: ${issue.observations.join(', ')}` : ''}`;
1623 | }).join("\n") || "No related issues found";
1624 |
1625 | // Find parent project
1626 | const parentProjects = relatedEntities.incomingRelations
1627 | .filter((rel: { relation: Relation; source: Entity }) => rel.relation.relationType === "contains" && rel.source.entityType === "project")
1628 | .map((rel: { relation: Relation; source: Entity }) => rel.source);
1629 |
1630 | const projectName = parentProjects.length > 0 ? parentProjects[0].name : "Unknown project";
1631 |
1632 | // Find blocking tasks or issues
1633 | const blockingItems = relatedEntities.outgoingRelations
1634 | .filter((rel: { relation: Relation; target: Entity }) => rel.relation.relationType === "blocked_by")
1635 | .map((rel: { relation: Relation; target: Entity }) => rel.target);
1636 |
1637 | // Get status for each blocking item
1638 | const blockingStatuses: Record<string, string> = {};
1639 | for (const item of blockingItems) {
1640 | const itemStatus = await knowledgeGraphManager.getEntityStatus(item.name);
1641 | if (itemStatus) {
1642 | blockingStatuses[item.name] = itemStatus;
1643 | }
1644 | }
1645 |
1646 | const blockingText = blockingItems.map((item: Entity) => {
1647 | const itemStatus = blockingStatuses[item.name] || "unknown";
1648 | return `- **${item.name}** (${item.entityType}, ${itemStatus})${item.observations.length > 0 ? `: ${item.observations.join(', ')}` : ''}`;
1649 | }).join("\n") || "No blocking items";
1650 |
1651 | // Find task sequencing
1652 | const precedingTasks: string[] = [];
1653 | const followingTasks: string[] = [];
1654 |
1655 | // Get the graph to find sequencing relations
1656 | const graph = await knowledgeGraphManager.readGraph();
1657 |
1658 | for (const relation of graph.relations) {
1659 | if (relation.from === entityName && relation.relationType === 'precedes') {
1660 | followingTasks.push(relation.to);
1661 | }
1662 | if (relation.to === entityName && relation.relationType === 'precedes') {
1663 | precedingTasks.push(relation.from);
1664 | }
1665 | }
1666 |
1667 | const sequencingText = `### Preceding Tasks\n${precedingTasks.length > 0 ? precedingTasks.map(t => `- ${t}`).join('\n') : 'None'}\n\n### Following Tasks\n${followingTasks.length > 0 ? followingTasks.map(t => `- ${t}`).join('\n') : 'None'}`;
1668 |
1669 | contextMessage = `# Task Context: ${entityName}
1670 |
1671 | ## Overview
1672 | - **Project**: ${projectName}
1673 | - **Status**: ${status}
1674 | - **Priority**: ${priority || "normal"}
1675 |
1676 | ## Observations
1677 | ${observationsList}
1678 |
1679 | ## Related Issues
1680 | ${issuesText}
1681 |
1682 | ## Blocked By
1683 | ${blockingText}
1684 |
1685 | ## Task Sequencing
1686 | ${sequencingText}`;
1687 | }
1688 | else if (entityType === "milestone") {
1689 | // Get milestone progress
1690 | const milestoneProgress = await knowledgeGraphManager.getMilestoneProgress(entityName);
1691 |
1692 | contextMessage = `# Milestone Context: ${entityName}
1693 |
1694 | ## Overview
1695 | - **Status**: ${status}
1696 | - **Progress**: ${milestoneProgress.progress?.percentage || 0}% complete
1697 | - **Complete**: ${milestoneProgress.progress?.complete ? "Yes" : "No"}
1698 |
1699 | ## Observations
1700 | ${observationsList}
1701 |
1702 | ## Tasks
1703 | ### Completed (${milestoneProgress.tasks?.completed?.length || 0})
1704 | ${milestoneProgress.tasks?.completed?.map((task: Entity) => {
1705 | return `- **${task.name}**${task.observations.length > 0 ? `: ${task.observations.join(', ')}` : ''}`;
1706 | }).join("\n") || "No completed tasks"}
1707 |
1708 | ### In Progress (${milestoneProgress.tasks?.inProgress?.length || 0})
1709 | ${milestoneProgress.tasks?.inProgress?.map((task: Entity) => {
1710 | return `- **${task.name}**${task.observations.length > 0 ? `: ${task.observations.join(', ')}` : ''}`;
1711 | }).join("\n") || "No in-progress tasks"}
1712 |
1713 | ### Not Started (${milestoneProgress.tasks?.notStarted?.length || 0})
1714 | ${milestoneProgress.tasks?.notStarted?.map((task: Entity) => {
1715 | return `- **${task.name}**${task.observations.length > 0 ? `: ${task.observations.join(', ')}` : ''}`;
1716 | }).join("\n") || "No not-started tasks"}
1717 |
1718 | ## Task Sequencing
1719 | ${Object.keys(milestoneProgress.taskSequencing || {}).length > 0
1720 | ? Object.entries(milestoneProgress.taskSequencing).map(([taskName, sequence]: [string, any]) => {
1721 | return `- **${taskName}**:\n - Precedes: ${sequence.followingTasks.length > 0 ? sequence.followingTasks.join(', ') : 'None'}\n - Follows: ${sequence.precedingTasks.length > 0 ? sequence.precedingTasks.join(', ') : 'None'}`;
1722 | }).join("\n")
1723 | : "No task sequencing information available"}`;
1724 | }
1725 |
1726 | return {
1727 | content: [{
1728 | type: "text",
1729 | text: contextMessage
1730 | }]
1731 | };
1732 | } catch (error) {
1733 | return {
1734 | content: [{
1735 | type: "text",
1736 | text: JSON.stringify({
1737 | success: false,
1738 | error: error instanceof Error ? error.message : String(error)
1739 | }, null, 2)
1740 | }]
1741 | };
1742 | }
1743 | }
1744 | );
1745 |
1746 | // Helper function to process each stage of endsession
1747 | async function processStage(params: {
1748 | sessionId: string;
1749 | stage: string;
1750 | stageNumber: number;
1751 | totalStages: number;
1752 | analysis?: string;
1753 | stageData?: any;
1754 | nextStageNeeded: boolean;
1755 | isRevision?: boolean;
1756 | revisesStage?: number;
1757 | }, previousStages: any[]): Promise<any> {
1758 | // Process based on the stage
1759 | switch (params.stage) {
1760 | case "summary":
1761 | // Process summary stage
1762 | return {
1763 | stage: "summary",
1764 | stageNumber: params.stageNumber,
1765 | analysis: params.analysis || "",
1766 | stageData: params.stageData || {
1767 | summary: "",
1768 | duration: "",
1769 | focus: ""
1770 | },
1771 | completed: !params.nextStageNeeded
1772 | };
1773 |
1774 | case "achievements":
1775 | // Process achievements stage
1776 | return {
1777 | stage: "achievements",
1778 | stageNumber: params.stageNumber,
1779 | analysis: params.analysis || "",
1780 | stageData: params.stageData || { achievements: [] },
1781 | completed: !params.nextStageNeeded
1782 | };
1783 |
1784 | case "taskUpdates":
1785 | // Process task updates stage
1786 | return {
1787 | stage: "taskUpdates",
1788 | stageNumber: params.stageNumber,
1789 | analysis: params.analysis || "",
1790 | stageData: params.stageData || { taskUpdates: [] },
1791 | completed: !params.nextStageNeeded
1792 | };
1793 |
1794 | case "newTasks":
1795 | // Process new tasks stage
1796 | return {
1797 | stage: "newTasks",
1798 | stageNumber: params.stageNumber,
1799 | analysis: params.analysis || "",
1800 | stageData: params.stageData || { newTasks: [] },
1801 | completed: !params.nextStageNeeded
1802 | };
1803 |
1804 | case "projectStatus":
1805 | // Process project status stage
1806 | return {
1807 | stage: "projectStatus",
1808 | stageNumber: params.stageNumber,
1809 | analysis: params.analysis || "",
1810 | stageData: params.stageData || {
1811 | projectName: "",
1812 | projectStatus: "",
1813 | projectObservation: ""
1814 | },
1815 | completed: !params.nextStageNeeded
1816 | };
1817 |
1818 | case "assembly":
1819 | // Final assembly stage - compile all arguments for end session
1820 | return {
1821 | stage: "assembly",
1822 | stageNumber: params.stageNumber,
1823 | analysis: "Final assembly of endsession arguments",
1824 | stageData: assembleEndSessionArgs(previousStages),
1825 | completed: true
1826 | };
1827 |
1828 | default:
1829 | throw new Error(`Unknown stage: ${params.stage}`);
1830 | }
1831 | }
1832 |
1833 | // Helper function to assemble the final end session arguments
1834 | function assembleEndSessionArgs(stages: any[]): any {
1835 | const summaryStage = stages.find(s => s.stage === "summary");
1836 | const achievementsStage = stages.find(s => s.stage === "achievements");
1837 | const taskUpdatesStage = stages.find(s => s.stage === "taskUpdates");
1838 | const newTasksStage = stages.find(s => s.stage === "newTasks");
1839 | const projectStatusStage = stages.find(s => s.stage === "projectStatus");
1840 |
1841 | return {
1842 | summary: summaryStage?.stageData?.summary || "",
1843 | duration: summaryStage?.stageData?.duration || "unknown",
1844 | focus: summaryStage?.stageData?.focus || "",
1845 | achievements: JSON.stringify(achievementsStage?.stageData?.achievements || []),
1846 | taskUpdates: JSON.stringify(taskUpdatesStage?.stageData?.taskUpdates || []),
1847 | projectName: projectStatusStage?.stageData?.projectName || "",
1848 | projectStatus: projectStatusStage?.stageData?.projectStatus || "",
1849 | projectObservation: projectStatusStage?.stageData?.projectObservation || "",
1850 | newTasks: JSON.stringify(newTasksStage?.stageData?.newTasks || [])
1851 | };
1852 | }
1853 |
1854 | /**
1855 | * End session by processing all stages and recording the final results.
1856 | * Only use this tool if the user asks for it.
1857 | *
1858 | * Usage examples:
1859 | *
1860 | * 1. Starting the end session process with the summary stage:
1861 | * {
1862 | * "sessionId": "dev_1234567890_abc123", // From startsession
1863 | * "stage": "summary",
1864 | * "stageNumber": 1,
1865 | * "totalStages": 6, // Total stages you plan to use
1866 | * "analysis": "Analyzed progress on the authentication system",
1867 | * "stageData": {
1868 | * "summary": "Completed the login functionality and fixed related bugs",
1869 | * "duration": "3 hours",
1870 | * "focus": "AuthSystem" // Project/component name
1871 | * },
1872 | * "nextStageNeeded": true, // More stages coming
1873 | * "isRevision": false
1874 | * }
1875 | *
1876 | * 2. Middle stage for achievements:
1877 | * {
1878 | * "sessionId": "dev_1234567890_abc123",
1879 | * "stage": "achievements",
1880 | * "stageNumber": 2,
1881 | * "totalStages": 6,
1882 | * "analysis": "Listed key accomplishments",
1883 | * "stageData": {
1884 | * "achievements": [
1885 | * "Implemented password reset functionality",
1886 | * "Fixed login redirect bug",
1887 | * "Added error handling for authentication failures"
1888 | * ]
1889 | * },
1890 | * "nextStageNeeded": true,
1891 | * "isRevision": false
1892 | * }
1893 | *
1894 | * 3. Final assembly stage:
1895 | * {
1896 | * "sessionId": "dev_1234567890_abc123",
1897 | * "stage": "assembly",
1898 | * "stageNumber": 6,
1899 | * "totalStages": 6,
1900 | * "nextStageNeeded": false, // This completes the session
1901 | * "isRevision": false
1902 | * }
1903 | */
1904 | server.tool(
1905 | "endsession",
1906 | toolDescriptions["endsession"],
1907 | {
1908 | sessionId: z.string().describe("The unique session identifier obtained from startsession"),
1909 | stage: z.string().describe("Current stage of analysis: 'summary', 'achievements', 'taskUpdates', 'newTasks', 'projectStatus', or 'assembly'"),
1910 | stageNumber: z.number().int().positive().describe("The sequence number of the current stage (starts at 1)"),
1911 | totalStages: z.number().int().positive().describe("Total number of stages in the workflow (typically 6 for standard workflow)"),
1912 | analysis: z.string().optional().describe("Text analysis or observations for the current stage"),
1913 | stageData: z.record(z.string(), z.any()).optional().describe(`Stage-specific data structure - format depends on the stage type:
1914 | - For 'summary' stage: { summary: "Session summary text", duration: "2 hours", focus: "ProjectName" }
1915 | - For 'achievements' stage: { achievements: ["Implemented feature X", "Fixed bug Y", "Refactored component Z"] }
1916 | - For 'taskUpdates' stage: { taskUpdates: [{ name: "Task1", status: "completed" }, { name: "Task2", status: "in_progress" }] }
1917 | - For 'newTasks' stage: { newTasks: [{ name: "NewTask1", description: "Implement feature A", priority: "high" }] }
1918 | - For 'projectStatus' stage: { projectName: "ProjectName", projectStatus: "in_progress", projectObservation: "Making good progress" }
1919 | - For 'assembly' stage: no stageData needed - automatic assembly of previous stages`),
1920 | nextStageNeeded: z.boolean().describe("Whether additional stages are needed after this one (false for final stage)"),
1921 | isRevision: z.boolean().optional().describe("Whether this is revising a previous stage"),
1922 | revisesStage: z.number().int().positive().optional().describe("If revising, which stage number is being revised")
1923 | },
1924 | async (params) => {
1925 | try {
1926 |
1927 | // Load session states from persistent storage
1928 | const sessionStates = await loadSessionStates();
1929 |
1930 | // Validate session ID
1931 | if (!sessionStates.has(params.sessionId)) {
1932 | return {
1933 | content: [{
1934 | type: "text",
1935 | text: JSON.stringify({
1936 | success: false,
1937 | error: `Session with ID ${params.sessionId} not found. Please start a new session with startsession.`
1938 | }, null, 2)
1939 | }]
1940 | };
1941 | }
1942 |
1943 | // Get or initialize session state
1944 | let sessionState = sessionStates.get(params.sessionId) || [];
1945 |
1946 | // Process the current stage
1947 | const stageResult = await processStage(params, sessionState);
1948 |
1949 | // Store updated state
1950 | if (params.isRevision && params.revisesStage) {
1951 | // Find the analysis stages in the session state
1952 | const analysisStages = sessionState.filter(item => item.type === 'analysis_stage') || [];
1953 |
1954 | if (params.revisesStage <= analysisStages.length) {
1955 | // Replace the revised stage
1956 | analysisStages[params.revisesStage - 1] = {
1957 | type: 'analysis_stage',
1958 | ...stageResult
1959 | };
1960 | } else {
1961 | // Add as a new stage
1962 | analysisStages.push({
1963 | type: 'analysis_stage',
1964 | ...stageResult
1965 | });
1966 | }
1967 |
1968 | // Update the session state with the modified analysis stages
1969 | sessionState = [
1970 | ...sessionState.filter(item => item.type !== 'analysis_stage'),
1971 | ...analysisStages
1972 | ];
1973 | } else {
1974 | // Add new stage
1975 | sessionState.push({
1976 | type: 'analysis_stage',
1977 | ...stageResult
1978 | });
1979 | }
1980 |
1981 | // Update in-memory and persistent storage
1982 | sessionStates.set(params.sessionId, sessionState);
1983 | await saveSessionStates(sessionStates);
1984 |
1985 | // Check if this is the final assembly stage and no more stages are needed
1986 | if (params.stage === "assembly" && !params.nextStageNeeded) {
1987 | // Get the assembled arguments
1988 | const args = stageResult.stageData;
1989 |
1990 | try {
1991 | // Parse arguments
1992 | const summary = args.summary;
1993 | const duration = args.duration;
1994 | const focus = args.focus;
1995 | const achievements = args.achievements ? JSON.parse(args.achievements) : [];
1996 | const taskUpdates = args.taskUpdates ? JSON.parse(args.taskUpdates) : [];
1997 | const projectUpdate = {
1998 | name: args.projectName,
1999 | status: args.projectStatus,
2000 | observation: args.projectObservation
2001 | };
2002 | const newTasks = args.newTasks ? JSON.parse(args.newTasks) : [];
2003 |
2004 | // 2. Create achievement entities and link to focus project
2005 | const achievementEntities = achievements.map((achievement: string, i: number) => ({
2006 | name: `Achievement_${new Date().getTime()}_${i + 1}`,
2007 | entityType: "decision",
2008 | observations: [achievement]
2009 | }));
2010 |
2011 | if (achievementEntities.length > 0) {
2012 | await knowledgeGraphManager.createEntities(achievementEntities);
2013 |
2014 | // Link achievements to focus project
2015 | const achievementRelations = achievementEntities.map((achievement: {name: string}) => ({
2016 | from: focus,
2017 | to: achievement.name,
2018 | relationType: "contains"
2019 | }));
2020 |
2021 | await knowledgeGraphManager.createRelations(achievementRelations);
2022 | }
2023 |
2024 | // 3. Update task statuses
2025 | for (const task of taskUpdates) {
2026 | // First find the task entity
2027 | const taskGraph = await knowledgeGraphManager.searchNodes(`name:${task.name}`);
2028 | if (taskGraph.entities.length > 0) {
2029 | // Update the status observation
2030 | const taskEntity = taskGraph.entities[0];
2031 |
2032 | // Set task status
2033 | try {
2034 | const statusValue = task.status === "completed" || task.status === "complete" ? "complete" :
2035 | task.status === "in_progress" ? "active" : "inactive";
2036 | await knowledgeGraphManager.setEntityStatus(task.name, statusValue);
2037 | } catch (error) {
2038 | console.error(`Error updating status for task ${task.name}: ${error}`);
2039 | }
2040 |
2041 | // If completed, link to this session
2042 | if (task.status === "complete" || task.status === "completed") {
2043 | await knowledgeGraphManager.createRelations([{
2044 | from: focus,
2045 | to: task.name,
2046 | relationType: "resolves"
2047 | }]);
2048 | }
2049 | }
2050 | }
2051 |
2052 | // 4. Update project status
2053 | const projectGraph = await knowledgeGraphManager.searchNodes(`name:${projectUpdate.name}`);
2054 | if (projectGraph.entities.length > 0) {
2055 | const projectEntity = projectGraph.entities[0];
2056 |
2057 | // Add project observation if specified
2058 | if (projectUpdate.observation) {
2059 | await knowledgeGraphManager.addObservations([{
2060 | entityName: projectUpdate.name,
2061 | contents: [projectUpdate.observation]
2062 | }]);
2063 | }
2064 |
2065 | // Set project status
2066 | try {
2067 | const statusValue = projectUpdate.status === "completed" || projectUpdate.status === "complete" ? "complete" :
2068 | projectUpdate.status === "in_progress" || projectUpdate.status === "active" ? "active" : "inactive";
2069 | await knowledgeGraphManager.setEntityStatus(projectUpdate.name, statusValue);
2070 | } catch (error) {
2071 | console.error(`Error updating status for project ${projectUpdate.name}: ${error}`);
2072 | }
2073 | }
2074 |
2075 | // 5. Create new tasks
2076 | if (newTasks && newTasks.length > 0) {
2077 | const taskEntities = newTasks.map((task: {name: string, description: string, priority?: string, precedes?: string, follows?: string}, i: number) => ({
2078 | name: task.name,
2079 | entityType: "task",
2080 | observations: [
2081 | task.description
2082 | ]
2083 | }));
2084 |
2085 | await knowledgeGraphManager.createEntities(taskEntities);
2086 |
2087 | // Set status, priority, and sequencing for each task
2088 | for (const task of newTasks) {
2089 | // Set task status to active by default
2090 | try {
2091 | await knowledgeGraphManager.setEntityStatus(task.name, "active");
2092 | } catch (error) {
2093 | console.error(`Error setting status for new task ${task.name}: ${error}`);
2094 | }
2095 |
2096 | // Set task priority if specified
2097 | if (task.priority) {
2098 | try {
2099 | const priorityValue = task.priority.toLowerCase() === "high" ? "high" : "low";
2100 | await knowledgeGraphManager.setEntityPriority(task.name, priorityValue);
2101 | } catch (error) {
2102 | console.error(`Error setting priority for new task ${task.name}: ${error}`);
2103 | }
2104 | }
2105 |
2106 | // Create sequencing relationships if specified
2107 | try {
2108 | // This task precedes another task
2109 | if (task.precedes) {
2110 | await knowledgeGraphManager.createRelations([{
2111 | from: task.name,
2112 | to: task.precedes,
2113 | relationType: "precedes"
2114 | }]);
2115 | }
2116 |
2117 | // This task follows another task
2118 | if (task.follows) {
2119 | await knowledgeGraphManager.createRelations([{
2120 | from: task.follows,
2121 | to: task.name,
2122 | relationType: "precedes"
2123 | }]);
2124 | }
2125 | } catch (error) {
2126 | console.error(`Error setting sequencing for task ${task.name}: ${error}`);
2127 | }
2128 | }
2129 |
2130 | // Link tasks to project
2131 | const taskRelations = taskEntities.map((task: {name: string}) => ({
2132 | from: projectUpdate.name,
2133 | to: task.name,
2134 | relationType: "contains"
2135 | }));
2136 |
2137 | await knowledgeGraphManager.createRelations(taskRelations);
2138 | }
2139 |
2140 | // Record session completion in persistent storage
2141 | sessionState.push({
2142 | type: 'session_completed',
2143 | timestamp: new Date().toISOString(),
2144 | summary: summary,
2145 | project: focus
2146 | });
2147 |
2148 | sessionStates.set(params.sessionId, sessionState);
2149 | await saveSessionStates(sessionStates);
2150 |
2151 | // Prepare the summary message
2152 | const summaryMessage = `# Development Session Recorded
2153 |
2154 | I've recorded your development session focusing on ${focus}.
2155 |
2156 | ## Achievements Documented
2157 | ${achievements.map((a: string) => `- ${a}`).join('\n') || "No achievements recorded."}
2158 |
2159 | ## Task Updates
2160 | ${taskUpdates.map((t: {name: string, status: string}) => `- ${t.name}: ${t.status}`).join('\n') || "No task updates."}
2161 |
2162 | ## Project Status
2163 | Project ${projectUpdate.name} has been updated to: ${projectUpdate.status}
2164 |
2165 | ${newTasks && newTasks.length > 0 ? `## New Tasks Added
2166 | ${newTasks.map((t: {name: string, description: string, priority?: string}) => `- ${t.name}: ${t.description} (Priority: ${t.priority || "medium"})`).join('\n')}` : "No new tasks added."}
2167 |
2168 | ## Session Summary
2169 | ${summary}
2170 |
2171 | Would you like me to perform any additional updates to the development knowledge graph?`;
2172 |
2173 | // Return the final result with the session recorded message
2174 | return {
2175 | content: [{
2176 | type: "text",
2177 | text: JSON.stringify({
2178 | success: true,
2179 | stageCompleted: params.stage,
2180 | nextStageNeeded: false,
2181 | stageResult: stageResult,
2182 | sessionRecorded: true,
2183 | summaryMessage: summaryMessage
2184 | }, null, 2)
2185 | }]
2186 | };
2187 | } catch (error) {
2188 | return {
2189 | content: [{
2190 | type: "text",
2191 | text: JSON.stringify({
2192 | success: false,
2193 | error: `Error recording development session: ${error instanceof Error ? error.message : String(error)}`
2194 | }, null, 2)
2195 | }]
2196 | };
2197 | }
2198 | } else {
2199 | // This is not the final stage or more stages are needed
2200 | // Return intermediate result
2201 | return {
2202 | content: [{
2203 | type: "text",
2204 | text: JSON.stringify({
2205 | success: true,
2206 | stageCompleted: params.stage,
2207 | nextStageNeeded: params.nextStageNeeded,
2208 | stageResult: stageResult,
2209 | endSessionArgs: params.stage === "assembly" ? stageResult.stageData : null
2210 | }, null, 2)
2211 | }]
2212 | };
2213 | }
2214 | } catch (error) {
2215 | return {
2216 | content: [{
2217 | type: "text",
2218 | text: JSON.stringify({
2219 | success: false,
2220 | error: error instanceof Error ? error.message : String(error)
2221 | }, null, 2)
2222 | }]
2223 | };
2224 | }
2225 | }
2226 | );
2227 |
2228 | // Connect the server to the transport
2229 | const transport = new StdioServerTransport();
2230 | await server.connect(transport);
2231 | } catch (error) {
2232 | console.error("Error starting server:", error);
2233 | process.exit(1);
2234 | }
2235 | }
2236 |
2237 | // Run the main function
2238 | main().catch(error => {
2239 | console.error("Unhandled error:", error);
2240 | process.exit(1);
2241 | });
2242 |
2243 | // Export the KnowledgeGraphManager for testing
2244 | export { KnowledgeGraphManager };
```