product-spec-mcp 0.3.32 → 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
@@ -29976,7 +29976,7 @@ function registerProductSpecAssist(server) {
29976
29976
  function createServer() {
29977
29977
  const server = new McpServer({
29978
29978
  name: "product-spec-mcp",
29979
- version: "0.3.32"
29979
+ version: "0.3.34"
29980
29980
  });
29981
29981
  registerSpecInterrogate(server);
29982
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.32",
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