local-model-suitability-mcp 1.1.12 → 1.1.14
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 +6 -0
- package/icon.svg +5 -0
- package/package.json +1 -1
- package/src/server.js +27 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.14] - 2026-06-11
|
|
4
|
+
- fix: bump version past existing npm publish (1.1.13 already on registry)
|
|
5
|
+
|
|
6
|
+
## [1.1.13] - 2026-06-11
|
|
7
|
+
- feat: per-tool kill switch + per-minute rate limiting on AI tools
|
|
8
|
+
|
|
3
9
|
## [1.1.12] - 2026-06-08
|
|
4
10
|
- fix: BEFORE trigger language, consequence-first limit error
|
|
5
11
|
|
package/icon.svg
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
2
|
+
<rect width="32" height="32" rx="6" fill="#080A0F"/>
|
|
3
|
+
<circle cx="16" cy="16" r="10" fill="none" stroke="#00E5C3" stroke-width="2"/>
|
|
4
|
+
<polyline points="11,16 14.5,19.5 21,12.5" fill="none" stroke="#00E5C3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
</svg>
|
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.
|
|
4
|
+
"version": "1.1.14",
|
|
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.
|
|
6
|
+
const VERSION = '1.1.14';
|
|
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
|
}
|