@relentlessbuild/decs-mcp 0.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/dist/tools.js ADDED
@@ -0,0 +1,304 @@
1
+ import path from "node:path";
2
+ import { z } from "zod";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import { resolveCredentials } from "./config.js";
5
+ import { partitionDecisions, renderDecisionContextMarkdown, toDecisionSummary } from "./decision-context.js";
6
+ import { DecsError, toDecsError } from "./errors.js";
7
+ import { readRepoDecsConfig, writeRepoDecsConfig } from "./repo-config.js";
8
+ import { RelentlessApiClient } from "./relentless-api.js";
9
+ const LIST_SCHEMA = z.object({
10
+ cwd: z.string().trim().min(1).optional(),
11
+ spaceId: z.string().trim().min(1).optional(),
12
+ isKeyDecision: z.boolean().optional(),
13
+ limit: z.number().int().positive().max(100).optional()
14
+ });
15
+ const GET_CONTEXT_SCHEMA = z.object({
16
+ cwd: z.string().trim().min(1).optional(),
17
+ spaceId: z.string().trim().min(1).optional(),
18
+ includeRecentLimit: z.number().int().positive().max(100).optional()
19
+ });
20
+ const CREATE_DECISION_SCHEMA = z.object({
21
+ cwd: z.string().trim().min(1).optional(),
22
+ parentId: z.string().trim().min(1).optional(),
23
+ title: z.string().trim().min(1),
24
+ what: z.string().trim().min(1),
25
+ why: z.string().trim().min(1),
26
+ purpose: z.string().trim().min(1),
27
+ constraints: z.string().trim().min(1),
28
+ isKeyDecision: z.boolean().optional()
29
+ });
30
+ const UPDATE_DECISION_SCHEMA = z
31
+ .object({
32
+ decisionId: z.string().trim().min(1),
33
+ title: z.string().trim().min(1).optional(),
34
+ what: z.string().trim().min(1).optional(),
35
+ why: z.string().trim().min(1).optional(),
36
+ purpose: z.string().trim().min(1).optional(),
37
+ constraints: z.string().trim().min(1).optional(),
38
+ isKeyDecision: z.boolean().optional()
39
+ })
40
+ .refine((data) => data.title !== undefined ||
41
+ data.what !== undefined ||
42
+ data.why !== undefined ||
43
+ data.purpose !== undefined ||
44
+ data.constraints !== undefined ||
45
+ data.isKeyDecision !== undefined, "At least one field must be provided for update");
46
+ const INIT_SPACE_SCHEMA = z.object({
47
+ projectNodeId: z.string().trim().min(1),
48
+ projectName: z.string().trim().min(1).optional(),
49
+ cwd: z.string().trim().min(1).optional()
50
+ });
51
+ const WRITE_REPO_CONFIG_SCHEMA = z.object({
52
+ spaceId: z.string().trim().min(1),
53
+ cwd: z.string().trim().min(1).optional()
54
+ });
55
+ const TOOLS = [
56
+ {
57
+ name: "decs_get_context",
58
+ description: "Fetches decision history from Relentless and returns key decisions plus recent decisions formatted for prompt context.",
59
+ inputSchema: {
60
+ type: "object",
61
+ properties: {
62
+ cwd: { type: "string" },
63
+ spaceId: { type: "string" },
64
+ includeRecentLimit: { type: "number", minimum: 1, maximum: 100 }
65
+ },
66
+ additionalProperties: false
67
+ }
68
+ },
69
+ {
70
+ name: "decs_list",
71
+ description: "Lists decisions from the configured Relentless decisions space or a supplied spaceId.",
72
+ inputSchema: {
73
+ type: "object",
74
+ properties: {
75
+ cwd: { type: "string" },
76
+ spaceId: { type: "string" },
77
+ isKeyDecision: { type: "boolean" },
78
+ limit: { type: "number", minimum: 1, maximum: 100 }
79
+ },
80
+ additionalProperties: false
81
+ }
82
+ },
83
+ {
84
+ name: "decs_create",
85
+ description: "Creates a decision node in Relentless. Use for architectural decisions after explicit user confirmation.",
86
+ inputSchema: {
87
+ type: "object",
88
+ required: ["title", "what", "why", "purpose", "constraints"],
89
+ properties: {
90
+ cwd: { type: "string" },
91
+ parentId: { type: "string" },
92
+ title: { type: "string" },
93
+ what: { type: "string" },
94
+ why: { type: "string" },
95
+ purpose: { type: "string" },
96
+ constraints: { type: "string" },
97
+ isKeyDecision: { type: "boolean" }
98
+ },
99
+ additionalProperties: false
100
+ }
101
+ },
102
+ {
103
+ name: "decs_update",
104
+ description: "Updates an existing decision node by id. Missing fields are preserved from the existing node.",
105
+ inputSchema: {
106
+ type: "object",
107
+ required: ["decisionId"],
108
+ properties: {
109
+ decisionId: { type: "string" },
110
+ title: { type: "string" },
111
+ what: { type: "string" },
112
+ why: { type: "string" },
113
+ purpose: { type: "string" },
114
+ constraints: { type: "string" },
115
+ isKeyDecision: { type: "boolean" }
116
+ },
117
+ additionalProperties: false
118
+ }
119
+ },
120
+ {
121
+ name: "decs_init_space",
122
+ description: "Creates a '<projectName> - Decisions' collection inside the supplied Relentless project node.",
123
+ inputSchema: {
124
+ type: "object",
125
+ required: ["projectNodeId"],
126
+ properties: {
127
+ projectNodeId: { type: "string" },
128
+ projectName: { type: "string" },
129
+ cwd: { type: "string" }
130
+ },
131
+ additionalProperties: false
132
+ }
133
+ },
134
+ {
135
+ name: "decs_write_repo_config",
136
+ description: "Writes a .decs.json file at repo root (or cwd) pointing to a Relentless decisions space id.",
137
+ inputSchema: {
138
+ type: "object",
139
+ required: ["spaceId"],
140
+ properties: {
141
+ spaceId: { type: "string" },
142
+ cwd: { type: "string" }
143
+ },
144
+ additionalProperties: false
145
+ }
146
+ }
147
+ ];
148
+ function jsonResult(data) {
149
+ return {
150
+ content: [
151
+ {
152
+ type: "text",
153
+ text: JSON.stringify(data, null, 2)
154
+ }
155
+ ]
156
+ };
157
+ }
158
+ function errorResult(error) {
159
+ const normalized = toDecsError(error);
160
+ return {
161
+ isError: true,
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: JSON.stringify({
166
+ code: normalized.code,
167
+ message: normalized.message,
168
+ details: normalized.details
169
+ }, null, 2)
170
+ }
171
+ ]
172
+ };
173
+ }
174
+ function resolveSpaceId(input) {
175
+ if (input.spaceId) {
176
+ return input.spaceId;
177
+ }
178
+ const repoConfig = readRepoDecsConfig(input.cwd ?? process.cwd());
179
+ return repoConfig.config.relentlessSpaceId;
180
+ }
181
+ function getClient() {
182
+ return new RelentlessApiClient(resolveCredentials());
183
+ }
184
+ export function registerToolHandlers(server) {
185
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
186
+ tools: TOOLS
187
+ }));
188
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
189
+ try {
190
+ const toolName = request.params.name;
191
+ const rawArgs = request.params.arguments ?? {};
192
+ const client = getClient();
193
+ if (toolName === "decs_get_context") {
194
+ const args = GET_CONTEXT_SCHEMA.parse(rawArgs);
195
+ const spaceId = resolveSpaceId(args);
196
+ const decisions = (await client.listDecisions(spaceId)).map(toDecisionSummary);
197
+ const { keyDecisions, recentDecisions } = partitionDecisions(decisions, args.includeRecentLimit ?? 10);
198
+ return jsonResult({
199
+ spaceId,
200
+ counts: {
201
+ total: decisions.length,
202
+ key: keyDecisions.length,
203
+ recent: recentDecisions.length
204
+ },
205
+ keyDecisions,
206
+ recentDecisions,
207
+ markdown: renderDecisionContextMarkdown({ keyDecisions, recentDecisions })
208
+ });
209
+ }
210
+ if (toolName === "decs_list") {
211
+ const args = LIST_SCHEMA.parse(rawArgs);
212
+ const spaceId = resolveSpaceId(args);
213
+ let decisions = (await client.listDecisions(spaceId)).map(toDecisionSummary);
214
+ if (args.isKeyDecision !== undefined) {
215
+ decisions = decisions.filter((decision) => decision.isKeyDecision === args.isKeyDecision);
216
+ }
217
+ const sorted = decisions.sort((a, b) => {
218
+ const aTs = a.updatedAt ? Date.parse(a.updatedAt) : 0;
219
+ const bTs = b.updatedAt ? Date.parse(b.updatedAt) : 0;
220
+ return bTs - aTs;
221
+ });
222
+ const limit = args.limit ?? 25;
223
+ return jsonResult({
224
+ spaceId,
225
+ total: sorted.length,
226
+ decisions: sorted.slice(0, limit)
227
+ });
228
+ }
229
+ if (toolName === "decs_create") {
230
+ const args = CREATE_DECISION_SCHEMA.parse(rawArgs);
231
+ const parentId = args.parentId ?? resolveSpaceId(args);
232
+ const created = await client.createNode({
233
+ kind: "decision",
234
+ title: args.title,
235
+ parentId,
236
+ content: {
237
+ what: args.what,
238
+ why: args.why,
239
+ purpose: args.purpose,
240
+ constraints: args.constraints,
241
+ isKeyDecision: args.isKeyDecision ?? false
242
+ }
243
+ });
244
+ return jsonResult({
245
+ created: true,
246
+ decision: toDecisionSummary(created)
247
+ });
248
+ }
249
+ if (toolName === "decs_update") {
250
+ const args = UPDATE_DECISION_SCHEMA.parse(rawArgs);
251
+ const current = await client.getNode(args.decisionId);
252
+ const currentContent = (current.content ?? {});
253
+ const updated = await client.patchNode(args.decisionId, {
254
+ title: args.title ?? current.title,
255
+ content: {
256
+ what: args.what ?? String(currentContent.what ?? ""),
257
+ why: args.why ?? String(currentContent.why ?? ""),
258
+ purpose: args.purpose ?? String(currentContent.purpose ?? ""),
259
+ constraints: args.constraints ?? String(currentContent.constraints ?? ""),
260
+ isKeyDecision: args.isKeyDecision ?? Boolean(currentContent.isKeyDecision === true)
261
+ }
262
+ });
263
+ return jsonResult({
264
+ updated: true,
265
+ decision: toDecisionSummary(updated)
266
+ });
267
+ }
268
+ if (toolName === "decs_init_space") {
269
+ const args = INIT_SPACE_SCHEMA.parse(rawArgs);
270
+ await client.getNode(args.projectNodeId);
271
+ const cwd = args.cwd ?? process.cwd();
272
+ const projectName = args.projectName ?? path.basename(path.resolve(cwd));
273
+ const createdSpace = await client.createNode({
274
+ kind: "collection",
275
+ title: `${projectName} - Decisions`,
276
+ parentId: args.projectNodeId
277
+ });
278
+ if (!createdSpace.id) {
279
+ throw new DecsError("UPSTREAM_ERROR", "Relentless returned a collection without an id");
280
+ }
281
+ return jsonResult({
282
+ projectNodeId: args.projectNodeId,
283
+ projectName,
284
+ spaceId: createdSpace.id,
285
+ spaceTitle: createdSpace.title
286
+ });
287
+ }
288
+ if (toolName === "decs_write_repo_config") {
289
+ const args = WRITE_REPO_CONFIG_SCHEMA.parse(rawArgs);
290
+ const result = writeRepoDecsConfig(args.spaceId, args.cwd ?? process.cwd());
291
+ return jsonResult({
292
+ saved: true,
293
+ relentlessSpaceId: args.spaceId,
294
+ configPath: result.path,
295
+ repoRoot: result.repoRoot
296
+ });
297
+ }
298
+ throw new DecsError("VALIDATION_ERROR", `Unknown tool: ${toolName}`);
299
+ }
300
+ catch (error) {
301
+ return errorResult(error);
302
+ }
303
+ });
304
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ # Codex Quickstart (DECS)
2
+
3
+ This is the simplest path for Codex users.
4
+
5
+ ## One-Time Setup
6
+
7
+ Run:
8
+
9
+ ```bash
10
+ npx @relentless/decs-mcp setup codex --repo /path/to/your/repo
11
+ ```
12
+
13
+ The setup command will:
14
+
15
+ 1. Prompt for your Relentless credentials (API key, URL, buildspace id).
16
+ 2. Save them to `~/.relentless/decs-config.json`.
17
+ 3. Add/update DECS MCP config in `~/.codex/config.toml`.
18
+ 4. Install DECS prompt shortcuts in `~/.codex/prompts/`.
19
+ 5. Merge DECS guidance into your repo `AGENTS.md`.
20
+
21
+ If you cloned this repository locally, you can also run:
22
+
23
+ ```bash
24
+ ./install-codex.sh /path/to/your/repo
25
+ ```
26
+
27
+ The local script writes Codex MCP config pointing to your local `dist/index.js`.
28
+
29
+ ## Enable DECS in a Repo
30
+
31
+ After setup, in Codex run:
32
+
33
+ ```text
34
+ npx @relentless/decs-mcp init <project-node-id> [project-name] --repo /path/to/your/repo
35
+ ```
36
+
37
+ This creates a decisions collection in Relentless and writes `.decs.json` in your repo.
38
+
39
+ Optional in-chat prompt command:
40
+
41
+ ```text
42
+ /prompts:decs-init <project-node-id> [project-name]
43
+ ```
44
+
45
+ ## Daily Usage
46
+
47
+ - `/prompts:decs-context`: read key/recent decisions
48
+ - `/prompts:decs-log`: create a new decision (asks confirmation before write)
49
+ - `/prompts:decs-update`: update an existing decision (asks confirmation before write)
50
+
51
+ ## Validation
52
+
53
+ Run:
54
+
55
+ ```bash
56
+ npx @relentless/decs-mcp doctor --repo /path/to/your/repo
57
+ ```
58
+
59
+ It checks credentials, Codex MCP wiring, and `.decs.json`.
@@ -0,0 +1,37 @@
1
+ # Legacy Claude Code Hooks Setup
2
+
3
+ This is the original hook-based workflow for Claude Code CLI.
4
+
5
+ ## One-Time Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/RelentlessToph/relentless-decs.git
9
+ cd relentless-decs
10
+ ./install.sh
11
+ ```
12
+
13
+ Create `~/.claude/decs-config.json`:
14
+
15
+ ```json
16
+ {
17
+ "relentlessApiKey": "rlnt_...",
18
+ "relentlessUrl": "https://relentless.build",
19
+ "buildspaceId": "..."
20
+ }
21
+ ```
22
+
23
+ ## Enable in a Repository
24
+
25
+ In Claude Code, run:
26
+
27
+ ```text
28
+ /init-decs-project <project-node-id> [project-name]
29
+ ```
30
+
31
+ This writes `.decs.json` and links the repository to a Relentless decisions collection.
32
+
33
+ ## Hook Behavior
34
+
35
+ - Session start: loads prior decisions
36
+ - Prompt submit: refreshes when decision-related keywords appear
37
+ - Session stop: prompts decision hygiene and recording
@@ -0,0 +1,121 @@
1
+ # Relentless DECS MCP Client Guide
2
+
3
+ This guide covers general MCP usage plus client-specific setup.
4
+
5
+ ## Shared Prerequisites
6
+
7
+ - Node.js 18+
8
+ - A Relentless API key
9
+ - Buildspace id (UUID shown in your Relentless URL)
10
+
11
+ Credentials are stored in:
12
+
13
+ - `~/.relentless/decs-config.json` (preferred)
14
+ - `~/.claude/decs-config.json` (legacy fallback)
15
+
16
+ Format:
17
+
18
+ ```json
19
+ {
20
+ "relentlessApiKey": "rlnt_...",
21
+ "relentlessUrl": "https://relentless.build",
22
+ "buildspaceId": "..."
23
+ }
24
+ ```
25
+
26
+ ## Codex
27
+
28
+ Recommended:
29
+
30
+ ```bash
31
+ npx @relentless/decs-mcp setup codex --repo /path/to/your/repo
32
+ ```
33
+
34
+ Local clone alternative:
35
+
36
+ ```bash
37
+ ./install-codex.sh /path/to/your/repo
38
+ ```
39
+
40
+ Manual target file:
41
+
42
+ - `~/.codex/config.toml`
43
+
44
+ Expected MCP block:
45
+
46
+ ```toml
47
+ [mcp_servers.relentless_decs]
48
+ command = "npx"
49
+ args = ["-y", "@relentless/decs-mcp", "serve"]
50
+
51
+ [mcp_servers.relentless_decs.env]
52
+ RELENTLESS_API_KEY = "rlnt_..."
53
+ RELENTLESS_URL = "https://relentless.build"
54
+ RELENTLESS_BUILDSPACE_ID = "..."
55
+ ```
56
+
57
+ ## Claude Desktop
58
+
59
+ ### Manual setup
60
+
61
+ Use the correct platform path:
62
+
63
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
64
+ - Windows: `%APPDATA%\\Claude\\claude_desktop_config.json`
65
+
66
+ Add:
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "relentless-decs": {
72
+ "command": "npx",
73
+ "args": ["-y", "@relentless/decs-mcp", "serve"],
74
+ "env": {
75
+ "RELENTLESS_API_KEY": "rlnt_...",
76
+ "RELENTLESS_URL": "https://relentless.build",
77
+ "RELENTLESS_BUILDSPACE_ID": "..."
78
+ }
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### Optional automation
85
+
86
+ ```bash
87
+ npx @relentless/decs-mcp setup claude-desktop
88
+ ```
89
+
90
+ Local clone alternative:
91
+
92
+ ```bash
93
+ ./install-claude-desktop-mcp.sh
94
+ ```
95
+
96
+ Optional flags:
97
+
98
+ - `--platform macos|windows`
99
+ - `--dry-run`
100
+ - `--yes`
101
+ - `--use-local-server` (for local clone installs)
102
+
103
+ The command performs idempotent merge and writes a `.bak` backup before changes.
104
+
105
+ ## MCP Tools Exposed
106
+
107
+ - `decs_get_context`
108
+ - `decs_list`
109
+ - `decs_create`
110
+ - `decs_update`
111
+ - `decs_init_space`
112
+ - `decs_write_repo_config`
113
+
114
+ ## Troubleshooting
115
+
116
+ - `doctor` command:
117
+ - `npx @relentless/decs-mcp doctor --repo /path/to/your/repo`
118
+ - If Codex/Claude does not show tools:
119
+ - restart client fully
120
+ - confirm config path and JSON/TOML validity
121
+ - verify `npx -y @relentless/decs-mcp serve` runs
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@relentlessbuild/decs-mcp",
3
+ "version": "0.2.0",
4
+ "description": "Relentless DECS MCP server for Codex, Claude Desktop, and other MCP clients.",
5
+ "type": "module",
6
+ "bin": {
7
+ "relentless-decs-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "codex",
12
+ "docs",
13
+ "templates",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json",
18
+ "clean": "rm -rf dist coverage",
19
+ "dev": "tsx src/index.ts",
20
+ "setup:codex": "tsx src/index.ts setup codex",
21
+ "setup:claude-desktop": "tsx src/index.ts setup claude-desktop",
22
+ "doctor": "tsx src/index.ts doctor",
23
+ "start": "node dist/index.js",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "engines": {
29
+ "node": ">=18.18.0"
30
+ },
31
+ "dependencies": {
32
+ "@iarna/toml": "^2.2.5",
33
+ "@modelcontextprotocol/sdk": "^1.17.3",
34
+ "zod": "^3.24.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.12.0",
38
+ "tsx": "^4.19.2",
39
+ "typescript": "^5.7.3",
40
+ "vitest": "^2.1.8"
41
+ }
42
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "mcpServers": {
3
+ "relentless-decs": {
4
+ "command": "npx",
5
+ "args": [
6
+ "-y",
7
+ "@relentless/decs-mcp",
8
+ "serve"
9
+ ],
10
+ "env": {
11
+ "RELENTLESS_API_KEY": "rlnt_your_api_key",
12
+ "RELENTLESS_URL": "https://relentless.build",
13
+ "RELENTLESS_BUILDSPACE_ID": "your-buildspace-id"
14
+ }
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,10 @@
1
+ # ~/.codex/config.toml
2
+
3
+ [mcp_servers.relentless_decs]
4
+ command = "npx"
5
+ args = ["-y", "@relentless/decs-mcp", "serve"]
6
+
7
+ [mcp_servers.relentless_decs.env]
8
+ RELENTLESS_API_KEY = "rlnt_your_api_key"
9
+ RELENTLESS_URL = "https://relentless.build"
10
+ RELENTLESS_BUILDSPACE_ID = "your-buildspace-id"
@@ -0,0 +1,5 @@
1
+ {
2
+ "relentlessApiKey": "rlnt_your_api_key",
3
+ "relentlessUrl": "https://relentless.build",
4
+ "buildspaceId": "your-buildspace-id"
5
+ }