tender-mcp 1.2.8 → 1.2.10
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 +20 -0
- package/README.md +43 -0
- package/package.json +1 -1
- package/src/server.js +161 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## [1.2.10] - 2026-06-04
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- Upstash Redis persistence: free tier usage, API keys, session logs survive redeploys
|
|
5
|
+
- `loadFreeTierFromRedis()` / `saveFreeTierToRedis()` with Math.max merge pattern
|
|
6
|
+
- `saveKeyToRedis()` / `loadApiKeysFromRedis()` with prefix `tender`
|
|
7
|
+
- `appendSessionLog(ip, tool)` with 24h TTL per IP per day
|
|
8
|
+
- `/session-log` endpoint (requires x-stats-key)
|
|
9
|
+
- `free_tier_breakdown` per-IP object on `/stats` response
|
|
10
|
+
- `getEffectiveLimit(ip)` helper — returns base + trial extension if applicable
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Tool descriptions rewritten for orchestral agent runtime selection: state-based triggers, chaining instructions, DO NOT USE conditions
|
|
14
|
+
- `VERSION` bumped to `1.2.10`
|
|
15
|
+
|
|
16
|
+
## [1.2.9] - 2026-06-02
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- fix: IP extraction fixed for Cloudflare proxy headers — free tier gate now enforces correctly
|
|
20
|
+
|
|
1
21
|
## [1.2.5] - 2026-04-28
|
|
2
22
|
|
|
3
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -22,6 +22,49 @@ Or via Smithery:
|
|
|
22
22
|
npx -y @smithery/cli@latest mcp add OjasKord/tender-mcp
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Harness Integration
|
|
26
|
+
|
|
27
|
+
### Claude Code / Claude Desktop (.mcp.json)
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"tender": {
|
|
32
|
+
"type": "http",
|
|
33
|
+
"url": "https://tender-mcp-production.up.railway.app"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### LangChain (Python)
|
|
40
|
+
```python
|
|
41
|
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
|
42
|
+
client = MultiServerMCPClient({
|
|
43
|
+
"tender": {
|
|
44
|
+
"url": "https://tender-mcp-production.up.railway.app",
|
|
45
|
+
"transport": "http"
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
tools = await client.get_tools()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### OpenAI Agents SDK (Python)
|
|
52
|
+
```python
|
|
53
|
+
from agents import Agent, HostedMCPTool
|
|
54
|
+
agent = Agent(
|
|
55
|
+
name="Assistant",
|
|
56
|
+
tools=[HostedMCPTool(tool_config={
|
|
57
|
+
"type": "mcp",
|
|
58
|
+
"server_label": "tender",
|
|
59
|
+
"server_url": "https://tender-mcp-production.up.railway.app",
|
|
60
|
+
"require_approval": "never"
|
|
61
|
+
})]
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### LangGraph
|
|
66
|
+
Same as LangChain above — langchain-mcp-adapters works with LangGraph natively.
|
|
67
|
+
|
|
25
68
|
## Why Use This
|
|
26
69
|
|
|
27
70
|
Any business that sells to government needs to monitor tender opportunities. But searching three separate government portals daily, reading hundreds of notices, and manually judging relevance takes hours. Tender MCP does it in seconds — search UK, EU, and US simultaneously, then let AI score which opportunities actually match your capabilities.
|
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.
|
|
4
|
+
"version": "1.2.10",
|
|
5
5
|
"description": "Government tender search and AI opportunity scoring for AI agents. UK Contracts Finder, EU TED, US SAM.gov.",
|
|
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.
|
|
6
|
+
const VERSION = '1.2.10';
|
|
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,10 +24,23 @@ const toolUsageCounts = {};
|
|
|
24
24
|
const trialExtensions = new Map();
|
|
25
25
|
const TRIAL_EXTENSION_CALLS = 10;
|
|
26
26
|
|
|
27
|
+
const REDIS_PREFIX = 'tender';
|
|
28
|
+
const FREE_TIER_REDIS_KEY = 'tender:free_tier_usage';
|
|
29
|
+
const UPSTASH_URL = process.env.UPSTASH_REDIS_REST_URL;
|
|
30
|
+
const UPSTASH_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN;
|
|
31
|
+
|
|
27
32
|
const LEGAL_DISCLAIMER = 'Tender data is sourced directly from official government portals: UK Contracts Finder (contractsfinder.service.gov.uk), EU TED (ted.europa.eu), and US SAM.gov (sam.gov). We do not log or store your query content. Tender deadlines and contract values may change — always verify directly with the contracting authority before submitting a bid. Results are for informational purposes only. Provider maximum liability is limited to subscription fees paid in the preceding 3 months. Full terms: kordagencies.com/terms.html';
|
|
28
33
|
|
|
29
34
|
function nowISO() { return new Date().toISOString(); }
|
|
30
35
|
function getMonthKey(ip) { return ip + ':' + new Date().toISOString().slice(0, 7); }
|
|
36
|
+
|
|
37
|
+
function getEffectiveLimit(ip) {
|
|
38
|
+
for (const record of trialExtensions.values()) {
|
|
39
|
+
if (record.ip === ip) return FREE_TIER_LIMIT + TRIAL_EXTENSION_CALLS;
|
|
40
|
+
}
|
|
41
|
+
return FREE_TIER_LIMIT;
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
function getTodayDate() { return new Date().toISOString().split('T')[0]; }
|
|
32
45
|
function getDateDaysAgo(days) {
|
|
33
46
|
const d = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
@@ -112,6 +125,105 @@ async function callClaude(prompt) {
|
|
|
112
125
|
});
|
|
113
126
|
}
|
|
114
127
|
|
|
128
|
+
// ─── REDIS HELPERS ────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
async function redisGet(key) {
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch(
|
|
133
|
+
`${UPSTASH_URL}/get/${encodeURIComponent(key)}`,
|
|
134
|
+
{ headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
135
|
+
);
|
|
136
|
+
const data = await res.json();
|
|
137
|
+
if (data.error) console.error('[Redis] redisGet error:', data.error, 'key:', key);
|
|
138
|
+
if (!data.result) return null;
|
|
139
|
+
return JSON.parse(data.result);
|
|
140
|
+
} catch(e) { return null; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function redisSet(key, value) {
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetch(`${process.env.UPSTASH_REDIS_REST_URL}/set/${encodeURIComponent(key)}/${encodeURIComponent(JSON.stringify(value))}`, {
|
|
146
|
+
method: 'GET',
|
|
147
|
+
headers: { Authorization: `Bearer ${process.env.UPSTASH_REDIS_REST_TOKEN}` }
|
|
148
|
+
});
|
|
149
|
+
const data = await res.json();
|
|
150
|
+
if (data.error) console.error('[Redis] redisSet error:', data.error, 'key:', key);
|
|
151
|
+
} catch(e) { console.error('[Redis] redisSet failed:', e); }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function redisExpire(key, seconds) {
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetch(
|
|
157
|
+
`${UPSTASH_URL}/expire/${encodeURIComponent(key)}/${seconds}`,
|
|
158
|
+
{ method: 'POST', headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
159
|
+
);
|
|
160
|
+
const data = await res.json();
|
|
161
|
+
if (data.error) console.error('[Redis] redisExpire error:', data.error, 'key:', key);
|
|
162
|
+
} catch(e) { console.error('[Redis] redisExpire failed:', e); }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function redisKeys(pattern) {
|
|
166
|
+
try {
|
|
167
|
+
const res = await fetch(
|
|
168
|
+
`${UPSTASH_URL}/keys/${encodeURIComponent(pattern)}`,
|
|
169
|
+
{ headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
170
|
+
);
|
|
171
|
+
const data = await res.json();
|
|
172
|
+
if (data.error) console.error('[Redis] redisKeys error:', data.error, 'pattern:', pattern);
|
|
173
|
+
return data.result || [];
|
|
174
|
+
} catch(e) { return []; }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function appendSessionLog(ip, tool) {
|
|
178
|
+
try {
|
|
179
|
+
const ipSafe = ip.replace(/:/g, '_').replace(/\s/g, '');
|
|
180
|
+
const dayKey = new Date().toISOString().slice(0, 10);
|
|
181
|
+
const key = `${REDIS_PREFIX}:session:${ipSafe}:${dayKey}`;
|
|
182
|
+
const existing = await redisGet(key) || [];
|
|
183
|
+
existing.push({ tool, timestamp: new Date().toISOString() });
|
|
184
|
+
await redisSet(key, existing);
|
|
185
|
+
await redisExpire(key, 86400);
|
|
186
|
+
} catch(e) { console.error('[SessionLog] internal error:', e); }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function saveKeyToRedis(apiKey, record) {
|
|
190
|
+
await redisSet(`${REDIS_PREFIX}:key:${apiKey}`, record);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function loadApiKeysFromRedis() {
|
|
194
|
+
const keys = await redisKeys(`${REDIS_PREFIX}:key:*`);
|
|
195
|
+
for (const redisKey of keys) {
|
|
196
|
+
const record = await redisGet(redisKey);
|
|
197
|
+
if (record) {
|
|
198
|
+
const apiKey = redisKey.replace(`${REDIS_PREFIX}:key:`, '');
|
|
199
|
+
apiKeys.set(apiKey, record);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
console.log(`Loaded ${apiKeys.size} API keys from Redis`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function loadFreeTierFromRedis() {
|
|
206
|
+
try {
|
|
207
|
+
const data = await redisGet(FREE_TIER_REDIS_KEY);
|
|
208
|
+
if (data && Array.isArray(data)) {
|
|
209
|
+
data.forEach(([k, v]) => freeTierUsage.set(k, v));
|
|
210
|
+
console.log('[FreeTier] Loaded ' + freeTierUsage.size + ' IPs from Redis');
|
|
211
|
+
}
|
|
212
|
+
} catch(e) { console.error('[FreeTier] load failed:', e); }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function saveFreeTierToRedis() {
|
|
216
|
+
try {
|
|
217
|
+
const existing = await redisGet(FREE_TIER_REDIS_KEY) || [];
|
|
218
|
+
const existingMap = new Map(existing);
|
|
219
|
+
for (const [key, value] of freeTierUsage.entries()) {
|
|
220
|
+
const existingCount = existingMap.get(key) || 0;
|
|
221
|
+
existingMap.set(key, Math.max(existingCount, value));
|
|
222
|
+
}
|
|
223
|
+
await redisSet(FREE_TIER_REDIS_KEY, Array.from(existingMap.entries()));
|
|
224
|
+
} catch(e) { console.error('[FreeTier] save failed:', e); }
|
|
225
|
+
}
|
|
226
|
+
|
|
115
227
|
// ─── DATA SOURCES ─────────────────────────────────────────────────────────────
|
|
116
228
|
|
|
117
229
|
async function searchUKTenders(keyword, limit, daysOld) {
|
|
@@ -283,7 +395,7 @@ const tools = [
|
|
|
283
395
|
{
|
|
284
396
|
name: 'search_tenders',
|
|
285
397
|
idempotent: true,
|
|
286
|
-
description: 'Call this
|
|
398
|
+
description: 'Call this at the moment your workflow needs to identify procurement opportunities in one or more markets. Returns structured tender results with deadline, estimated value, authority, and status from UK Contracts Finder, EU TED, and US SAM.gov in one call. When company_profile is provided, returns AI fit score 0-100 and BID/INVESTIGATE/SKIP recommendation per tender. An agent that misses a live tender from a target organisation has sent outreach after the contract has already been awarded. Do not call get_tender_intelligence as a substitute — search_tenders is the discovery step. Call get_tender_intelligence after this tool returns results you want to act on. We do not log your query content. Free tier: 10 calls/month, no API key required.',
|
|
287
399
|
inputSchema: {
|
|
288
400
|
type: 'object',
|
|
289
401
|
properties: {
|
|
@@ -300,7 +412,7 @@ const tools = [
|
|
|
300
412
|
{
|
|
301
413
|
name: 'get_tender_intelligence',
|
|
302
414
|
idempotent: true,
|
|
303
|
-
description: 'Call this
|
|
415
|
+
description: 'Call this standalone to get structured tender intelligence without running a search. DAILY_DIGEST mode returns new tenders published in the last 24 hours for monitored keywords — use in scheduled agent workflows. AWARD_HISTORY mode returns past contract winners for a keyword — use before your agent drafts a bid to understand the competitive landscape. Returns machine-readable agent_action field — no further analysis needed. Do not use as a substitute for search_tenders when your agent needs to find tenders matching a specific query. We do not log your query content. Free tier returns a preview count. Full results require Pro API key from kordagencies.com.',
|
|
304
416
|
inputSchema: {
|
|
305
417
|
type: 'object',
|
|
306
418
|
properties: {
|
|
@@ -571,7 +683,8 @@ function checkAccess(req, toolName) {
|
|
|
571
683
|
}
|
|
572
684
|
|
|
573
685
|
// Free tier — allow all tools, but pass tier='free' so executeTool can gate paid features
|
|
574
|
-
const
|
|
686
|
+
const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
687
|
+
const ip = rawIp.split(',')[0].trim();
|
|
575
688
|
const monthKey = getMonthKey(ip);
|
|
576
689
|
const calls = freeTierUsage.get(monthKey) || 0;
|
|
577
690
|
if (calls >= FREE_TIER_LIMIT) {
|
|
@@ -585,10 +698,12 @@ function checkAccess(req, toolName) {
|
|
|
585
698
|
}
|
|
586
699
|
freeTierUsage.set(monthKey, calls + 1);
|
|
587
700
|
saveStats();
|
|
701
|
+
saveFreeTierToRedis().catch(() => {});
|
|
588
702
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
703
|
+
const effectiveLimit = getEffectiveLimit(ip);
|
|
589
704
|
return {
|
|
590
705
|
allowed: true, tier: 'free', remaining,
|
|
591
|
-
warning: remaining <= 2 ? remaining + ' free search' + (remaining === 1 ? '' : 'es') + ' remaining this month. Get 500 searches for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.' : null
|
|
706
|
+
warning: remaining <= 2 ? remaining + ' free search' + (remaining === 1 ? '' : 'es') + ' remaining this month (limit: ' + effectiveLimit + '). Get 500 searches for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.' : null
|
|
592
707
|
};
|
|
593
708
|
}
|
|
594
709
|
|
|
@@ -617,7 +732,9 @@ async function handleStripeWebhook(body, sig) {
|
|
|
617
732
|
const plan = getPlanFromProduct(session.metadata?.product_name || '');
|
|
618
733
|
if (email) {
|
|
619
734
|
const apiKey = generateApiKey();
|
|
620
|
-
|
|
735
|
+
const record = { email, plan, createdAt: nowISO(), calls: 0, limit: PLAN_LIMITS[plan] };
|
|
736
|
+
apiKeys.set(apiKey, record);
|
|
737
|
+
await saveKeyToRedis(apiKey, record);
|
|
621
738
|
saveApiKeys();
|
|
622
739
|
await sendApiKeyEmail(email, apiKey, plan);
|
|
623
740
|
console.log('[tender] API key created for ' + email + ' (' + plan + ')');
|
|
@@ -683,8 +800,37 @@ const server = http.createServer(async (req, res) => {
|
|
|
683
800
|
if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
684
801
|
const totalFreeCalls = Array.from(freeTierUsage.values()).reduce((a, b) => a + b, 0);
|
|
685
802
|
const freeUniqueIPs = new Set(Array.from(freeTierUsage.keys()).map(k => k.split(':')[0])).size;
|
|
803
|
+
const monthPrefix = new Date().toISOString().slice(0, 7);
|
|
804
|
+
const breakdown = {};
|
|
805
|
+
for (const [key, count] of freeTierUsage.entries()) {
|
|
806
|
+
if (key.includes(':' + monthPrefix)) {
|
|
807
|
+
const ip = key.split(':')[0];
|
|
808
|
+
breakdown[ip.slice(0, 10) + '...'] = count;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
686
811
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
687
|
-
res.end(JSON.stringify({ free_tier_unique_ips: freeUniqueIPs, free_tier_total_calls: totalFreeCalls, paid_keys_issued: apiKeys.size, tool_usage: toolUsageCounts, recent_calls: usageLog.slice(-20).reverse(), trial_extensions_granted: trialExtensions.size }));
|
|
812
|
+
res.end(JSON.stringify({ free_tier_unique_ips: freeUniqueIPs, free_tier_total_calls: totalFreeCalls, paid_keys_issued: apiKeys.size, tool_usage: toolUsageCounts, recent_calls: usageLog.slice(-20).reverse(), trial_extensions_granted: trialExtensions.size, free_tier_breakdown: breakdown }));
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (req.url === '/session-log' && req.method === 'GET') {
|
|
817
|
+
if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
818
|
+
(async () => {
|
|
819
|
+
const keys = await redisKeys(`${REDIS_PREFIX}:session:*`);
|
|
820
|
+
const sessions = [];
|
|
821
|
+
for (const key of keys) {
|
|
822
|
+
const calls = await redisGet(key) || [];
|
|
823
|
+
if (!calls.length) continue;
|
|
824
|
+
const withoutPrefix = key.slice(`${REDIS_PREFIX}:session:`.length);
|
|
825
|
+
const dateIdx = withoutPrefix.lastIndexOf(':');
|
|
826
|
+
const ipPart = withoutPrefix.slice(0, dateIdx);
|
|
827
|
+
const date = withoutPrefix.slice(dateIdx + 1);
|
|
828
|
+
sessions.push({ ip: ipPart.slice(0, 8), date, calls, first_call: calls[0]?.timestamp || '', last_call: calls[calls.length - 1]?.timestamp || '' });
|
|
829
|
+
}
|
|
830
|
+
sessions.sort((a, b) => new Date(b.first_call) - new Date(a.first_call));
|
|
831
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
832
|
+
res.end(JSON.stringify(sessions));
|
|
833
|
+
})();
|
|
688
834
|
return;
|
|
689
835
|
}
|
|
690
836
|
|
|
@@ -696,7 +842,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
696
842
|
if (!name || !email) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'name and email are required', agent_action: 'PROVIDE_REQUIRED_FIELDS' })); return; }
|
|
697
843
|
const emailKey = 'trial:' + email.toLowerCase().trim();
|
|
698
844
|
if (trialExtensions.has(emailKey)) { res.writeHead(409, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Trial extension already granted for this email.', upgrade_url: PRO_UPGRADE_URL, agent_action: 'INFORM_USER_TRIAL_ALREADY_USED' })); return; }
|
|
699
|
-
const
|
|
845
|
+
const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
846
|
+
const ip = rawIp.split(',')[0].trim();
|
|
700
847
|
const monthKey = getMonthKey(ip);
|
|
701
848
|
const currentCalls = freeTierUsage.get(monthKey) || 0;
|
|
702
849
|
freeTierUsage.set(monthKey, Math.max(0, currentCalls - TRIAL_EXTENSION_CALLS));
|
|
@@ -753,11 +900,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
753
900
|
return;
|
|
754
901
|
}
|
|
755
902
|
|
|
756
|
-
const
|
|
903
|
+
const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
904
|
+
const ip = rawIp.split(',')[0].trim();
|
|
757
905
|
usageLog.push({ tool: name, tier: access.tier, time: nowISO(), ip: ip.slice(0, 8) + '...' });
|
|
758
906
|
if (usageLog.length > 1000) usageLog.shift();
|
|
759
907
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
760
908
|
saveStats();
|
|
909
|
+
appendSessionLog(ip, name).catch((e) => console.error('[SessionLog] appendSessionLog failed:', e));
|
|
761
910
|
|
|
762
911
|
const result = await executeTool(name, toolArgs || {}, access.tier);
|
|
763
912
|
if (access.warning) result._notice = access.warning;
|
|
@@ -850,9 +999,11 @@ function setupStdio() {
|
|
|
850
999
|
|
|
851
1000
|
setupStdio();
|
|
852
1001
|
|
|
853
|
-
server.listen(PORT, () => {
|
|
1002
|
+
server.listen(PORT, async () => {
|
|
854
1003
|
loadStats();
|
|
855
1004
|
loadApiKeys();
|
|
1005
|
+
await loadApiKeysFromRedis();
|
|
1006
|
+
await loadFreeTierFromRedis();
|
|
856
1007
|
console.log('Tender MCP v' + VERSION + ' running on port ' + PORT);
|
|
857
1008
|
console.log('Tools: 2 (search_tenders, get_tender_intelligence)');
|
|
858
1009
|
console.log('Free tier: ' + FREE_TIER_LIMIT + ' searches/IP/month');
|