tender-mcp 1.2.13 → 1.2.15

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,3 +1,9 @@
1
+ ## [1.2.15] - 2026-06-11
2
+ - fix: bump version past existing npm publish (1.2.14 already on registry)
3
+
4
+ ## [1.2.14] - 2026-06-11
5
+ - feat: per-tool kill switch + per-minute rate limiting on AI tools
6
+
1
7
  ## [1.2.13] - 2026-06-08
2
8
  - fix: BEFORE trigger language, consequence-first limit error
3
9
 
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": "tender-mcp",
3
3
  "mcpName": "io.github.OjasKord/tender-mcp",
4
- "version": "1.2.13",
4
+ "version": "1.2.15",
5
5
  "description": "Government tender search for AI agents. UK, EU, US contracts with AI bid scoring. BID/SKIP verdict with deadline and value in one call.",
6
6
  "main": "src/server.js",
7
7
  "scripts": {
package/src/server.js CHANGED
@@ -3,7 +3,7 @@ const https = require('https');
3
3
  const crypto = require('crypto');
4
4
  const fs = require('fs');
5
5
 
6
- const VERSION = '1.2.13';
6
+ const VERSION = '1.2.15';
7
7
  const PRO_UPGRADE_URL = 'https://buy.stripe.com/9B600i5k1bPv2xC6Fqebu0n';
8
8
  const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/7sY7sKaEldXDegk0h2ebu0o';
9
9
  const PERSIST_FILE = '/tmp/tender_stats.json';
@@ -24,6 +24,22 @@ const toolUsageCounts = {};
24
24
  const trialExtensions = new Map();
25
25
  const TRIAL_EXTENSION_CALLS = 10;
26
26
 
27
+ const perMinuteUsage = new Map();
28
+
29
+ function checkPerMinuteLimit(ip, toolName, limit) {
30
+ const minuteKey = ip + ':' + toolName + ':' + new Date().toISOString().slice(0, 16);
31
+ const count = perMinuteUsage.get(minuteKey) || 0;
32
+ if (count >= limit) return false;
33
+ perMinuteUsage.set(minuteKey, count + 1);
34
+ if (perMinuteUsage.size > 10000) {
35
+ const currentMinute = new Date().toISOString().slice(0, 16);
36
+ for (const [key] of perMinuteUsage) {
37
+ if (!key.includes(currentMinute)) perMinuteUsage.delete(key);
38
+ }
39
+ }
40
+ return true;
41
+ }
42
+
27
43
  const REDIS_PREFIX = 'tender';
28
44
  const FREE_TIER_REDIS_KEY = 'tender:free_tier_usage';
29
45
  const UPSTASH_URL = process.env.UPSTASH_REDIS_REST_URL;
@@ -943,6 +959,19 @@ const server = http.createServer(async (req, res) => {
943
959
  response = { jsonrpc: '2.0', id: request.id, result: { prompts: [] } };
944
960
  } else if (request.method === 'tools/call') {
945
961
  const { name, arguments: toolArgs } = request.params;
962
+ const killSwitchKey = 'TOOL_DISABLED_' + name.toUpperCase().replace(/[^A-Z0-9]/g, '_');
963
+ if (process.env[killSwitchKey] === 'true') {
964
+ res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
965
+ res.end(JSON.stringify({ 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 }) }] } }));
966
+ return;
967
+ }
968
+ const _rawIpKs = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
969
+ const _clientIpKs = _rawIpKs.split(',')[0].trim();
970
+ if (['search_tenders', 'get_tender_intelligence'].includes(name) && !checkPerMinuteLimit(_clientIpKs, name, 10)) {
971
+ res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
972
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'Rate limit exceeded — maximum 10 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: 10, window: '1 minute' }) }] } }));
973
+ return;
974
+ }
946
975
  const access = checkAccess(req, name);
947
976
 
948
977
  if (!access.allowed) {
@@ -1033,6 +1062,11 @@ function setupStdio() {
1033
1062
  resp = { jsonrpc: '2.0', id: req.id, result: { prompts: [] } };
1034
1063
  } else if (req.method === 'tools/call') {
1035
1064
  const { name, arguments: toolArgs } = req.params || {};
1065
+ const _ks = 'TOOL_DISABLED_' + (name || '').toUpperCase().replace(/[^A-Z0-9]/g, '_');
1066
+ if (process.env[_ks] === 'true') {
1067
+ process.stdout.write(JSON.stringify({ 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 }) }] } }) + '\n');
1068
+ continue;
1069
+ }
1036
1070
  executeTool(name, toolArgs || {}, 'pro').then(result => {
1037
1071
  process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } }) + '\n');
1038
1072
  }).catch(err => {