local-model-suitability-mcp 1.1.19 → 1.1.21
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/smithery.yaml +1 -0
- package/src/server.js +22 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.21] - 2026-06-23
|
|
4
|
+
- fix: gate returns HTTP 402 (x402 standard for non-transient quota)
|
|
5
|
+
|
|
6
|
+
## [1.1.20] - 2026-06-20
|
|
7
|
+
- feat: email notification on free tier gate hit
|
|
8
|
+
|
|
3
9
|
## [1.1.19] - 2026-06-18
|
|
4
10
|
- feat: revoke API key on Stripe refund
|
|
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.21",
|
|
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/smithery.yaml
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
name: Local Model Suitability MCP
|
|
2
|
+
agentRole: "AI Model Routing Specialist — equip before routing a task to a cloud model to check whether a local model can handle it instead"
|
|
2
3
|
description: "Before routing any task to a cloud model, call check_local_viability. Returns LOCAL/CLOUD/EITHER verdict with recommended model - saves inference cost on every routable call. 2 focused tools. Compact schema. Minimal agent context overhead."
|
|
3
4
|
categories:
|
|
4
5
|
- Dev Tools
|
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.21';
|
|
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 });
|
|
@@ -678,6 +695,7 @@ const server = createServer(async (req, res) => {
|
|
|
678
695
|
try {
|
|
679
696
|
const request = JSON.parse(body);
|
|
680
697
|
let response;
|
|
698
|
+
let statusCode = 200;
|
|
681
699
|
|
|
682
700
|
if (request.method === 'initialize') {
|
|
683
701
|
response = {
|
|
@@ -713,6 +731,8 @@ const server = createServer(async (req, res) => {
|
|
|
713
731
|
const access = checkAccess(clientIp, apiKey);
|
|
714
732
|
|
|
715
733
|
if (!access.allowed) {
|
|
734
|
+
statusCode = 402;
|
|
735
|
+
notifyGateHit('Local Model Suitability', clientIp, 'check_local_viability', getFreeTierCount(clientIp), PRO_UPGRADE_URL);
|
|
716
736
|
response = {
|
|
717
737
|
jsonrpc: '2.0', id: request.id,
|
|
718
738
|
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 }) }] }
|
|
@@ -760,7 +780,7 @@ const server = createServer(async (req, res) => {
|
|
|
760
780
|
response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found: ' + request.method } };
|
|
761
781
|
}
|
|
762
782
|
|
|
763
|
-
res.writeHead(
|
|
783
|
+
res.writeHead(statusCode, { ...cors, 'Content-Type': 'application/json' });
|
|
764
784
|
res.end(JSON.stringify(response));
|
|
765
785
|
} catch(e) {
|
|
766
786
|
res.writeHead(400, { ...cors, 'Content-Type': 'application/json' });
|