local-model-suitability-mcp 1.1.25 → 1.1.28
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 +11 -0
- package/glama.json +16 -6
- package/package.json +1 -1
- package/src/server.js +20 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.27] - 2026-06-29
|
|
4
|
+
- feat: add GET /.well-known/glama.json ownership endpoint for Glama registry verification
|
|
5
|
+
|
|
6
|
+
## [1.1.26] - 2026-06-28
|
|
7
|
+
- fix: gate email dedup — notifyGateHit now writes lms:gate_email:{ip} to Redis with 1-hour TTL; retries within the hour suppressed
|
|
8
|
+
- fix: 402 gate response agent_action changed to HALT_WORKFLOW; added retryable: false, retry_after_ms: null
|
|
9
|
+
- fix: trial_extension structured field already present; agent_action now actionable for agents
|
|
10
|
+
|
|
11
|
+
## [1.1.25] - 2026-06-28
|
|
12
|
+
- feat: owner key bypass (OWNER_KEY env var) — fleet owner bypasses free tier and paid-only gates
|
|
13
|
+
|
|
3
14
|
## [1.1.24] - 2026-06-26
|
|
4
15
|
- fix: trial extension requests now written to Redis (lms:trial:{email}) on grant -- permanent audit trail that survives redeploys; previously in-memory only
|
|
5
16
|
|
package/glama.json
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"description": "Check whether a task can run on a local model instead of cloud. Saves money on every call that does not need cloud inference. AI-powered routing.",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
2
|
+
"$schema": "https://glama.ai/mcp/servers/schema.json",
|
|
3
|
+
"name": "Local Model Suitability MCP",
|
|
4
|
+
"description": "Check whether a task can run on a local model instead of cloud. Saves money on every call that does not need cloud inference. AI-powered routing. Returns LOCAL/CLOUD/EITHER verdict with cost saving estimate and specific local model recommendations.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"categories": [
|
|
7
|
+
"developer-tools"
|
|
8
|
+
],
|
|
9
|
+
"remote": {
|
|
10
|
+
"transport": "sse",
|
|
11
|
+
"url": "https://local-model-suitability-mcp-production.up.railway.app/sse"
|
|
12
|
+
},
|
|
7
13
|
"tools": [
|
|
8
14
|
{
|
|
9
15
|
"name": "check_local_viability",
|
|
10
16
|
"description": "Call BEFORE every cloud inference call. Returns LOCAL/CLOUD/EITHER verdict with cost saving estimate and specific local model recommendations."
|
|
11
17
|
}
|
|
12
|
-
]
|
|
18
|
+
],
|
|
19
|
+
"links": {
|
|
20
|
+
"homepage": "https://kordagencies.com",
|
|
21
|
+
"npm": "https://www.npmjs.com/package/local-model-suitability-mcp"
|
|
22
|
+
}
|
|
13
23
|
}
|
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.28",
|
|
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.27';
|
|
7
7
|
const FIRST_DEPLOYED = '2026-04-13T06:41:38Z';
|
|
8
8
|
const LIFETIME_CALLS_REDIS_KEY = 'lms:lifetime_calls';
|
|
9
9
|
const UPTIME_HEARTBEAT_KEY = 'lms:uptime:heartbeat_count';
|
|
@@ -121,14 +121,21 @@ function truncateIp(ip) {
|
|
|
121
121
|
return parts.length === 4 ? parts.slice(0, 3).join('.') + '.0' : ip;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function notifyGateHit(serverName, ip, toolName, totalCalls, stripeUrl) {
|
|
124
|
+
async function notifyGateHit(serverName, ip, toolName, totalCalls, stripeUrl) {
|
|
125
|
+
const ip24 = truncateIp(ip);
|
|
126
|
+
const dedupKey = REDIS_PREFIX + ':gate_email:' + ip24;
|
|
127
|
+
try {
|
|
128
|
+
const recent = await redisGet(dedupKey);
|
|
129
|
+
if (recent) { console.log('[GateNotify] suppressed duplicate for ' + ip24); return; }
|
|
130
|
+
await redisSet(dedupKey, new Date().toISOString());
|
|
131
|
+
await redisExpire(dedupKey, 3600);
|
|
132
|
+
} catch(e) { /* Redis unavailable — fall through and send */ }
|
|
125
133
|
if (!process.env.RESEND_API_KEY) return;
|
|
126
|
-
const
|
|
127
|
-
const html = '<p>Server: ' + serverName + '</p><p>IP: ' + maskedIp + '</p><p>Tool: ' + (toolName || 'unknown') + '</p><p>Calls this month: ' + totalCalls + '</p><p>Time: ' + new Date().toISOString() + '</p><p>Upgrade: ' + stripeUrl + '</p>';
|
|
134
|
+
const html = '<p>Server: ' + serverName + '</p><p>IP: ' + ip24 + '</p><p>Tool: ' + (toolName || 'unknown') + '</p><p>Calls this month: ' + totalCalls + '</p><p>Time: ' + new Date().toISOString() + '</p><p>Upgrade: ' + stripeUrl + '</p>';
|
|
128
135
|
fetch('https://api.resend.com/emails', {
|
|
129
136
|
method: 'POST',
|
|
130
137
|
headers: { 'Authorization': `Bearer ${process.env.RESEND_API_KEY}`, 'Content-Type': 'application/json' },
|
|
131
|
-
body: JSON.stringify({ from: 'Kord Agencies <ojas@kordagencies.com>', to: 'ojas@kordagencies.com', subject: '[Gate Hit] ' + serverName + ' — ' +
|
|
138
|
+
body: JSON.stringify({ from: 'Kord Agencies <ojas@kordagencies.com>', to: 'ojas@kordagencies.com', subject: '[Gate Hit] ' + serverName + ' — ' + ip24 + ' hit free tier limit', html })
|
|
132
139
|
}).then(r => { if (!r.ok) r.text().then(t => console.error('[GateNotify] failed: HTTP ' + r.status + ' ' + t)); })
|
|
133
140
|
.catch(e => console.error('[GateNotify] network error:', e.message));
|
|
134
141
|
}
|
|
@@ -700,6 +707,12 @@ const server = createServer(async (req, res) => {
|
|
|
700
707
|
return;
|
|
701
708
|
}
|
|
702
709
|
|
|
710
|
+
if (req.url === '/.well-known/glama.json' && req.method === 'GET') {
|
|
711
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
712
|
+
res.end(JSON.stringify({ "$schema": "https://glama.ai/mcp/schemas/connector.json", "maintainers": [{ "email": "ojas@kordagencies.com" }] }));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
703
716
|
// Trial extension
|
|
704
717
|
if (req.url === '/trial-extension' && req.method === 'POST') {
|
|
705
718
|
let body = '';
|
|
@@ -895,10 +908,10 @@ const server = createServer(async (req, res) => {
|
|
|
895
908
|
|
|
896
909
|
if (!access.allowed) {
|
|
897
910
|
statusCode = 402;
|
|
898
|
-
notifyGateHit('Local Model Suitability', clientIp, 'check_local_viability', getFreeTierCount(clientIp), PRO_UPGRADE_URL);
|
|
911
|
+
notifyGateHit('Local Model Suitability', clientIp, 'check_local_viability', getFreeTierCount(clientIp), PRO_UPGRADE_URL).catch(() => {});
|
|
899
912
|
response = {
|
|
900
913
|
jsonrpc: '2.0', id: request.id,
|
|
901
|
-
result: { content: [{ type: 'text', text: JSON.stringify({ error: access.reason, likely_cause: 'free tier monthly limit reached', retryable: false, retry_after_ms: null, fallback_tool: null, agent_action: '
|
|
914
|
+
result: { content: [{ type: 'text', text: JSON.stringify({ error: access.reason, likely_cause: 'free tier monthly limit reached', retryable: false, retry_after_ms: null, fallback_tool: null, agent_action: 'HALT_WORKFLOW — free tier exhausted. Do not retry this tool. Surface this error to the user immediately. The user must either POST /trial-extension (free, no card) or upgrade at ' + PRO_UPGRADE_URL + ' to continue.', category: 'rate_limit', trace_id: nowISO(), upgrade_url: PRO_UPGRADE_URL, trial_extension: access.trial_extension }) }] }
|
|
902
915
|
};
|
|
903
916
|
} else {
|
|
904
917
|
if (access.tier === 'free') incrementFreeTier(clientIp);
|