local-model-suitability-mcp 1.1.12 → 1.1.13

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/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.13] - 2026-06-11
4
+ - feat: per-tool kill switch + per-minute rate limiting on AI tools
5
+
3
6
  ## [1.1.12] - 2026-06-08
4
7
  - fix: BEFORE trigger language, consequence-first limit error
5
8
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "local-model-suitability-mcp",
3
3
  "mcpName": "io.github.OjasKord/local-model-suitability-mcp",
4
- "version": "1.1.12",
4
+ "version": "1.1.13",
5
5
  "description": "AI model router for agents. Checks whether a local model can handle the task before calling cloud inference. LOCAL/CLOUD verdict saves cost on every call.",
6
6
  "main": "src/server.js",
7
7
  "type": "module",
package/src/server.js CHANGED
@@ -3,7 +3,7 @@ import { createHmac, timingSafeEqual } from 'crypto';
3
3
  import { readFileSync, writeFileSync } from 'fs';
4
4
  import Anthropic from '@anthropic-ai/sdk';
5
5
 
6
- const VERSION = '1.1.12';
6
+ const VERSION = '1.1.13';
7
7
  const PRO_UPGRADE_URL = 'https://buy.stripe.com/cNibJ08wd7zf6NS0h2ebu0p';
8
8
  const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/28E9AS27PbPvfkoe7Sebu0q';
9
9
  const PERSIST_FILE = '/tmp/lms_stats.json';
@@ -26,6 +26,22 @@ let stats = {
26
26
  const trialExtensions = new Map();
27
27
  const TRIAL_EXTENSION_CALLS = 10;
28
28
 
29
+ const perMinuteUsage = new Map();
30
+
31
+ function checkPerMinuteLimit(ip, toolName, limit) {
32
+ const minuteKey = ip + ':' + toolName + ':' + new Date().toISOString().slice(0, 16);
33
+ const count = perMinuteUsage.get(minuteKey) || 0;
34
+ if (count >= limit) return false;
35
+ perMinuteUsage.set(minuteKey, count + 1);
36
+ if (perMinuteUsage.size > 10000) {
37
+ const currentMinute = new Date().toISOString().slice(0, 16);
38
+ for (const [key] of perMinuteUsage) {
39
+ if (!key.includes(currentMinute)) perMinuteUsage.delete(key);
40
+ }
41
+ }
42
+ return true;
43
+ }
44
+
29
45
  const REDIS_PREFIX = 'lms';
30
46
  const FREE_TIER_REDIS_KEY = 'lms:free_tier_usage';
31
47
  const UPSTASH_URL = process.env.UPSTASH_REDIS_REST_URL;
@@ -619,6 +635,11 @@ const server = createServer(async (req, res) => {
619
635
  } else if (request.method === 'prompts/list') {
620
636
  response = { jsonrpc: '2.0', id: request.id, result: { prompts: [] } };
621
637
  } else if (request.method === 'tools/call' && request.params?.name === 'check_local_viability') {
638
+ if (process.env['TOOL_DISABLED_CHECK_LOCAL_VIABILITY'] === 'true') {
639
+ response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'This tool is temporarily unavailable for maintenance.', agent_action: 'RETRY_IN_30_MIN', retryable: true, retry_after_ms: 1800000 }) }] } };
640
+ } else if (!checkPerMinuteLimit(clientIp, 'check_local_viability', 5)) {
641
+ response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'Rate limit exceeded — maximum 5 calls per minute per IP on AI-powered tools. Your workflow is calling this tool too rapidly.', agent_action: 'RETRY_IN_60_SEC', retryable: true, retry_after_ms: 60000, limit: 5, window: '1 minute' }) }] } };
642
+ } else {
622
643
  const { task, quality_threshold, data_sensitivity } = request.params.arguments || {};
623
644
 
624
645
  if (!task || task.trim().length === 0) {
@@ -672,6 +693,7 @@ const server = createServer(async (req, res) => {
672
693
  }
673
694
  }
674
695
  }
696
+ }
675
697
  } else {
676
698
  response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found: ' + request.method } };
677
699
  }
@@ -715,6 +737,9 @@ function setupStdio() {
715
737
  } else if (req.method === 'prompts/list') {
716
738
  response = { jsonrpc: '2.0', id: req.id, result: { prompts: [] } };
717
739
  } else if (req.method === 'tools/call' && req.params?.name === 'check_local_viability') {
740
+ if (process.env['TOOL_DISABLED_CHECK_LOCAL_VIABILITY'] === 'true') {
741
+ response = { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'This tool is temporarily unavailable for maintenance.', agent_action: 'RETRY_IN_30_MIN', retryable: true, retry_after_ms: 1800000 }) }] } };
742
+ } else {
718
743
  const { task, quality_threshold, data_sensitivity } = req.params.arguments || {};
719
744
  if (!task || task.trim().length === 0) {
720
745
  response = { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'task is required', likely_cause: 'required field missing or malformed', retryable: false, retry_after_ms: null, fallback_tool: null, agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', trace_id: nowISO(), _disclaimer: LEGAL_DISCLAIMER }) }] } };
@@ -726,6 +751,7 @@ function setupStdio() {
726
751
  response = { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: e.message, likely_cause: 'AI routing analysis failed — transient Anthropic API issue', retryable: true, retry_after_ms: 120000, fallback_tool: null, agent_action: 'RETRY_IN_2_MIN', category: 'ai_failure', trace_id: nowISO(), _disclaimer: LEGAL_DISCLAIMER }) }] } };
727
752
  }
728
753
  }
754
+ }
729
755
  } else {
730
756
  response = { jsonrpc: '2.0', id: req.id, error: { code: -32601, message: 'Method not found: ' + req.method } };
731
757
  }