@gotza02/sequential-thinking 2026.2.7 → 2026.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -24
- package/dist/codestore.js +69 -0
- package/dist/codestore.test.js +59 -0
- package/dist/filesystem.test.js +48 -0
- package/dist/graph.js +13 -5
- package/dist/graph.test.js +53 -0
- package/dist/index.js +4 -0
- package/dist/integration.test.js +58 -0
- package/dist/lib.js +29 -4
- package/dist/notes.js +65 -43
- package/dist/notes.test.js +74 -0
- package/dist/registration.test.js +39 -0
- package/dist/repro_dollar.js +30 -0
- package/dist/repro_dollar_simple.js +22 -0
- package/dist/repro_history.js +41 -0
- package/dist/repro_path.js +17 -0
- package/dist/repro_ts_req.js +3 -0
- package/dist/server.test.js +32 -0
- package/dist/stress.test.js +68 -0
- package/dist/system_test.js +51 -0
- package/dist/test_ts_req.js +46 -0
- package/dist/tools/codestore.js +51 -0
- package/dist/tools/coding.js +15 -1
- package/dist/tools/filesystem.js +12 -9
- package/dist/tools/graph.js +3 -1
- package/dist/tools/notes.js +9 -6
- package/dist/tools/thinking.js +7 -0
- package/dist/tools/web.js +7 -2
- package/dist/utils.js +103 -2
- package/dist/web_read.test.js +60 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
# Sequential Thinking MCP Server (Extended Edition) 🧠✨
|
|
2
2
|
|
|
3
|
-
**MCP Server ที่ยกระดับ AI ให้เป็นวิศวกรซอฟต์แวร์อัจฉริยะ ด้วยระบบ Deepest Thinking
|
|
3
|
+
**MCP Server ที่ยกระดับ AI ให้เป็นวิศวกรซอฟต์แวร์อัจฉริยะ ด้วยระบบ Deepest Thinking, การวิเคราะห์ Codebase เชิงลึก และฐานข้อมูลความรู้ (Code Database)**
|
|
4
4
|
|
|
5
|
-
โปรเจกต์นี้คือส่วนขยายขั้นสูงของ Sequential Thinking ที่รวมเอาความสามารถในการวางแผนที่เป็นระบบ, การหาข้อมูลทั่วโลก (Web Search), การสร้างแผนผังความสัมพันธ์ของโค้ด (Dependency Graph)
|
|
5
|
+
โปรเจกต์นี้คือส่วนขยายขั้นสูงของ Sequential Thinking ที่รวมเอาความสามารถในการวางแผนที่เป็นระบบ, การหาข้อมูลทั่วโลก (Web Search), การสร้างแผนผังความสัมพันธ์ของโค้ด (Dependency Graph), การจัดการหน่วยความจำระยะยาว และ **ฐานข้อมูลความรู้โค้ด (Code Database)** เข้าด้วยกัน เพื่อให้ AI สามารถทำงานที่ซับซ้อนได้อย่างอิสระและแม่นยำ
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## 🌟 จุดเด่นของเวอร์ชัน Extended
|
|
10
10
|
|
|
11
11
|
1. **Deepest Thinking Protocol**: บังคับให้ AI วิเคราะห์ปัญหาแบบเป็นลำดับขั้นตอน (Step-by-step) พร้อมการแตกกิ่งความคิด (Branching) และการทบทวนตัวเอง (Reflexion) เพื่อหาทางเลือกที่ดีที่สุด
|
|
12
|
-
2. **Codebase Intelligence**: ระบบ `ProjectKnowledgeGraph` ที่ใช้ TypeScript Compiler API
|
|
13
|
-
3. **
|
|
14
|
-
4. **
|
|
12
|
+
2. **Codebase Intelligence**: ระบบ `ProjectKnowledgeGraph` ที่ใช้ TypeScript Compiler API และ Regex (สำหรับ Python/Go) ในการสแกนความสัมพันธ์ระหว่างไฟล์และ Exported Symbols
|
|
13
|
+
3. **Code Database (CodeStore)**: ระบบจัดเก็บ Snippets และ Architectural Patterns ลงในไฟล์ JSON ถาวร ช่วยให้ AI "จดจำ" วิธีแก้ปัญหาและนำกลับมาใช้ใหม่ได้
|
|
14
|
+
4. **Deep Coding Workflow**: เครื่องมือใหม่สำหรับการแก้ไขโค้ดที่ต้องผ่านการวิเคราะห์บริบท (Context Document) และการวางแผนที่ผ่านการตรวจสอบเหตุผลแล้วเท่านั้น
|
|
15
|
+
5. **Smart Notes**: ระบบบันทึกที่มี **Priority Level** และ **Expiration Date** ช่วยจัดลำดับความสำคัญของงานได้ดียิ่งขึ้น
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
@@ -25,7 +26,9 @@
|
|
|
25
26
|
| `GOOGLE_SEARCH_CX` | Google Custom Search Engine ID (CX) | ❌ (Optional) |
|
|
26
27
|
| `THOUGHTS_STORAGE_PATH` | ไฟล์เก็บประวัติความคิดเพื่อใช้ต่อเนื่อง | `thoughts_history.json` |
|
|
27
28
|
| `NOTES_STORAGE_PATH` | ไฟล์เก็บความจำระยะยาว (กฎโปรเจกต์, ความชอบ) | `project_notes.json` |
|
|
29
|
+
| `CODE_DB_PATH` | ไฟล์เก็บฐานข้อมูลความรู้โค้ด (Snippets/Patterns) | `code_database.json` |
|
|
28
30
|
| `THOUGHT_DELAY_MS` | เวลาหน่วงระหว่างคิด (เพื่อความสมจริงและการจัดการ Rate Limit) | `0` |
|
|
31
|
+
| `LOG_LEVEL` | ระดับการแสดงผล Log (debug, info, warn, error) | `info` |
|
|
29
32
|
|
|
30
33
|
---
|
|
31
34
|
|
|
@@ -39,31 +42,24 @@
|
|
|
39
42
|
### 💻 2. Deep Coding & Codebase Context
|
|
40
43
|
* **`deep_code_analyze`**: 🌟 **(ใหม่)** สร้าง Codebase Context Document รวบรวมเนื้อหาไฟล์, Symbols และความสัมพันธ์ (Import/Export) เพื่อให้ AI "เรียนรู้" ก่อนแก้โค้ด
|
|
41
44
|
* **`deep_code_edit`**: 🌟 **(ใหม่)** แก้ไขโค้ดแบบ Surgical Edit โดยต้องระบุ `reasoning` (เหตุผลเชิงลึก) ที่ผ่านการคิดวิเคราะห์มาแล้ว
|
|
42
|
-
* **`build_project_graph`**: สแกนโปรเจกต์เพื่อสร้างแผนผังความสัมพันธ์ทั้งหมด
|
|
45
|
+
* **`build_project_graph`**: สแกนโปรเจกต์เพื่อสร้างแผนผังความสัมพันธ์ทั้งหมด (รองรับ TS, JS, PY, GO)
|
|
43
46
|
* **`get_file_relationships`**: ตรวจสอบว่าไฟล์เป้าหมายกระทบกับส่วนไหนของระบบบ้าง
|
|
44
47
|
* **`search_code`**: ค้นหา Code Pattern ทั้งโปรเจกต์แบบชาญฉลาด
|
|
45
48
|
|
|
46
|
-
###
|
|
49
|
+
### 📚 3. Knowledge & Memory (CodeStore)
|
|
50
|
+
* **`add_code_snippet`**: 🌟 **(ใหม่)** บันทึก Snippet โค้ดที่ใช้งานบ่อยลงฐานข้อมูล
|
|
51
|
+
* **`search_code_db`**: 🌟 **(ใหม่)** ค้นหาความรู้เดิมจากฐานข้อมูล (ช่วยลดเวลาในการเริ่มงานใหม่)
|
|
52
|
+
* **`learn_architecture_pattern`**: 🌟 **(ใหม่)** บันทึกรูปแบบโครงสร้างของระบบ
|
|
53
|
+
* **`manage_notes`**: (ปรับปรุงใหม่!) จัดการบันทึกพร้อมระบุ **Priority** และ **Expiration**
|
|
54
|
+
|
|
55
|
+
### 🌐 4. External Research
|
|
47
56
|
* **`web_search`**: ค้นหาข้อมูลล่าสุดจากโลกภายนอก (Brave/Exa/Google)
|
|
48
57
|
* **`read_webpage`**: อ่านเนื้อหาเว็บแปลงเป็น Markdown ที่สะอาดและเข้าใจง่าย
|
|
49
58
|
* **`fetch`**: ดึงข้อมูล Raw จาก API ต่างๆ
|
|
50
59
|
|
|
51
|
-
### 📁
|
|
60
|
+
### 📁 5. System & Persistence
|
|
52
61
|
* **`read_file` / `write_file` / `edit_file`**: จัดการไฟล์ในระบบ
|
|
53
|
-
* **`shell_execute`**: รันคำสั่ง Terminal (เช่น
|
|
54
|
-
* **`manage_notes`**: จัดการหน่วยความจำระยะยาวข้ามเซสชัน
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
## 🔄 The "Deep Coding" Workflow (ขั้นตอนการทำงานที่แนะนำ)
|
|
59
|
-
|
|
60
|
-
เพื่อให้ AI ทำงานได้อย่างเทพที่สุด แนะนำให้ใช้ลำดับการทำงานดังนี้:
|
|
61
|
-
|
|
62
|
-
1. **Map the Codebase**: รัน `build_project_graph` เพื่อให้ AI เข้าใจโครงสร้างโปรเจกต์ทั้งหมด
|
|
63
|
-
2. **Deep Analysis**: ใช้ `deep_code_analyze` กับไฟล์ที่เกี่ยวข้อง เพื่อสร้างบริบทและความเข้าใจ symbols ที่ใช้งาน
|
|
64
|
-
3. **Strategic Planning**: ใช้ `sequentialthinking` เพื่อวางแผนแบบ Deepest Thinking (แตกกิ่งความคิด, ทดสอบสมมติฐาน)
|
|
65
|
-
4. **Precision Edit**: ลงมือแก้ไขด้วย `deep_code_edit` หรือ `edit_file` พร้อมระบุเหตุผลที่วิเคราะห์มาแล้ว
|
|
66
|
-
5. **Verify**: ใช้ `shell_execute` รันคำสั่ง Test หรือ Lint เพื่อตรวจสอบความถูกต้อง
|
|
62
|
+
* **`shell_execute`**: รันคำสั่ง Terminal (มีระบบป้องกันคำสั่งอันตราย เช่น `rm -rf /`)
|
|
67
63
|
|
|
68
64
|
---
|
|
69
65
|
|
|
@@ -93,7 +89,7 @@ git clone https://github.com/gotza02/sequential-thinking.git && cd sequential-th
|
|
|
93
89
|
```bash
|
|
94
90
|
npm run build
|
|
95
91
|
```
|
|
96
|
-
4. **Configuration**:
|
|
92
|
+
4. **Configuration**: ตั้งค่า AI Client ตามตัวอย่างด้านล่าง
|
|
97
93
|
|
|
98
94
|
---
|
|
99
95
|
|
|
@@ -117,6 +113,7 @@ git clone https://github.com/gotza02/sequential-thinking.git && cd sequential-th
|
|
|
117
113
|
"GOOGLE_SEARCH_CX": "YOUR_GOOGLE_SEARCH_ENGINE_ID",
|
|
118
114
|
"THOUGHTS_STORAGE_PATH": "thoughts_history.json",
|
|
119
115
|
"NOTES_STORAGE_PATH": "project_notes.json",
|
|
116
|
+
"CODE_DB_PATH": "code_database.json",
|
|
120
117
|
"THOUGHT_DELAY_MS": "1000",
|
|
121
118
|
"DISABLE_THOUGHT_LOGGING": "false"
|
|
122
119
|
}
|
|
@@ -139,6 +136,7 @@ git clone https://github.com/gotza02/sequential-thinking.git && cd sequential-th
|
|
|
139
136
|
"EXA_API_KEY": "YOUR_EXA_KEY",
|
|
140
137
|
"GOOGLE_SEARCH_API_KEY": "YOUR_GOOGLE_API_KEY",
|
|
141
138
|
"GOOGLE_SEARCH_CX": "YOUR_GOOGLE_SEARCH_ENGINE_ID",
|
|
139
|
+
"CODE_DB_PATH": "code_database.json",
|
|
142
140
|
"THOUGHT_DELAY_MS": "1000"
|
|
143
141
|
}
|
|
144
142
|
}
|
|
@@ -150,6 +148,16 @@ git clone https://github.com/gotza02/sequential-thinking.git && cd sequential-th
|
|
|
150
148
|
|
|
151
149
|
---
|
|
152
150
|
|
|
151
|
+
## 🛠️ ติดตั้งคำสั่งระบบ (System Instruction Setup)
|
|
152
|
+
|
|
153
|
+
คุณสามารถติดตั้งคำสั่งระบบ **Ultimate Deep Engineer** ให้กับ Gemini และ Claude ได้โดยอัตโนมัติด้วย Script นี้:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
chmod +x install-ultimate-engineer.sh && ./install-ultimate-engineer.sh
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
153
161
|
## 🤖 Recommended System Instruction (The Ultimate Deep Engineer)
|
|
154
162
|
|
|
155
163
|
ให้ Copy ข้อความด้านล่างนี้ไปใส่ใน **System Prompt** ของ AI Agent เพื่อประสิทธิภาพสูงสุด:
|
|
@@ -164,6 +172,7 @@ You are a Senior AI Software Engineer equipped with the Sequential Thinking MCP
|
|
|
164
172
|
- **Atomic Analysis:** Use `thoughtType: 'analysis'` to break every request into atomic requirements and constraints before proposing solutions.
|
|
165
173
|
- **Mandatory Reflexion:** Use `thoughtType: 'reflexion'` frequently to critique your own logic, identify potential edge cases, and challenge your assumptions.
|
|
166
174
|
- **Tree of Thoughts:** For critical architectural decisions, use branching to explore multiple paths (Conservative vs. Aggressive) and evaluate them using `thoughtType: 'evaluation'`.
|
|
175
|
+
- **Loop Breaker:** If a bug persists after 2 attempts or reasoning loops, MUST create a new branch (`branchFromThought`) to change strategy completely.
|
|
167
176
|
- **Verified Completion:** Only set `nextThoughtNeeded: false` when the solution is definitive, verified, and follows project standards.
|
|
168
177
|
|
|
169
178
|
## 🏗 CODEBASE INTELLIGENCE PROTOCOL
|
|
@@ -185,10 +194,11 @@ You are a Senior AI Software Engineer equipped with the Sequential Thinking MCP
|
|
|
185
194
|
|
|
186
195
|
## 📝 PERSISTENT MEMORY
|
|
187
196
|
- **Long-term Knowledge:** Use `manage_notes` to save architectural decisions, user preferences, project conventions, and "lessons learned" that must persist across sessions.
|
|
197
|
+
- **Code Database:** Use `add_code_snippet` to store reusable patterns and `search_code_db` to retrieve them.
|
|
188
198
|
- **Session Continuity:** Your thought history is saved. If you restart, review the history to maintain context.
|
|
189
199
|
```
|
|
190
200
|
|
|
191
201
|
---
|
|
192
202
|
|
|
193
203
|
## License
|
|
194
|
-
MIT - พัฒนาโดย @gotza02
|
|
204
|
+
MIT - พัฒนาโดย @gotza02/sequential-thinking
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { AsyncMutex } from './utils.js';
|
|
4
|
+
export class CodeDatabase {
|
|
5
|
+
filePath;
|
|
6
|
+
db = { snippets: [], patterns: {} };
|
|
7
|
+
loaded = false;
|
|
8
|
+
mutex = new AsyncMutex();
|
|
9
|
+
constructor(storagePath = 'code_database.json') {
|
|
10
|
+
this.filePath = path.resolve(storagePath);
|
|
11
|
+
}
|
|
12
|
+
async load() {
|
|
13
|
+
if (this.loaded)
|
|
14
|
+
return;
|
|
15
|
+
try {
|
|
16
|
+
const data = await fs.readFile(this.filePath, 'utf-8');
|
|
17
|
+
this.db = JSON.parse(data);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
this.db = { snippets: [], patterns: {} };
|
|
21
|
+
}
|
|
22
|
+
this.loaded = true;
|
|
23
|
+
}
|
|
24
|
+
async save() {
|
|
25
|
+
await fs.writeFile(this.filePath, JSON.stringify(this.db, null, 2), 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
async addSnippet(snippet) {
|
|
28
|
+
return this.mutex.dispatch(async () => {
|
|
29
|
+
await this.load();
|
|
30
|
+
const newSnippet = {
|
|
31
|
+
...snippet,
|
|
32
|
+
id: Math.random().toString(36).substring(2, 9),
|
|
33
|
+
updatedAt: new Date().toISOString()
|
|
34
|
+
};
|
|
35
|
+
this.db.snippets.push(newSnippet);
|
|
36
|
+
await this.save();
|
|
37
|
+
return newSnippet;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async searchSnippets(query) {
|
|
41
|
+
return this.mutex.dispatch(async () => {
|
|
42
|
+
await this.load();
|
|
43
|
+
const q = query.toLowerCase();
|
|
44
|
+
return this.db.snippets.filter(s => s.title.toLowerCase().includes(q) ||
|
|
45
|
+
s.description.toLowerCase().includes(q) ||
|
|
46
|
+
s.tags.some(t => t.toLowerCase().includes(q)) ||
|
|
47
|
+
s.code.toLowerCase().includes(q));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async learnPattern(name, description) {
|
|
51
|
+
return this.mutex.dispatch(async () => {
|
|
52
|
+
await this.load();
|
|
53
|
+
this.db.patterns[name] = description;
|
|
54
|
+
await this.save();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async getPattern(name) {
|
|
58
|
+
return this.mutex.dispatch(async () => {
|
|
59
|
+
await this.load();
|
|
60
|
+
return this.db.patterns[name] || null;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async listAllPatterns() {
|
|
64
|
+
return this.mutex.dispatch(async () => {
|
|
65
|
+
await this.load();
|
|
66
|
+
return this.db.patterns;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import { CodeDatabase } from './codestore.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
vi.mock('fs/promises');
|
|
5
|
+
describe('CodeDatabase', () => {
|
|
6
|
+
let db;
|
|
7
|
+
const testPath = 'test_code_db.json';
|
|
8
|
+
let mockStore = '{"snippets": [], "patterns": {}}';
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockStore = '{"snippets": [], "patterns": {}}';
|
|
11
|
+
fs.readFile.mockImplementation(async () => mockStore);
|
|
12
|
+
fs.writeFile.mockImplementation(async (path, data) => {
|
|
13
|
+
mockStore = data;
|
|
14
|
+
});
|
|
15
|
+
db = new CodeDatabase(testPath);
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
it('should add a code snippet', async () => {
|
|
21
|
+
const snippet = await db.addSnippet({
|
|
22
|
+
title: "Quick Sort",
|
|
23
|
+
language: "typescript",
|
|
24
|
+
code: "function sort() {}",
|
|
25
|
+
description: "A sorting algorithm",
|
|
26
|
+
tags: ["algo"]
|
|
27
|
+
});
|
|
28
|
+
expect(snippet.id).toBeDefined();
|
|
29
|
+
expect(snippet.title).toBe("Quick Sort");
|
|
30
|
+
const stored = JSON.parse(mockStore);
|
|
31
|
+
expect(stored.snippets).toHaveLength(1);
|
|
32
|
+
});
|
|
33
|
+
it('should search snippets', async () => {
|
|
34
|
+
await db.addSnippet({
|
|
35
|
+
title: "React Hook",
|
|
36
|
+
language: "ts",
|
|
37
|
+
code: "const useX = () => {}",
|
|
38
|
+
description: "Custom hook",
|
|
39
|
+
tags: ["react"]
|
|
40
|
+
});
|
|
41
|
+
await db.addSnippet({
|
|
42
|
+
title: "Python Script",
|
|
43
|
+
language: "py",
|
|
44
|
+
code: "print('hello')",
|
|
45
|
+
description: "Hello world",
|
|
46
|
+
tags: ["python"]
|
|
47
|
+
});
|
|
48
|
+
const results = await db.searchSnippets("hook");
|
|
49
|
+
expect(results).toHaveLength(1);
|
|
50
|
+
expect(results[0].title).toBe("React Hook");
|
|
51
|
+
});
|
|
52
|
+
it('should learn and retrieve patterns', async () => {
|
|
53
|
+
await db.learnPattern("Repository Pattern", "Separate data access from business logic.");
|
|
54
|
+
const pattern = await db.getPattern("Repository Pattern");
|
|
55
|
+
expect(pattern).toContain("Separate data access");
|
|
56
|
+
const all = await db.listAllPatterns();
|
|
57
|
+
expect(all["Repository Pattern"]).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
|
2
|
+
import { validatePath } from './utils.js';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
describe('FileSystem Security', () => {
|
|
5
|
+
// Mock process.cwd to be a known fixed path
|
|
6
|
+
const mockCwd = '/app/project';
|
|
7
|
+
// We need to spy on process.cwd and path.resolve behavior if we want to be strict,
|
|
8
|
+
// but validatePath uses path.resolve(requestedPath).
|
|
9
|
+
// Let's just mock path.resolve to behave as expected, or trust the real path module
|
|
10
|
+
// but ensure we set up the cwd check correctly.
|
|
11
|
+
// The easiest way is to mock process.cwd().
|
|
12
|
+
const originalCwd = process.cwd;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.spyOn(process, 'cwd').mockReturnValue(mockCwd);
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
});
|
|
19
|
+
it('should allow paths within the project root', () => {
|
|
20
|
+
const p = validatePath('src/index.ts');
|
|
21
|
+
expect(p).toBe(path.resolve(mockCwd, 'src/index.ts'));
|
|
22
|
+
});
|
|
23
|
+
it('should allow explicit ./ paths', () => {
|
|
24
|
+
const p = validatePath('./package.json');
|
|
25
|
+
expect(p).toBe(path.resolve(mockCwd, 'package.json'));
|
|
26
|
+
});
|
|
27
|
+
it('should block traversal to parent directory', () => {
|
|
28
|
+
expect(() => {
|
|
29
|
+
validatePath('../outside.txt');
|
|
30
|
+
}).toThrow(/Access denied/);
|
|
31
|
+
});
|
|
32
|
+
it('should block multiple level traversal', () => {
|
|
33
|
+
expect(() => {
|
|
34
|
+
validatePath('src/../../etc/passwd');
|
|
35
|
+
}).toThrow(/Access denied/);
|
|
36
|
+
});
|
|
37
|
+
it('should block absolute paths outside root', () => {
|
|
38
|
+
// path.resolve('/etc/passwd') returns '/etc/passwd' on linux-like
|
|
39
|
+
// path.resolve('C:/Windows') on windows...
|
|
40
|
+
// We rely on path.resolve behavior.
|
|
41
|
+
// If system is linux-like:
|
|
42
|
+
if (path.sep === '/') {
|
|
43
|
+
expect(() => {
|
|
44
|
+
validatePath('/etc/passwd');
|
|
45
|
+
}).toThrow(/Access denied/);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
package/dist/graph.js
CHANGED
|
@@ -103,6 +103,14 @@ export class ProjectKnowledgeGraph {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
// 4. Import Equals: import x = require('...')
|
|
107
|
+
else if (ts.isImportEqualsDeclaration(node)) {
|
|
108
|
+
if (ts.isExternalModuleReference(node.moduleReference)) {
|
|
109
|
+
if (ts.isStringLiteral(node.moduleReference.expression)) {
|
|
110
|
+
imports.push(node.moduleReference.expression.text);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
106
114
|
ts.forEachChild(node, visit);
|
|
107
115
|
};
|
|
108
116
|
visit(sourceFile);
|
|
@@ -121,13 +129,13 @@ export class ProjectKnowledgeGraph {
|
|
|
121
129
|
// Basic Regex for generic symbols and imports
|
|
122
130
|
if (ext === '.py') {
|
|
123
131
|
// Python: import x, from x import y, def func, class Cls
|
|
124
|
-
const importMatches = content.matchAll(
|
|
132
|
+
const importMatches = content.matchAll(/^\s*(?:import|from)\s+([a-zA-Z0-9_.]+)/gm);
|
|
125
133
|
for (const match of importMatches)
|
|
126
134
|
imports.push(match[1]);
|
|
127
|
-
const funcMatches = content.matchAll(
|
|
135
|
+
const funcMatches = content.matchAll(/^\s*def\s+([a-zA-Z0-9_]+)/gm);
|
|
128
136
|
for (const match of funcMatches)
|
|
129
137
|
symbols.push(`def:${match[1]}`);
|
|
130
|
-
const classMatches = content.matchAll(
|
|
138
|
+
const classMatches = content.matchAll(/^\s*class\s+([a-zA-Z0-9_]+)/gm);
|
|
131
139
|
for (const match of classMatches)
|
|
132
140
|
symbols.push(`class:${match[1]}`);
|
|
133
141
|
}
|
|
@@ -136,7 +144,7 @@ export class ProjectKnowledgeGraph {
|
|
|
136
144
|
const importMatches = content.matchAll(/import\s+"([^"]+)"/g);
|
|
137
145
|
for (const match of importMatches)
|
|
138
146
|
imports.push(match[1]);
|
|
139
|
-
const funcMatches = content.matchAll(
|
|
147
|
+
const funcMatches = content.matchAll(/^\s*func\s+([a-zA-Z0-9_]+)/gm);
|
|
140
148
|
for (const match of funcMatches)
|
|
141
149
|
symbols.push(`func:${match[1]}`);
|
|
142
150
|
}
|
|
@@ -176,7 +184,7 @@ export class ProjectKnowledgeGraph {
|
|
|
176
184
|
return absolutePath;
|
|
177
185
|
}
|
|
178
186
|
// 2. Try appending extensions
|
|
179
|
-
const extensions = ['.ts', '.js', '.tsx', '.jsx', '.json', '/index.ts', '/index.js'];
|
|
187
|
+
const extensions = ['.ts', '.js', '.tsx', '.jsx', '.json', '.py', '.go', '.rs', '.java', '.c', '.cpp', '.h', '/index.ts', '/index.js'];
|
|
180
188
|
for (const ext of extensions) {
|
|
181
189
|
const p = absolutePath + ext;
|
|
182
190
|
if (this.nodes.has(p)) {
|
package/dist/graph.test.js
CHANGED
|
@@ -76,4 +76,57 @@ describe('ProjectKnowledgeGraph', () => {
|
|
|
76
76
|
const relationships = graph.getRelationships('/app/index.js');
|
|
77
77
|
expect(relationships?.imports).toContain('Button.jsx');
|
|
78
78
|
});
|
|
79
|
+
it('should handle circular dependencies', async () => {
|
|
80
|
+
const contentA = `import { b } from './b'; export const a = 1;`;
|
|
81
|
+
const contentB = `import { a } from './a'; export const b = 2;`;
|
|
82
|
+
fs.readdir.mockResolvedValue([
|
|
83
|
+
{ name: 'a.ts', isDirectory: () => false },
|
|
84
|
+
{ name: 'b.ts', isDirectory: () => false }
|
|
85
|
+
]);
|
|
86
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
87
|
+
if (filePath.endsWith('a.ts'))
|
|
88
|
+
return contentA;
|
|
89
|
+
if (filePath.endsWith('b.ts'))
|
|
90
|
+
return contentB;
|
|
91
|
+
return '';
|
|
92
|
+
});
|
|
93
|
+
await graph.build('/app');
|
|
94
|
+
const relA = graph.getRelationships('/app/a.ts');
|
|
95
|
+
const relB = graph.getRelationships('/app/b.ts');
|
|
96
|
+
expect(relA?.imports).toContain('b.ts');
|
|
97
|
+
expect(relA?.importedBy).toContain('b.ts');
|
|
98
|
+
expect(relB?.imports).toContain('a.ts');
|
|
99
|
+
expect(relB?.importedBy).toContain('a.ts');
|
|
100
|
+
});
|
|
101
|
+
it('should gracefully handle missing imports', async () => {
|
|
102
|
+
const contentA = `import { ghost } from './ghost';`;
|
|
103
|
+
fs.readdir.mockResolvedValue([
|
|
104
|
+
{ name: 'a.ts', isDirectory: () => false }
|
|
105
|
+
]);
|
|
106
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
107
|
+
if (filePath.endsWith('a.ts'))
|
|
108
|
+
return contentA;
|
|
109
|
+
return '';
|
|
110
|
+
});
|
|
111
|
+
await graph.build('/app');
|
|
112
|
+
const relA = graph.getRelationships('/app/a.ts');
|
|
113
|
+
// ghost.ts doesn't exist, so imports should be empty (filtered out)
|
|
114
|
+
expect(relA?.imports).toHaveLength(0);
|
|
115
|
+
});
|
|
116
|
+
it('should ignore directory traversal attempts outside root', async () => {
|
|
117
|
+
// If we pretend root is /app, and we try to import ../outside
|
|
118
|
+
const contentA = `import { secret } from '../secret';`;
|
|
119
|
+
fs.readdir.mockResolvedValue([
|
|
120
|
+
{ name: 'a.ts', isDirectory: () => false }
|
|
121
|
+
]);
|
|
122
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
123
|
+
if (filePath.endsWith('a.ts'))
|
|
124
|
+
return contentA;
|
|
125
|
+
return '';
|
|
126
|
+
});
|
|
127
|
+
await graph.build('/app');
|
|
128
|
+
const relA = graph.getRelationships('/app/a.ts');
|
|
129
|
+
// Should be empty because '../secret' is not in the scanned file list (nodes)
|
|
130
|
+
expect(relA?.imports).toHaveLength(0);
|
|
131
|
+
});
|
|
79
132
|
});
|
package/dist/index.js
CHANGED
|
@@ -4,12 +4,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { SequentialThinkingServer } from './lib.js';
|
|
5
5
|
import { ProjectKnowledgeGraph } from './graph.js';
|
|
6
6
|
import { NotesManager } from './notes.js';
|
|
7
|
+
import { CodeDatabase } from './codestore.js';
|
|
7
8
|
import { registerThinkingTools } from './tools/thinking.js';
|
|
8
9
|
import { registerWebTools } from './tools/web.js';
|
|
9
10
|
import { registerFileSystemTools } from './tools/filesystem.js';
|
|
10
11
|
import { registerGraphTools } from './tools/graph.js';
|
|
11
12
|
import { registerNoteTools } from './tools/notes.js';
|
|
12
13
|
import { registerCodingTools } from './tools/coding.js';
|
|
14
|
+
import { registerCodeDbTools } from './tools/codestore.js';
|
|
13
15
|
const server = new McpServer({
|
|
14
16
|
name: "sequential-thinking-server",
|
|
15
17
|
version: "2026.2.6",
|
|
@@ -17,6 +19,7 @@ const server = new McpServer({
|
|
|
17
19
|
const thinkingServer = new SequentialThinkingServer(process.env.THOUGHTS_STORAGE_PATH || 'thoughts_history.json', parseInt(process.env.THOUGHT_DELAY_MS || '0', 10));
|
|
18
20
|
const knowledgeGraph = new ProjectKnowledgeGraph();
|
|
19
21
|
const notesManager = new NotesManager(process.env.NOTES_STORAGE_PATH || 'project_notes.json');
|
|
22
|
+
const codeDb = new CodeDatabase(process.env.CODE_DB_PATH || 'code_database.json');
|
|
20
23
|
// Register tools
|
|
21
24
|
registerThinkingTools(server, thinkingServer);
|
|
22
25
|
registerWebTools(server);
|
|
@@ -24,6 +27,7 @@ registerFileSystemTools(server);
|
|
|
24
27
|
registerGraphTools(server, knowledgeGraph);
|
|
25
28
|
registerNoteTools(server, notesManager);
|
|
26
29
|
registerCodingTools(server, knowledgeGraph);
|
|
30
|
+
registerCodeDbTools(server, codeDb);
|
|
27
31
|
async function runServer() {
|
|
28
32
|
const transport = new StdioServerTransport();
|
|
29
33
|
await server.connect(transport);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
// We need to import the register functions to get the callbacks,
|
|
3
|
+
// but we can just use the classes directly for this integration logic test.
|
|
4
|
+
// Actually, testing the *interaction* via the tool layer is better.
|
|
5
|
+
import { registerThinkingTools } from './tools/thinking.js';
|
|
6
|
+
import { registerGraphTools } from './tools/graph.js';
|
|
7
|
+
import { registerNoteTools } from './tools/notes.js';
|
|
8
|
+
describe('Integration Workflow', () => {
|
|
9
|
+
let toolCallbacks = {};
|
|
10
|
+
const mockServer = {
|
|
11
|
+
tool: (name, desc, schema, cb) => {
|
|
12
|
+
toolCallbacks[name] = cb;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
// Mocks
|
|
16
|
+
const mockThinking = { processThought: vi.fn(), clearHistory: vi.fn() };
|
|
17
|
+
const mockGraph = { build: vi.fn(), getRelationships: vi.fn() };
|
|
18
|
+
const mockNotes = { addNote: vi.fn() };
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
toolCallbacks = {};
|
|
21
|
+
registerThinkingTools(mockServer, mockThinking);
|
|
22
|
+
registerGraphTools(mockServer, mockGraph);
|
|
23
|
+
registerNoteTools(mockServer, mockNotes);
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
it('should support a full analysis workflow', async () => {
|
|
27
|
+
// Step 1: Start Thinking
|
|
28
|
+
mockThinking.processThought.mockResolvedValue({
|
|
29
|
+
content: [{ type: "text", text: "Thought processed" }]
|
|
30
|
+
});
|
|
31
|
+
await toolCallbacks['sequentialthinking']({
|
|
32
|
+
thought: "Analyze architecture",
|
|
33
|
+
thoughtNumber: 1,
|
|
34
|
+
totalThoughts: 5,
|
|
35
|
+
nextThoughtNeeded: true,
|
|
36
|
+
thoughtType: 'analysis'
|
|
37
|
+
});
|
|
38
|
+
expect(mockThinking.processThought).toHaveBeenCalledWith(expect.objectContaining({
|
|
39
|
+
thought: "Analyze architecture"
|
|
40
|
+
}));
|
|
41
|
+
// Step 2: Build Graph
|
|
42
|
+
mockGraph.build.mockResolvedValue({ nodeCount: 10, totalFiles: 20 });
|
|
43
|
+
await toolCallbacks['build_project_graph']({ path: '.' });
|
|
44
|
+
expect(mockGraph.build).toHaveBeenCalled();
|
|
45
|
+
// Step 3: Get Relationships
|
|
46
|
+
mockGraph.getRelationships.mockReturnValue({ imports: ['utils.ts'] });
|
|
47
|
+
const relResult = await toolCallbacks['get_file_relationships']({ filePath: 'src/index.ts' });
|
|
48
|
+
expect(JSON.parse(relResult.content[0].text).imports).toContain('utils.ts');
|
|
49
|
+
// Step 4: Add Note
|
|
50
|
+
mockNotes.addNote.mockResolvedValue({ id: '123', title: 'Arch Note' });
|
|
51
|
+
await toolCallbacks['manage_notes']({
|
|
52
|
+
action: 'add',
|
|
53
|
+
title: 'Architecture Review',
|
|
54
|
+
content: 'Found circular deps'
|
|
55
|
+
});
|
|
56
|
+
expect(mockNotes.addNote).toHaveBeenCalledWith('Architecture Review', 'Found circular deps', undefined, undefined, undefined);
|
|
57
|
+
});
|
|
58
|
+
});
|
package/dist/lib.js
CHANGED
|
@@ -9,6 +9,7 @@ export class SequentialThinkingServer {
|
|
|
9
9
|
storagePath;
|
|
10
10
|
delayMs;
|
|
11
11
|
isSaving = false;
|
|
12
|
+
hasPendingSave = false;
|
|
12
13
|
constructor(storagePath = 'thoughts_history.json', delayMs = 0) {
|
|
13
14
|
this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
|
|
14
15
|
this.storagePath = path.resolve(storagePath);
|
|
@@ -33,8 +34,7 @@ export class SequentialThinkingServer {
|
|
|
33
34
|
}
|
|
34
35
|
async saveHistory() {
|
|
35
36
|
if (this.isSaving) {
|
|
36
|
-
|
|
37
|
-
setTimeout(() => this.saveHistory(), 100);
|
|
37
|
+
this.hasPendingSave = true;
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
this.isSaving = true;
|
|
@@ -49,6 +49,10 @@ export class SequentialThinkingServer {
|
|
|
49
49
|
}
|
|
50
50
|
finally {
|
|
51
51
|
this.isSaving = false;
|
|
52
|
+
if (this.hasPendingSave) {
|
|
53
|
+
this.hasPendingSave = false;
|
|
54
|
+
this.saveHistory();
|
|
55
|
+
}
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
async clearHistory() {
|
|
@@ -70,9 +74,30 @@ export class SequentialThinkingServer {
|
|
|
70
74
|
// Remove the range and insert summary
|
|
71
75
|
const removedCount = endIndex - startIndex + 1;
|
|
72
76
|
this.thoughtHistory.splice(startIndex - 1, removedCount, summaryThought);
|
|
73
|
-
// Renumber subsequent thoughts
|
|
77
|
+
// Renumber subsequent thoughts and update references
|
|
78
|
+
const shiftAmount = removedCount - 1;
|
|
74
79
|
for (let i = startIndex; i < this.thoughtHistory.length; i++) {
|
|
75
|
-
this.thoughtHistory[i]
|
|
80
|
+
const t = this.thoughtHistory[i];
|
|
81
|
+
// Update own number
|
|
82
|
+
t.thoughtNumber -= shiftAmount;
|
|
83
|
+
// Update references (branchFromThought)
|
|
84
|
+
if (t.branchFromThought) {
|
|
85
|
+
if (t.branchFromThought > endIndex) {
|
|
86
|
+
t.branchFromThought -= shiftAmount;
|
|
87
|
+
}
|
|
88
|
+
else if (t.branchFromThought >= startIndex) {
|
|
89
|
+
t.branchFromThought = startIndex; // Point to summary
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Update references (revisesThought)
|
|
93
|
+
if (t.revisesThought) {
|
|
94
|
+
if (t.revisesThought > endIndex) {
|
|
95
|
+
t.revisesThought -= shiftAmount;
|
|
96
|
+
}
|
|
97
|
+
else if (t.revisesThought >= startIndex) {
|
|
98
|
+
t.revisesThought = startIndex; // Point to summary
|
|
99
|
+
}
|
|
100
|
+
}
|
|
76
101
|
}
|
|
77
102
|
// Rebuild branches (simplification: clear and let it rebuild if needed, or just clear)
|
|
78
103
|
this.branches = {};
|