@gotza02/sequential-thinking 2026.1.19 → 2026.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,16 @@ An MCP server implementation that provides a tool for dynamic and reflective pro
14
14
  - **Self-Reflexion**: Review and correct previous thoughts (`reflexion`) to improve accuracy.
15
15
  - **Dynamic Adjustment**: Adjust the total number of thoughts dynamically as understanding deepens.
16
16
 
17
+ ## System Instruction for AI
18
+
19
+ When connected to this server, you should follow these guidelines to maximize problem-solving effectiveness:
20
+
21
+ 1. **Iterative Thinking**: Use the `sequentialthinking` tool for every complex task. Do not try to provide a final answer immediately. Instead, break the problem into steps, generate hypotheses, and evaluate them.
22
+ 2. **Context Discovery**: Before modifying code, always use `build_project_graph` followed by `get_project_graph_summary` to understand the project architecture. Use `get_file_relationships` to see how specific files are connected.
23
+ 3. **Fact Verification**: Use `web_search` to verify current facts, library versions, or documentation. Use `fetch` to retrieve raw data from external sources when needed.
24
+ 4. **Local Execution**: Use `shell_execute` to run tests, linters, or build commands to verify your changes. Always read files using `read_file` before attempting to write or modify them.
25
+ 5. **Persistence**: The thinking process is saved automatically. You can resume previous sessions by reviewing the `thoughts_history.json` file if needed.
26
+
17
27
  ## Tool
18
28
 
19
29
  ### sequentialthinking
@@ -31,7 +41,10 @@ Search the web using Brave or Exa APIs.
31
41
  - `provider` (enum, optional): 'brave', 'exa', or 'google'.
32
42
 
33
43
  **Configuration:**
34
- Requires `BRAVE_API_KEY` or `EXA_API_KEY` environment variables.
44
+ Requires one of the following environment variable sets:
45
+ - `BRAVE_API_KEY` (for Brave Search)
46
+ - `EXA_API_KEY` (for Exa Search)
47
+ - `GOOGLE_API_KEY` and `GOOGLE_CX` (for Google Custom Search)
35
48
 
36
49
  ### fetch
37
50
 
@@ -127,6 +140,23 @@ npm run build
127
140
  npm test
128
141
  ```
129
142
 
143
+ ## Recent Updates (v2026.1.21)
144
+
145
+ - **Performance & Accuracy**:
146
+ - Replaced Regex-based code analysis with **TypeScript Compiler API (AST)** for 100% accurate import/export detection.
147
+ - Improved `web_search` robustness and error handling.
148
+ - **Persistence**:
149
+ - Implemented **File-based Persistence** for the thinking process. Your thoughts are now saved to `thoughts_history.json` automatically.
150
+ - Switched to **Asynchronous File I/O** to prevent server blocking.
151
+ - **Bug Fixes**:
152
+ - Fixed duplicate import entries in the project graph.
153
+ - Resolved memory growth issues in long-running sessions.
154
+
155
+ ## Recent Updates (v2026.1.20)
156
+
157
+ - **New Features**:
158
+ - Added support for **Google Custom Search** in the `web_search` tool. (Requires `GOOGLE_API_KEY` and `GOOGLE_CX`).
159
+
130
160
  ## Recent Updates (v2026.1.18)
131
161
 
132
162
  - **Bug Fixes**:
package/dist/graph.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
+ import ts from 'typescript';
3
4
  export class ProjectKnowledgeGraph {
4
5
  nodes = new Map();
5
6
  rootDir = '';
@@ -45,30 +46,48 @@ export class ProjectKnowledgeGraph {
45
46
  }
46
47
  async parseFile(filePath) {
47
48
  try {
48
- let content = await fs.readFile(filePath, 'utf-8');
49
- // Remove comments before parsing imports
50
- // 1. Remove block comments /* ... */
51
- content = content.replace(/\/\*[\s\S]*?\*\//g, '');
52
- // 2. Remove line comments // ...
53
- content = content.replace(/\/\/.*$/gm, '');
54
- const importRegex = /import\s+(?:[\w\s{},*]+from\s+)?['"]([^'"]+)['"]/g;
55
- const dynamicImportRegex = /import\(['"]([^'"]+)['"]\)/g;
56
- const requireRegex = /require\(['"]([^'"]+)['"]\)/g;
57
- const allMatches = [
58
- ...content.matchAll(importRegex),
59
- ...content.matchAll(dynamicImportRegex),
60
- ...content.matchAll(requireRegex)
61
- ];
49
+ const content = await fs.readFile(filePath, 'utf-8');
50
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
51
+ const imports = [];
52
+ const visit = (node) => {
53
+ // 1. Static imports: import ... from '...'
54
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
55
+ if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
56
+ imports.push(node.moduleSpecifier.text);
57
+ }
58
+ }
59
+ // 2. Dynamic imports: import('...')
60
+ else if (ts.isCallExpression(node)) {
61
+ if (node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length > 0) {
62
+ const arg = node.arguments[0];
63
+ if (ts.isStringLiteral(arg)) {
64
+ imports.push(arg.text);
65
+ }
66
+ }
67
+ // 3. CommonJS: require('...')
68
+ else if (ts.isIdentifier(node.expression) && node.expression.text === 'require' && node.arguments.length > 0) {
69
+ const arg = node.arguments[0];
70
+ if (ts.isStringLiteral(arg)) {
71
+ imports.push(arg.text);
72
+ }
73
+ }
74
+ }
75
+ ts.forEachChild(node, visit);
76
+ };
77
+ visit(sourceFile);
62
78
  const currentNode = this.nodes.get(filePath);
63
79
  if (!currentNode)
64
80
  return;
65
- for (const match of allMatches) {
66
- const importPath = match[1];
81
+ for (const importPath of imports) {
67
82
  if (importPath.startsWith('.')) {
68
83
  const resolvedPath = await this.resolvePath(path.dirname(filePath), importPath);
69
84
  if (resolvedPath && this.nodes.has(resolvedPath)) {
70
- currentNode.imports.push(resolvedPath);
71
- this.nodes.get(resolvedPath)?.importedBy.push(filePath);
85
+ if (!currentNode.imports.includes(resolvedPath)) {
86
+ currentNode.imports.push(resolvedPath);
87
+ }
88
+ if (!this.nodes.get(resolvedPath)?.importedBy.includes(filePath)) {
89
+ this.nodes.get(resolvedPath)?.importedBy.push(filePath);
90
+ }
72
91
  }
73
92
  }
74
93
  }
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ const server = new McpServer({
12
12
  name: "sequential-thinking-server",
13
13
  version: "2026.1.18",
14
14
  });
15
- const thinkingServer = new SequentialThinkingServer();
15
+ const thinkingServer = new SequentialThinkingServer(process.env.THOUGHTS_STORAGE_PATH || 'thoughts_history.json');
16
16
  const knowledgeGraph = new ProjectKnowledgeGraph();
17
17
  // --- Sequential Thinking Tool ---
18
18
  server.tool("sequentialthinking", `A detailed tool for dynamic and reflective problem-solving through thoughts.
@@ -92,7 +92,7 @@ You should:
92
92
  options: z.array(z.string()).optional().describe("List of options generated"),
93
93
  selectedOption: z.string().optional().describe("The option selected")
94
94
  }, async (args) => {
95
- const result = thinkingServer.processThought(args);
95
+ const result = await thinkingServer.processThought(args);
96
96
  return {
97
97
  content: result.content,
98
98
  isError: result.isError
@@ -105,15 +105,17 @@ server.tool("web_search", "Search the web using Brave or Exa APIs (requires API
105
105
  provider: z.enum(['brave', 'exa', 'google']).optional().describe("Preferred search provider")
106
106
  }, async ({ query, provider }) => {
107
107
  try {
108
- // Priority: User Preference > Brave > Exa
108
+ // Priority: User Preference > Brave > Exa > Google
109
109
  let selectedProvider = provider;
110
110
  if (!selectedProvider) {
111
111
  if (process.env.BRAVE_API_KEY)
112
112
  selectedProvider = 'brave';
113
113
  else if (process.env.EXA_API_KEY)
114
114
  selectedProvider = 'exa';
115
+ else if (process.env.GOOGLE_API_KEY)
116
+ selectedProvider = 'google';
115
117
  else
116
- return { content: [{ type: "text", text: "Error: No search provider configured. Please set BRAVE_API_KEY or EXA_API_KEY." }], isError: true };
118
+ return { content: [{ type: "text", text: "Error: No search provider configured. Please set BRAVE_API_KEY, EXA_API_KEY, or GOOGLE_API_KEY." }], isError: true };
117
119
  }
118
120
  if (selectedProvider === 'brave') {
119
121
  if (!process.env.BRAVE_API_KEY)
@@ -142,6 +144,23 @@ server.tool("web_search", "Search the web using Brave or Exa APIs (requires API
142
144
  const data = await response.json();
143
145
  return { content: [{ type: "text", text: JSON.stringify(data.results || data, null, 2) }] };
144
146
  }
147
+ if (selectedProvider === 'google') {
148
+ if (!process.env.GOOGLE_API_KEY)
149
+ throw new Error("GOOGLE_API_KEY not found");
150
+ if (!process.env.GOOGLE_CX)
151
+ throw new Error("GOOGLE_CX (Search Engine ID) not found");
152
+ const response = await fetch(`https://www.googleapis.com/customsearch/v1?key=${process.env.GOOGLE_API_KEY}&cx=${process.env.GOOGLE_CX}&q=${encodeURIComponent(query)}&num=5`);
153
+ if (!response.ok)
154
+ throw new Error(`Google API error: ${response.statusText}`);
155
+ const data = await response.json();
156
+ // Extract relevant fields to keep output clean
157
+ const results = data.items?.map((item) => ({
158
+ title: item.title,
159
+ link: item.link,
160
+ snippet: item.snippet
161
+ })) || [];
162
+ return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
163
+ }
145
164
  return { content: [{ type: "text", text: "Error: Unsupported or unconfigured provider." }], isError: true };
146
165
  }
147
166
  catch (error) {
package/dist/lib.js CHANGED
@@ -1,10 +1,53 @@
1
1
  import chalk from 'chalk';
2
+ import * as fs from 'fs/promises';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import * as path from 'path';
2
5
  export class SequentialThinkingServer {
3
6
  thoughtHistory = [];
4
7
  branches = {};
5
8
  disableThoughtLogging;
6
- constructor() {
9
+ storagePath;
10
+ constructor(storagePath = 'thoughts_history.json') {
7
11
  this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
12
+ this.storagePath = path.resolve(storagePath);
13
+ this.loadHistory();
14
+ }
15
+ loadHistory() {
16
+ try {
17
+ if (existsSync(this.storagePath)) {
18
+ const data = readFileSync(this.storagePath, 'utf-8');
19
+ const history = JSON.parse(data);
20
+ if (Array.isArray(history)) {
21
+ this.thoughtHistory = []; // Reset to avoid duplicates
22
+ this.branches = {};
23
+ history.forEach(thought => this.addToMemory(thought));
24
+ }
25
+ }
26
+ }
27
+ catch (error) {
28
+ console.error(`Error loading history from ${this.storagePath}:`, error);
29
+ }
30
+ }
31
+ async saveHistory() {
32
+ try {
33
+ await fs.writeFile(this.storagePath, JSON.stringify(this.thoughtHistory, null, 2), 'utf-8');
34
+ }
35
+ catch (error) {
36
+ console.error(`Error saving history to ${this.storagePath}:`, error);
37
+ }
38
+ }
39
+ addToMemory(input) {
40
+ if (input.thoughtNumber > input.totalThoughts) {
41
+ input.totalThoughts = input.thoughtNumber;
42
+ }
43
+ this.thoughtHistory.push(input);
44
+ if (input.branchFromThought && input.branchId) {
45
+ const branchKey = `${input.branchFromThought}-${input.branchId}`;
46
+ if (!this.branches[branchKey]) {
47
+ this.branches[branchKey] = [];
48
+ }
49
+ this.branches[branchKey].push(input);
50
+ }
8
51
  }
9
52
  formatThought(thoughtData) {
10
53
  const { thoughtNumber, totalThoughts, thought, isRevision, revisesThought, branchFromThought, branchId, thoughtType, score, options, selectedOption } = thoughtData;
@@ -52,19 +95,10 @@ export class SequentialThinkingServer {
52
95
  │ ${thought.padEnd(borderLength - 2)} │${extraContent}
53
96
  └${border}┘`;
54
97
  }
55
- processThought(input) {
98
+ async processThought(input) {
56
99
  try {
57
- if (input.thoughtNumber > input.totalThoughts) {
58
- input.totalThoughts = input.thoughtNumber;
59
- }
60
- this.thoughtHistory.push(input);
61
- if (input.branchFromThought && input.branchId) {
62
- const branchKey = `${input.branchFromThought}-${input.branchId}`;
63
- if (!this.branches[branchKey]) {
64
- this.branches[branchKey] = [];
65
- }
66
- this.branches[branchKey].push(input);
67
- }
100
+ this.addToMemory(input);
101
+ await this.saveHistory();
68
102
  if (!this.disableThoughtLogging) {
69
103
  const formattedThought = this.formatThought(input);
70
104
  console.error(formattedThought);
@@ -1,11 +1,21 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
1
+ import { describe, it, expect, beforeEach, afterAll } from 'vitest';
2
2
  import { SequentialThinkingServer } from './lib.js';
3
+ import * as fs from 'fs';
3
4
  describe('SequentialThinkingServer', () => {
4
5
  let server;
6
+ const testStoragePath = 'test_thoughts.json';
5
7
  beforeEach(() => {
6
- server = new SequentialThinkingServer();
8
+ if (fs.existsSync(testStoragePath)) {
9
+ fs.unlinkSync(testStoragePath);
10
+ }
11
+ server = new SequentialThinkingServer(testStoragePath);
7
12
  });
8
- it('should process a basic linear thought', () => {
13
+ afterAll(() => {
14
+ if (fs.existsSync(testStoragePath)) {
15
+ fs.unlinkSync(testStoragePath);
16
+ }
17
+ });
18
+ it('should process a basic linear thought', async () => {
9
19
  const input = {
10
20
  thought: "First step",
11
21
  thoughtNumber: 1,
@@ -13,15 +23,15 @@ describe('SequentialThinkingServer', () => {
13
23
  nextThoughtNeeded: true,
14
24
  thoughtType: 'analysis'
15
25
  };
16
- const result = server.processThought(input);
26
+ const result = await server.processThought(input);
17
27
  expect(result.isError).toBeUndefined();
18
28
  const content = JSON.parse(result.content[0].text);
19
29
  expect(content.thoughtNumber).toBe(1);
20
30
  expect(content.thoughtHistoryLength).toBe(1);
21
31
  });
22
- it('should handle branching correctly', () => {
32
+ it('should handle branching correctly', async () => {
23
33
  // Initial thought
24
- server.processThought({
34
+ await server.processThought({
25
35
  thought: "Root thought",
26
36
  thoughtNumber: 1,
27
37
  totalThoughts: 3,
@@ -37,7 +47,7 @@ describe('SequentialThinkingServer', () => {
37
47
  branchId: "branch-A",
38
48
  thoughtType: 'generation'
39
49
  };
40
- const result1 = server.processThought(branch1Input);
50
+ const result1 = await server.processThought(branch1Input);
41
51
  const content1 = JSON.parse(result1.content[0].text);
42
52
  expect(content1.branches).toContain("1-branch-A");
43
53
  // Branch 2
@@ -50,12 +60,12 @@ describe('SequentialThinkingServer', () => {
50
60
  branchId: "branch-B",
51
61
  thoughtType: 'generation'
52
62
  };
53
- const result2 = server.processThought(branch2Input);
63
+ const result2 = await server.processThought(branch2Input);
54
64
  const content2 = JSON.parse(result2.content[0].text);
55
65
  expect(content2.branches).toContain("1-branch-B");
56
66
  expect(content2.branches.length).toBe(2);
57
67
  });
58
- it('should handle evaluation with scores', () => {
68
+ it('should handle evaluation with scores', async () => {
59
69
  const input = {
60
70
  thought: "Evaluating option X",
61
71
  thoughtNumber: 3,
@@ -65,20 +75,20 @@ describe('SequentialThinkingServer', () => {
65
75
  score: 8,
66
76
  options: ['Option X', 'Option Y']
67
77
  };
68
- const result = server.processThought(input);
78
+ const result = await server.processThought(input);
69
79
  expect(result.isError).toBeUndefined();
70
80
  // Since we don't return the score in the simple JSON response (only in logs or history),
71
81
  // we mainly check that it doesn't crash and processes correctly.
72
82
  // If we exposed history in the response, we could check that too.
73
83
  });
74
- it('should adjust totalThoughts if thoughtNumber exceeds it', () => {
84
+ it('should adjust totalThoughts if thoughtNumber exceeds it', async () => {
75
85
  const input = {
76
86
  thought: "Unexpected long process",
77
87
  thoughtNumber: 6,
78
88
  totalThoughts: 5,
79
89
  nextThoughtNeeded: true
80
90
  };
81
- const result = server.processThought(input);
91
+ const result = await server.processThought(input);
82
92
  const content = JSON.parse(result.content[0].text);
83
93
  expect(content.totalThoughts).toBe(6);
84
94
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotza02/sequential-thinking",
3
- "version": "2026.1.19",
3
+ "version": "2026.1.21",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -30,6 +30,7 @@
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/sdk": "^1.24.0",
32
32
  "chalk": "^5.3.0",
33
+ "typescript": "^5.3.3",
33
34
  "yargs": "^17.7.2"
34
35
  },
35
36
  "devDependencies": {
@@ -37,7 +38,6 @@
37
38
  "@types/yargs": "^17.0.32",
38
39
  "@vitest/coverage-v8": "^2.1.8",
39
40
  "shx": "^0.3.4",
40
- "typescript": "^5.3.3",
41
41
  "vitest": "^2.1.8"
42
42
  }
43
- }
43
+ }