devscribe-reason 1.0.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.
@@ -0,0 +1,24 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(''''version:'''', d.get\\(''''version''''\\)\\); print\\(''''main:'''', d.get\\(''''main''''\\)\\); print\\(''''exports keys:'''', list\\(d.get\\(''''exports'''',{}\\).keys\\(\\)\\)[:20]\\)\")",
5
+ "Bash(claude --version)",
6
+ "Read(//Users/giannihart/.claude/**)",
7
+ "Bash(node -e \"const http = require\\(''http''\\); const crypto = require\\(''crypto''\\); console.log\\(''http:'', typeof http.createServer\\); console.log\\(''crypto:'', typeof crypto.createHmac\\);\")",
8
+ "Bash(node -e \":*)",
9
+ "Bash(timeout 5 node src/index.js)",
10
+ "Bash(timeout 3 node src/index.js)",
11
+ "Bash(node -c src/index.js)",
12
+ "Bash(chmod +x scripts/setup.js)",
13
+ "Bash(npm install:*)",
14
+ "Bash(npm link:*)",
15
+ "Bash(devscribe-reason)",
16
+ "Bash(claude mcp:*)",
17
+ "Bash(GITHUB_TOKEN=ghp_test GITHUB_REPO=owner/repo npm run setup)",
18
+ "Bash(npm unlink:*)",
19
+ "Bash(GITHUB_TOKEN=ghp_test npm run setup)",
20
+ "Bash(node -c scripts/setup.js)",
21
+ "Bash(git -C . status --short)"
22
+ ]
23
+ }
24
+ }
package/.env.example ADDED
@@ -0,0 +1,10 @@
1
+ # Required for committing reasoning docs to GitHub
2
+ GITHUB_TOKEN=ghp_your_personal_access_token
3
+ # Optional: auto-detected from git remote if not set
4
+ GITHUB_REPO=owner/repo
5
+ GITHUB_BRANCH=main
6
+
7
+ # Optional: enable GitHub webhook auto-finalization
8
+ # Both WEBHOOK_PORT and GITHUB_WEBHOOK_SECRET must be set to activate
9
+ # WEBHOOK_PORT=3001
10
+ # GITHUB_WEBHOOK_SECRET=your_webhook_secret_from_github
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # Devscribe Reason
2
+
3
+ An MCP server that automatically captures the reasoning behind engineering decisions made during Claude Code sessions and commits them as structured markdown files to `/docs/decisions/` in your GitHub repo.
4
+
5
+ ## The Problem
6
+
7
+ When developers and AI agents write code, the reasoning behind decisions — why this approach over alternatives, what tradeoffs were made, what was considered and rejected — lives only in the chat session and disappears when it closes. Devscribe Reason captures that reasoning automatically in real time.
8
+
9
+ ## Quick Start
10
+
11
+ **One command, 30 seconds, fully configured:**
12
+
13
+ ```bash
14
+ GITHUB_TOKEN=ghp_your_personal_access_token npx devscribe-reason
15
+ ```
16
+
17
+ That's it. The MCP server is now active in all your Claude Code sessions. Reasoning docs auto-commit to `/docs/decisions/`.
18
+
19
+ ## How It Works
20
+
21
+ The server exposes four MCP tools that Claude Code calls throughout a coding session **automatically** — no manual configuration required:
22
+
23
+ | Tool | When It's Called | What It Does |
24
+ |------|-----------------|--------------|
25
+ | `log_intent` | Start of session | Records what you're building and why |
26
+ | `log_decision` | When a technical choice is made | Captures the decision, reasoning, and code context |
27
+ | `log_alternative` | When an approach is rejected | Documents what was considered and why it was dropped |
28
+ | `finalize_reasoning_doc` | When work is done | Automatically triggered when you say "we're done", "open a PR", etc. |
29
+
30
+ ## Setup
31
+
32
+ ### Option 1: NPM (Recommended)
33
+
34
+ ```bash
35
+ # Install globally and configure in one step
36
+ GITHUB_TOKEN=ghp_your_personal_access_token npx devscribe-reason
37
+ ```
38
+
39
+ The setup script will:
40
+ - Detect your Claude Code installation automatically
41
+ - Register devscribe-reason as a user-level MCP server (works in all repos)
42
+ - Auto-detect your GitHub repo from the current directory
43
+ - Done! Restart Claude Code
44
+
45
+ ### Option 2: Docker / Development
46
+
47
+ ```bash
48
+ git clone https://github.com/YOUR_USERNAME/Devscribe-Reason.git
49
+ cd Devscribe-Reason
50
+ npm install
51
+ npm run setup
52
+ ```
53
+
54
+ ### Environment Variables
55
+
56
+ **Required:**
57
+ - `GITHUB_TOKEN` — [Personal Access Token](https://github.com/settings/tokens) with `repo` scope
58
+
59
+ **Optional (auto-detected):**
60
+ - `GITHUB_REPO` — GitHub repo (`owner/repo`), auto-detected from git remote
61
+ - `GITHUB_BRANCH` — Target branch (default: `main`)
62
+
63
+ **Optional (webhook auto-finalization):**
64
+ - `WEBHOOK_PORT` — Port for GitHub webhook listener (e.g., `3001`)
65
+ - `GITHUB_WEBHOOK_SECRET` — GitHub webhook secret (both env vars required to activate)
66
+
67
+ ## Features
68
+
69
+ ✅ **Zero setup** — Inject `instructions` via MCP protocol, no CLAUDE.md needed
70
+ ✅ **Automatic logging** — Captures decisions silently throughout your session
71
+ ✅ **Natural language triggers** — Say "we're done", "ship it", "open a PR" — anything works
72
+ ✅ **GitHub webhook** — Auto-finalize reasoning docs when PRs are opened (optional)
73
+ ✅ **Auto-detect repo** — Parses git remote to populate `GITHUB_REPO` automatically
74
+ ✅ **Global MCP server** — Works in every repo, configured once at user level
75
+
76
+ ## Output
77
+
78
+ Each session produces a markdown file in `/docs/decisions/` like:
79
+
80
+ ```
81
+ docs/decisions/2026-03-06-auth-refactor-oauth.md
82
+ ```
83
+
84
+ With structured sections: Intent, Key Decisions, Alternatives Considered, Open Questions, and Links.
85
+
86
+ ## GitHub Webhook (Optional)
87
+
88
+ To auto-commit reasoning docs when PRs are opened:
89
+
90
+ 1. Create a GitHub App or use a Personal Access Token with webhook permissions
91
+ 2. Run the server with:
92
+ ```bash
93
+ WEBHOOK_PORT=3001 GITHUB_WEBHOOK_SECRET=your_secret npm start
94
+ ```
95
+ 3. Add webhook in GitHub repo settings:
96
+ - Payload URL: `https://your-domain.com/webhook`
97
+ - Events: `Pull Requests`
98
+ - Secret: `your_secret`
99
+
100
+ When a PR is opened, the reasoning doc is automatically committed to the PR's base branch.
101
+
102
+ ## Troubleshooting
103
+
104
+ **"Could not find the 'claude' command"**
105
+ - Ensure Claude Code CLI or VS Code/Cursor with Claude Code extension is installed
106
+ - Reinstall: `npm run setup`
107
+
108
+ **"MCP server already exists"**
109
+ - The server is already registered. To re-register: `claude mcp remove --scope user devscribe-reason` then run setup again
110
+
111
+ **Reasoning docs not committing**
112
+ - Verify `GITHUB_TOKEN` has `repo` scope
113
+ - Check that `GITHUB_REPO` is set correctly: `echo $GITHUB_REPO`
114
+ - Verify the repo exists and the token has push access
115
+
116
+ ## License
117
+
118
+ MIT
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "devscribe-reason",
3
+ "version": "1.0.0",
4
+ "description": "MCP server that captures engineering decision reasoning and commits structured markdown to GitHub",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "devscribe-reason": "scripts/setup.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "setup": "node scripts/setup.js"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "reasoning",
17
+ "decisions",
18
+ "documentation",
19
+ "claude-code"
20
+ ],
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.12.1",
24
+ "@octokit/rest": "^21.1.1",
25
+ "dotenv": "^16.4.7",
26
+ "glob": "^10.5.0",
27
+ "uuid": "^11.1.0"
28
+ }
29
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+ import { resolve, dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { homedir } from "node:os";
8
+ import { globSync } from "glob";
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const serverPath = resolve(__dirname, "../src/index.js");
12
+ const token = process.env.GITHUB_TOKEN || "";
13
+ const repo = process.env.GITHUB_REPO || "";
14
+ const branch = process.env.GITHUB_BRANCH || "main";
15
+
16
+ // Try to find the claude binary in common locations
17
+ function findClaudeBinary() {
18
+ // 1. Try 'which claude' if claude is in PATH
19
+ try {
20
+ const result = execSync("which claude", { stdio: "pipe", timeout: 1000 }).toString().trim();
21
+ if (result) return result;
22
+ } catch {
23
+ // Not in PATH
24
+ }
25
+
26
+ // 2. Try Cursor extensions (macOS)
27
+ try {
28
+ const cursorPattern = join(homedir(), ".cursor/extensions/*/resources/native-binary/claude");
29
+ const matches = globSync(cursorPattern, { absolute: true });
30
+ if (matches.length > 0) return matches[0];
31
+ } catch {
32
+ // globSync not available or pattern didn't match
33
+ }
34
+
35
+ // 3. Try VS Code extensions (macOS)
36
+ try {
37
+ const vscodePattern = join(homedir(), ".vscode/extensions/*/resources/native-binary/claude");
38
+ const matches = globSync(vscodePattern, { absolute: true });
39
+ if (matches.length > 0) return matches[0];
40
+ } catch {
41
+ // globSync not available or pattern didn't match
42
+ }
43
+
44
+ // 4. Try Claude Desktop installation (macOS)
45
+ const claudeDesktopPath = join(
46
+ homedir(),
47
+ "Library/Application Support/Claude/claude-code/claude"
48
+ );
49
+ if (existsSync(claudeDesktopPath)) return claudeDesktopPath;
50
+
51
+ // 5. Try Claude Desktop on Linux
52
+ const claudeLinuxPath = join(homedir(), ".local/share/Claude/claude");
53
+ if (existsSync(claudeLinuxPath)) return claudeLinuxPath;
54
+
55
+ return null;
56
+ }
57
+
58
+ // Build env args for the CLI
59
+ const envArgs = [
60
+ token && `-e GITHUB_TOKEN=${token}`,
61
+ repo && `-e GITHUB_REPO=${repo}`,
62
+ `-e GITHUB_BRANCH=${branch}`,
63
+ ]
64
+ .filter(Boolean)
65
+ .join(" ");
66
+
67
+ const claudeCmd = findClaudeBinary();
68
+
69
+ if (!claudeCmd) {
70
+ console.error(
71
+ "\n❌ Could not find the 'claude' command. Please ensure you have:\n"
72
+ );
73
+ console.error(" • Claude Code CLI installed (https://claude.com/claude-code)");
74
+ console.error(" • Or VS Code with the Claude Code extension");
75
+ console.error(" • Or Cursor with Claude Code support\n");
76
+ console.error("After installation, run: npm run setup\n");
77
+ process.exit(1);
78
+ }
79
+
80
+ const cmd = `${claudeCmd} mcp add --scope user devscribe-reason node ${serverPath} ${envArgs}`;
81
+
82
+ try {
83
+ execSync(cmd, { stdio: "inherit" });
84
+ console.log(
85
+ "\n✅ Setup complete. devscribe-reason is now active in all your Claude Code sessions."
86
+ );
87
+ console.log(
88
+ "\nTo verify: Open Claude Code and check Settings > MCP Servers\n"
89
+ );
90
+ } catch (err) {
91
+ console.error("\n❌ Setup failed. Run this command manually:\n");
92
+ console.error(` ${cmd}\n`);
93
+ process.exit(1);
94
+ }
package/src/CLAUDE.md ADDED
@@ -0,0 +1,100 @@
1
+ # Devscribe Reason
2
+
3
+ An MCP server that automatically captures the reasoning behind engineering decisions made during Claude Code sessions and commits them as structured markdown files to `/docs/decisions/` in your GitHub repo.
4
+
5
+ ## The Problem
6
+
7
+ When developers and AI agents write code, the reasoning behind decisions — why this approach over alternatives, what tradeoffs were made, what was considered and rejected — lives only in the chat session and disappears when it closes. Devscribe Reason captures that reasoning automatically in real time.
8
+
9
+ ## How It Works
10
+
11
+ The server exposes four MCP tools that Claude Code calls throughout a coding session:
12
+
13
+ | Tool | When It's Called | What It Does |
14
+ |------|-----------------|--------------|
15
+ | `log_intent` | Start of session | Records what you're building and why |
16
+ | `log_decision` | When a technical choice is made | Captures the decision, reasoning, and code context |
17
+ | `log_alternative` | When an approach is rejected | Documents what was considered and why it was dropped |
18
+ | `finalize_reasoning_doc` | End of session / PR time | Builds a structured markdown doc and commits it to GitHub |
19
+
20
+ ## Setup
21
+
22
+ ### 1. Clone and install
23
+
24
+ ```bash
25
+ git clone https://github.com/YOUR_USERNAME/Devscribe-Reason.git
26
+ cd Devscribe-Reason
27
+ npm install
28
+ ```
29
+
30
+ ### 2. Configure environment variables
31
+
32
+ ```bash
33
+ cp .env.example .env
34
+ ```
35
+
36
+ Edit `.env` with your values:
37
+
38
+ ```
39
+ GITHUB_TOKEN=ghp_your_personal_access_token
40
+ GITHUB_REPO=owner/repo
41
+ GITHUB_BRANCH=main
42
+ ```
43
+
44
+ **GitHub Token:** Create a [Personal Access Token](https://github.com/settings/tokens) with `repo` scope (or `contents: write` for fine-grained tokens).
45
+
46
+ ### 3. Connect to Claude Code
47
+
48
+ Add this to your Claude Code MCP settings (`~/.claude/claude_desktop_config.json` or via the Claude Code CLI):
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "devscribe-reason": {
54
+ "command": "node",
55
+ "args": ["/absolute/path/to/Devscribe-Reason/src/index.js"],
56
+ "env": {
57
+ "GITHUB_TOKEN": "ghp_your_token",
58
+ "GITHUB_REPO": "owner/repo",
59
+ "GITHUB_BRANCH": "main"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ Or using the Claude Code CLI:
67
+
68
+ ```bash
69
+ claude mcp add devscribe-reason node /absolute/path/to/Devscribe-Reason/src/index.js \
70
+ -e GITHUB_TOKEN=ghp_your_token \
71
+ -e GITHUB_REPO=owner/repo \
72
+ -e GITHUB_BRANCH=main
73
+ ```
74
+
75
+ ### 4. Add system prompt instructions
76
+
77
+ Add these instructions to your Claude Code system prompt (via CLAUDE.md or settings) so it uses the tools automatically:
78
+
79
+ ```
80
+ When the devscribe-reason MCP server is connected:
81
+ - Call log_intent at the start of every session before doing any work
82
+ - Call log_decision whenever making a significant technical or architectural choice
83
+ - Call log_alternative whenever an approach is considered and rejected
84
+ - Call finalize_reasoning_doc when work is complete or a PR is ready
85
+ - Never ask permission to log — just do it silently in the background
86
+ ```
87
+
88
+ ## Output
89
+
90
+ Each session produces a markdown file in `/docs/decisions/` like:
91
+
92
+ ```
93
+ docs/decisions/2026-03-06-auth-refactor-oauth.md
94
+ ```
95
+
96
+ See the file for structured sections: Intent, Key Decisions, Alternatives Considered, Open Questions, and Links.
97
+
98
+ ## License
99
+
100
+ MIT
package/src/index.js ADDED
@@ -0,0 +1,620 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+ import { Octokit } from "@octokit/rest";
10
+ import { v4 as uuidv4 } from "uuid";
11
+ import dotenv from "dotenv";
12
+ import { createServer as createHttpServer } from "node:http";
13
+ import { createHmac, timingSafeEqual } from "node:crypto";
14
+
15
+ dotenv.config();
16
+
17
+ // --- Session state (in-memory) ---
18
+
19
+ const sessions = new Map();
20
+
21
+ function getOrCreateSession(sessionId) {
22
+ if (!sessions.has(sessionId)) {
23
+ sessions.set(sessionId, {
24
+ id: sessionId || uuidv4(),
25
+ intent: null,
26
+ projectContext: null,
27
+ decisions: [],
28
+ alternatives: [],
29
+ openQuestions: [],
30
+ createdAt: new Date().toISOString(),
31
+ });
32
+ }
33
+ return sessions.get(sessionId);
34
+ }
35
+
36
+ // Use a default session for simplicity — Claude Code calls tools sequentially in one session
37
+ const DEFAULT_SESSION = uuidv4();
38
+
39
+ // --- Markdown generation ---
40
+
41
+ function buildMarkdown(session, prTitle, prDescription, branch) {
42
+ const date = new Date().toISOString().split("T")[0];
43
+ const lines = [];
44
+
45
+ lines.push(`# ${prTitle}`);
46
+ lines.push(`**Date:** ${date} `);
47
+ lines.push(`**Branch:** ${branch || process.env.GITHUB_BRANCH || "main"} `);
48
+ lines.push(`**Session ID:** ${session.id}`);
49
+ lines.push("");
50
+
51
+ // Intent
52
+ lines.push("## Intent");
53
+ if (session.intent) {
54
+ lines.push(session.intent);
55
+ if (session.projectContext) {
56
+ lines.push("");
57
+ lines.push(`**Project context:** ${session.projectContext}`);
58
+ }
59
+ } else {
60
+ lines.push("_No intent was logged for this session._");
61
+ }
62
+ lines.push("");
63
+
64
+ // Key Decisions
65
+ lines.push("## Key Decisions");
66
+ if (session.decisions.length === 0) {
67
+ lines.push("_No decisions were logged for this session._");
68
+ } else {
69
+ session.decisions.forEach((d, i) => {
70
+ lines.push(`### Decision ${i + 1}: ${d.decision.split(".")[0].slice(0, 80)}`);
71
+ lines.push(`**What:** ${d.decision} `);
72
+ lines.push(`**Why:** ${d.reasoning} `);
73
+ lines.push(`**Code context:** ${d.codeContext}`);
74
+ lines.push("");
75
+ });
76
+ }
77
+ lines.push("");
78
+
79
+ // Alternatives Considered
80
+ lines.push("## Alternatives Considered");
81
+ if (session.alternatives.length === 0) {
82
+ lines.push("_No alternatives were logged for this session._");
83
+ } else {
84
+ session.alternatives.forEach((a, i) => {
85
+ lines.push(`### Alternative ${i + 1}: ${a.alternative.split(".")[0].slice(0, 80)}`);
86
+ lines.push(`**What was considered:** ${a.alternative} `);
87
+ lines.push(`**Why rejected:** ${a.rejectionReason}`);
88
+ lines.push("");
89
+ });
90
+ }
91
+ lines.push("");
92
+
93
+ // Open Questions
94
+ lines.push("## Open Questions");
95
+ if (session.openQuestions.length === 0) {
96
+ lines.push("_No open questions were flagged during this session._");
97
+ } else {
98
+ session.openQuestions.forEach((q) => {
99
+ lines.push(`- ${q}`);
100
+ });
101
+ }
102
+ lines.push("");
103
+
104
+ // Links
105
+ lines.push("## Links");
106
+ if (prDescription) {
107
+ lines.push(`- **PR Description:** ${prDescription}`);
108
+ }
109
+ lines.push("- Related decisions: _see `/docs/decisions/` for other decision records_");
110
+ lines.push("");
111
+
112
+ return lines.join("\n");
113
+ }
114
+
115
+ function toKebabCase(str) {
116
+ return str
117
+ .toLowerCase()
118
+ .replace(/[^a-z0-9\s-]/g, "")
119
+ .replace(/\s+/g, "-")
120
+ .replace(/-+/g, "-")
121
+ .replace(/^-|-$/g, "")
122
+ .slice(0, 60);
123
+ }
124
+
125
+ // --- GitHub commit ---
126
+
127
+ async function commitToGitHub(filePath, content, commitMessage, branch) {
128
+ const token = process.env.GITHUB_TOKEN;
129
+ const repo = process.env.GITHUB_REPO;
130
+
131
+ if (!token || !repo) {
132
+ throw new Error(
133
+ "Missing GITHUB_TOKEN or GITHUB_REPO environment variables. " +
134
+ "Set them in your .env file or environment."
135
+ );
136
+ }
137
+
138
+ const [owner, repoName] = repo.split("/");
139
+ const targetBranch = branch || process.env.GITHUB_BRANCH || "main";
140
+
141
+ const octokit = new Octokit({ auth: token });
142
+
143
+ // Get the latest commit SHA on the branch
144
+ const { data: ref } = await octokit.git.getRef({
145
+ owner,
146
+ repo: repoName,
147
+ ref: `heads/${targetBranch}`,
148
+ });
149
+ const latestCommitSha = ref.object.sha;
150
+
151
+ // Get the tree SHA of the latest commit
152
+ const { data: commit } = await octokit.git.getCommit({
153
+ owner,
154
+ repo: repoName,
155
+ commit_sha: latestCommitSha,
156
+ });
157
+ const treeSha = commit.tree.sha;
158
+
159
+ // Create a blob with the file content
160
+ const { data: blob } = await octokit.git.createBlob({
161
+ owner,
162
+ repo: repoName,
163
+ content: Buffer.from(content).toString("base64"),
164
+ encoding: "base64",
165
+ });
166
+
167
+ // Create a new tree with the file
168
+ const { data: newTree } = await octokit.git.createTree({
169
+ owner,
170
+ repo: repoName,
171
+ base_tree: treeSha,
172
+ tree: [
173
+ {
174
+ path: filePath,
175
+ mode: "100644",
176
+ type: "blob",
177
+ sha: blob.sha,
178
+ },
179
+ ],
180
+ });
181
+
182
+ // Create a new commit
183
+ const { data: newCommit } = await octokit.git.createCommit({
184
+ owner,
185
+ repo: repoName,
186
+ message: commitMessage,
187
+ tree: newTree.sha,
188
+ parents: [latestCommitSha],
189
+ });
190
+
191
+ // Update the branch reference
192
+ await octokit.git.updateRef({
193
+ owner,
194
+ repo: repoName,
195
+ ref: `heads/${targetBranch}`,
196
+ sha: newCommit.sha,
197
+ });
198
+
199
+ return {
200
+ commitSha: newCommit.sha,
201
+ commitUrl: newCommit.html_url,
202
+ };
203
+ }
204
+
205
+ // --- Finalization helper (reusable by MCP tool and webhook) ---
206
+
207
+ let finalizationInFlight = false;
208
+
209
+ async function runFinalization(prTitle, prDescription, branch) {
210
+ if (finalizationInFlight) {
211
+ throw new Error("Finalization is already in progress. Please wait.");
212
+ }
213
+ finalizationInFlight = true;
214
+ try {
215
+ const session = getOrCreateSession(DEFAULT_SESSION);
216
+ const targetBranch = branch || process.env.GITHUB_BRANCH || "main";
217
+ const markdown = buildMarkdown(session, prTitle, prDescription, targetBranch);
218
+ const date = new Date().toISOString().split("T")[0];
219
+ const fileName = `${date}-${toKebabCase(prTitle)}.md`;
220
+ const filePath = `docs/decisions/${fileName}`;
221
+ const commitMessage = `docs: add reasoning doc for "${prTitle}"`;
222
+
223
+ const result = await commitToGitHub(filePath, markdown, commitMessage, targetBranch);
224
+
225
+ // Clear the session after successful commit
226
+ sessions.delete(DEFAULT_SESSION);
227
+
228
+ return {
229
+ filePath,
230
+ commitSha: result.commitSha,
231
+ branch: targetBranch,
232
+ decisionsCount: session.decisions.length,
233
+ alternativesCount: session.alternatives.length,
234
+ hasIntent: Boolean(session.intent),
235
+ };
236
+ } finally {
237
+ finalizationInFlight = false;
238
+ }
239
+ }
240
+
241
+ // --- MCP Server setup ---
242
+
243
+ const SERVER_INSTRUCTIONS = `You are connected to the devscribe-reason MCP server, which automatically documents your engineering decisions.
244
+
245
+ REQUIRED BEHAVIOR - follow these rules silently without asking permission:
246
+
247
+ 1. SESSION START: Call log_intent immediately at the start of every session, before doing any other work. Capture what the developer is trying to build and why.
248
+
249
+ 2. DURING WORK: Call log_decision whenever you make any meaningful technical choice — architecture, library selection, data model design, API design, file/module structure, or implementation approach. Do not ask; just log it silently.
250
+
251
+ 3. DURING WORK: Call log_alternative whenever you evaluate an approach and decide against it. Do not ask; just log it silently.
252
+
253
+ 4. SESSION END: Call finalize_reasoning_doc when the developer indicates the session is ending. Recognize these signal phrases:
254
+ - "we're done", "all done", "that's it", "done for now"
255
+ - "open a PR", "create a PR", "make a PR", "submit a PR"
256
+ - "commit this", "ship it", "push this", "merge this"
257
+ - "finalize", "wrap up", "finish up"
258
+ - Any request to open or create a pull request
259
+
260
+ Never ask permission before calling any of these tools. Log silently in the background.`;
261
+
262
+ const server = new Server(
263
+ {
264
+ name: "devscribe-reason",
265
+ version: "1.0.0",
266
+ },
267
+ {
268
+ capabilities: {
269
+ tools: {},
270
+ },
271
+ instructions: SERVER_INSTRUCTIONS,
272
+ }
273
+ );
274
+
275
+ // List tools
276
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
277
+ return {
278
+ tools: [
279
+ {
280
+ name: "log_intent",
281
+ description:
282
+ "Log the intent for the current coding session. Call this at the START of every session before doing any work. Captures what the developer is trying to build and why.",
283
+ inputSchema: {
284
+ type: "object",
285
+ properties: {
286
+ intent: {
287
+ type: "string",
288
+ description: "What the developer is trying to build or accomplish",
289
+ },
290
+ project_context: {
291
+ type: "string",
292
+ description:
293
+ "Relevant background context about the project, codebase, or feature area",
294
+ },
295
+ },
296
+ required: ["intent"],
297
+ },
298
+ },
299
+ {
300
+ name: "log_decision",
301
+ description:
302
+ "Log a significant architectural or technical decision. Call this whenever you make a meaningful choice about approach, architecture, library selection, data model design, API design, or implementation strategy. Do NOT ask the developer for permission — just log it silently.",
303
+ inputSchema: {
304
+ type: "object",
305
+ properties: {
306
+ decision: {
307
+ type: "string",
308
+ description: "What was decided",
309
+ },
310
+ reasoning: {
311
+ type: "string",
312
+ description: "Why this approach was chosen over alternatives",
313
+ },
314
+ code_context: {
315
+ type: "string",
316
+ description:
317
+ "Which files, functions, or modules this decision affects",
318
+ },
319
+ },
320
+ required: ["decision", "reasoning", "code_context"],
321
+ },
322
+ },
323
+ {
324
+ name: "log_alternative",
325
+ description:
326
+ "Log an approach that was considered but rejected. Call this whenever you evaluate an alternative approach and decide against it. Do NOT ask the developer for permission — just log it silently.",
327
+ inputSchema: {
328
+ type: "object",
329
+ properties: {
330
+ alternative: {
331
+ type: "string",
332
+ description: "What approach or solution was considered",
333
+ },
334
+ rejection_reason: {
335
+ type: "string",
336
+ description: "Why this approach was not chosen",
337
+ },
338
+ },
339
+ required: ["alternative", "rejection_reason"],
340
+ },
341
+ },
342
+ {
343
+ name: "finalize_reasoning_doc",
344
+ description:
345
+ 'Finalize and commit the reasoning document for this session. Call this when the developer signals the session is ending, including: "we\'re done", "all done", "that\'s it", "done for now", "open a PR", "create a PR", "make a PR", "submit a PR", "commit this", "ship it", "push this", "merge this", "finalize", "wrap up", or "finish up". Structures all logged decisions into a clean markdown document and commits it to /docs/decisions/ in the GitHub repo.',
346
+ inputSchema: {
347
+ type: "object",
348
+ properties: {
349
+ pr_title: {
350
+ type: "string",
351
+ description: "The PR title or feature name for this reasoning document",
352
+ },
353
+ pr_description: {
354
+ type: "string",
355
+ description: "Optional PR description or summary",
356
+ },
357
+ branch: {
358
+ type: "string",
359
+ description:
360
+ "Target branch to commit to. Defaults to GITHUB_BRANCH env var or 'main'",
361
+ },
362
+ },
363
+ required: ["pr_title"],
364
+ },
365
+ },
366
+ ],
367
+ };
368
+ });
369
+
370
+ // Handle tool calls
371
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
372
+ const { name, arguments: args } = request.params;
373
+ const session = getOrCreateSession(DEFAULT_SESSION);
374
+
375
+ try {
376
+ switch (name) {
377
+ case "log_intent": {
378
+ session.intent = args.intent;
379
+ session.projectContext = args.project_context || null;
380
+ return {
381
+ content: [
382
+ {
383
+ type: "text",
384
+ text: `Intent logged for session ${session.id}: "${args.intent}"`,
385
+ },
386
+ ],
387
+ };
388
+ }
389
+
390
+ case "log_decision": {
391
+ session.decisions.push({
392
+ decision: args.decision,
393
+ reasoning: args.reasoning,
394
+ codeContext: args.code_context,
395
+ timestamp: new Date().toISOString(),
396
+ });
397
+ return {
398
+ content: [
399
+ {
400
+ type: "text",
401
+ text: `Decision #${session.decisions.length} logged: "${args.decision.slice(0, 80)}..."`,
402
+ },
403
+ ],
404
+ };
405
+ }
406
+
407
+ case "log_alternative": {
408
+ session.alternatives.push({
409
+ alternative: args.alternative,
410
+ rejectionReason: args.rejection_reason,
411
+ timestamp: new Date().toISOString(),
412
+ });
413
+ return {
414
+ content: [
415
+ {
416
+ type: "text",
417
+ text: `Alternative #${session.alternatives.length} logged: "${args.alternative.slice(0, 80)}..."`,
418
+ },
419
+ ],
420
+ };
421
+ }
422
+
423
+ case "finalize_reasoning_doc": {
424
+ const prTitle = args.pr_title;
425
+ const prDescription = args.pr_description || null;
426
+ const branch = args.branch || process.env.GITHUB_BRANCH || "main";
427
+
428
+ try {
429
+ const r = await runFinalization(prTitle, prDescription, branch);
430
+
431
+ return {
432
+ content: [
433
+ {
434
+ type: "text",
435
+ text: [
436
+ `Reasoning document committed successfully!`,
437
+ `File: ${r.filePath}`,
438
+ `Commit: ${r.commitSha}`,
439
+ `Branch: ${r.branch}`,
440
+ ``,
441
+ `Summary:`,
442
+ `- ${r.decisionsCount} decisions logged`,
443
+ `- ${r.alternativesCount} alternatives documented`,
444
+ `- Intent: ${r.hasIntent ? "captured" : "not logged"}`,
445
+ ].join("\n"),
446
+ },
447
+ ],
448
+ };
449
+ } catch (err) {
450
+ return {
451
+ content: [
452
+ {
453
+ type: "text",
454
+ text: `Failed to commit reasoning doc to GitHub: ${err.message}\n\nThe document was generated but could not be committed. Check your GITHUB_TOKEN, GITHUB_REPO, and GITHUB_BRANCH environment variables.`,
455
+ },
456
+ ],
457
+ isError: true,
458
+ };
459
+ }
460
+ }
461
+
462
+ default:
463
+ return {
464
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
465
+ isError: true,
466
+ };
467
+ }
468
+ } catch (err) {
469
+ return {
470
+ content: [
471
+ {
472
+ type: "text",
473
+ text: `Error in ${name}: ${err.message}`,
474
+ },
475
+ ],
476
+ isError: true,
477
+ };
478
+ }
479
+ });
480
+
481
+ // --- Webhook server (optional, for auto-finalization on PR open) ---
482
+
483
+ function verifyGitHubSignature(rawBody, sigHeader, secret) {
484
+ if (!sigHeader?.startsWith("sha256=")) return false;
485
+ const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
486
+ const a = Buffer.from(sigHeader);
487
+ const b = Buffer.from(expected);
488
+ if (a.length !== b.length) return false;
489
+ try {
490
+ return timingSafeEqual(a, b);
491
+ } catch {
492
+ return false;
493
+ }
494
+ }
495
+
496
+ function startWebhookServer(port, secret) {
497
+ const httpServer = createHttpServer((req, res) => {
498
+ // Only accept POST to /webhook
499
+ if (req.method !== "POST" || req.url !== "/webhook") {
500
+ res.writeHead(404);
501
+ res.end("Not found");
502
+ return;
503
+ }
504
+
505
+ const chunks = [];
506
+ req.on("data", (chunk) => chunks.push(chunk));
507
+ req.on("end", async () => {
508
+ const rawBody = Buffer.concat(chunks);
509
+
510
+ // Verify signature
511
+ const sig = req.headers["x-hub-signature-256"];
512
+ if (!verifyGitHubSignature(rawBody, sig, secret)) {
513
+ console.error("[webhook] Invalid signature - rejecting request");
514
+ res.writeHead(401);
515
+ res.end("Invalid signature");
516
+ return;
517
+ }
518
+
519
+ let payload;
520
+ try {
521
+ payload = JSON.parse(rawBody.toString("utf8"));
522
+ } catch (e) {
523
+ res.writeHead(400);
524
+ res.end("Invalid JSON");
525
+ return;
526
+ }
527
+
528
+ // Only handle pull_request.opened events
529
+ const event = req.headers["x-github-event"];
530
+ if (event !== "pull_request" || payload.action !== "opened") {
531
+ res.writeHead(200);
532
+ res.end("Event ignored");
533
+ return;
534
+ }
535
+
536
+ const prTitle = payload.pull_request?.title || "Untitled PR";
537
+ const prUrl = payload.pull_request?.html_url || "";
538
+ const branch = payload.pull_request?.base?.ref || process.env.GITHUB_BRANCH || "main";
539
+ const prDescription = prUrl ? `PR: ${prUrl}` : null;
540
+
541
+ console.error(`[webhook] PR opened: "${prTitle}" - triggering finalization`);
542
+
543
+ try {
544
+ const r = await runFinalization(prTitle, prDescription, branch);
545
+ console.error(`[webhook] Committed ${r.filePath} (${r.commitSha})`);
546
+ res.writeHead(200, { "Content-Type": "application/json" });
547
+ res.end(JSON.stringify({ ok: true, filePath: r.filePath, commitSha: r.commitSha }));
548
+ } catch (err) {
549
+ console.error(`[webhook] Finalization failed: ${err.message}`);
550
+ res.writeHead(500);
551
+ res.end(JSON.stringify({ ok: false, error: err.message }));
552
+ }
553
+ });
554
+
555
+ req.on("error", (err) => {
556
+ console.error("[webhook] Request error:", err.message);
557
+ res.writeHead(500);
558
+ res.end("Internal error");
559
+ });
560
+ });
561
+
562
+ httpServer.listen(port, "127.0.0.1", () => {
563
+ console.error(`[webhook] Listening on http://127.0.0.1:${port}/webhook`);
564
+ });
565
+
566
+ httpServer.on("error", (err) => {
567
+ console.error(`[webhook] Server error: ${err.message}`);
568
+ });
569
+
570
+ return httpServer;
571
+ }
572
+
573
+ // --- Start server ---
574
+
575
+ async function main() {
576
+ // Auto-detect GITHUB_REPO from git remote if not set
577
+ if (!process.env.GITHUB_REPO) {
578
+ try {
579
+ const { execSync } = await import("node:child_process");
580
+ const remoteUrl = execSync("git remote get-url origin", {
581
+ stdio: ["pipe", "pipe", "pipe"],
582
+ timeout: 3000,
583
+ })
584
+ .toString()
585
+ .trim();
586
+ const match =
587
+ remoteUrl.match(/git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/) ||
588
+ remoteUrl.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
589
+ if (match) {
590
+ process.env.GITHUB_REPO = match[1];
591
+ console.error(`[auto-detect] GITHUB_REPO set to: ${process.env.GITHUB_REPO}`);
592
+ }
593
+ } catch {
594
+ // Not a git repo, or git not installed, or no remote — silently ignore
595
+ }
596
+ }
597
+
598
+ // Start webhook server if opt-in env vars are present
599
+ const webhookPort = process.env.WEBHOOK_PORT
600
+ ? parseInt(process.env.WEBHOOK_PORT, 10)
601
+ : null;
602
+ const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET || null;
603
+
604
+ if (webhookPort && webhookSecret) {
605
+ startWebhookServer(webhookPort, webhookSecret);
606
+ } else if (webhookPort || webhookSecret) {
607
+ console.error(
608
+ "[webhook] Skipped: both WEBHOOK_PORT and GITHUB_WEBHOOK_SECRET must be set to enable the webhook server."
609
+ );
610
+ }
611
+
612
+ const transport = new StdioServerTransport();
613
+ await server.connect(transport);
614
+ console.error("Devscribe Reason MCP server running on stdio");
615
+ }
616
+
617
+ main().catch((err) => {
618
+ console.error("Fatal error:", err);
619
+ process.exit(1);
620
+ });