@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 CHANGED
@@ -1,177 +1,217 @@
1
1
  # ThinkSeq MCP Server
2
2
 
3
- <img src="assets/logo.svg" alt="ThinkSeq MCP Server Logo" width="225" />
3
+ [![npm version](https://img.shields.io/npm/v/@j0hanz/thinkseq-mcp)](https://www.npmjs.com/package/@j0hanz/thinkseq-mcp) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D24-green)](https://nodejs.org) [![TypeScript](https://img.shields.io/badge/TypeScript-5.9%2B-blue)](https://www.typescriptlang.org) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-%5E1.26-purple)](https://modelcontextprotocol.io)
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/@j0hanz/thinkseq-mcp.svg)](https://www.npmjs.com/package/@j0hanz/thinkseq-mcp)[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)[![Powered by MCP](https://img.shields.io/badge/MCP-SDK-blue)](https://github.com/modelcontextprotocol/typescript-sdk)
5
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0078d7?logo=visualstudiocode&logoColor=white)](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) [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?logo=visualstudiocode&logoColor=white)](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
- [![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=thinkseq&inputs=%5B%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40j0hanz%2Fthinkseq-mcp%40latest%22%5D%7D) [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=thinkseq&inputs=%5B%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40j0hanz%2Fthinkseq-mcp%40latest%22%5D%7D&quality=insiders)
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
- [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=thinkseq&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBqMGhhbnovdGhpbmtzZXEtbWNwQGxhdGVzdCJdfQ==)
10
-
11
- An MCP server for structured, sequential thinking with revision support.
9
+ ---
12
10
 
13
11
  ## Overview
14
12
 
15
- ThinkSeq exposes a single MCP tool, `thinkseq`, that enables Language Models to "think" in a structured, step-by-step manner. It maintains an in-memory history of thoughts, calculates progress, and critically allows for **destructive revision**—where a model can realize a mistake, "rewind" to a previous step, and branch off with a correction. This capability mirrors human reasoning patterns and improves problem-solving accuracy for complex tasks.
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 Thinking**: Records thoughts as discrete steps with auto-incrementing numbers.
20
- - **Progress Tracking**: Automatically calculates progress (0.0 to 1.0) based on estimated total thoughts.
21
- - **Revision Support**: Allows models to revise specific past thoughts, superseding the old path and starting a new reasoning branch.
22
- - **Context Awareness**: Returns recent thoughts and revision context with every tool call to keep the model grounded.
23
- - **Session Isolation**: Supports multiple concurrent thinking sessions via `sessionId`.
24
- - **Memory Management**: Configurable limits for max thoughts and memory usage to prevent resource exhaustion.
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
- - **Runtime**: Node.js >=22.0.0
29
- - **Language**: TypeScript 5.9+
30
- - **MCP SDK**: `@modelcontextprotocol/sdk`
31
- - **Validation**: `zod`
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
- ```text
36
- c:\thinkseq-mcp
37
- ├── dist/ # Compiled JavaScript
52
+ ```
53
+ thinkseq-mcp/
38
54
  ├── src/
39
- │ ├── app.ts # Application entry and MCP wiring
40
- │ ├── engine.ts # Core thinking engine logic
41
- │ ├── engineConfig.ts # Configuration defaults
42
- │ ├── index.ts # CLI entrypoint
43
- │ ├── lib/ # Utilities (CLI, logging, types)
44
- │ ├── schemas/ # Zod schemas for inputs/outputs
45
- └── tools/ # MCP tool definitions
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**: Version 22.0.0 or higher.
78
+ - **Node.js** 24
79
+ - **npm** (included with Node.js)
53
80
 
54
- ## Quickstart
81
+ ---
55
82
 
56
- To run the server using `npx`:
83
+ ## Quickstart
57
84
 
58
85
  ```bash
59
86
  npx -y @j0hanz/thinkseq-mcp@latest
60
87
  ```
61
88
 
62
- <details>
63
- <summary><b>Quick Test with MCP Inspector</b></summary>
89
+ Add to your MCP client configuration:
64
90
 
65
- You can inspect the tools using the MCP Inspector:
66
-
67
- ```bash
68
- npx @modelcontextprotocol/inspector npx -y @j0hanz/thinkseq-mcp@latest
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
- </details>
102
+ ---
72
103
 
73
104
  ## Installation
74
105
 
75
- ### Using NPX (Recommended)
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
- ### From Source
112
+ ### Global Install
84
113
 
85
- 1. Clone the repository:
114
+ ```bash
115
+ npm install -g @j0hanz/thinkseq-mcp
116
+ thinkseq
117
+ ```
118
+
119
+ ### From Source
86
120
 
87
- ```bash
88
- git clone https://github.com/j0hanz/thinkseq-mcp-server.git
89
- cd thinkseq-mcp-server
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
- 2. Install dependencies:
129
+ ---
93
130
 
94
- ```bash
95
- npm install
96
- ```
131
+ ## Configuration
97
132
 
98
- 3. Build the project:
133
+ ### CLI Arguments
99
134
 
100
- ```bash
101
- npm run build
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
- 4. Run the server:
143
+ ### Engine Defaults
105
144
 
106
- ```bash
107
- node dist/index.js
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
- ## Configuration
155
+ ### Environment Variables
111
156
 
112
- The server is configured via CLI arguments.
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
- | Argument | Description | Default |
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
- ## MCP Surface
163
+ ## Usage
123
164
 
124
- ### Tools
165
+ ### stdio (default and only transport)
125
166
 
126
- #### `thinkseq`
167
+ ```bash
168
+ # Direct
169
+ npx -y @j0hanz/thinkseq-mcp@latest
127
170
 
128
- Record a concise thinking step. Be brief: capture only the essential insight, calculation, or decision.
171
+ # With options
172
+ npx -y @j0hanz/thinkseq-mcp@latest --max-thoughts 1000 --max-memory-mb 200
173
+ ```
129
174
 
130
- **Parameters:**
175
+ ---
131
176
 
132
- | Name | Type | Required | Description |
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
- **Returns:**
179
+ ### Tools
140
180
 
141
- A JSON object containing the current state of the thinking process, including:
181
+ #### `thinkseq`
142
182
 
143
- - `thoughtNumber`: The current step number.
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
- **Example Input:**
185
+ **Parameters:**
150
186
 
151
- ```json
152
- {
153
- "thought": "I need to calculate the fibonacci sequence up to 10.",
154
- "totalThoughts": 5
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
- **Example Output:**
194
+ **Returns** (structured output):
159
195
 
160
196
  ```json
161
197
  {
162
198
  "ok": true,
163
199
  "result": {
164
200
  "thoughtNumber": 1,
165
- "totalThoughts": 5,
166
- "progress": 0.2,
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": "I need to calculate the fibonacci sequence up to 10."
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
- _No resources are currently exposed by this server._
257
+ | URI | MIME Type | Description |
258
+ | ------------------------- | --------------- | ------------------------- |
259
+ | `internal://instructions` | `text/markdown` | Server usage instructions |
185
260
 
186
261
  ### Prompts
187
262
 
188
- _No prompts are currently exposed by this server._
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><b>VS Code (mcp.json)</b></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><b>Claude Desktop</b></summary>
308
+ <summary>Cursor</summary>
210
309
 
211
- Add this to your `claude_desktop_config.json`:
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><b>Cursor</b></summary>
332
+ <summary>Windsurf</summary>
228
333
 
229
- 1. Navigate to **Settings** > **General** > **MCP**.
230
- 2. Click **Add New MCP Server**.
231
- 3. Name: `thinkseq`
232
- 4. Type: `command`
233
- 5. Command: `npx -y @j0hanz/thinkseq-mcp@latest`
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
- - **Stdio Transport**: This server runs over stdio. It creates a console bridge to intercept `console.log` calls and redirect them to standard error (stderr) to prevent interfering with the JSON-RPC protocol.
240
- - **Memory Limits**: The server enforces maximum thought counts and memory usage limits (configurable via CLI) to prevent memory exhaustion attacks.
241
- - **Input Validation**: All inputs are strictly validated using Zod schemas.
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
- 1. **Install dependencies**:
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
- ```bash
248
- npm install
249
- ```
407
+ Publishing is automated via GitHub Actions (`.github/workflows/publish.yml`):
250
408
 
251
- 2. **Run in development mode**:
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
- ```bash
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
- 3. **Run tests**:
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
- ```bash
261
- npm test
262
- ```
435
+ ### stdout/stderr Guidance
263
436
 
264
- 4. **Lint and Format**:
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
- ```bash
267
- npm run lint
268
- npm run format
269
- ```
439
+ ---
270
440
 
271
- ## Contributing & License
441
+ ## License
272
442
 
273
- This project is licensed under the MIT License.
443
+ [MIT](https://opensource.org/licenses/MIT)
package/dist/app.js CHANGED
@@ -1,7 +1,8 @@
1
- import { readFileSync } from 'node:fs';
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 = readFileSync(iconPath);
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
- resolved.publishLifecycleEvent({
44
- type: 'lifecycle.started',
45
- ts: resolved.now(),
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 { readFileSync } from 'node:fs';
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 readFileSync(INSTRUCTIONS_URL, { encoding: 'utf8' });
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
- function registerInstructionsResource(server) {
25
- server.registerResource('instructions', new ResourceTemplate('internal://instructions', {
26
- list: () => ({
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: uri.href,
39
- text: INSTRUCTIONS_TEXT,
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
- emit({
39
- type: 'lifecycle.shutdown',
40
- ts: timestamp(),
41
- signal,
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
- while (this.#map.size > this.#max) {
35
- const oldestKey = this.#map.keys().next().value;
36
- if (!oldestKey)
37
- return;
38
- if (oldestKey === this.#pinnedKey) {
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 (!pinned) {
41
- // Invariant broken; safest is to stop evicting.
42
- return;
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(oldestKey);
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) {
@@ -1,43 +1,56 @@
1
1
  # ThinkSeq Instructions
2
2
 
3
- > Guidance for the Agent: These instructions are available as a resource (`internal://instructions`) or prompt (`get-help`). Load them when you are unsure about tool usage.
3
+ > Available as resource (`internal://instructions`) or prompt (`get-help`). Load when unsure about tool usage.
4
4
 
5
- ## 1. Core Capability
5
+ ---
6
6
 
7
- - **Domain:** In-memory, sequential thinking with revision (destructive rewind) per session.
8
- - **Primary Resources:** `Thoughts`, `RevisionChain`, `ProgressContext`.
7
+ ## CORE CAPABILITY
9
8
 
10
- ## 2. The "Golden Path" Workflows (Critical)
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
- _Describe the standard order of operations using ONLY tools that exist._
13
+ ---
13
14
 
14
- ### Workflow A: Capture a reasoning chain
15
+ ## THE "GOLDEN PATH" WORKFLOWS (CRITICAL)
15
16
 
16
- 1. Call `thinkseq` with `thought` (optionally `totalThoughts`, `sessionId`).
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
- > Constraint: Keep each step atomic; one decision per call.
22
+ NOTE: Keep each step atomic; one decision per call.
20
23
 
21
- ### Workflow B: Revise a prior step
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
- > Constraint: Never guess `revisesThought`; always pick from `revisableThoughts`.
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
- ## 3. Tool Nuances & Gotchas
45
+ ## ERROR HANDLING STRATEGY
29
46
 
30
- _Do NOT repeat JSON schema. Focus on behavior and pitfalls._
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
- - **`thinkseq`**
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
- ## 4. Error Handling Strategy
54
+ ## RESOURCES
40
55
 
41
- - **`E_REVISION_MISSING` / `E_REVISION_TARGET_NOT_FOUND`**: Fetch `revisableThoughts` and retry with a valid ID.
42
- - **`E_THINK`**: Verify inputs and retry once.
43
- - **`E_TIMEOUT`**: Reduce thought length.
56
+ - `internal://instructions`: This document.
@@ -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
- export type LifecycleEvent = {
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 {};
@@ -30,5 +30,5 @@ export function publishToolEvent(event) {
30
30
  safePublish(toolChannel, attachContext(event));
31
31
  }
32
32
  export function publishLifecycleEvent(event) {
33
- safePublish(lifecycleChannel, event);
33
+ safePublish(lifecycleChannel, attachContext(event));
34
34
  }
@@ -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, options?: CreateErrorResponseOptions): ErrorResponse;
17
+ export declare function createErrorResponse(code: string, message: string, result?: unknown): ErrorResponse;
@@ -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, options = {}) {
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: includeTextContent
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
  };
@@ -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: 'info', logger: 'console', data: text })
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 send = createConsoleSender(target);
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
- send(text);
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(send);
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
  }
@@ -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
- return true;
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
- return true;
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
- const methodMessage = parseJsonRpcMethodMessage(message);
111
- if (!methodMessage) {
112
- originalOnMessage(message, extra);
113
- return;
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
- handleInitializeMethod(methodMessage, state, originalOnMessage, message, extra);
119
- return;
120
- }
121
- if (!state.sawInitialize) {
122
- handleNonInitializedRequest(methodMessage, transport);
123
- return;
124
- }
125
- originalOnMessage(message, extra);
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
- // MCP stdio is line-delimited JSON-RPC (one object per line). JSON-RPC
149
- // batching is removed in newer revisions; treat arrays as invalid.
150
- if (isInvalidJsonRpcMessageShape(message)) {
151
- sendError(transport, ErrorCode.InvalidRequest, 'Invalid Request');
152
- return;
153
- }
154
- originalOnMessage(message, extra);
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) {
@@ -37,7 +37,7 @@ export type ProcessResult = {
37
37
  thoughtHistoryLength: number;
38
38
  hasRevisions: boolean;
39
39
  activePathLength: number;
40
- revisableThoughts: number[];
40
+ revisableThoughts: readonly number[];
41
41
  revisableThoughtsTotal: number;
42
42
  context: ContextSummary;
43
43
  };
@@ -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, includeTextContent) {
44
- return createErrorResponse(code, message, undefined, { includeTextContent });
42
+ function buildErrorResponse(code, message) {
43
+ return createErrorResponse(code, message);
45
44
  }
46
- function buildToolResponse(result, options) {
45
+ function buildToolResponse(result) {
47
46
  if (!result.ok)
48
- return buildErrorResponse(result.error.code, result.error.message, options.includeTextContent);
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: options.includeTextContent
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, includeTextContent) {
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, { includeTextContent });
122
+ return buildToolResponse(result);
125
123
  }
126
124
  catch (err) {
127
- return handleThoughtError(err, start, extra, includeTextContent);
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, includeTextContent) {
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, includeTextContent);
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, includeTextContent);
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.1.0",
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
- ".": "./dist/index.js"
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.25.3",
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": "^25.2.0",
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": ">=22.0.0"
78
+ "node": ">=24"
76
79
  }
77
80
  }