product-spec-mcp 0.3.31 → 0.3.34
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 +15 -8
- package/docs/online-pm-gate.md +29 -1
- package/package.json +1 -1
- package/workers/pm-intent-gate.mjs +20 -13
- package/workers/wrangler.toml.example +1 -0
package/dist/index.cjs
CHANGED
|
@@ -24073,8 +24073,8 @@ function looseStringArray(description) {
|
|
|
24073
24073
|
var SpecCompileInputSchema = external_exports.object({
|
|
24074
24074
|
raw_idea: external_exports.string().describe("\u7528\u6237\u539F\u59CB\u60F3\u6CD5"),
|
|
24075
24075
|
answers: external_exports.record(external_exports.string(), external_exports.any()).optional().describe("\u7528\u6237\u5BF9\u8FFD\u95EE\u7684\u56DE\u7B54"),
|
|
24076
|
-
allow_assumptions:
|
|
24077
|
-
min_readiness_score:
|
|
24076
|
+
allow_assumptions: looseBoolean.optional().default(true).describe("\u662F\u5426\u5141\u8BB8\u4F7F\u7528\u9ED8\u8BA4\u5047\u8BBE"),
|
|
24077
|
+
min_readiness_score: looseNumber.optional().default(70).describe("\u6700\u4F4E\u53EF\u63A5\u53D7\u7684 readiness \u5206\u6570")
|
|
24078
24078
|
});
|
|
24079
24079
|
|
|
24080
24080
|
// src/schemas/outputs/specCompile.output.ts
|
|
@@ -24262,6 +24262,13 @@ function buildSpec(rawIdea, context, readiness) {
|
|
|
24262
24262
|
if (hasStructuredAnswers) {
|
|
24263
24263
|
assumptions.push("\u5F53\u524D\u8F93\u5165\u5305\u542B\u591A\u4E2A\u7ED3\u6784\u5316\u7B54\u6848\uFF0C\u4F46\u672A\u5339\u914D\u5230\u7A33\u5B9A domain pack\uFF1B\u4E0D\u8981\u628A\u5B83\u5957\u5165\u62A5\u540D\u3001\u7535\u5546\u3001\u9884\u7EA6\u3001\u5185\u5BB9\u793E\u533A\u3001\u5DE5\u5355\u6216\u77E5\u8BC6\u5E93\u6A21\u677F\u3002");
|
|
24264
24264
|
}
|
|
24265
|
+
if (personalLocalTool) {
|
|
24266
|
+
assumptions.push(
|
|
24267
|
+
"\u6570\u636E\u9ED8\u8BA4\u4FDD\u5B58\u5728\u5F53\u524D\u6D4F\u89C8\u5668 localStorage \u4E2D\uFF0C\u5237\u65B0\u540E\u4FDD\u7559\uFF0C\u4F46\u4E0D\u505A\u8DE8\u8BBE\u5907\u540C\u6B65\u3002",
|
|
24268
|
+
"\u7B2C\u4E00\u7248\u9ED8\u8BA4\u4E0D\u9700\u8981\u767B\u5F55\u3001\u6CE8\u518C\u3001\u540E\u53F0\u7BA1\u7406\u6216\u7BA1\u7406\u5458\u89D2\u8272\u3002",
|
|
24269
|
+
"\u9875\u9762\u9AD8\u7EA7\u611F\u53EA\u4F5C\u4E3A UI \u98CE\u683C\u548C\u54CD\u5E94\u5F0F\u9A8C\u6536\u8981\u6C42\uFF0C\u4E0D\u4F5C\u4E3A\u540E\u7AEF\u6216\u670D\u52A1\u5668\u6570\u636E\u5E93\u4FE1\u53F7\u3002"
|
|
24270
|
+
);
|
|
24271
|
+
}
|
|
24265
24272
|
const needsBackend = technicalProfile.needsBackend || normalizedContext.need_backend === true || normalizedContext.data_persistence === true || normalizedContext.user_roles === true || normalizedContext.backend_need === true;
|
|
24266
24273
|
const dataModel = buildDataModel(normalizedContext, needsBackend, technicalProfile, rawIdea);
|
|
24267
24274
|
const successCriteria = personalLocalTool ? buildLocalFirstSuccessCriteria(rawIdea) : ["\u6838\u5FC3\u529F\u80FD\u53EF\u7528", "\u65E0\u660E\u663E Bug", "\u7528\u6237\u4F53\u9A8C\u6D41\u7545"];
|
|
@@ -25829,7 +25836,7 @@ var ArchitectureDecideInputSchema = external_exports.object({
|
|
|
25829
25836
|
product_type: external_exports.string().describe("\u4EA7\u54C1\u7C7B\u578B\u63CF\u8FF0"),
|
|
25830
25837
|
platform: external_exports.enum(["web", "mini_program", "app", "backend"]).describe("\u76EE\u6807\u5E73\u53F0"),
|
|
25831
25838
|
features: looseStringArray("\u529F\u80FD\u5217\u8868"),
|
|
25832
|
-
commercial_intent:
|
|
25839
|
+
commercial_intent: looseBoolean.optional().default(false).describe("\u662F\u5426\u6709\u5546\u4E1A\u5316\u610F\u56FE"),
|
|
25833
25840
|
expected_users: external_exports.enum(["individual", "small_team", "enterprise", "massive"]).optional().default("individual").describe("\u9884\u671F\u7528\u6237\u89C4\u6A21")
|
|
25834
25841
|
});
|
|
25835
25842
|
|
|
@@ -26906,9 +26913,9 @@ var AcceptanceGenerateInputSchema = external_exports.object({
|
|
|
26906
26913
|
product_type: external_exports.string().describe("\u4EA7\u54C1\u7C7B\u578B"),
|
|
26907
26914
|
features: looseStringArray("\u529F\u80FD\u5217\u8868"),
|
|
26908
26915
|
platform: external_exports.enum(["web", "mini_program", "app", "backend"]).describe("\u76EE\u6807\u5E73\u53F0"),
|
|
26909
|
-
has_backend:
|
|
26910
|
-
has_payment:
|
|
26911
|
-
has_auth:
|
|
26916
|
+
has_backend: looseBoolean.optional().default(false).describe("\u662F\u5426\u6709\u540E\u7AEF"),
|
|
26917
|
+
has_payment: looseBoolean.optional().default(false).describe("\u662F\u5426\u6D89\u53CA\u652F\u4ED8"),
|
|
26918
|
+
has_auth: looseBoolean.optional().default(false).describe("\u662F\u5426\u6D89\u53CA\u9274\u6743")
|
|
26912
26919
|
});
|
|
26913
26920
|
|
|
26914
26921
|
// src/schemas/outputs/acceptanceGenerate.output.ts
|
|
@@ -27852,7 +27859,7 @@ var ProductSpecAssistInputSchema = external_exports.object({
|
|
|
27852
27859
|
known_context: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("\u5DF2\u6709\u4E0A\u4E0B\u6587"),
|
|
27853
27860
|
preferred_platform: external_exports.enum(["web", "mini_program", "app", "backend", "unknown"]).optional().default("unknown").describe("\u7528\u6237\u5DF2\u77E5\u5E73\u53F0"),
|
|
27854
27861
|
strictness: external_exports.enum(["light", "normal", "grill"]).optional().default("normal").describe("\u8FFD\u95EE\u5F3A\u5EA6"),
|
|
27855
|
-
auto_execute:
|
|
27862
|
+
auto_execute: looseBoolean.optional().default(true).describe("\u662F\u5426\u5141\u8BB8\u81EA\u52A8\u8C03\u7528\u5BF9\u5E94 engine")
|
|
27856
27863
|
});
|
|
27857
27864
|
|
|
27858
27865
|
// src/schemas/outputs/productSpecAssist.output.ts
|
|
@@ -29969,7 +29976,7 @@ function registerProductSpecAssist(server) {
|
|
|
29969
29976
|
function createServer() {
|
|
29970
29977
|
const server = new McpServer({
|
|
29971
29978
|
name: "product-spec-mcp",
|
|
29972
|
-
version: "0.3.
|
|
29979
|
+
version: "0.3.34"
|
|
29973
29980
|
});
|
|
29974
29981
|
registerSpecInterrogate(server);
|
|
29975
29982
|
registerSpecCompile(server);
|
package/docs/online-pm-gate.md
CHANGED
|
@@ -53,6 +53,7 @@ Default LLM provider:
|
|
|
53
53
|
LLM_PROVIDER = "mimo"
|
|
54
54
|
LLM_BASE_URL = "https://token-plan-cn.xiaomimimo.com/v1"
|
|
55
55
|
LLM_MODEL = "mimo-v2.5"
|
|
56
|
+
DAILY_LLM_LIMIT = "20"
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
To switch later to DeepSeek, change the Worker vars to:
|
|
@@ -74,9 +75,36 @@ Runtime behavior:
|
|
|
74
75
|
|
|
75
76
|
- Prompt cache key: `cache:{model}:{promptHash}:pm-gate-v1`
|
|
76
77
|
- Cache TTL: 7 days
|
|
77
|
-
- LLM quota:
|
|
78
|
+
- LLM quota: `DAILY_LLM_LIMIT` non-cached LLM decisions per IP per Shanghai calendar day. Default: 20.
|
|
78
79
|
- User message sent to LLM: max 500 characters
|
|
79
80
|
- LLM max output tokens: 600
|
|
80
81
|
- LLM temperature: 0.1
|
|
81
82
|
|
|
83
|
+
## Change LLM Daily Quota
|
|
84
|
+
|
|
85
|
+
`DAILY_LLM_LIMIT` controls the number of non-cached LLM gate calls allowed per IP per Shanghai calendar day. It is a Worker runtime variable, not an npm package setting.
|
|
86
|
+
|
|
87
|
+
Default:
|
|
88
|
+
|
|
89
|
+
```toml
|
|
90
|
+
DAILY_LLM_LIMIT = "20"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
To change it from local config:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cd /Users/george/Documents/product-spec-mcp/workers
|
|
97
|
+
# edit DAILY_LLM_LIMIT in wrangler.toml
|
|
98
|
+
npx wrangler deploy
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
To change it from Cloudflare Dashboard:
|
|
102
|
+
|
|
103
|
+
1. Open Worker `product-spec-pm-intent-gate`.
|
|
104
|
+
2. Go to Variables and Secrets.
|
|
105
|
+
3. Edit plaintext variable `DAILY_LLM_LIMIT`.
|
|
106
|
+
4. Save/deploy the Worker configuration.
|
|
107
|
+
|
|
108
|
+
Changing only this quota does not require an npm release. npm only needs to be published when the package code, bundled Worker file, or documentation should be distributed to npm users.
|
|
109
|
+
|
|
82
110
|
If the Worker is unreachable, rate-limited, returns invalid JSON, or returns invalid enum fields, the local MCP falls back to the local PM Gate decision.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "product-spec-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.34",
|
|
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",
|
|
@@ -4,7 +4,7 @@ const DEFAULT_MIMO_BASE_URL = "https://token-plan-cn.xiaomimimo.com/v1";
|
|
|
4
4
|
const DEFAULT_MIMO_MODEL = "mimo-v2.5";
|
|
5
5
|
const DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com";
|
|
6
6
|
const DEFAULT_DEEPSEEK_MODEL = "deepseek-chat";
|
|
7
|
-
const
|
|
7
|
+
const DEFAULT_DAILY_LIMIT = 20;
|
|
8
8
|
|
|
9
9
|
export default {
|
|
10
10
|
async fetch(request, env) {
|
|
@@ -34,6 +34,7 @@ export default {
|
|
|
34
34
|
const cached = await env.PROMPT_CACHE?.get(cacheKey, "json");
|
|
35
35
|
const ipKey = await rateLimitKey(request, env);
|
|
36
36
|
const resetAt = nextShanghaiMidnightIso();
|
|
37
|
+
const dailyLimit = resolveDailyLimit(env);
|
|
37
38
|
|
|
38
39
|
if (cached?.decision) {
|
|
39
40
|
await maybeStoreSample(env, telemetryMode, body, cached.decision, cached.decision, {
|
|
@@ -52,15 +53,15 @@ export default {
|
|
|
52
53
|
cacheHit: true,
|
|
53
54
|
},
|
|
54
55
|
rateLimit: {
|
|
55
|
-
limit:
|
|
56
|
-
remaining: await remainingForKey(env, ipKey),
|
|
56
|
+
limit: dailyLimit,
|
|
57
|
+
remaining: await remainingForKey(env, ipKey, dailyLimit),
|
|
57
58
|
resetAt,
|
|
58
59
|
},
|
|
59
60
|
privacy: privacyResult(telemetryMode),
|
|
60
61
|
});
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
const limit = await consumeLimit(env, ipKey, resetAt);
|
|
64
|
+
const limit = await consumeLimit(env, ipKey, resetAt, dailyLimit);
|
|
64
65
|
if (!limit.allowed) {
|
|
65
66
|
await maybeStoreSample(env, telemetryMode, body, null, body.ruleDecision || {}, {
|
|
66
67
|
llmUsed: 0,
|
|
@@ -71,7 +72,7 @@ export default {
|
|
|
71
72
|
return json({
|
|
72
73
|
decision: fallbackDecision(body.ruleDecision),
|
|
73
74
|
llmGate: { used: false, provider: llm.provider, model: llm.model, cacheHit: false },
|
|
74
|
-
rateLimit: { limit:
|
|
75
|
+
rateLimit: { limit: dailyLimit, remaining: 0, resetAt },
|
|
75
76
|
privacy: privacyResult(telemetryMode),
|
|
76
77
|
}, 429);
|
|
77
78
|
}
|
|
@@ -121,7 +122,7 @@ export default {
|
|
|
121
122
|
...(fallbackReason ? { fallbackReason } : {}),
|
|
122
123
|
},
|
|
123
124
|
rateLimit: {
|
|
124
|
-
limit:
|
|
125
|
+
limit: dailyLimit,
|
|
125
126
|
remaining: limit.remaining,
|
|
126
127
|
resetAt,
|
|
127
128
|
},
|
|
@@ -195,6 +196,12 @@ function resolveLlmConfig(env) {
|
|
|
195
196
|
};
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
function resolveDailyLimit(env) {
|
|
200
|
+
const parsed = Number(env.DAILY_LLM_LIMIT || DEFAULT_DAILY_LIMIT);
|
|
201
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_DAILY_LIMIT;
|
|
202
|
+
return Math.floor(parsed);
|
|
203
|
+
}
|
|
204
|
+
|
|
198
205
|
async function callOpenAiCompatible(llm, prompt) {
|
|
199
206
|
if (!llm.apiKey) throw new Error(`missing_${llm.provider}_api_key`);
|
|
200
207
|
const response = await fetch(`${normalizeBaseUrl(llm.baseUrl)}/chat/completions`, {
|
|
@@ -343,20 +350,20 @@ function fallbackDecision(ruleDecision) {
|
|
|
343
350
|
};
|
|
344
351
|
}
|
|
345
352
|
|
|
346
|
-
async function consumeLimit(env, key, resetAt) {
|
|
347
|
-
if (!env.PROMPT_CACHE) return { allowed: true, remaining:
|
|
353
|
+
async function consumeLimit(env, key, resetAt, dailyLimit) {
|
|
354
|
+
if (!env.PROMPT_CACHE) return { allowed: true, remaining: dailyLimit - 1 };
|
|
348
355
|
const current = Number(await env.PROMPT_CACHE.get(key) || "0");
|
|
349
|
-
if (current >=
|
|
356
|
+
if (current >= dailyLimit) return { allowed: false, remaining: 0 };
|
|
350
357
|
const next = current + 1;
|
|
351
358
|
const resetSeconds = Math.max(60, Math.floor((new Date(resetAt).getTime() - Date.now()) / 1000));
|
|
352
359
|
await env.PROMPT_CACHE.put(key, String(next), { expirationTtl: resetSeconds });
|
|
353
|
-
return { allowed: true, remaining: Math.max(0,
|
|
360
|
+
return { allowed: true, remaining: Math.max(0, dailyLimit - next) };
|
|
354
361
|
}
|
|
355
362
|
|
|
356
|
-
async function remainingForKey(env, key) {
|
|
357
|
-
if (!env.PROMPT_CACHE) return
|
|
363
|
+
async function remainingForKey(env, key, dailyLimit) {
|
|
364
|
+
if (!env.PROMPT_CACHE) return dailyLimit;
|
|
358
365
|
const current = Number(await env.PROMPT_CACHE.get(key) || "0");
|
|
359
|
-
return Math.max(0,
|
|
366
|
+
return Math.max(0, dailyLimit - current);
|
|
360
367
|
}
|
|
361
368
|
|
|
362
369
|
async function rateLimitKey(request, env) {
|