speclock 1.1.0 → 1.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/package.json +5 -2
- package/src/mcp/http-server.js +306 -0
- package/src/mcp/server.js +52 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speclock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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,306 @@
|
|
|
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
|
+
`You have SpecLock connected — an AI Continuity Engine by ${AUTHOR}. Follow these rules AUTOMATICALLY without the user ever having to ask:`,
|
|
53
|
+
``,
|
|
54
|
+
`## ON EVERY NEW CONVERSATION / SESSION START:`,
|
|
55
|
+
`1. IMMEDIATELY call speclock_session_briefing BEFORE responding to the user's first message.`,
|
|
56
|
+
`2. If it returns "not initialized", call speclock_init first, then proceed normally.`,
|
|
57
|
+
`3. Use the briefing context to understand the project's goal, constraints, decisions, and history.`,
|
|
58
|
+
`4. Never ask the user to "initialize SpecLock" or "start a session" — just do it silently.`,
|
|
59
|
+
``,
|
|
60
|
+
`## AUTOMATIC CONTEXT CAPTURE (do this silently as you work):`,
|
|
61
|
+
`- When the user describes what they're building → call speclock_set_goal with a concise summary.`,
|
|
62
|
+
`- When the user says something is non-negotiable (e.g., "always use Supabase", "never remove auth", "must support mobile") → call speclock_add_lock to save it as an immutable constraint.`,
|
|
63
|
+
`- When the user makes a technology/architecture choice (e.g., "let's use React Query", "we'll go with Stripe") → call speclock_add_decision.`,
|
|
64
|
+
`- When you complete a significant piece of work → call speclock_log_change.`,
|
|
65
|
+
`- Look for keywords like "always", "never", "must", "don't ever", "make sure to keep" — these signal constraints that should be locked.`,
|
|
66
|
+
``,
|
|
67
|
+
`## BEFORE MAKING SIGNIFICANT CHANGES:`,
|
|
68
|
+
`- Call speclock_check_conflict with a description of what you're about to do.`,
|
|
69
|
+
`- If a conflict is detected (confidence HIGH), WARN the user and explain which constraint would be violated.`,
|
|
70
|
+
`- Do NOT proceed with the change unless the user explicitly says to override or remove the lock.`,
|
|
71
|
+
``,
|
|
72
|
+
`## WHEN THE USER ASKS TO REMOVE SOMETHING THEY PREVIOUSLY LOCKED:`,
|
|
73
|
+
`- Remind them: "You previously locked this constraint: [constraint text]. Want me to remove the lock and proceed?"`,
|
|
74
|
+
`- If they confirm, call speclock_remove_lock, then proceed.`,
|
|
75
|
+
``,
|
|
76
|
+
`## AT THE END OF A CONVERSATION (when the user says goodbye, or the conversation naturally ends):`,
|
|
77
|
+
`- Call speclock_session_summary with a brief description of what was accomplished.`,
|
|
78
|
+
``,
|
|
79
|
+
`## IMPORTANT:`,
|
|
80
|
+
`- NEVER mention "SpecLock" to the user unless they ask about it. Just use it silently in the background.`,
|
|
81
|
+
`- The user should feel like the AI simply "remembers everything" — SpecLock is invisible infrastructure.`,
|
|
82
|
+
`- If the user asks "how do you remember this?" you can explain SpecLock is connected as a memory layer.`,
|
|
83
|
+
`- When showing the user what you know from previous sessions, present it naturally, not as a data dump.`,
|
|
84
|
+
].join("\n"),
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Tool 1: speclock_init
|
|
89
|
+
server.tool("speclock_init", "Initialize SpecLock in the current project directory.", {}, async () => {
|
|
90
|
+
const brain = ensureInit(PROJECT_ROOT);
|
|
91
|
+
return { content: [{ type: "text", text: `SpecLock initialized for "${brain.project.name}" at ${brain.project.root}` }] };
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Tool 2: speclock_get_context
|
|
95
|
+
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 }) => {
|
|
96
|
+
ensureInit(PROJECT_ROOT);
|
|
97
|
+
const text = format === "json" ? JSON.stringify(generateContextPack(PROJECT_ROOT), null, 2) : generateContext(PROJECT_ROOT);
|
|
98
|
+
return { content: [{ type: "text", text }] };
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Tool 3: speclock_set_goal
|
|
102
|
+
server.tool("speclock_set_goal", "Set or update the project goal.", { text: z.string().min(1).describe("The project goal text") }, async ({ text }) => {
|
|
103
|
+
ensureInit(PROJECT_ROOT);
|
|
104
|
+
setGoal(PROJECT_ROOT, text);
|
|
105
|
+
return { content: [{ type: "text", text: `Goal updated: ${text}` }] };
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Tool 4: speclock_add_lock
|
|
109
|
+
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 }) => {
|
|
110
|
+
ensureInit(PROJECT_ROOT);
|
|
111
|
+
const lock = addLock(PROJECT_ROOT, text, tags, source);
|
|
112
|
+
return { content: [{ type: "text", text: `Lock added [${lock.id}]: ${text}` }] };
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Tool 5: speclock_remove_lock
|
|
116
|
+
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 }) => {
|
|
117
|
+
ensureInit(PROJECT_ROOT);
|
|
118
|
+
removeLock(PROJECT_ROOT, lockId);
|
|
119
|
+
return { content: [{ type: "text", text: `Lock ${lockId} removed.` }] };
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Tool 6: speclock_add_decision
|
|
123
|
+
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 }) => {
|
|
124
|
+
ensureInit(PROJECT_ROOT);
|
|
125
|
+
const d = addDecision(PROJECT_ROOT, text, tags, source);
|
|
126
|
+
return { content: [{ type: "text", text: `Decision recorded [${d.id}]: ${text}` }] };
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Tool 7: speclock_add_note
|
|
130
|
+
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 }) => {
|
|
131
|
+
ensureInit(PROJECT_ROOT);
|
|
132
|
+
const brain = readBrain(PROJECT_ROOT);
|
|
133
|
+
const note = { id: newId(), text, pinned, createdAt: nowIso() };
|
|
134
|
+
brain.state.notes.push(note);
|
|
135
|
+
writeBrain(PROJECT_ROOT, brain);
|
|
136
|
+
appendEvent(PROJECT_ROOT, { type: "note_added", noteId: note.id, text });
|
|
137
|
+
bumpEvents(PROJECT_ROOT);
|
|
138
|
+
return { content: [{ type: "text", text: `Note added [${note.id}]: ${text}` }] };
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Tool 8: speclock_set_deploy_facts
|
|
142
|
+
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) => {
|
|
143
|
+
ensureInit(PROJECT_ROOT);
|
|
144
|
+
updateDeployFacts(PROJECT_ROOT, params);
|
|
145
|
+
return { content: [{ type: "text", text: `Deploy facts updated: ${JSON.stringify(params)}` }] };
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Tool 9: speclock_log_change
|
|
149
|
+
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 }) => {
|
|
150
|
+
ensureInit(PROJECT_ROOT);
|
|
151
|
+
logChange(PROJECT_ROOT, summary, files);
|
|
152
|
+
return { content: [{ type: "text", text: `Change logged: ${summary}` }] };
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Tool 10: speclock_get_changes
|
|
156
|
+
server.tool("speclock_get_changes", "Get recent file changes tracked by SpecLock.", { limit: z.number().int().min(1).max(100).default(20) }, async ({ limit }) => {
|
|
157
|
+
ensureInit(PROJECT_ROOT);
|
|
158
|
+
const events = readEvents(PROJECT_ROOT);
|
|
159
|
+
const changes = events.filter((e) => ["file_created", "file_changed", "file_deleted", "manual_change"].includes(e.type)).slice(-limit);
|
|
160
|
+
return { content: [{ type: "text", text: changes.length ? JSON.stringify(changes, null, 2) : "No recent changes." }] };
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Tool 11: speclock_get_events
|
|
164
|
+
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 }) => {
|
|
165
|
+
ensureInit(PROJECT_ROOT);
|
|
166
|
+
let events = readEvents(PROJECT_ROOT);
|
|
167
|
+
if (type) events = events.filter((e) => e.type === type);
|
|
168
|
+
if (since) events = events.filter((e) => e.ts > since);
|
|
169
|
+
events = events.slice(-limit);
|
|
170
|
+
return { content: [{ type: "text", text: events.length ? JSON.stringify(events, null, 2) : "No matching events." }] };
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Tool 12: speclock_check_conflict
|
|
174
|
+
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 }) => {
|
|
175
|
+
ensureInit(PROJECT_ROOT);
|
|
176
|
+
const result = checkConflict(PROJECT_ROOT, proposedAction);
|
|
177
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Tool 13: speclock_session_briefing
|
|
181
|
+
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 }) => {
|
|
182
|
+
ensureInit(PROJECT_ROOT);
|
|
183
|
+
const briefing = getSessionBriefing(PROJECT_ROOT, toolName);
|
|
184
|
+
return { content: [{ type: "text", text: briefing }] };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Tool 14: speclock_session_summary
|
|
188
|
+
server.tool("speclock_session_summary", "End the current session and record what was accomplished.", { summary: z.string().min(1) }, async ({ summary }) => {
|
|
189
|
+
ensureInit(PROJECT_ROOT);
|
|
190
|
+
endSession(PROJECT_ROOT, summary);
|
|
191
|
+
return { content: [{ type: "text", text: `Session ended. Summary recorded: ${summary}` }] };
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Tool 15: speclock_checkpoint
|
|
195
|
+
server.tool("speclock_checkpoint", "Create a named git tag checkpoint for easy rollback.", { name: z.string().min(1) }, async ({ name }) => {
|
|
196
|
+
ensureInit(PROJECT_ROOT);
|
|
197
|
+
const tag = `speclock-${name}`;
|
|
198
|
+
try {
|
|
199
|
+
createTag(PROJECT_ROOT, tag);
|
|
200
|
+
appendEvent(PROJECT_ROOT, { type: "checkpoint_created", tag });
|
|
201
|
+
return { content: [{ type: "text", text: `Checkpoint created: ${tag}` }] };
|
|
202
|
+
} catch (err) {
|
|
203
|
+
return { content: [{ type: "text", text: `Checkpoint failed: ${err.message}` }] };
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Tool 16: speclock_repo_status
|
|
208
|
+
server.tool("speclock_repo_status", "Get current git repository status.", {}, async () => {
|
|
209
|
+
ensureInit(PROJECT_ROOT);
|
|
210
|
+
try {
|
|
211
|
+
const status = captureStatus(PROJECT_ROOT);
|
|
212
|
+
const diff = getDiffSummary(PROJECT_ROOT);
|
|
213
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...status, diffSummary: diff }, null, 2) }] };
|
|
214
|
+
} catch (err) {
|
|
215
|
+
return { content: [{ type: "text", text: `Git status failed: ${err.message}` }] };
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Tool 17: speclock_suggest_locks
|
|
220
|
+
server.tool("speclock_suggest_locks", "AI-powered lock suggestions based on project patterns.", {}, async () => {
|
|
221
|
+
ensureInit(PROJECT_ROOT);
|
|
222
|
+
const suggestions = suggestLocks(PROJECT_ROOT);
|
|
223
|
+
if (!suggestions.length) return { content: [{ type: "text", text: "No lock suggestions at this time." }] };
|
|
224
|
+
const text = suggestions.map((s, i) => `${i + 1}. **${s.text}**\n Source: ${s.source}\n Reason: ${s.reason}`).join("\n\n");
|
|
225
|
+
return { content: [{ type: "text", text: `## Lock Suggestions\n\n${text}\n\n---\n*SpecLock v${VERSION} — Developed by ${AUTHOR}*` }] };
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Tool 18: speclock_detect_drift
|
|
229
|
+
server.tool("speclock_detect_drift", "Scan recent changes for constraint violations.", {}, async () => {
|
|
230
|
+
ensureInit(PROJECT_ROOT);
|
|
231
|
+
const drift = detectDrift(PROJECT_ROOT);
|
|
232
|
+
if (!drift.length) return { content: [{ type: "text", text: "No drift detected. All changes align with active locks." }] };
|
|
233
|
+
const text = drift.map((d, i) => `${i + 1}. **${d.type}**: ${d.summary}\n Lock: ${d.lockText}\n Confidence: ${d.confidence}`).join("\n\n");
|
|
234
|
+
return { content: [{ type: "text", text: `## Drift Detected\n\n${text}` }] };
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Tool 19: speclock_health
|
|
238
|
+
server.tool("speclock_health", "Health check with completeness score and multi-agent timeline.", {}, async () => {
|
|
239
|
+
ensureInit(PROJECT_ROOT);
|
|
240
|
+
const brain = readBrain(PROJECT_ROOT);
|
|
241
|
+
const events = readEvents(PROJECT_ROOT);
|
|
242
|
+
let score = 0;
|
|
243
|
+
const checks = [];
|
|
244
|
+
if (brain.project.goal) { score += 20; checks.push("- [x] Goal set"); } else checks.push("- [ ] Goal missing");
|
|
245
|
+
if (brain.state.locks.filter(l => l.active).length > 0) { score += 20; checks.push("- [x] Locks defined"); } else checks.push("- [ ] No active locks");
|
|
246
|
+
if (brain.state.decisions.length > 0) { score += 15; checks.push("- [x] Decisions recorded"); } else checks.push("- [ ] No decisions");
|
|
247
|
+
if (brain.state.sessions.length > 0) { score += 15; checks.push("- [x] Sessions tracked"); } else checks.push("- [ ] No sessions");
|
|
248
|
+
if (brain.deploy?.provider) { score += 10; checks.push("- [x] Deploy facts set"); } else checks.push("- [ ] Deploy facts missing");
|
|
249
|
+
if (brain.state.notes.length > 0) { score += 10; checks.push("- [x] Notes present"); } else checks.push("- [ ] No notes");
|
|
250
|
+
if (events.length > 10) { score += 10; checks.push("- [x] Rich event history"); } else checks.push("- [ ] Limited events");
|
|
251
|
+
const grade = score >= 90 ? "A" : score >= 70 ? "B" : score >= 50 ? "C" : score >= 30 ? "D" : "F";
|
|
252
|
+
const sessions = brain.state.sessions.slice(-5);
|
|
253
|
+
const agentTimeline = sessions.length ? "\n\n### Recent Sessions\n" + sessions.map(s => `- **${s.tool || "unknown"}** @ ${s.startedAt}${s.summary ? ": " + s.summary : ""}`).join("\n") : "";
|
|
254
|
+
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}*` }] };
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return server;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// --- HTTP Server ---
|
|
261
|
+
const app = createMcpExpressApp({ host: "0.0.0.0" });
|
|
262
|
+
|
|
263
|
+
app.post("/mcp", async (req, res) => {
|
|
264
|
+
const server = createSpecLockServer();
|
|
265
|
+
try {
|
|
266
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
267
|
+
await server.connect(transport);
|
|
268
|
+
await transport.handleRequest(req, res, req.body);
|
|
269
|
+
res.on("close", () => {
|
|
270
|
+
transport.close();
|
|
271
|
+
server.close();
|
|
272
|
+
});
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error("Error handling MCP request:", error);
|
|
275
|
+
if (!res.headersSent) {
|
|
276
|
+
res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
app.get("/mcp", async (req, res) => {
|
|
282
|
+
res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
app.delete("/mcp", async (req, res) => {
|
|
286
|
+
res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Health check endpoint
|
|
290
|
+
app.get("/", (req, res) => {
|
|
291
|
+
res.json({
|
|
292
|
+
name: "speclock",
|
|
293
|
+
version: VERSION,
|
|
294
|
+
author: AUTHOR,
|
|
295
|
+
description: "AI Continuity Engine — Kill AI amnesia",
|
|
296
|
+
tools: 19,
|
|
297
|
+
mcp_endpoint: "/mcp",
|
|
298
|
+
npm: "https://www.npmjs.com/package/speclock",
|
|
299
|
+
github: "https://github.com/sgroy10/speclock",
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
304
|
+
app.listen(PORT, "0.0.0.0", () => {
|
|
305
|
+
console.log(`SpecLock MCP HTTP Server v${VERSION} running on port ${PORT} — Developed by ${AUTHOR}`);
|
|
306
|
+
});
|
package/src/mcp/server.js
CHANGED
|
@@ -56,8 +56,40 @@ const AUTHOR = "Sandeep Roy";
|
|
|
56
56
|
const server = new McpServer(
|
|
57
57
|
{ name: "speclock", version: VERSION },
|
|
58
58
|
{
|
|
59
|
-
instructions:
|
|
60
|
-
`SpecLock
|
|
59
|
+
instructions: [
|
|
60
|
+
`You have SpecLock connected — an AI Continuity Engine by ${AUTHOR}. Follow these rules AUTOMATICALLY without the user ever having to ask:`,
|
|
61
|
+
``,
|
|
62
|
+
`## ON EVERY NEW CONVERSATION / SESSION START:`,
|
|
63
|
+
`1. IMMEDIATELY call speclock_session_briefing BEFORE responding to the user's first message.`,
|
|
64
|
+
`2. If it returns "not initialized", call speclock_init first, then proceed normally.`,
|
|
65
|
+
`3. Use the briefing context to understand the project's goal, constraints, decisions, and history.`,
|
|
66
|
+
`4. Never ask the user to "initialize SpecLock" or "start a session" — just do it silently.`,
|
|
67
|
+
``,
|
|
68
|
+
`## AUTOMATIC CONTEXT CAPTURE (do this silently as you work):`,
|
|
69
|
+
`- When the user describes what they're building → call speclock_set_goal with a concise summary.`,
|
|
70
|
+
`- When the user says something is non-negotiable (e.g., "always use Supabase", "never remove auth", "must support mobile") → call speclock_add_lock to save it as an immutable constraint.`,
|
|
71
|
+
`- When the user makes a technology/architecture choice (e.g., "let's use React Query", "we'll go with Stripe") → call speclock_add_decision.`,
|
|
72
|
+
`- When you complete a significant piece of work → call speclock_log_change.`,
|
|
73
|
+
`- Look for keywords like "always", "never", "must", "don't ever", "make sure to keep" — these signal constraints that should be locked.`,
|
|
74
|
+
``,
|
|
75
|
+
`## BEFORE MAKING SIGNIFICANT CHANGES:`,
|
|
76
|
+
`- Call speclock_check_conflict with a description of what you're about to do.`,
|
|
77
|
+
`- If a conflict is detected (confidence HIGH), WARN the user and explain which constraint would be violated.`,
|
|
78
|
+
`- Do NOT proceed with the change unless the user explicitly says to override or remove the lock.`,
|
|
79
|
+
``,
|
|
80
|
+
`## WHEN THE USER ASKS TO REMOVE SOMETHING THEY PREVIOUSLY LOCKED:`,
|
|
81
|
+
`- Remind them: "You previously locked this constraint: [constraint text]. Want me to remove the lock and proceed?"`,
|
|
82
|
+
`- If they confirm, call speclock_remove_lock, then proceed.`,
|
|
83
|
+
``,
|
|
84
|
+
`## AT THE END OF A CONVERSATION (when the user says goodbye, or the conversation naturally ends):`,
|
|
85
|
+
`- Call speclock_session_summary with a brief description of what was accomplished.`,
|
|
86
|
+
``,
|
|
87
|
+
`## IMPORTANT:`,
|
|
88
|
+
`- NEVER mention "SpecLock" to the user unless they ask about it. Just use it silently in the background.`,
|
|
89
|
+
`- The user should feel like the AI simply "remembers everything" — SpecLock is invisible infrastructure.`,
|
|
90
|
+
`- If the user asks "how do you remember this?" you can explain SpecLock is connected as a memory layer.`,
|
|
91
|
+
`- When showing the user what you know from previous sessions, present it naturally, not as a data dump.`,
|
|
92
|
+
].join("\n"),
|
|
61
93
|
}
|
|
62
94
|
);
|
|
63
95
|
|
|
@@ -721,10 +753,22 @@ server.tool(
|
|
|
721
753
|
}
|
|
722
754
|
);
|
|
723
755
|
|
|
724
|
-
// ---
|
|
725
|
-
|
|
726
|
-
|
|
756
|
+
// --- Smithery sandbox export ---
|
|
757
|
+
export default function createSandboxServer() {
|
|
758
|
+
return server;
|
|
759
|
+
}
|
|
727
760
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
761
|
+
// --- Start server (skip when bundled as CJS for Smithery scanning) ---
|
|
762
|
+
const isScanMode = typeof import.meta.url === "undefined";
|
|
763
|
+
|
|
764
|
+
if (!isScanMode) {
|
|
765
|
+
const transport = new StdioServerTransport();
|
|
766
|
+
server.connect(transport).then(() => {
|
|
767
|
+
process.stderr.write(
|
|
768
|
+
`SpecLock MCP v${VERSION} running (stdio) — Developed by ${AUTHOR}. Root: ${PROJECT_ROOT}${os.EOL}`
|
|
769
|
+
);
|
|
770
|
+
}).catch((err) => {
|
|
771
|
+
process.stderr.write(`SpecLock fatal: ${err.message}${os.EOL}`);
|
|
772
|
+
process.exit(1);
|
|
773
|
+
});
|
|
774
|
+
}
|