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 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.27"
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.27",
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, no markdown.",
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
- { role: "system", content: "You are a terse product intent classifier. Output JSON only." },
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
- const content = data?.choices?.[0]?.message?.content;
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 start = text.indexOf("{");
218
- const end = text.lastIndexOf("}");
219
- if (start < 0 || end <= start) return null;
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(text.slice(start, end + 1));
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;