product-spec-mcp 0.3.27 → 0.3.29
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/README.md +1 -1
- package/dist/index.cjs +17 -4
- package/docs/online-pm-gate.md +1 -1
- package/package.json +1 -1
- package/workers/pm-intent-gate.mjs +89 -7
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ npm run dev
|
|
|
61
61
|
```bash
|
|
62
62
|
PRODUCT_SPEC_REMOTE_GATE_URL=https://gate.example.com/v1/pm-intent
|
|
63
63
|
PRODUCT_SPEC_REMOTE_GATE_TOKEN=replace-with-token
|
|
64
|
-
PRODUCT_SPEC_REMOTE_GATE_TIMEOUT_MS=
|
|
64
|
+
PRODUCT_SPEC_REMOTE_GATE_TIMEOUT_MS=10000
|
|
65
65
|
PRODUCT_SPEC_REMOTE_GATE_MODE=auto
|
|
66
66
|
PRODUCT_SPEC_TELEMETRY=off
|
|
67
67
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -23811,7 +23811,7 @@ function decideRecommendedDeployment(needType, technicalShape, maintenanceMode,
|
|
|
23811
23811
|
if (accessTopology === "lan_only") return "local_lan_server_sqlite";
|
|
23812
23812
|
if (accessTopology === "internet_ip") return "cheap_vps_sqlite_by_ip";
|
|
23813
23813
|
if (accessTopology === "public_domain") return "vps_domain_https";
|
|
23814
|
-
if (technicalShape === "light_backend_json_sqlite") return "
|
|
23814
|
+
if (technicalShape === "light_backend_json_sqlite") return "unknown";
|
|
23815
23815
|
return "unknown";
|
|
23816
23816
|
}
|
|
23817
23817
|
function decideRoute(needType, technicalShape, maintenanceMode) {
|
|
@@ -23833,13 +23833,19 @@ function enforceHardRules(decision) {
|
|
|
23833
23833
|
needType: "multi_user_collaboration",
|
|
23834
23834
|
maintenanceMode: "runtime_collaboration",
|
|
23835
23835
|
technicalShape: "light_backend_json_sqlite",
|
|
23836
|
-
recommendedDeployment: decision.accessTopology
|
|
23836
|
+
recommendedDeployment: recommendedDeploymentForCollaboration(decision.accessTopology),
|
|
23837
23837
|
route: "spec_interrogate",
|
|
23838
23838
|
mustNotUse: Array.from(/* @__PURE__ */ new Set([...decision.mustNotUse, "static_display", "local_storage_only"]))
|
|
23839
23839
|
};
|
|
23840
23840
|
}
|
|
23841
23841
|
return decision;
|
|
23842
23842
|
}
|
|
23843
|
+
function recommendedDeploymentForCollaboration(accessTopology) {
|
|
23844
|
+
if (accessTopology === "lan_only") return "local_lan_server_sqlite";
|
|
23845
|
+
if (accessTopology === "internet_ip") return "cheap_vps_sqlite_by_ip";
|
|
23846
|
+
if (accessTopology === "public_domain") return "vps_domain_https";
|
|
23847
|
+
return "unknown";
|
|
23848
|
+
}
|
|
23843
23849
|
function buildMustNotUse(needType, maintenanceMode) {
|
|
23844
23850
|
const items = [];
|
|
23845
23851
|
if (needType === "multi_user_collaboration") items.push("static_display", "local_storage_only");
|
|
@@ -27702,7 +27708,7 @@ async function callRemotePmIntentGate(message, context, localDecision) {
|
|
|
27702
27708
|
const url = process.env.PRODUCT_SPEC_REMOTE_GATE_URL;
|
|
27703
27709
|
if (!url) return null;
|
|
27704
27710
|
const controller = new AbortController();
|
|
27705
|
-
const timeout = Number(process.env.PRODUCT_SPEC_REMOTE_GATE_TIMEOUT_MS ||
|
|
27711
|
+
const timeout = Number(process.env.PRODUCT_SPEC_REMOTE_GATE_TIMEOUT_MS || 1e4);
|
|
27706
27712
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
27707
27713
|
try {
|
|
27708
27714
|
const response = await fetch(url, {
|
|
@@ -27874,6 +27880,7 @@ function enforceRemoteHardRules(remote, local) {
|
|
|
27874
27880
|
needType: "multi_user_collaboration",
|
|
27875
27881
|
maintenanceMode: "runtime_collaboration",
|
|
27876
27882
|
technicalShape: "light_backend_json_sqlite",
|
|
27883
|
+
recommendedDeployment: recommendedDeploymentForCollaboration2(remote.accessTopology),
|
|
27877
27884
|
route: "spec_interrogate",
|
|
27878
27885
|
mustNotUse: mergeStrings(remote.mustNotUse, ["static_display", "local_storage_only"])
|
|
27879
27886
|
};
|
|
@@ -27891,6 +27898,12 @@ function enforceRemoteHardRules(remote, local) {
|
|
|
27891
27898
|
}
|
|
27892
27899
|
return remote;
|
|
27893
27900
|
}
|
|
27901
|
+
function recommendedDeploymentForCollaboration2(accessTopology) {
|
|
27902
|
+
if (accessTopology === "lan_only") return "local_lan_server_sqlite";
|
|
27903
|
+
if (accessTopology === "internet_ip") return "cheap_vps_sqlite_by_ip";
|
|
27904
|
+
if (accessTopology === "public_domain") return "vps_domain_https";
|
|
27905
|
+
return "unknown";
|
|
27906
|
+
}
|
|
27894
27907
|
function hasRouteShapeConflict(decision) {
|
|
27895
27908
|
return decision.route === "architecture_decide" && decision.technicalShape !== "static_page";
|
|
27896
27909
|
}
|
|
@@ -29682,7 +29695,7 @@ function registerProductSpecAssist(server) {
|
|
|
29682
29695
|
function createServer() {
|
|
29683
29696
|
const server = new McpServer({
|
|
29684
29697
|
name: "product-spec-mcp",
|
|
29685
|
-
version: "0.3.
|
|
29698
|
+
version: "0.3.29"
|
|
29686
29699
|
});
|
|
29687
29700
|
registerSpecInterrogate(server);
|
|
29688
29701
|
registerSpecCompile(server);
|
package/docs/online-pm-gate.md
CHANGED
|
@@ -7,7 +7,7 @@ P0 online gate is an HTTP classifier for low-confidence or conflicting local PM
|
|
|
7
7
|
```bash
|
|
8
8
|
PRODUCT_SPEC_REMOTE_GATE_URL=https://gate.example.com/v1/pm-intent
|
|
9
9
|
PRODUCT_SPEC_REMOTE_GATE_TOKEN=replace-with-token
|
|
10
|
-
PRODUCT_SPEC_REMOTE_GATE_TIMEOUT_MS=
|
|
10
|
+
PRODUCT_SPEC_REMOTE_GATE_TIMEOUT_MS=10000
|
|
11
11
|
PRODUCT_SPEC_REMOTE_GATE_MODE=auto
|
|
12
12
|
PRODUCT_SPEC_TELEMETRY=off
|
|
13
13
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "product-spec-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.29",
|
|
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;
|