speclock 1.1.0 → 1.1.1
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/package.json +5 -2
- package/src/mcp/http-server.js +274 -0
- package/src/mcp/server.js +18 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speclock",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "AI continuity engine — MCP server + CLI that kills AI amnesia. Maintains project memory, enforces constraints, and detects drift across AI coding sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp/server.js",
|
|
@@ -55,5 +55,8 @@
|
|
|
55
55
|
"src/",
|
|
56
56
|
"README.md",
|
|
57
57
|
"LICENSE"
|
|
58
|
-
]
|
|
58
|
+
],
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"esbuild": "^0.27.3"
|
|
61
|
+
}
|
|
59
62
|
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLock MCP HTTP Server — for Railway / remote deployment
|
|
3
|
+
* Wraps the same 19 tools as the stdio server using Streamable HTTP transport.
|
|
4
|
+
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import os from "os";
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
10
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import {
|
|
13
|
+
ensureInit,
|
|
14
|
+
setGoal,
|
|
15
|
+
addLock,
|
|
16
|
+
removeLock,
|
|
17
|
+
addDecision,
|
|
18
|
+
addNote,
|
|
19
|
+
updateDeployFacts,
|
|
20
|
+
logChange,
|
|
21
|
+
checkConflict,
|
|
22
|
+
getSessionBriefing,
|
|
23
|
+
endSession,
|
|
24
|
+
suggestLocks,
|
|
25
|
+
detectDrift,
|
|
26
|
+
} from "../core/engine.js";
|
|
27
|
+
import { generateContext, generateContextPack } from "../core/context.js";
|
|
28
|
+
import {
|
|
29
|
+
readBrain,
|
|
30
|
+
readEvents,
|
|
31
|
+
newId,
|
|
32
|
+
nowIso,
|
|
33
|
+
appendEvent,
|
|
34
|
+
bumpEvents,
|
|
35
|
+
writeBrain,
|
|
36
|
+
} from "../core/storage.js";
|
|
37
|
+
import {
|
|
38
|
+
captureStatus,
|
|
39
|
+
createTag,
|
|
40
|
+
getDiffSummary,
|
|
41
|
+
} from "../core/git.js";
|
|
42
|
+
|
|
43
|
+
const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
44
|
+
const VERSION = "1.1.0";
|
|
45
|
+
const AUTHOR = "Sandeep Roy";
|
|
46
|
+
|
|
47
|
+
function createSpecLockServer() {
|
|
48
|
+
const server = new McpServer(
|
|
49
|
+
{ name: "speclock", version: VERSION },
|
|
50
|
+
{
|
|
51
|
+
instructions:
|
|
52
|
+
`SpecLock is an AI continuity engine. Developed by ${AUTHOR}. Call speclock_session_briefing at the start of a new session, and speclock_session_summary before ending. Use speclock_get_context to refresh your project understanding at any time.`,
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Tool 1: speclock_init
|
|
57
|
+
server.tool("speclock_init", "Initialize SpecLock in the current project directory.", {}, async () => {
|
|
58
|
+
const brain = ensureInit(PROJECT_ROOT);
|
|
59
|
+
return { content: [{ type: "text", text: `SpecLock initialized for "${brain.project.name}" at ${brain.project.root}` }] };
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Tool 2: speclock_get_context
|
|
63
|
+
server.tool("speclock_get_context", "THE KEY TOOL. Returns the full structured context pack.", { format: z.enum(["markdown", "json"]).default("markdown").describe("Output format") }, async ({ format }) => {
|
|
64
|
+
ensureInit(PROJECT_ROOT);
|
|
65
|
+
const text = format === "json" ? JSON.stringify(generateContextPack(PROJECT_ROOT), null, 2) : generateContext(PROJECT_ROOT);
|
|
66
|
+
return { content: [{ type: "text", text }] };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Tool 3: speclock_set_goal
|
|
70
|
+
server.tool("speclock_set_goal", "Set or update the project goal.", { text: z.string().min(1).describe("The project goal text") }, async ({ text }) => {
|
|
71
|
+
ensureInit(PROJECT_ROOT);
|
|
72
|
+
setGoal(PROJECT_ROOT, text);
|
|
73
|
+
return { content: [{ type: "text", text: `Goal updated: ${text}` }] };
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Tool 4: speclock_add_lock
|
|
77
|
+
server.tool("speclock_add_lock", "Add a non-negotiable constraint (SpecLock).", { text: z.string().min(1).describe("The constraint text"), tags: z.array(z.string()).default([]).describe("Category tags"), source: z.enum(["user", "agent"]).default("agent").describe("Who created this lock") }, async ({ text, tags, source }) => {
|
|
78
|
+
ensureInit(PROJECT_ROOT);
|
|
79
|
+
const lock = addLock(PROJECT_ROOT, text, tags, source);
|
|
80
|
+
return { content: [{ type: "text", text: `Lock added [${lock.id}]: ${text}` }] };
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Tool 5: speclock_remove_lock
|
|
84
|
+
server.tool("speclock_remove_lock", "Remove (deactivate) a SpecLock by its ID.", { lockId: z.string().min(1).describe("The lock ID to remove") }, async ({ lockId }) => {
|
|
85
|
+
ensureInit(PROJECT_ROOT);
|
|
86
|
+
removeLock(PROJECT_ROOT, lockId);
|
|
87
|
+
return { content: [{ type: "text", text: `Lock ${lockId} removed.` }] };
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Tool 6: speclock_add_decision
|
|
91
|
+
server.tool("speclock_add_decision", "Record an architectural or design decision.", { text: z.string().min(1).describe("The decision text"), tags: z.array(z.string()).default([]), source: z.enum(["user", "agent"]).default("agent") }, async ({ text, tags, source }) => {
|
|
92
|
+
ensureInit(PROJECT_ROOT);
|
|
93
|
+
const d = addDecision(PROJECT_ROOT, text, tags, source);
|
|
94
|
+
return { content: [{ type: "text", text: `Decision recorded [${d.id}]: ${text}` }] };
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Tool 7: speclock_add_note
|
|
98
|
+
server.tool("speclock_add_note", "Add a pinned note for reference.", { text: z.string().min(1).describe("The note text"), pinned: z.boolean().default(true).describe("Whether to pin this note") }, async ({ text, pinned }) => {
|
|
99
|
+
ensureInit(PROJECT_ROOT);
|
|
100
|
+
const brain = readBrain(PROJECT_ROOT);
|
|
101
|
+
const note = { id: newId(), text, pinned, createdAt: nowIso() };
|
|
102
|
+
brain.state.notes.push(note);
|
|
103
|
+
writeBrain(PROJECT_ROOT, brain);
|
|
104
|
+
appendEvent(PROJECT_ROOT, { type: "note_added", noteId: note.id, text });
|
|
105
|
+
bumpEvents(PROJECT_ROOT);
|
|
106
|
+
return { content: [{ type: "text", text: `Note added [${note.id}]: ${text}` }] };
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Tool 8: speclock_set_deploy_facts
|
|
110
|
+
server.tool("speclock_set_deploy_facts", "Record deployment configuration facts.", { provider: z.string().optional(), branch: z.string().optional(), autoDeploy: z.boolean().optional(), url: z.string().optional(), notes: z.string().optional() }, async (params) => {
|
|
111
|
+
ensureInit(PROJECT_ROOT);
|
|
112
|
+
updateDeployFacts(PROJECT_ROOT, params);
|
|
113
|
+
return { content: [{ type: "text", text: `Deploy facts updated: ${JSON.stringify(params)}` }] };
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Tool 9: speclock_log_change
|
|
117
|
+
server.tool("speclock_log_change", "Manually log a significant change.", { summary: z.string().min(1).describe("Brief description of the change"), files: z.array(z.string()).default([]).describe("Files affected") }, async ({ summary, files }) => {
|
|
118
|
+
ensureInit(PROJECT_ROOT);
|
|
119
|
+
logChange(PROJECT_ROOT, summary, files);
|
|
120
|
+
return { content: [{ type: "text", text: `Change logged: ${summary}` }] };
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Tool 10: speclock_get_changes
|
|
124
|
+
server.tool("speclock_get_changes", "Get recent file changes tracked by SpecLock.", { limit: z.number().int().min(1).max(100).default(20) }, async ({ limit }) => {
|
|
125
|
+
ensureInit(PROJECT_ROOT);
|
|
126
|
+
const events = readEvents(PROJECT_ROOT);
|
|
127
|
+
const changes = events.filter((e) => ["file_created", "file_changed", "file_deleted", "manual_change"].includes(e.type)).slice(-limit);
|
|
128
|
+
return { content: [{ type: "text", text: changes.length ? JSON.stringify(changes, null, 2) : "No recent changes." }] };
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Tool 11: speclock_get_events
|
|
132
|
+
server.tool("speclock_get_events", "Get the event log, optionally filtered by type.", { type: z.string().optional(), limit: z.number().int().min(1).max(200).default(50), since: z.string().optional() }, async ({ type, limit, since }) => {
|
|
133
|
+
ensureInit(PROJECT_ROOT);
|
|
134
|
+
let events = readEvents(PROJECT_ROOT);
|
|
135
|
+
if (type) events = events.filter((e) => e.type === type);
|
|
136
|
+
if (since) events = events.filter((e) => e.ts > since);
|
|
137
|
+
events = events.slice(-limit);
|
|
138
|
+
return { content: [{ type: "text", text: events.length ? JSON.stringify(events, null, 2) : "No matching events." }] };
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Tool 12: speclock_check_conflict
|
|
142
|
+
server.tool("speclock_check_conflict", "Check if a proposed action conflicts with any active SpecLock.", { proposedAction: z.string().min(1).describe("Description of the action") }, async ({ proposedAction }) => {
|
|
143
|
+
ensureInit(PROJECT_ROOT);
|
|
144
|
+
const result = checkConflict(PROJECT_ROOT, proposedAction);
|
|
145
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Tool 13: speclock_session_briefing
|
|
149
|
+
server.tool("speclock_session_briefing", "Start a new session and get a full briefing.", { toolName: z.enum(["claude-code", "cursor", "codex", "windsurf", "cline", "unknown"]).default("unknown") }, async ({ toolName }) => {
|
|
150
|
+
ensureInit(PROJECT_ROOT);
|
|
151
|
+
const briefing = getSessionBriefing(PROJECT_ROOT, toolName);
|
|
152
|
+
return { content: [{ type: "text", text: briefing }] };
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Tool 14: speclock_session_summary
|
|
156
|
+
server.tool("speclock_session_summary", "End the current session and record what was accomplished.", { summary: z.string().min(1) }, async ({ summary }) => {
|
|
157
|
+
ensureInit(PROJECT_ROOT);
|
|
158
|
+
endSession(PROJECT_ROOT, summary);
|
|
159
|
+
return { content: [{ type: "text", text: `Session ended. Summary recorded: ${summary}` }] };
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Tool 15: speclock_checkpoint
|
|
163
|
+
server.tool("speclock_checkpoint", "Create a named git tag checkpoint for easy rollback.", { name: z.string().min(1) }, async ({ name }) => {
|
|
164
|
+
ensureInit(PROJECT_ROOT);
|
|
165
|
+
const tag = `speclock-${name}`;
|
|
166
|
+
try {
|
|
167
|
+
createTag(PROJECT_ROOT, tag);
|
|
168
|
+
appendEvent(PROJECT_ROOT, { type: "checkpoint_created", tag });
|
|
169
|
+
return { content: [{ type: "text", text: `Checkpoint created: ${tag}` }] };
|
|
170
|
+
} catch (err) {
|
|
171
|
+
return { content: [{ type: "text", text: `Checkpoint failed: ${err.message}` }] };
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Tool 16: speclock_repo_status
|
|
176
|
+
server.tool("speclock_repo_status", "Get current git repository status.", {}, async () => {
|
|
177
|
+
ensureInit(PROJECT_ROOT);
|
|
178
|
+
try {
|
|
179
|
+
const status = captureStatus(PROJECT_ROOT);
|
|
180
|
+
const diff = getDiffSummary(PROJECT_ROOT);
|
|
181
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...status, diffSummary: diff }, null, 2) }] };
|
|
182
|
+
} catch (err) {
|
|
183
|
+
return { content: [{ type: "text", text: `Git status failed: ${err.message}` }] };
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Tool 17: speclock_suggest_locks
|
|
188
|
+
server.tool("speclock_suggest_locks", "AI-powered lock suggestions based on project patterns.", {}, async () => {
|
|
189
|
+
ensureInit(PROJECT_ROOT);
|
|
190
|
+
const suggestions = suggestLocks(PROJECT_ROOT);
|
|
191
|
+
if (!suggestions.length) return { content: [{ type: "text", text: "No lock suggestions at this time." }] };
|
|
192
|
+
const text = suggestions.map((s, i) => `${i + 1}. **${s.text}**\n Source: ${s.source}\n Reason: ${s.reason}`).join("\n\n");
|
|
193
|
+
return { content: [{ type: "text", text: `## Lock Suggestions\n\n${text}\n\n---\n*SpecLock v${VERSION} — Developed by ${AUTHOR}*` }] };
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Tool 18: speclock_detect_drift
|
|
197
|
+
server.tool("speclock_detect_drift", "Scan recent changes for constraint violations.", {}, async () => {
|
|
198
|
+
ensureInit(PROJECT_ROOT);
|
|
199
|
+
const drift = detectDrift(PROJECT_ROOT);
|
|
200
|
+
if (!drift.length) return { content: [{ type: "text", text: "No drift detected. All changes align with active locks." }] };
|
|
201
|
+
const text = drift.map((d, i) => `${i + 1}. **${d.type}**: ${d.summary}\n Lock: ${d.lockText}\n Confidence: ${d.confidence}`).join("\n\n");
|
|
202
|
+
return { content: [{ type: "text", text: `## Drift Detected\n\n${text}` }] };
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Tool 19: speclock_health
|
|
206
|
+
server.tool("speclock_health", "Health check with completeness score and multi-agent timeline.", {}, async () => {
|
|
207
|
+
ensureInit(PROJECT_ROOT);
|
|
208
|
+
const brain = readBrain(PROJECT_ROOT);
|
|
209
|
+
const events = readEvents(PROJECT_ROOT);
|
|
210
|
+
let score = 0;
|
|
211
|
+
const checks = [];
|
|
212
|
+
if (brain.project.goal) { score += 20; checks.push("- [x] Goal set"); } else checks.push("- [ ] Goal missing");
|
|
213
|
+
if (brain.state.locks.filter(l => l.active).length > 0) { score += 20; checks.push("- [x] Locks defined"); } else checks.push("- [ ] No active locks");
|
|
214
|
+
if (brain.state.decisions.length > 0) { score += 15; checks.push("- [x] Decisions recorded"); } else checks.push("- [ ] No decisions");
|
|
215
|
+
if (brain.state.sessions.length > 0) { score += 15; checks.push("- [x] Sessions tracked"); } else checks.push("- [ ] No sessions");
|
|
216
|
+
if (brain.deploy?.provider) { score += 10; checks.push("- [x] Deploy facts set"); } else checks.push("- [ ] Deploy facts missing");
|
|
217
|
+
if (brain.state.notes.length > 0) { score += 10; checks.push("- [x] Notes present"); } else checks.push("- [ ] No notes");
|
|
218
|
+
if (events.length > 10) { score += 10; checks.push("- [x] Rich event history"); } else checks.push("- [ ] Limited events");
|
|
219
|
+
const grade = score >= 90 ? "A" : score >= 70 ? "B" : score >= 50 ? "C" : score >= 30 ? "D" : "F";
|
|
220
|
+
const sessions = brain.state.sessions.slice(-5);
|
|
221
|
+
const agentTimeline = sessions.length ? "\n\n### Recent Sessions\n" + sessions.map(s => `- **${s.tool || "unknown"}** @ ${s.startedAt}${s.summary ? ": " + s.summary : ""}`).join("\n") : "";
|
|
222
|
+
return { content: [{ type: "text", text: `## SpecLock Health Check\n\nScore: **${score}/100** (Grade: ${grade})\nEvents: ${brain.events.count} | Reverts: ${brain.state.reverts.length}\n\n### Checks\n${checks.join("\n")}${agentTimeline}\n\n---\n*SpecLock v${VERSION} — Developed by ${AUTHOR}*` }] };
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return server;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- HTTP Server ---
|
|
229
|
+
const app = createMcpExpressApp({ host: "0.0.0.0" });
|
|
230
|
+
|
|
231
|
+
app.post("/mcp", async (req, res) => {
|
|
232
|
+
const server = createSpecLockServer();
|
|
233
|
+
try {
|
|
234
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
235
|
+
await server.connect(transport);
|
|
236
|
+
await transport.handleRequest(req, res, req.body);
|
|
237
|
+
res.on("close", () => {
|
|
238
|
+
transport.close();
|
|
239
|
+
server.close();
|
|
240
|
+
});
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error("Error handling MCP request:", error);
|
|
243
|
+
if (!res.headersSent) {
|
|
244
|
+
res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
app.get("/mcp", async (req, res) => {
|
|
250
|
+
res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
app.delete("/mcp", async (req, res) => {
|
|
254
|
+
res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Health check endpoint
|
|
258
|
+
app.get("/", (req, res) => {
|
|
259
|
+
res.json({
|
|
260
|
+
name: "speclock",
|
|
261
|
+
version: VERSION,
|
|
262
|
+
author: AUTHOR,
|
|
263
|
+
description: "AI Continuity Engine — Kill AI amnesia",
|
|
264
|
+
tools: 19,
|
|
265
|
+
mcp_endpoint: "/mcp",
|
|
266
|
+
npm: "https://www.npmjs.com/package/speclock",
|
|
267
|
+
github: "https://github.com/sgroy10/flowkeeper",
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
272
|
+
app.listen(PORT, "0.0.0.0", () => {
|
|
273
|
+
console.log(`SpecLock MCP HTTP Server v${VERSION} running on port ${PORT} — Developed by ${AUTHOR}`);
|
|
274
|
+
});
|
package/src/mcp/server.js
CHANGED
|
@@ -721,10 +721,22 @@ server.tool(
|
|
|
721
721
|
}
|
|
722
722
|
);
|
|
723
723
|
|
|
724
|
-
// ---
|
|
725
|
-
|
|
726
|
-
|
|
724
|
+
// --- Smithery sandbox export ---
|
|
725
|
+
export default function createSandboxServer() {
|
|
726
|
+
return server;
|
|
727
|
+
}
|
|
727
728
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
729
|
+
// --- Start server (skip when bundled as CJS for Smithery scanning) ---
|
|
730
|
+
const isScanMode = typeof import.meta.url === "undefined";
|
|
731
|
+
|
|
732
|
+
if (!isScanMode) {
|
|
733
|
+
const transport = new StdioServerTransport();
|
|
734
|
+
server.connect(transport).then(() => {
|
|
735
|
+
process.stderr.write(
|
|
736
|
+
`SpecLock MCP v${VERSION} running (stdio) — Developed by ${AUTHOR}. Root: ${PROJECT_ROOT}${os.EOL}`
|
|
737
|
+
);
|
|
738
|
+
}).catch((err) => {
|
|
739
|
+
process.stderr.write(`SpecLock fatal: ${err.message}${os.EOL}`);
|
|
740
|
+
process.exit(1);
|
|
741
|
+
});
|
|
742
|
+
}
|