product-spec-mcp 0.3.27 → 0.3.28
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/index.cjs +1 -1
- package/package.json +1 -1
- package/workers/pm-intent-gate.mjs +89 -7
package/dist/index.cjs
CHANGED
|
@@ -29682,7 +29682,7 @@ function registerProductSpecAssist(server) {
|
|
|
29682
29682
|
function createServer() {
|
|
29683
29683
|
const server = new McpServer({
|
|
29684
29684
|
name: "product-spec-mcp",
|
|
29685
|
-
version: "0.3.
|
|
29685
|
+
version: "0.3.28"
|
|
29686
29686
|
});
|
|
29687
29687
|
registerSpecInterrogate(server);
|
|
29688
29688
|
registerSpecCompile(server);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "product-spec-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.28",
|
|
4
4
|
"description": "MCP Server for product specification - requirement interrogation, architecture decision, UI translation, debug guidance, and acceptance generation",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -137,7 +137,21 @@ function isAuthorized(request, env) {
|
|
|
137
137
|
|
|
138
138
|
function buildGatePrompt(message, rule, choices) {
|
|
139
139
|
return JSON.stringify({
|
|
140
|
-
task: "Choose the best PM gate only. Return strict JSON
|
|
140
|
+
task: "Choose the best PM gate only. Return strict JSON only.",
|
|
141
|
+
example: {
|
|
142
|
+
bestGate: "data_visualization_site",
|
|
143
|
+
usageScope: "self",
|
|
144
|
+
maintenanceMode: "agent_assisted",
|
|
145
|
+
accessTopology: "single_device",
|
|
146
|
+
confidence: "medium",
|
|
147
|
+
strongSignals: ["xlsx"],
|
|
148
|
+
weakSignals: ["website"],
|
|
149
|
+
coreObjects: ["xlsx file"],
|
|
150
|
+
states: [],
|
|
151
|
+
actions: ["parse xlsx", "render chart"],
|
|
152
|
+
mustNotUse: ["admin_backend_by_default"],
|
|
153
|
+
boundaryQuestionIds: ["data_update_mode"],
|
|
154
|
+
},
|
|
141
155
|
output: {
|
|
142
156
|
bestGate: "one needType enum",
|
|
143
157
|
usageScope: "one usageScope enum",
|
|
@@ -193,15 +207,25 @@ async function callOpenAiCompatible(llm, prompt) {
|
|
|
193
207
|
model: llm.model,
|
|
194
208
|
temperature: 0.1,
|
|
195
209
|
max_tokens: 600,
|
|
210
|
+
response_format: { type: "json_object" },
|
|
196
211
|
messages: [
|
|
197
|
-
{
|
|
212
|
+
{
|
|
213
|
+
role: "system",
|
|
214
|
+
content: [
|
|
215
|
+
"You are a terse product intent classifier.",
|
|
216
|
+
"Return exactly one valid JSON object.",
|
|
217
|
+
"Do not use markdown, code fences, comments, or prose.",
|
|
218
|
+
"Use only enum values supplied by the user.",
|
|
219
|
+
].join(" "),
|
|
220
|
+
},
|
|
198
221
|
{ role: "user", content: prompt },
|
|
199
222
|
],
|
|
200
223
|
}),
|
|
201
224
|
});
|
|
202
225
|
if (!response.ok) throw new Error(`${llm.provider}_http_${response.status}`);
|
|
203
226
|
const data = await response.json();
|
|
204
|
-
|
|
227
|
+
if (data?.error) throw new Error(`${llm.provider}_error_${data.error.code || data.error.type || "unknown"}`);
|
|
228
|
+
const content = extractOpenAiCompatibleContent(data);
|
|
205
229
|
if (typeof content !== "string" || !content.trim()) throw new Error(`${llm.provider}_empty_content`);
|
|
206
230
|
return content;
|
|
207
231
|
}
|
|
@@ -210,21 +234,79 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
210
234
|
return String(baseUrl || "").replace(/\/+$/, "");
|
|
211
235
|
}
|
|
212
236
|
|
|
237
|
+
function extractOpenAiCompatibleContent(data) {
|
|
238
|
+
const choice = data?.choices?.[0];
|
|
239
|
+
const message = choice?.message || {};
|
|
240
|
+
const content = message.content;
|
|
241
|
+
if (typeof content === "string" && content.trim()) return content;
|
|
242
|
+
if (Array.isArray(content)) {
|
|
243
|
+
const text = content
|
|
244
|
+
.map((part) => {
|
|
245
|
+
if (typeof part === "string") return part;
|
|
246
|
+
if (typeof part?.text === "string") return part.text;
|
|
247
|
+
if (typeof part?.content === "string") return part.content;
|
|
248
|
+
return "";
|
|
249
|
+
})
|
|
250
|
+
.join("");
|
|
251
|
+
if (text.trim()) return text;
|
|
252
|
+
}
|
|
253
|
+
if (typeof message.reasoning_content === "string" && message.reasoning_content.trim()) return message.reasoning_content;
|
|
254
|
+
if (typeof choice?.text === "string" && choice.text.trim()) return choice.text;
|
|
255
|
+
if (typeof data?.output_text === "string" && data.output_text.trim()) return data.output_text;
|
|
256
|
+
return "";
|
|
257
|
+
}
|
|
258
|
+
|
|
213
259
|
function extractJson(text) {
|
|
214
260
|
try {
|
|
215
261
|
return JSON.parse(text);
|
|
216
262
|
} catch {
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
263
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
264
|
+
if (fenced) {
|
|
265
|
+
try {
|
|
266
|
+
return JSON.parse(fenced[1]);
|
|
267
|
+
} catch {
|
|
268
|
+
// Continue to balanced object extraction below.
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const candidate = extractFirstBalancedObject(text);
|
|
272
|
+
if (!candidate) return null;
|
|
220
273
|
try {
|
|
221
|
-
return JSON.parse(
|
|
274
|
+
return JSON.parse(candidate);
|
|
222
275
|
} catch {
|
|
223
276
|
return null;
|
|
224
277
|
}
|
|
225
278
|
}
|
|
226
279
|
}
|
|
227
280
|
|
|
281
|
+
function extractFirstBalancedObject(text) {
|
|
282
|
+
const start = text.indexOf("{");
|
|
283
|
+
if (start < 0) return "";
|
|
284
|
+
let depth = 0;
|
|
285
|
+
let inString = false;
|
|
286
|
+
let escaped = false;
|
|
287
|
+
for (let i = start; i < text.length; i += 1) {
|
|
288
|
+
const char = text[i];
|
|
289
|
+
if (inString) {
|
|
290
|
+
if (escaped) {
|
|
291
|
+
escaped = false;
|
|
292
|
+
} else if (char === "\\") {
|
|
293
|
+
escaped = true;
|
|
294
|
+
} else if (char === "\"") {
|
|
295
|
+
inString = false;
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (char === "\"") {
|
|
300
|
+
inString = true;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (char === "{") depth += 1;
|
|
304
|
+
if (char === "}") depth -= 1;
|
|
305
|
+
if (depth === 0) return text.slice(start, i + 1);
|
|
306
|
+
}
|
|
307
|
+
return "";
|
|
308
|
+
}
|
|
309
|
+
|
|
228
310
|
function sanitizeDecision(raw) {
|
|
229
311
|
if (!raw || typeof raw !== "object") return null;
|
|
230
312
|
const bestGate = raw.bestGate || raw.needType;
|