@j0hanz/thinkseq-mcp 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +300 -130
- package/dist/app.js +11 -8
- package/dist/appConfig/runDependencies.js +26 -20
- package/dist/appConfig/shutdown.js +8 -5
- package/dist/engine/thoughtStore.d.ts +1 -1
- package/dist/engine.js +11 -13
- package/dist/instructions.md +36 -23
- package/dist/lib/context.js +11 -3
- package/dist/lib/diagnostics.d.ts +4 -1
- package/dist/lib/diagnostics.js +1 -1
- package/dist/lib/errors.d.ts +1 -4
- package/dist/lib/errors.js +6 -5
- package/dist/lib/mcpLogging.js +25 -9
- package/dist/lib/stdioGuards.js +72 -24
- package/dist/lib/types.d.ts +1 -1
- package/dist/tools/thinkseq.js +12 -15
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -1,177 +1,217 @@
|
|
|
1
1
|
# ThinkSeq MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@j0hanz/thinkseq-mcp) [](https://opensource.org/licenses/MIT) [](https://nodejs.org) [](https://www.typescriptlang.org) [](https://modelcontextprotocol.io)
|
|
4
4
|
|
|
5
|
-
[](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%7B%22name%22%3A%22thinkseq%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40j0hanz%2Fthinkseq-mcp%40latest%22%5D%7D) [](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%7B%22name%22%3A%22thinkseq%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40j0hanz%2Fthinkseq-mcp%40latest%22%5D%7D)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A thinking and reasoning engine for the Model Context Protocol (MCP). Enables AI assistants to capture structured, sequential reasoning chains with full revision (destructive rewind) support, session isolation, and progress tracking.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
An MCP server for structured, sequential thinking with revision support.
|
|
9
|
+
---
|
|
12
10
|
|
|
13
11
|
## Overview
|
|
14
12
|
|
|
15
|
-
ThinkSeq
|
|
13
|
+
ThinkSeq provides an MCP server that exposes a single `thinkseq` tool for recording concise thinking steps. Each step is stored in-memory within isolated sessions, and the engine supports revising earlier steps — superseding the target and all subsequent active thoughts while preserving the full audit trail. The server communicates exclusively over **stdio** transport.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
16
|
|
|
17
17
|
## Key Features
|
|
18
18
|
|
|
19
|
-
- **Sequential
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
- **Sequential thinking steps** — Record atomic reasoning steps (up to 8,000 characters each) in order
|
|
20
|
+
- **Destructive revision (rewind)** — Revise any active thought; the target and all later thoughts are superseded, continuing from the corrected step
|
|
21
|
+
- **Session isolation** — Multiple independent thought histories via session IDs with pinned LRU eviction (default: 50 sessions)
|
|
22
|
+
- **Progress tracking** — Automatic `progress` (0–1) and `isComplete` signals returned with every response
|
|
23
|
+
|
|
24
|
+
---
|
|
25
25
|
|
|
26
26
|
## Tech Stack
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
| Component | Technology |
|
|
29
|
+
| --------------- | ---------------------------------------- |
|
|
30
|
+
| Runtime | Node.js ≥ 24 |
|
|
31
|
+
| Language | TypeScript 5.9+ |
|
|
32
|
+
| MCP SDK | `@modelcontextprotocol/sdk` ^1.26.0 |
|
|
33
|
+
| Validation | `zod` ^4.3.6 |
|
|
34
|
+
| Test framework | Native Node.js Test Runner (`node:test`) |
|
|
35
|
+
| Package manager | npm |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Architecture
|
|
40
|
+
|
|
41
|
+
1. **CLI** parses arguments (`--max-thoughts`, `--max-memory-mb`, etc.)
|
|
42
|
+
2. **`run()`** resolves dependencies and reads `package.json` for server identity
|
|
43
|
+
3. **McpServer** is created with embedded instructions, the `thinkseq` tool, `internal://instructions` resource, and `get-help` prompt
|
|
44
|
+
4. **StdioServerTransport** connects the server (stdio only)
|
|
45
|
+
5. **Stdio guards** are installed: initialization enforcement, invalid message rejection, parse error responder
|
|
46
|
+
6. **Shutdown handlers** listen for `SIGTERM`/`SIGINT` and gracefully close server, engine, and transport within a configurable timeout
|
|
47
|
+
|
|
48
|
+
---
|
|
32
49
|
|
|
33
50
|
## Repository Structure
|
|
34
51
|
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
├── dist/ # Compiled JavaScript
|
|
52
|
+
```
|
|
53
|
+
thinkseq-mcp/
|
|
38
54
|
├── src/
|
|
39
|
-
│ ├──
|
|
40
|
-
│ ├── engine
|
|
41
|
-
│ ├──
|
|
42
|
-
│ ├──
|
|
43
|
-
│ ├──
|
|
44
|
-
│ ├──
|
|
45
|
-
│
|
|
55
|
+
│ ├── appConfig/ # Environment, run dependencies, shutdown, types
|
|
56
|
+
│ ├── engine/ # Revision logic, thought queries, thought store
|
|
57
|
+
│ ├── lib/ # CLI, context, diagnostics, errors, MCP logging, stdio guards
|
|
58
|
+
│ ├── schemas/ # Zod input/output schemas
|
|
59
|
+
│ ├── tools/ # MCP tool registration (thinkseq)
|
|
60
|
+
│ ├── app.ts # Core application wiring and lifecycle
|
|
61
|
+
│ ├── engine.ts # ThinkingEngine class (session management, processing)
|
|
62
|
+
│ ├── engineConfig.ts # Engine constants and defaults
|
|
63
|
+
│ ├── index.ts # Entry point (CLI → run)
|
|
64
|
+
│ └── instructions.md # Server instructions (bundled as resource/prompt)
|
|
65
|
+
├── tests/ # Unit and integration tests
|
|
66
|
+
├── scripts/
|
|
67
|
+
│ └── tasks.mjs # Custom build/test task runner
|
|
68
|
+
├── assets/
|
|
69
|
+
│ └── logo.svg # Server icon
|
|
46
70
|
├── package.json
|
|
47
71
|
└── tsconfig.json
|
|
48
72
|
```
|
|
49
73
|
|
|
74
|
+
---
|
|
75
|
+
|
|
50
76
|
## Requirements
|
|
51
77
|
|
|
52
|
-
- **Node.js
|
|
78
|
+
- **Node.js** ≥ 24
|
|
79
|
+
- **npm** (included with Node.js)
|
|
53
80
|
|
|
54
|
-
|
|
81
|
+
---
|
|
55
82
|
|
|
56
|
-
|
|
83
|
+
## Quickstart
|
|
57
84
|
|
|
58
85
|
```bash
|
|
59
86
|
npx -y @j0hanz/thinkseq-mcp@latest
|
|
60
87
|
```
|
|
61
88
|
|
|
62
|
-
|
|
63
|
-
<summary><b>Quick Test with MCP Inspector</b></summary>
|
|
89
|
+
Add to your MCP client configuration:
|
|
64
90
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"mcpServers": {
|
|
94
|
+
"thinkseq": {
|
|
95
|
+
"command": "npx",
|
|
96
|
+
"args": ["-y", "@j0hanz/thinkseq-mcp@latest"]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
69
100
|
```
|
|
70
101
|
|
|
71
|
-
|
|
102
|
+
---
|
|
72
103
|
|
|
73
104
|
## Installation
|
|
74
105
|
|
|
75
|
-
###
|
|
76
|
-
|
|
77
|
-
This server is designed to be run directly via `npx` in your MCP client configuration.
|
|
106
|
+
### NPX (recommended)
|
|
78
107
|
|
|
79
108
|
```bash
|
|
80
109
|
npx -y @j0hanz/thinkseq-mcp@latest
|
|
81
110
|
```
|
|
82
111
|
|
|
83
|
-
###
|
|
112
|
+
### Global Install
|
|
84
113
|
|
|
85
|
-
|
|
114
|
+
```bash
|
|
115
|
+
npm install -g @j0hanz/thinkseq-mcp
|
|
116
|
+
thinkseq
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### From Source
|
|
86
120
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
121
|
+
```bash
|
|
122
|
+
git clone https://github.com/j0hanz/thinkseq-mcp-server.git
|
|
123
|
+
cd thinkseq-mcp-server
|
|
124
|
+
npm install
|
|
125
|
+
npm run build
|
|
126
|
+
npm start
|
|
127
|
+
```
|
|
91
128
|
|
|
92
|
-
|
|
129
|
+
---
|
|
93
130
|
|
|
94
|
-
|
|
95
|
-
npm install
|
|
96
|
-
```
|
|
131
|
+
## Configuration
|
|
97
132
|
|
|
98
|
-
|
|
133
|
+
### CLI Arguments
|
|
99
134
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
135
|
+
| Flag | Type | Description |
|
|
136
|
+
| ------------------------------------ | ------ | ----------------------------------- |
|
|
137
|
+
| `--max-thoughts <number>` | number | Max thoughts to keep in memory |
|
|
138
|
+
| `--max-memory-mb <number>` | number | Max memory (MB) for stored thoughts |
|
|
139
|
+
| `--shutdown-timeout-ms <number>` | number | Graceful shutdown timeout (ms) |
|
|
140
|
+
| `--package-read-timeout-ms <number>` | number | Package.json read timeout (ms) |
|
|
141
|
+
| `-h`, `--help` | flag | Show help text |
|
|
103
142
|
|
|
104
|
-
|
|
143
|
+
### Engine Defaults
|
|
105
144
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
| Parameter | Default | Max |
|
|
146
|
+
| ------------------------- | -------- | ------ |
|
|
147
|
+
| Max thoughts per session | 500 | 10,000 |
|
|
148
|
+
| Max memory | 100 MB | — |
|
|
149
|
+
| Thought overhead estimate | 200 B | — |
|
|
150
|
+
| Max sessions (LRU) | 50 | 10,000 |
|
|
151
|
+
| Default total thoughts | 3 | 25 |
|
|
152
|
+
| Default shutdown timeout | 5,000 ms | — |
|
|
153
|
+
| Package read timeout | 2,000 ms | — |
|
|
109
154
|
|
|
110
|
-
|
|
155
|
+
### Environment Variables
|
|
111
156
|
|
|
112
|
-
|
|
157
|
+
| Variable | Default | Description |
|
|
158
|
+
| ------------------------------- | ------- | ------------------------------------------------------------------------------ |
|
|
159
|
+
| `THINKSEQ_INCLUDE_TEXT_CONTENT` | `true` | Set to `0`, `false`, `no`, or `off` to omit JSON string content from responses |
|
|
113
160
|
|
|
114
|
-
|
|
115
|
-
| :----------------------------------- | :-------------------------------------------- | :------ |
|
|
116
|
-
| `--max-thoughts <number>` | Max thoughts to keep in memory before pruning | 500 |
|
|
117
|
-
| `--max-memory-mb <number>` | Max memory (MB) for stored thoughts | 100 |
|
|
118
|
-
| `--shutdown-timeout-ms <number>` | Graceful shutdown timeout in ms | 5000 |
|
|
119
|
-
| `--package-read-timeout-ms <number>` | Package.json read timeout in ms | 2000 |
|
|
120
|
-
| `-h, --help` | Show help message | - |
|
|
161
|
+
---
|
|
121
162
|
|
|
122
|
-
##
|
|
163
|
+
## Usage
|
|
123
164
|
|
|
124
|
-
###
|
|
165
|
+
### stdio (default and only transport)
|
|
125
166
|
|
|
126
|
-
|
|
167
|
+
```bash
|
|
168
|
+
# Direct
|
|
169
|
+
npx -y @j0hanz/thinkseq-mcp@latest
|
|
127
170
|
|
|
128
|
-
|
|
171
|
+
# With options
|
|
172
|
+
npx -y @j0hanz/thinkseq-mcp@latest --max-thoughts 1000 --max-memory-mb 200
|
|
173
|
+
```
|
|
129
174
|
|
|
130
|
-
|
|
175
|
+
---
|
|
131
176
|
|
|
132
|
-
|
|
133
|
-
| :--------------- | :------ | :------: | :-------------------------------------------------------------------------------------------------------------------- |
|
|
134
|
-
| `thought` | string | Yes | Your current thinking step (1-8000 chars). |
|
|
135
|
-
| `sessionId` | string | No | Optional session identifier (max 200 chars) to isolate thought histories. |
|
|
136
|
-
| `totalThoughts` | integer | No | Estimated total thoughts (1-25, default: 3). |
|
|
137
|
-
| `revisesThought` | integer | No | Revise a previous thought by number. The original is preserved for audit, but the active chain rewinds to this point. |
|
|
177
|
+
## MCP Surface
|
|
138
178
|
|
|
139
|
-
|
|
179
|
+
### Tools
|
|
140
180
|
|
|
141
|
-
|
|
181
|
+
#### `thinkseq`
|
|
142
182
|
|
|
143
|
-
|
|
144
|
-
- `progress`: A value between 0 and 1 indicating completion.
|
|
145
|
-
- `isComplete`: Boolean indicating if the thought process is finished.
|
|
146
|
-
- `revisableThoughts`: Array of thought numbers that can be revised.
|
|
147
|
-
- `context`: Recent thoughts and revision information.
|
|
183
|
+
Record a concise thinking step. Supports sequential appending and destructive revision of prior steps.
|
|
148
184
|
|
|
149
|
-
**
|
|
185
|
+
**Parameters:**
|
|
150
186
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
187
|
+
| Name | Type | Required | Default | Description |
|
|
188
|
+
| ---------------- | ------ | -------- | ----------- | -------------------------------------------------------------------------- |
|
|
189
|
+
| `thought` | string | Yes | — | Your current thinking step (1–8,000 characters) |
|
|
190
|
+
| `sessionId` | string | No | `"default"` | Session identifier to isolate thought histories (1–200 chars) |
|
|
191
|
+
| `totalThoughts` | number | No | `3` | Estimated total thoughts (1–25) |
|
|
192
|
+
| `revisesThought` | number | No | — | Revise a previous thought by number (≥ 1). Original is preserved for audit |
|
|
157
193
|
|
|
158
|
-
**
|
|
194
|
+
**Returns** (structured output):
|
|
159
195
|
|
|
160
196
|
```json
|
|
161
197
|
{
|
|
162
198
|
"ok": true,
|
|
163
199
|
"result": {
|
|
164
200
|
"thoughtNumber": 1,
|
|
165
|
-
"totalThoughts":
|
|
166
|
-
"progress": 0.
|
|
201
|
+
"totalThoughts": 3,
|
|
202
|
+
"progress": 0.333,
|
|
167
203
|
"isComplete": false,
|
|
204
|
+
"thoughtHistoryLength": 1,
|
|
205
|
+
"hasRevisions": false,
|
|
206
|
+
"activePathLength": 1,
|
|
168
207
|
"revisableThoughts": [1],
|
|
208
|
+
"revisableThoughtsTotal": 1,
|
|
169
209
|
"context": {
|
|
170
210
|
"recentThoughts": [
|
|
171
211
|
{
|
|
172
212
|
"stepIndex": 1,
|
|
173
213
|
"number": 1,
|
|
174
|
-
"preview": "
|
|
214
|
+
"preview": "First step of reasoning..."
|
|
175
215
|
}
|
|
176
216
|
]
|
|
177
217
|
}
|
|
@@ -179,18 +219,77 @@ A JSON object containing the current state of the thinking process, including:
|
|
|
179
219
|
}
|
|
180
220
|
```
|
|
181
221
|
|
|
222
|
+
**Revision example:**
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"thought": "Better approach: use caching instead",
|
|
227
|
+
"revisesThought": 2
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Returns additional `revisionInfo` in `context`:
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"context": {
|
|
236
|
+
"recentThoughts": [...],
|
|
237
|
+
"revisionInfo": {
|
|
238
|
+
"revises": 2,
|
|
239
|
+
"supersedes": [2, 3],
|
|
240
|
+
"supersedesTotal": 2
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Error codes:**
|
|
247
|
+
|
|
248
|
+
| Code | Description |
|
|
249
|
+
| ------------------------------ | ------------------------------------------ |
|
|
250
|
+
| `E_THINK` | General processing error |
|
|
251
|
+
| `E_REVISION_MISSING` | `revisesThought` required but not provided |
|
|
252
|
+
| `E_REVISION_TARGET_NOT_FOUND` | Target thought number does not exist |
|
|
253
|
+
| `E_REVISION_TARGET_SUPERSEDED` | Target thought was already superseded |
|
|
254
|
+
|
|
182
255
|
### Resources
|
|
183
256
|
|
|
184
|
-
|
|
257
|
+
| URI | MIME Type | Description |
|
|
258
|
+
| ------------------------- | --------------- | ------------------------- |
|
|
259
|
+
| `internal://instructions` | `text/markdown` | Server usage instructions |
|
|
185
260
|
|
|
186
261
|
### Prompts
|
|
187
262
|
|
|
188
|
-
|
|
263
|
+
| Name | Description |
|
|
264
|
+
| ---------- | -------------------------------------- |
|
|
265
|
+
| `get-help` | Get usage instructions for this server |
|
|
266
|
+
|
|
267
|
+
---
|
|
189
268
|
|
|
190
269
|
## Client Configuration Examples
|
|
191
270
|
|
|
192
271
|
<details>
|
|
193
|
-
<summary
|
|
272
|
+
<summary>VS Code / VS Code Insiders</summary>
|
|
273
|
+
|
|
274
|
+
Add to your VS Code MCP settings (`.vscode/mcp.json` or User Settings):
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"mcpServers": {
|
|
279
|
+
"thinkseq": {
|
|
280
|
+
"command": "npx",
|
|
281
|
+
"args": ["-y", "@j0hanz/thinkseq-mcp@latest"]
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
</details>
|
|
288
|
+
|
|
289
|
+
<details>
|
|
290
|
+
<summary>Claude Desktop</summary>
|
|
291
|
+
|
|
292
|
+
Add to `claude_desktop_config.json`:
|
|
194
293
|
|
|
195
294
|
```json
|
|
196
295
|
{
|
|
@@ -206,9 +305,9 @@ _No prompts are currently exposed by this server._
|
|
|
206
305
|
</details>
|
|
207
306
|
|
|
208
307
|
<details>
|
|
209
|
-
<summary
|
|
308
|
+
<summary>Cursor</summary>
|
|
210
309
|
|
|
211
|
-
Add
|
|
310
|
+
Add to Cursor MCP settings:
|
|
212
311
|
|
|
213
312
|
```json
|
|
214
313
|
{
|
|
@@ -221,53 +320,124 @@ Add this to your `claude_desktop_config.json`:
|
|
|
221
320
|
}
|
|
222
321
|
```
|
|
223
322
|
|
|
323
|
+
Or use the deeplink:
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
cursor://anysphere.cursor-deeplink/mcp/install?name=thinkseq&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBqMGhhbnovdGhpbmtzZXEtbWNwQGxhdGVzdCJdfQ==
|
|
327
|
+
```
|
|
328
|
+
|
|
224
329
|
</details>
|
|
225
330
|
|
|
226
331
|
<details>
|
|
227
|
-
<summary
|
|
332
|
+
<summary>Windsurf</summary>
|
|
228
333
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
334
|
+
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
335
|
+
|
|
336
|
+
```json
|
|
337
|
+
{
|
|
338
|
+
"mcpServers": {
|
|
339
|
+
"thinkseq": {
|
|
340
|
+
"command": "npx",
|
|
341
|
+
"args": ["-y", "@j0hanz/thinkseq-mcp@latest"]
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
234
346
|
|
|
235
347
|
</details>
|
|
236
348
|
|
|
349
|
+
---
|
|
350
|
+
|
|
237
351
|
## Security
|
|
238
352
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
- **
|
|
353
|
+
### stdio Transport
|
|
354
|
+
|
|
355
|
+
- **stdout protection:** `console.log` and `console.warn` are bridged to MCP logging messages to prevent polluting the JSON-RPC stdio channel.
|
|
356
|
+
- **Initialization enforcement:** The server rejects any request before a valid `initialize` handshake and requires `notifications/initialized` before accepting tool calls.
|
|
357
|
+
- **Invalid message rejection:** Non-object and batch JSON-RPC messages are rejected with proper error codes.
|
|
358
|
+
- **Parse error handling:** Malformed JSON on stdin receives a JSON-RPC Parse Error response.
|
|
359
|
+
|
|
360
|
+
### Process Safety
|
|
361
|
+
|
|
362
|
+
- Unhandled rejections and uncaught exceptions are caught and result in a clean process exit.
|
|
363
|
+
- Graceful shutdown on `SIGTERM`/`SIGINT` with configurable timeout (default: 5 seconds).
|
|
364
|
+
|
|
365
|
+
---
|
|
242
366
|
|
|
243
367
|
## Development Workflow
|
|
244
368
|
|
|
245
|
-
|
|
369
|
+
### Install Dependencies
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
npm install
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Scripts
|
|
376
|
+
|
|
377
|
+
| Script | Command | Purpose |
|
|
378
|
+
| --------------- | -------------------------------------------- | ----------------------------------------------------------------- |
|
|
379
|
+
| `dev` | `tsc --watch --preserveWatchOutput` | Watch mode TypeScript compilation |
|
|
380
|
+
| `dev:run` | `node --env-file=.env --watch dist/index.js` | Run built server with auto-reload |
|
|
381
|
+
| `build` | `node scripts/tasks.mjs build` | Full build pipeline (clean → compile → validate → assets → chmod) |
|
|
382
|
+
| `start` | `node dist/index.js` | Run the built server |
|
|
383
|
+
| `test` | `node scripts/tasks.mjs test` | Run all tests (builds first) |
|
|
384
|
+
| `test:coverage` | `node scripts/tasks.mjs test --coverage` | Run tests with coverage |
|
|
385
|
+
| `type-check` | `node scripts/tasks.mjs type-check` | TypeScript type checking |
|
|
386
|
+
| `lint` | `eslint .` | Run ESLint |
|
|
387
|
+
| `lint:fix` | `eslint . --fix` | Auto-fix lint issues |
|
|
388
|
+
| `format` | `prettier --write .` | Format code with Prettier |
|
|
389
|
+
| `clean` | `node scripts/tasks.mjs clean` | Remove dist and build info files |
|
|
390
|
+
| `inspector` | `npx @modelcontextprotocol/inspector` | Launch MCP Inspector for debugging |
|
|
391
|
+
| `knip` | `knip` | Detect unused exports/dependencies |
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Build and Release
|
|
396
|
+
|
|
397
|
+
The build pipeline (`npm run build`) executes:
|
|
398
|
+
|
|
399
|
+
1. **Clean** — Remove `dist/` and `.tsbuildinfo` files
|
|
400
|
+
2. **Compile** — TypeScript compilation via `tsconfig.build.json`
|
|
401
|
+
3. **Validate** — Ensure `src/instructions.md` exists
|
|
402
|
+
4. **Copy assets** — Bundle `instructions.md` and `assets/` (including `logo.svg`) into `dist/`
|
|
403
|
+
5. **Make executable** — Set `dist/index.js` to mode `755`
|
|
404
|
+
|
|
405
|
+
### Publishing
|
|
246
406
|
|
|
247
|
-
|
|
248
|
-
npm install
|
|
249
|
-
```
|
|
407
|
+
Publishing is automated via GitHub Actions (`.github/workflows/publish.yml`):
|
|
250
408
|
|
|
251
|
-
|
|
409
|
+
- Triggered on GitHub release publication
|
|
410
|
+
- Pipeline: lint → type-check → test → coverage → build → `npm publish --access public`
|
|
411
|
+
- Uses npm Trusted Publishing (OIDC) for authentication
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Troubleshooting
|
|
416
|
+
|
|
417
|
+
### MCP Inspector
|
|
418
|
+
|
|
419
|
+
Use the MCP Inspector to debug and test the server interactively:
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
npx @modelcontextprotocol/inspector
|
|
423
|
+
```
|
|
252
424
|
|
|
253
|
-
|
|
254
|
-
npm run dev # Runs tsc in watch mode
|
|
255
|
-
npm run dev:run # Runs the built app in watch mode
|
|
256
|
-
```
|
|
425
|
+
### Common Issues
|
|
257
426
|
|
|
258
|
-
|
|
427
|
+
| Issue | Solution |
|
|
428
|
+
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
429
|
+
| Server not responding | Ensure Node.js ≥ 24 is installed; check `node --version` |
|
|
430
|
+
| `initialize must be first request` | Client must send `initialize` before any other request |
|
|
431
|
+
| `notifications/initialized must follow initialize` | Client must send `notifications/initialized` after successful `initialize` |
|
|
432
|
+
| Thoughts disappearing | Check `--max-thoughts` and `--max-memory-mb` limits; old thoughts are pruned when limits are reached |
|
|
433
|
+
| Session not found | Sessions are in-memory only; they reset on server restart. Max 50 sessions by default (LRU eviction) |
|
|
259
434
|
|
|
260
|
-
|
|
261
|
-
npm test
|
|
262
|
-
```
|
|
435
|
+
### stdout/stderr Guidance
|
|
263
436
|
|
|
264
|
-
|
|
437
|
+
When running as a stdio MCP server, **never write directly to stdout** from custom code — this would corrupt the JSON-RPC protocol. The server automatically bridges `console.log`/`console.warn` to MCP logging. Use `console.error` for debug output that bypasses MCP.
|
|
265
438
|
|
|
266
|
-
|
|
267
|
-
npm run lint
|
|
268
|
-
npm run format
|
|
269
|
-
```
|
|
439
|
+
---
|
|
270
440
|
|
|
271
|
-
##
|
|
441
|
+
## License
|
|
272
442
|
|
|
273
|
-
|
|
443
|
+
[MIT](https://opensource.org/licenses/MIT)
|
package/dist/app.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import { resolvePackageIdentity, resolveRunDependencies, } from './appConfig/runDependencies.js';
|
|
4
4
|
import { buildShutdownDependencies } from './appConfig/shutdown.js';
|
|
5
|
+
import { runWithContext } from './lib/context.js';
|
|
5
6
|
import { installConsoleBridge, installMcpLogging } from './lib/mcpLogging.js';
|
|
6
7
|
const toError = (value) => value instanceof Error ? value : new Error(String(value));
|
|
7
8
|
const createExit = (proc, exit) => exit ?? ((code) => proc.exit(code));
|
|
@@ -10,10 +11,10 @@ const createHandlerFor = (logError, exit) => (label) => (value) => {
|
|
|
10
11
|
logError(`thinkseq: ${label}: ${error.message}`);
|
|
11
12
|
exit(1);
|
|
12
13
|
};
|
|
13
|
-
function getLocalIconData() {
|
|
14
|
+
async function getLocalIconData() {
|
|
14
15
|
try {
|
|
15
16
|
const iconPath = new URL('../assets/logo.svg', import.meta.url);
|
|
16
|
-
const buffer =
|
|
17
|
+
const buffer = await readFile(iconPath);
|
|
17
18
|
if (buffer.length > 2 * 1024 * 1024) {
|
|
18
19
|
console.warn('Warning: logo.svg is larger than 2MB');
|
|
19
20
|
}
|
|
@@ -35,15 +36,17 @@ export async function run(deps = {}) {
|
|
|
35
36
|
const resolved = resolveRunDependencies(deps);
|
|
36
37
|
const pkg = await resolved.readPackageJson(AbortSignal.timeout(resolved.packageReadTimeoutMs));
|
|
37
38
|
const { name, version } = resolvePackageIdentity(pkg);
|
|
38
|
-
const localIcon = getLocalIconData();
|
|
39
|
+
const localIcon = await getLocalIconData();
|
|
39
40
|
const server = resolved.createServer(name, version, localIcon);
|
|
40
41
|
installMcpLogging(server);
|
|
41
42
|
const { flush: flushConsole, restore: restoreConsole } = installConsoleBridge(server);
|
|
42
43
|
process.on('exit', restoreConsole);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
runWithContext(() => {
|
|
45
|
+
resolved.publishLifecycleEvent({
|
|
46
|
+
type: 'lifecycle.started',
|
|
47
|
+
ts: resolved.now(),
|
|
48
|
+
});
|
|
49
|
+
}, { requestId: 'lifecycle.started' });
|
|
47
50
|
const engine = resolved.engineFactory();
|
|
48
51
|
resolved.registerTool(server, engine, localIcon);
|
|
49
52
|
const transport = await resolved.connectServer(server);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { ThinkingEngine } from '../engine.js';
|
|
@@ -9,9 +9,9 @@ import { registerThinkSeq } from '../tools/thinkseq.js';
|
|
|
9
9
|
import { installShutdownHandlers } from './shutdown.js';
|
|
10
10
|
const INSTRUCTIONS_URL = new URL('../instructions.md', import.meta.url);
|
|
11
11
|
const INSTRUCTIONS_FALLBACK = 'ThinkSeq is a tool for structured, sequential thinking with revision support.';
|
|
12
|
-
function readInstructionsText() {
|
|
12
|
+
async function readInstructionsText() {
|
|
13
13
|
try {
|
|
14
|
-
return
|
|
14
|
+
return await readFile(INSTRUCTIONS_URL, { encoding: 'utf8' });
|
|
15
15
|
}
|
|
16
16
|
catch {
|
|
17
17
|
return INSTRUCTIONS_FALLBACK;
|
|
@@ -21,28 +21,34 @@ function loadServerInstructions(raw) {
|
|
|
21
21
|
const trimmed = raw.trim();
|
|
22
22
|
return trimmed.length > 0 ? trimmed : INSTRUCTIONS_FALLBACK;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
resources: [
|
|
28
|
-
{
|
|
29
|
-
uri: 'internal://instructions',
|
|
30
|
-
name: 'Instructions',
|
|
31
|
-
mimeType: 'text/markdown',
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
}),
|
|
35
|
-
}), { title: 'Instructions', mimeType: 'text/markdown' }, (uri) => ({
|
|
36
|
-
contents: [
|
|
24
|
+
const INSTRUCTIONS_RESOURCE_TEMPLATE = new ResourceTemplate('internal://instructions', {
|
|
25
|
+
list: () => ({
|
|
26
|
+
resources: [
|
|
37
27
|
{
|
|
38
|
-
uri:
|
|
39
|
-
|
|
28
|
+
uri: 'internal://instructions',
|
|
29
|
+
name: 'Instructions',
|
|
40
30
|
mimeType: 'text/markdown',
|
|
41
31
|
},
|
|
42
32
|
],
|
|
43
|
-
})
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
const INSTRUCTIONS_METADATA = {
|
|
36
|
+
title: 'Instructions',
|
|
37
|
+
mimeType: 'text/markdown',
|
|
38
|
+
};
|
|
39
|
+
const readInstructionsResource = (uri) => ({
|
|
40
|
+
contents: [
|
|
41
|
+
{
|
|
42
|
+
uri: uri.href,
|
|
43
|
+
text: INSTRUCTIONS_TEXT,
|
|
44
|
+
mimeType: 'text/markdown',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
function registerInstructionsResource(server) {
|
|
49
|
+
server.registerResource('instructions', INSTRUCTIONS_RESOURCE_TEMPLATE, INSTRUCTIONS_METADATA, readInstructionsResource);
|
|
44
50
|
}
|
|
45
|
-
const INSTRUCTIONS_TEXT = readInstructionsText();
|
|
51
|
+
const INSTRUCTIONS_TEXT = await readInstructionsText();
|
|
46
52
|
const SERVER_INSTRUCTIONS = loadServerInstructions(INSTRUCTIONS_TEXT);
|
|
47
53
|
const DEFAULT_PACKAGE_READ_TIMEOUT_MS = 2000;
|
|
48
54
|
function buildServerCapabilities(overrides = {}) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runWithContext } from '../lib/context.js';
|
|
1
2
|
import { publishLifecycleEvent } from '../lib/diagnostics.js';
|
|
2
3
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5000;
|
|
3
4
|
function isRecord(value) {
|
|
@@ -35,11 +36,13 @@ function buildShutdownRunner(deps, proc) {
|
|
|
35
36
|
if (shuttingDown)
|
|
36
37
|
return;
|
|
37
38
|
shuttingDown = true;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
runWithContext(() => {
|
|
40
|
+
emit({
|
|
41
|
+
type: 'lifecycle.shutdown',
|
|
42
|
+
ts: timestamp(),
|
|
43
|
+
signal,
|
|
44
|
+
});
|
|
45
|
+
}, { requestId: `lifecycle.shutdown:${signal}` });
|
|
43
46
|
await closeAllWithinTimeout([deps.server, deps.engine, deps.transport], timeoutMs);
|
|
44
47
|
proc.exit(0);
|
|
45
48
|
};
|
|
@@ -24,7 +24,7 @@ export declare class ThoughtStore {
|
|
|
24
24
|
supersedesTotal: number;
|
|
25
25
|
};
|
|
26
26
|
getActiveThoughts(): readonly StoredThought[];
|
|
27
|
-
getActiveThoughtNumbers(max?: number): number[];
|
|
27
|
+
getActiveThoughtNumbers(max?: number): readonly number[];
|
|
28
28
|
getThoughtByNumber(thoughtNumber: number): StoredThought | undefined;
|
|
29
29
|
getTotalLength(): number;
|
|
30
30
|
pruneHistoryIfNeeded(): void;
|
package/dist/engine.js
CHANGED
|
@@ -31,22 +31,20 @@ class PinnedLruSessions {
|
|
|
31
31
|
this.#evictIfNeeded();
|
|
32
32
|
}
|
|
33
33
|
#evictIfNeeded() {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
if (this.#map.size <= this.#max)
|
|
35
|
+
return;
|
|
36
|
+
for (const key of this.#map.keys()) {
|
|
37
|
+
if (this.#map.size <= this.#max)
|
|
38
|
+
break;
|
|
39
|
+
if (key === this.#pinnedKey) {
|
|
39
40
|
const pinned = this.#map.get(this.#pinnedKey);
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
if (pinned) {
|
|
42
|
+
this.#map.delete(this.#pinnedKey);
|
|
43
|
+
this.#map.set(this.#pinnedKey, pinned);
|
|
43
44
|
}
|
|
44
|
-
// Move pinned to most-recent and try again.
|
|
45
|
-
this.#map.delete(this.#pinnedKey);
|
|
46
|
-
this.#map.set(this.#pinnedKey, pinned);
|
|
47
45
|
continue;
|
|
48
46
|
}
|
|
49
|
-
this.#map.delete(
|
|
47
|
+
this.#map.delete(key);
|
|
50
48
|
}
|
|
51
49
|
}
|
|
52
50
|
}
|
|
@@ -145,7 +143,7 @@ export class ThinkingEngine {
|
|
|
145
143
|
...(details.revisionOf !== undefined && {
|
|
146
144
|
revisionOf: details.revisionOf,
|
|
147
145
|
}),
|
|
148
|
-
byteLength: Buffer.byteLength(input.thought),
|
|
146
|
+
byteLength: Buffer.byteLength(input.thought, 'utf8'),
|
|
149
147
|
};
|
|
150
148
|
}
|
|
151
149
|
#resolveEffectiveTotalThoughts(store, input, fallback) {
|
package/dist/instructions.md
CHANGED
|
@@ -1,43 +1,56 @@
|
|
|
1
1
|
# ThinkSeq Instructions
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Available as resource (`internal://instructions`) or prompt (`get-help`). Load when unsure about tool usage.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **Primary Resources:** `Thoughts`, `RevisionChain`, `ProgressContext`.
|
|
7
|
+
## CORE CAPABILITY
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
- Domain: In-memory, sequential thinking with revision (destructive rewind) per session.
|
|
10
|
+
- Primary Resources: `Thoughts`, `RevisionChain`, `ProgressContext`.
|
|
11
|
+
- Tools: `thinkseq` (WRITE)
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
---
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
## THE "GOLDEN PATH" WORKFLOWS (CRITICAL)
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
### WORKFLOW A: Capture a reasoning chain
|
|
18
|
+
|
|
19
|
+
1. Call `thinkseq` with: `{ "thought": "..." }` (optionally `totalThoughts`, `sessionId`).
|
|
17
20
|
2. Continue calling `thinkseq` for each step.
|
|
18
21
|
3. Read `progress` and `isComplete` from the output to know when to stop.
|
|
19
|
-
|
|
22
|
+
NOTE: Keep each step atomic; one decision per call.
|
|
20
23
|
|
|
21
|
-
###
|
|
24
|
+
### WORKFLOW B: Revise a prior step
|
|
22
25
|
|
|
23
26
|
1. Call `thinkseq` to get the latest `revisableThoughts` list (returned in every response).
|
|
24
27
|
2. Call `thinkseq` again with `revisesThought` set to a valid entry from that list.
|
|
25
28
|
3. Continue from the revised step.
|
|
26
|
-
|
|
29
|
+
NOTE: Never guess `revisesThought`; always pick from `revisableThoughts`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## TOOL NUANCES & GOTCHAS
|
|
34
|
+
|
|
35
|
+
`thinkseq`
|
|
36
|
+
|
|
37
|
+
- Purpose: Append or revise a thought in the current session.
|
|
38
|
+
- Inputs: `thought` (1–8000 chars), `totalThoughts` (1–25), `revisesThought` (int ≥ 1).
|
|
39
|
+
- Side effects: Mutates in-memory thought history; revisions supersede the target and later active thoughts (destructive rewind).
|
|
40
|
+
- Defaults: If `totalThoughts` is omitted, the engine uses 3 or the last active total.
|
|
41
|
+
- Compatibility: Set `THINKSEQ_INCLUDE_TEXT_CONTENT=0` env var to omit JSON string content if needed.
|
|
42
|
+
|
|
43
|
+
---
|
|
27
44
|
|
|
28
|
-
##
|
|
45
|
+
## ERROR HANDLING STRATEGY
|
|
29
46
|
|
|
30
|
-
|
|
47
|
+
- `E_REVISION_MISSING` / `E_REVISION_TARGET_NOT_FOUND`: Fetch `revisableThoughts` and retry with a valid ID.
|
|
48
|
+
- `E_REVISION_TARGET_SUPERSEDED`: Target already superseded; pick an active thought from `revisableThoughts`.
|
|
49
|
+
- `E_THINK`: Verify inputs and retry once.
|
|
50
|
+
- `E_TIMEOUT`: Reduce thought length.
|
|
31
51
|
|
|
32
|
-
|
|
33
|
-
- **Purpose:** Append or revise a thought in the current session.
|
|
34
|
-
- **Inputs:** `thought` (1–8000 chars), `totalThoughts` (1–25), `revisesThought` (int ≥ 1).
|
|
35
|
-
- **Side effects:** Mutates in-memory thought history; revisions supersede the target and later active thoughts (destructive rewind).
|
|
36
|
-
- **Defaults:** If `totalThoughts` is omitted, the engine uses 3 or the last active total.
|
|
37
|
-
- **Compatibility:** Set `THINKSEQ_INCLUDE_TEXT_CONTENT=0` env var to omit JSON string content if needed.
|
|
52
|
+
---
|
|
38
53
|
|
|
39
|
-
##
|
|
54
|
+
## RESOURCES
|
|
40
55
|
|
|
41
|
-
-
|
|
42
|
-
- **`E_THINK`**: Verify inputs and retry once.
|
|
43
|
-
- **`E_TIMEOUT`**: Reduce thought length.
|
|
56
|
+
- `internal://instructions`: This document.
|
package/dist/lib/context.js
CHANGED
|
@@ -3,11 +3,19 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { performance } from 'node:perf_hooks';
|
|
4
4
|
const storage = new AsyncLocalStorage();
|
|
5
5
|
export function runWithContext(callback, context) {
|
|
6
|
+
const current = storage.getStore();
|
|
6
7
|
const store = {
|
|
7
|
-
requestId: context?.requestId ?? randomUUID(),
|
|
8
|
-
startedAt: context?.startedAt ?? performance.now(),
|
|
9
|
-
startedAtEpochMs: context?.startedAtEpochMs ?? Date.now(),
|
|
8
|
+
requestId: context?.requestId ?? current?.requestId ?? randomUUID(),
|
|
9
|
+
startedAt: context?.startedAt ?? current?.startedAt ?? performance.now(),
|
|
10
|
+
startedAtEpochMs: context?.startedAtEpochMs ?? current?.startedAtEpochMs ?? Date.now(),
|
|
10
11
|
};
|
|
12
|
+
if (!current)
|
|
13
|
+
return storage.run(store, callback);
|
|
14
|
+
if (current.requestId === store.requestId &&
|
|
15
|
+
current.startedAt === store.startedAt &&
|
|
16
|
+
current.startedAtEpochMs === store.startedAtEpochMs) {
|
|
17
|
+
return callback();
|
|
18
|
+
}
|
|
11
19
|
return storage.run(store, callback);
|
|
12
20
|
}
|
|
13
21
|
export function getRequestContext() {
|
|
@@ -25,7 +25,7 @@ type ToolEventBase = {
|
|
|
25
25
|
export type ToolEvent = ToolEventBase & {
|
|
26
26
|
context?: EventContext;
|
|
27
27
|
};
|
|
28
|
-
|
|
28
|
+
type LifecycleEventBase = {
|
|
29
29
|
type: 'lifecycle.started';
|
|
30
30
|
ts: number;
|
|
31
31
|
} | {
|
|
@@ -33,6 +33,9 @@ export type LifecycleEvent = {
|
|
|
33
33
|
ts: number;
|
|
34
34
|
signal: string;
|
|
35
35
|
};
|
|
36
|
+
export type LifecycleEvent = LifecycleEventBase & {
|
|
37
|
+
context?: EventContext;
|
|
38
|
+
};
|
|
36
39
|
export declare function publishToolEvent(event: ToolEvent): void;
|
|
37
40
|
export declare function publishLifecycleEvent(event: LifecycleEvent): void;
|
|
38
41
|
export {};
|
package/dist/lib/diagnostics.js
CHANGED
package/dist/lib/errors.d.ts
CHANGED
|
@@ -13,8 +13,5 @@ export interface ErrorResponse extends Record<string, unknown> {
|
|
|
13
13
|
};
|
|
14
14
|
isError: true;
|
|
15
15
|
}
|
|
16
|
-
export interface CreateErrorResponseOptions {
|
|
17
|
-
includeTextContent?: boolean;
|
|
18
|
-
}
|
|
19
16
|
export declare function getErrorMessage(error: unknown): string;
|
|
20
|
-
export declare function createErrorResponse(code: string, message: string, result?: unknown
|
|
17
|
+
export declare function createErrorResponse(code: string, message: string, result?: unknown): ErrorResponse;
|
package/dist/lib/errors.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
1
2
|
export function getErrorMessage(error) {
|
|
2
3
|
if (error instanceof Error)
|
|
3
4
|
return error.message;
|
|
4
5
|
if (typeof error === 'string' && error.length > 0)
|
|
5
6
|
return error;
|
|
7
|
+
if (error !== null && error !== undefined) {
|
|
8
|
+
return inspect(error, { depth: 2, breakLength: 120, maxArrayLength: 20 });
|
|
9
|
+
}
|
|
6
10
|
return 'Unknown error';
|
|
7
11
|
}
|
|
8
|
-
export function createErrorResponse(code, message, result
|
|
12
|
+
export function createErrorResponse(code, message, result) {
|
|
9
13
|
const structured = {
|
|
10
14
|
ok: false,
|
|
11
15
|
error: { code, message },
|
|
12
16
|
...(result !== undefined && { result }),
|
|
13
17
|
};
|
|
14
|
-
const includeTextContent = options.includeTextContent ?? true;
|
|
15
18
|
const response = {
|
|
16
|
-
content:
|
|
17
|
-
? [{ type: 'text', text: JSON.stringify(structured) }]
|
|
18
|
-
: [],
|
|
19
|
+
content: [{ type: 'text', text: JSON.stringify(structured) }],
|
|
19
20
|
structuredContent: structured,
|
|
20
21
|
isError: true,
|
|
21
22
|
};
|
package/dist/lib/mcpLogging.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import diagnosticsChannel from 'node:diagnostics_channel';
|
|
2
|
+
import { format } from 'node:util';
|
|
2
3
|
const TOOL_LOGGER = 'thinkseq.tool';
|
|
3
4
|
const LIFECYCLE_LOGGER = 'thinkseq.lifecycle';
|
|
4
5
|
const toolChannel = diagnosticsChannel.channel('thinkseq:tool');
|
|
@@ -58,37 +59,52 @@ export function installMcpLogging(target) {
|
|
|
58
59
|
lifecycleChannel.unsubscribe(onLifecycle);
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
|
-
function createConsoleSender(target) {
|
|
62
|
+
function createConsoleSender(target, level) {
|
|
62
63
|
return (text) => {
|
|
63
64
|
void target
|
|
64
|
-
.sendLoggingMessage({ level
|
|
65
|
+
.sendLoggingMessage({ level, logger: 'console', data: text })
|
|
65
66
|
.catch(() => undefined);
|
|
66
67
|
};
|
|
67
68
|
}
|
|
68
69
|
export function installConsoleBridge(target) {
|
|
69
70
|
const buffer = [];
|
|
70
71
|
let isReady = false;
|
|
71
|
-
const
|
|
72
|
+
const sendInfo = createConsoleSender(target, 'info');
|
|
73
|
+
const sendWarn = createConsoleSender(target, 'warning');
|
|
72
74
|
const originalLog = console.log;
|
|
75
|
+
const originalWarn = console.warn;
|
|
73
76
|
console.log = (...args) => {
|
|
74
|
-
const text = args
|
|
75
|
-
.map((a) => (typeof a === 'string' ? a : JSON.stringify(a)))
|
|
76
|
-
.join(' ');
|
|
77
|
+
const text = format(...args);
|
|
77
78
|
if (isReady) {
|
|
78
|
-
|
|
79
|
+
sendInfo(text);
|
|
79
80
|
}
|
|
80
81
|
else {
|
|
81
|
-
buffer.push(text);
|
|
82
|
+
buffer.push({ level: 'info', text });
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
console.warn = (...args) => {
|
|
86
|
+
const text = format(...args);
|
|
87
|
+
if (isReady) {
|
|
88
|
+
sendWarn(text);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
buffer.push({ level: 'warning', text });
|
|
82
92
|
}
|
|
83
93
|
};
|
|
84
94
|
return {
|
|
85
95
|
flush: () => {
|
|
86
96
|
isReady = true;
|
|
87
|
-
buffer.forEach(
|
|
97
|
+
buffer.forEach((item) => {
|
|
98
|
+
if (item.level === 'info')
|
|
99
|
+
sendInfo(item.text);
|
|
100
|
+
else
|
|
101
|
+
sendWarn(item.text);
|
|
102
|
+
});
|
|
88
103
|
buffer.length = 0;
|
|
89
104
|
},
|
|
90
105
|
restore: () => {
|
|
91
106
|
console.log = originalLog;
|
|
107
|
+
console.warn = originalWarn;
|
|
92
108
|
},
|
|
93
109
|
};
|
|
94
110
|
}
|
package/dist/lib/stdioGuards.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { getRequestContext, runWithContext } from './context.js';
|
|
2
3
|
const INIT_FIRST_ERROR_MESSAGE = 'initialize must be the first request';
|
|
4
|
+
const INITIALIZED_NOTIFICATION_METHOD = 'notifications/initialized';
|
|
5
|
+
const INIT_NOTIFICATION_REQUIRED_MESSAGE = 'notifications/initialized must follow initialize';
|
|
3
6
|
function isStdioMessageTransport(value) {
|
|
4
7
|
if (!value || typeof value !== 'object')
|
|
5
8
|
return false;
|
|
@@ -43,6 +46,29 @@ function parseJsonRpcResponse(message) {
|
|
|
43
46
|
const { error } = message;
|
|
44
47
|
return { id, error: hasError ? error : undefined };
|
|
45
48
|
}
|
|
49
|
+
function resolveMessageRequestId(message) {
|
|
50
|
+
const methodMessage = parseJsonRpcMethodMessage(message);
|
|
51
|
+
if (methodMessage?.hasId && methodMessage.id !== null) {
|
|
52
|
+
return String(methodMessage.id);
|
|
53
|
+
}
|
|
54
|
+
const response = parseJsonRpcResponse(message);
|
|
55
|
+
if (response && response.id !== null) {
|
|
56
|
+
return String(response.id);
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
function withMessageContext(message, handler) {
|
|
61
|
+
if (getRequestContext()) {
|
|
62
|
+
handler();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const requestId = resolveMessageRequestId(message);
|
|
66
|
+
if (requestId) {
|
|
67
|
+
runWithContext(handler, { requestId });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
runWithContext(handler);
|
|
71
|
+
}
|
|
46
72
|
function getIdKey(id) {
|
|
47
73
|
return id === null ? 'null' : String(id);
|
|
48
74
|
}
|
|
@@ -97,32 +123,51 @@ function trackInitializeResponse(message, state) {
|
|
|
97
123
|
function handleInitializeMethod(methodMessage, state, originalOnMessage, message, extra) {
|
|
98
124
|
state.pendingInitIds.add(getIdKey(methodMessage.id));
|
|
99
125
|
originalOnMessage(message, extra);
|
|
100
|
-
|
|
126
|
+
}
|
|
127
|
+
function handleInitializedNotification(state, originalOnMessage, message, extra) {
|
|
128
|
+
if (!state.sawInitialize)
|
|
129
|
+
return;
|
|
130
|
+
state.sawInitializedNotification = true;
|
|
131
|
+
originalOnMessage(message, extra);
|
|
101
132
|
}
|
|
102
133
|
function handleNonInitializedRequest(methodMessage, transport) {
|
|
103
134
|
if (methodMessage.hasId) {
|
|
104
135
|
sendError(transport, ErrorCode.InvalidRequest, INIT_FIRST_ERROR_MESSAGE, methodMessage.id);
|
|
105
136
|
}
|
|
106
|
-
|
|
137
|
+
}
|
|
138
|
+
function handleMissingInitializedNotification(methodMessage, transport) {
|
|
139
|
+
if (methodMessage.hasId) {
|
|
140
|
+
sendError(transport, ErrorCode.InvalidRequest, INIT_NOTIFICATION_REQUIRED_MESSAGE, methodMessage.id);
|
|
141
|
+
}
|
|
107
142
|
}
|
|
108
143
|
function createInitGuardHandler(state, originalOnMessage, transport) {
|
|
109
144
|
return (message, extra) => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
if (methodMessage.method === 'initialize') {
|
|
116
|
-
if (!methodMessage.hasId)
|
|
145
|
+
withMessageContext(message, () => {
|
|
146
|
+
const methodMessage = parseJsonRpcMethodMessage(message);
|
|
147
|
+
if (!methodMessage) {
|
|
148
|
+
originalOnMessage(message, extra);
|
|
117
149
|
return;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
150
|
+
}
|
|
151
|
+
if (methodMessage.method === 'initialize') {
|
|
152
|
+
if (!methodMessage.hasId)
|
|
153
|
+
return;
|
|
154
|
+
handleInitializeMethod(methodMessage, state, originalOnMessage, message, extra);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (methodMessage.method === INITIALIZED_NOTIFICATION_METHOD) {
|
|
158
|
+
handleInitializedNotification(state, originalOnMessage, message, extra);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!state.sawInitialize) {
|
|
162
|
+
handleNonInitializedRequest(methodMessage, transport);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!state.sawInitializedNotification) {
|
|
166
|
+
handleMissingInitializedNotification(methodMessage, transport);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
originalOnMessage(message, extra);
|
|
170
|
+
});
|
|
126
171
|
};
|
|
127
172
|
}
|
|
128
173
|
export function installStdioInitializationGuards(transport) {
|
|
@@ -133,6 +178,7 @@ export function installStdioInitializationGuards(transport) {
|
|
|
133
178
|
return;
|
|
134
179
|
const state = {
|
|
135
180
|
sawInitialize: false,
|
|
181
|
+
sawInitializedNotification: false,
|
|
136
182
|
pendingInitIds: new Set(),
|
|
137
183
|
};
|
|
138
184
|
wrapSendForInitTracking(transport, state);
|
|
@@ -145,13 +191,15 @@ export function installStdioInvalidMessageGuards(transport) {
|
|
|
145
191
|
if (!originalOnMessage)
|
|
146
192
|
return;
|
|
147
193
|
transport.onmessage = (message, extra) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
194
|
+
withMessageContext(message, () => {
|
|
195
|
+
// MCP stdio is line-delimited JSON-RPC (one object per line). JSON-RPC
|
|
196
|
+
// batching is removed in newer revisions; treat arrays as invalid.
|
|
197
|
+
if (isInvalidJsonRpcMessageShape(message)) {
|
|
198
|
+
sendError(transport, ErrorCode.InvalidRequest, 'Invalid Request');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
originalOnMessage(message, extra);
|
|
202
|
+
});
|
|
155
203
|
};
|
|
156
204
|
}
|
|
157
205
|
export function installStdioParseErrorResponder(transport) {
|
package/dist/lib/types.d.ts
CHANGED
package/dist/tools/thinkseq.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
|
-
import { APP_ENV } from '../appConfig/env.js';
|
|
3
2
|
import { runWithContext } from '../lib/context.js';
|
|
4
3
|
import { publishToolEvent } from '../lib/diagnostics.js';
|
|
5
4
|
import { createErrorResponse, getErrorMessage } from '../lib/errors.js';
|
|
@@ -40,16 +39,17 @@ function finalizeToolEvent(outcome, durationMs, extra) {
|
|
|
40
39
|
});
|
|
41
40
|
void sendProgress(extra, 1, 'failed');
|
|
42
41
|
}
|
|
43
|
-
function buildErrorResponse(code, message
|
|
44
|
-
return createErrorResponse(code, message
|
|
42
|
+
function buildErrorResponse(code, message) {
|
|
43
|
+
return createErrorResponse(code, message);
|
|
45
44
|
}
|
|
46
|
-
function buildToolResponse(result
|
|
45
|
+
function buildToolResponse(result) {
|
|
47
46
|
if (!result.ok)
|
|
48
|
-
return buildErrorResponse(result.error.code, result.error.message
|
|
47
|
+
return buildErrorResponse(result.error.code, result.error.message);
|
|
49
48
|
const structured = {
|
|
50
49
|
ok: true,
|
|
51
50
|
result: {
|
|
52
51
|
...result.result,
|
|
52
|
+
revisableThoughts: [...result.result.revisableThoughts],
|
|
53
53
|
context: {
|
|
54
54
|
...result.result.context,
|
|
55
55
|
recentThoughts: [...result.result.context.recentThoughts],
|
|
@@ -57,9 +57,7 @@ function buildToolResponse(result, options) {
|
|
|
57
57
|
},
|
|
58
58
|
};
|
|
59
59
|
return {
|
|
60
|
-
content:
|
|
61
|
-
? [{ type: 'text', text: JSON.stringify(structured) }]
|
|
62
|
-
: [],
|
|
60
|
+
content: [{ type: 'text', text: JSON.stringify(structured) }],
|
|
63
61
|
structuredContent: structured,
|
|
64
62
|
};
|
|
65
63
|
}
|
|
@@ -111,7 +109,7 @@ function resolveSessionId(input, extra) {
|
|
|
111
109
|
return String(found);
|
|
112
110
|
return 'default';
|
|
113
111
|
}
|
|
114
|
-
async function processThoughtWithTiming(engine, sessionId, normalized, extra
|
|
112
|
+
async function processThoughtWithTiming(engine, sessionId, normalized, extra) {
|
|
115
113
|
publishToolEvent({ type: 'tool.start', tool: 'thinkseq', ts: Date.now() });
|
|
116
114
|
const start = performance.now();
|
|
117
115
|
void sendProgress(extra, 0, 'started');
|
|
@@ -121,10 +119,10 @@ async function processThoughtWithTiming(engine, sessionId, normalized, extra, in
|
|
|
121
119
|
: engine.processThought(normalized));
|
|
122
120
|
const durationMs = getDurationMs(start);
|
|
123
121
|
handleThoughtResult(result, durationMs, extra);
|
|
124
|
-
return buildToolResponse(result
|
|
122
|
+
return buildToolResponse(result);
|
|
125
123
|
}
|
|
126
124
|
catch (err) {
|
|
127
|
-
return handleThoughtError(err, start, extra
|
|
125
|
+
return handleThoughtError(err, start, extra);
|
|
128
126
|
}
|
|
129
127
|
}
|
|
130
128
|
function handleThoughtResult(result, durationMs, extra) {
|
|
@@ -138,11 +136,11 @@ function handleThoughtResult(result, durationMs, extra) {
|
|
|
138
136
|
errorMessage: result.error.message,
|
|
139
137
|
}, durationMs, extra);
|
|
140
138
|
}
|
|
141
|
-
function handleThoughtError(err, start, extra
|
|
139
|
+
function handleThoughtError(err, start, extra) {
|
|
142
140
|
const errorMessage = getErrorMessage(err);
|
|
143
141
|
const durationMs = getDurationMs(start);
|
|
144
142
|
finalizeToolEvent({ ok: false, errorCode: 'E_THINK', errorMessage }, durationMs, extra);
|
|
145
|
-
return buildErrorResponse('E_THINK', errorMessage
|
|
143
|
+
return buildErrorResponse('E_THINK', errorMessage);
|
|
146
144
|
}
|
|
147
145
|
function buildThoughtData(input) {
|
|
148
146
|
return {
|
|
@@ -159,10 +157,9 @@ async function handleThinkSeq(engine, input, extra) {
|
|
|
159
157
|
const requestId = extra?.requestId;
|
|
160
158
|
const context = requestId === undefined ? undefined : { requestId: String(requestId) };
|
|
161
159
|
return runWithContext(async () => {
|
|
162
|
-
const includeTextContent = APP_ENV.INCLUDE_TEXT_CONTENT;
|
|
163
160
|
const normalized = buildThoughtData(input);
|
|
164
161
|
const sessionId = resolveSessionId(input, extra);
|
|
165
|
-
return processThoughtWithTiming(engine, sessionId, normalized, extra
|
|
162
|
+
return processThoughtWithTiming(engine, sessionId, normalized, extra);
|
|
166
163
|
}, context);
|
|
167
164
|
}
|
|
168
165
|
export function registerThinkSeq(server, engine, icon) {
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/thinkseq-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"mcpName": "io.github.j0hanz/thinkseq",
|
|
5
5
|
"description": "An thinking and reasoning engine for the Model Context Protocol (MCP).",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
|
-
".":
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
11
15
|
},
|
|
12
|
-
"sideEffects": false,
|
|
13
16
|
"bin": {
|
|
14
17
|
"thinkseq": "dist/index.js"
|
|
15
18
|
},
|
|
@@ -53,13 +56,13 @@
|
|
|
53
56
|
"homepage": "https://github.com/j0hanz/thinkseq-mcp-server#readme",
|
|
54
57
|
"license": "MIT",
|
|
55
58
|
"dependencies": {
|
|
56
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
59
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
57
60
|
"zod": "^4.3.6"
|
|
58
61
|
},
|
|
59
62
|
"devDependencies": {
|
|
60
63
|
"@eslint/js": "^9.39.2",
|
|
61
64
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
62
|
-
"@types/node": "^
|
|
65
|
+
"@types/node": "^24",
|
|
63
66
|
"eslint": "^9.39.2",
|
|
64
67
|
"eslint-config-prettier": "^10.1.8",
|
|
65
68
|
"eslint-plugin-de-morgan": "^2.0.0",
|
|
@@ -72,6 +75,6 @@
|
|
|
72
75
|
"typescript-eslint": "^8.54.0"
|
|
73
76
|
},
|
|
74
77
|
"engines": {
|
|
75
|
-
"node": ">=
|
|
78
|
+
"node": ">=24"
|
|
76
79
|
}
|
|
77
80
|
}
|