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 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: external_exports.union([external_exports.boolean(), looseBoolean]).optional().default(true).describe("\u662F\u5426\u5141\u8BB8\u4F7F\u7528\u9ED8\u8BA4\u5047\u8BBE"),
24077
- min_readiness_score: external_exports.union([external_exports.number(), looseNumber]).optional().default(70).describe("\u6700\u4F4E\u53EF\u63A5\u53D7\u7684 readiness \u5206\u6570")
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: external_exports.union([external_exports.boolean(), looseBoolean]).optional().default(false).describe("\u662F\u5426\u6709\u5546\u4E1A\u5316\u610F\u56FE"),
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: external_exports.union([external_exports.boolean(), looseBoolean]).optional().default(false).describe("\u662F\u5426\u6709\u540E\u7AEF"),
26910
- has_payment: external_exports.union([external_exports.boolean(), looseBoolean]).optional().default(false).describe("\u662F\u5426\u6D89\u53CA\u652F\u4ED8"),
26911
- has_auth: external_exports.union([external_exports.boolean(), looseBoolean]).optional().default(false).describe("\u662F\u5426\u6D89\u53CA\u9274\u6743")
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: external_exports.union([external_exports.boolean(), looseBoolean]).optional().default(true).describe("\u662F\u5426\u5141\u8BB8\u81EA\u52A8\u8C03\u7528\u5BF9\u5E94 engine")
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.31"
29979
+ version: "0.3.34"
29973
29980
  });
29974
29981
  registerSpecInterrogate(server);
29975
29982
  registerSpecCompile(server);
@@ -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: 3 non-cached LLM decisions per IP per Shanghai calendar day
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.31",
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 DAILY_LIMIT = 3;
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: DAILY_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: DAILY_LIMIT, remaining: 0, resetAt },
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: DAILY_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: DAILY_LIMIT - 1 };
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 >= DAILY_LIMIT) return { allowed: false, remaining: 0 };
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, DAILY_LIMIT - next) };
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 DAILY_LIMIT;
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, DAILY_LIMIT - current);
366
+ return Math.max(0, dailyLimit - current);
360
367
  }
361
368
 
362
369
  async function rateLimitKey(request, env) {
@@ -15,6 +15,7 @@ database_id = "replace-with-d1-database-id"
15
15
  LLM_PROVIDER = "mimo"
16
16
  LLM_BASE_URL = "https://token-plan-cn.xiaomimimo.com/v1"
17
17
  LLM_MODEL = "mimo-v2.5"
18
+ DAILY_LLM_LIMIT = "20"
18
19
 
19
20
  # Secrets to set with wrangler:
20
21
  # wrangler secret put GATE_TOKEN