local-model-suitability-mcp 1.1.18 → 1.1.20
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/package.json +1 -1
- package/src/server.js +74 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.20] - 2026-06-20
|
|
4
|
+
- feat: email notification on free tier gate hit
|
|
5
|
+
|
|
6
|
+
## [1.1.19] - 2026-06-18
|
|
7
|
+
- feat: revoke API key on Stripe refund
|
|
8
|
+
|
|
3
9
|
## [1.1.18] - 2026-06-17
|
|
4
10
|
- fix: Resend fetch now logs HTTP error responses; was silently swallowing non-2xx failures
|
|
5
11
|
|
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.20",
|
|
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.20';
|
|
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 ALLOWED_PAYMENT_LINK_IDS = ['plink_1TQzCBD6WvRe6sn3H1q5t2LF', 'plink_1TQzDSD6WvRe6sn3UM2G1EgX'];
|
|
@@ -105,6 +105,23 @@ function checkAccess(ip, apiKey) {
|
|
|
105
105
|
return { allowed: true, tier: 'free', remaining, count };
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function truncateIp(ip) {
|
|
109
|
+
const parts = (ip || '').split('.');
|
|
110
|
+
return parts.length === 4 ? parts.slice(0, 3).join('.') + '.0' : ip;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function notifyGateHit(serverName, ip, toolName, totalCalls, stripeUrl) {
|
|
114
|
+
if (!process.env.RESEND_API_KEY) return;
|
|
115
|
+
const maskedIp = truncateIp(ip);
|
|
116
|
+
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>';
|
|
117
|
+
fetch('https://api.resend.com/emails', {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Authorization': `Bearer ${process.env.RESEND_API_KEY}`, 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify({ from: 'Kord Agencies <ojas@kordagencies.com>', to: 'ojas@kordagencies.com', subject: '[Gate Hit] ' + serverName + ' — ' + maskedIp + ' hit free tier limit', html })
|
|
121
|
+
}).then(r => { if (!r.ok) r.text().then(t => console.error('[GateNotify] failed: HTTP ' + r.status + ' ' + t)); })
|
|
122
|
+
.catch(e => console.error('[GateNotify] network error:', e.message));
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
function logCall(tool, tier, ip) {
|
|
109
126
|
stats.tool_usage[tool] = (stats.tool_usage[tool] || 0) + 1;
|
|
110
127
|
stats.recent_calls.push({ tool, tier, time: nowISO(), ip });
|
|
@@ -161,6 +178,26 @@ async function redisKeys(pattern) {
|
|
|
161
178
|
} catch(e) { return []; }
|
|
162
179
|
}
|
|
163
180
|
|
|
181
|
+
async function redisDelete(key) {
|
|
182
|
+
try {
|
|
183
|
+
const res = await fetch(
|
|
184
|
+
`${UPSTASH_URL}/del/${encodeURIComponent(key)}`,
|
|
185
|
+
{ method: 'POST', headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
186
|
+
);
|
|
187
|
+
const data = await res.json();
|
|
188
|
+
if (data.error) console.error('[Redis] redisDelete error:', data.error, 'key:', key);
|
|
189
|
+
} catch(e) { console.error('[Redis] redisDelete failed:', e); }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function findCheckoutSessionEmail(paymentIntentId) {
|
|
193
|
+
const res = await fetch(
|
|
194
|
+
`https://api.stripe.com/v1/checkout/sessions?payment_intent=${encodeURIComponent(paymentIntentId)}`,
|
|
195
|
+
{ headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}` } }
|
|
196
|
+
);
|
|
197
|
+
const data = await res.json();
|
|
198
|
+
return data.data?.[0]?.customer_details?.email || data.data?.[0]?.customer_email || null;
|
|
199
|
+
}
|
|
200
|
+
|
|
164
201
|
async function appendSessionLog(ip, tool) {
|
|
165
202
|
try {
|
|
166
203
|
const ipSafe = ip.replace(/:/g, '_').replace(/\s/g, '');
|
|
@@ -411,6 +448,41 @@ async function handleStripeWebhook(body, sig) {
|
|
|
411
448
|
}
|
|
412
449
|
}
|
|
413
450
|
|
|
451
|
+
if (event.type === 'charge.refunded') {
|
|
452
|
+
if (!process.env.STRIPE_SECRET_KEY) {
|
|
453
|
+
console.error('[lms] STRIPE_SECRET_KEY not set — cannot revoke key on refund');
|
|
454
|
+
return { received: true, ignored: true, status: 200 };
|
|
455
|
+
}
|
|
456
|
+
const paymentIntentId = event.data.object.payment_intent;
|
|
457
|
+
if (!paymentIntentId) {
|
|
458
|
+
console.log('[lms] charge.refunded missing payment_intent — ignoring.');
|
|
459
|
+
return { received: true, ignored: true, status: 200 };
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
const email = await findCheckoutSessionEmail(paymentIntentId);
|
|
463
|
+
if (!email) {
|
|
464
|
+
console.log('[lms] No checkout session/email found for refunded payment_intent ' + paymentIntentId);
|
|
465
|
+
return { received: true, ignored: true, status: 200 };
|
|
466
|
+
}
|
|
467
|
+
let revokedKey = null;
|
|
468
|
+
for (const [key, record] of apiKeys.entries()) {
|
|
469
|
+
if (record.email === email) { revokedKey = key; break; }
|
|
470
|
+
}
|
|
471
|
+
if (!revokedKey) {
|
|
472
|
+
console.log('[lms] No API key found for ' + email + ' — refund received, nothing to revoke');
|
|
473
|
+
return { received: true, ignored: true, status: 200 };
|
|
474
|
+
}
|
|
475
|
+
apiKeys.delete(revokedKey);
|
|
476
|
+
await redisDelete(`${REDIS_PREFIX}:key:${revokedKey}`);
|
|
477
|
+
saveStats();
|
|
478
|
+
console.log('[Webhook] API key revoked for ' + email + ' — refund received');
|
|
479
|
+
return { received: true, revoked: true, status: 200 };
|
|
480
|
+
} catch(e) {
|
|
481
|
+
console.error('[lms] charge.refunded handling error:', e.message);
|
|
482
|
+
return { received: true, ignored: true, status: 200 };
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
414
486
|
return { received: true, status: 200 };
|
|
415
487
|
}
|
|
416
488
|
|
|
@@ -658,6 +730,7 @@ const server = createServer(async (req, res) => {
|
|
|
658
730
|
const access = checkAccess(clientIp, apiKey);
|
|
659
731
|
|
|
660
732
|
if (!access.allowed) {
|
|
733
|
+
notifyGateHit('Local Model Suitability', clientIp, 'check_local_viability', getFreeTierCount(clientIp), PRO_UPGRADE_URL);
|
|
661
734
|
response = {
|
|
662
735
|
jsonrpc: '2.0', id: request.id,
|
|
663
736
|
result: { content: [{ type: 'text', text: JSON.stringify({ error: `Free tier limit reached. Get 500 calls for $20 at ${PRO_UPGRADE_URL} -- calls never expire.`, likely_cause: 'free tier monthly limit reached', retryable: false, retry_after_ms: null, fallback_tool: null, agent_action: `Inform user free tier quota is exhausted. Get 500 calls for $20 at ${PRO_UPGRADE_URL} -- calls never expire.`, category: 'rate_limit', trace_id: nowISO(), upgrade_url: PRO_UPGRADE_URL }) }] }
|