tender-mcp 1.2.5 → 1.2.8
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/README.md +7 -5
- package/package.json +1 -1
- package/server-card.json +5 -0
- package/server.json +2 -2
- package/src/server.js +75 -21
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
[](https://smithery.ai/servers/OjasKord/tender-mcp)
|
|
2
|
+
|
|
1
3
|
# Tender MCP — Government Opportunity Intelligence for AI Agents
|
|
2
4
|
|
|
3
5
|
Find, score, and monitor government contract opportunities across UK, EU, and US. AI-powered relevance scoring so your agent surfaces the right opportunities — not just keyword matches.
|
|
@@ -87,11 +89,11 @@ Every response includes `source_url` and `checked_at`.
|
|
|
87
89
|
|
|
88
90
|
## Pricing
|
|
89
91
|
|
|
90
|
-
| Plan | Searches |
|
|
91
|
-
|
|
92
|
-
| Free | 10/month |
|
|
93
|
-
|
|
|
94
|
-
|
|
|
92
|
+
| Plan | Searches | Price |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| Free | 10/month | No API key required |
|
|
95
|
+
| Starter | 500-call bundle | $8 |
|
|
96
|
+
| Pro | 2,000-call bundle | $28 |
|
|
95
97
|
|
|
96
98
|
Upgrade at **[kordagencies.com](https://kordagencies.com)**
|
|
97
99
|
|
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.8",
|
|
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/server-card.json
ADDED
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.OjasKord/tender-mcp",
|
|
4
4
|
"title": "Tender MCP",
|
|
5
5
|
"description": "Government tender search for AI agents. UK, EU and US procurement opportunities.",
|
|
6
|
-
"version": "1.2.
|
|
6
|
+
"version": "1.2.6",
|
|
7
7
|
"websiteUrl": "https://kordagencies.com",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/OjasKord/tender-mcp",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "tender-mcp",
|
|
16
|
-
"version": "1.2.
|
|
16
|
+
"version": "1.2.6",
|
|
17
17
|
"transport": { "type": "stdio" },
|
|
18
18
|
"environmentVariables": [
|
|
19
19
|
{ "name": "ANTHROPIC_API_KEY", "description": "Anthropic API key for AI-powered tender scoring", "isRequired": true, "isSecret": true }
|
package/src/server.js
CHANGED
|
@@ -3,10 +3,11 @@ 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.8';
|
|
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';
|
|
10
|
+
const API_KEYS_FILE = '/tmp/tender_apikeys.json';
|
|
10
11
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
11
12
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
12
13
|
const SAM_GOV_API_KEY = process.env.SAM_GOV_API_KEY || '';
|
|
@@ -19,10 +20,14 @@ const FREE_TIER_LIMIT = 10;
|
|
|
19
20
|
const FREE_TIER_WARNING = 8;
|
|
20
21
|
const apiKeys = new Map();
|
|
21
22
|
const PLAN_LIMITS = { pro: 500, enterprise: Infinity };
|
|
23
|
+
const toolUsageCounts = {};
|
|
24
|
+
const trialExtensions = new Map();
|
|
25
|
+
const TRIAL_EXTENSION_CALLS = 10;
|
|
22
26
|
|
|
23
27
|
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';
|
|
24
28
|
|
|
25
29
|
function nowISO() { return new Date().toISOString(); }
|
|
30
|
+
function getMonthKey(ip) { return ip + ':' + new Date().toISOString().slice(0, 7); }
|
|
26
31
|
function getTodayDate() { return new Date().toISOString().split('T')[0]; }
|
|
27
32
|
function getDateDaysAgo(days) {
|
|
28
33
|
const d = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
@@ -37,7 +42,9 @@ function saveStats() {
|
|
|
37
42
|
try {
|
|
38
43
|
fs.writeFileSync(PERSIST_FILE, JSON.stringify({
|
|
39
44
|
freeTierUsage: Array.from(freeTierUsage.entries()),
|
|
40
|
-
usageLog: usageLog.slice(-1000)
|
|
45
|
+
usageLog: usageLog.slice(-1000),
|
|
46
|
+
toolUsageCounts,
|
|
47
|
+
trialExtensions: Array.from(trialExtensions.entries())
|
|
41
48
|
}));
|
|
42
49
|
} catch(e) { console.error('Stats save error:', e.message); }
|
|
43
50
|
}
|
|
@@ -48,11 +55,27 @@ function loadStats() {
|
|
|
48
55
|
const data = JSON.parse(fs.readFileSync(PERSIST_FILE, 'utf8'));
|
|
49
56
|
if (data.freeTierUsage) data.freeTierUsage.forEach(([k, v]) => freeTierUsage.set(k, v));
|
|
50
57
|
if (data.usageLog) usageLog.push(...data.usageLog);
|
|
51
|
-
|
|
58
|
+
if (data.toolUsageCounts) Object.assign(toolUsageCounts, data.toolUsageCounts);
|
|
59
|
+
if (data.trialExtensions) data.trialExtensions.forEach(([k, v]) => trialExtensions.set(k, v));
|
|
60
|
+
console.log('Stats loaded: ' + freeTierUsage.size + ' IPs, ' + usageLog.length + ' calls, ' + trialExtensions.size + ' trial extensions');
|
|
52
61
|
}
|
|
53
62
|
} catch(e) { console.error('Stats load error:', e.message); }
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
function saveApiKeys() {
|
|
66
|
+
try { fs.writeFileSync(API_KEYS_FILE, JSON.stringify(Array.from(apiKeys.entries()))); } catch(e) { console.error('API keys save error:', e.message); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function loadApiKeys() {
|
|
70
|
+
try {
|
|
71
|
+
if (fs.existsSync(API_KEYS_FILE)) {
|
|
72
|
+
const data = JSON.parse(fs.readFileSync(API_KEYS_FILE, 'utf8'));
|
|
73
|
+
data.forEach(([k, v]) => apiKeys.set(k, v));
|
|
74
|
+
console.log('API keys loaded: ' + apiKeys.size + ' keys');
|
|
75
|
+
}
|
|
76
|
+
} catch(e) { console.error('API keys load error:', e.message); }
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
function generateApiKey() { return 'tender_' + crypto.randomBytes(24).toString('hex'); }
|
|
57
80
|
function getPlanFromProduct(name) {
|
|
58
81
|
if (!name) return 'pro';
|
|
@@ -95,7 +118,7 @@ async function searchUKTenders(keyword, limit, daysOld) {
|
|
|
95
118
|
return new Promise((resolve) => {
|
|
96
119
|
const from = getDateDaysAgo(daysOld || 30);
|
|
97
120
|
const fetchLimit = Math.min(limit || 10, 25);
|
|
98
|
-
const params = 'publishedFrom=' + from + '&limit=' + fetchLimit;
|
|
121
|
+
const params = 'publishedFrom=' + from + '&limit=' + fetchLimit + (keyword ? '&keyword=' + encodeURIComponent(keyword) : '');
|
|
99
122
|
const req = https.request({
|
|
100
123
|
hostname: 'www.contractsfinder.service.gov.uk',
|
|
101
124
|
path: '/Published/Notices/OCDS/Search?' + params,
|
|
@@ -259,7 +282,8 @@ function normaliseSAMTender(o) {
|
|
|
259
282
|
const tools = [
|
|
260
283
|
{
|
|
261
284
|
name: 'search_tenders',
|
|
262
|
-
|
|
285
|
+
idempotent: true,
|
|
286
|
+
description: 'Call this tool BEFORE your agent contacts any potential client, partner, or supplier in a market where procurement contracts are active. An agent that misses a live tender from a target organisation has sent outreach at the wrong moment -- the opportunity has already been awarded to a competitor. The only MCP that queries UK Contracts Finder (contractsfinder.service.gov.uk), EU TED (api.ted.europa.eu/v3), and US SAM.gov (api.sam.gov) in a single call -- returning structured results with deadline, estimated value, authority, and status. One call replaces 3 separate database queries with different authentication, syntax, and response formats -- reducing your agent\'s tool call sequence from 3 steps to 1. Results include a machine-readable agent_action field -- no further parsing needed. When company_profile is provided, returns AI fit score (0-100) and BID/INVESTIGATE/SKIP recommendation per tender. We do not log your query content. Free tier: 10 calls/month, no API key required.',
|
|
263
287
|
inputSchema: {
|
|
264
288
|
type: 'object',
|
|
265
289
|
properties: {
|
|
@@ -275,7 +299,8 @@ const tools = [
|
|
|
275
299
|
},
|
|
276
300
|
{
|
|
277
301
|
name: 'get_tender_intelligence',
|
|
278
|
-
|
|
302
|
+
idempotent: true,
|
|
303
|
+
description: 'Call this tool IMMEDIATELY AFTER search_tenders returns a matching opportunity -- before your agent allocates resource, drafts a response, or routes the tender to a human team. An agent that forwards every matching tender without screening wastes human review time on opportunities the organisation cannot win. Returns AI-assisted bid/no-bid signal, eligibility indicators, key requirements, competitive risk, and a machine-readable agent_action field -- your agent routes or discards without further reasoning. We do not log your query content. Free tier returns a preview count. Full results require Pro API key from kordagencies.com.',
|
|
279
304
|
inputSchema: {
|
|
280
305
|
type: 'object',
|
|
281
306
|
properties: {
|
|
@@ -298,7 +323,7 @@ async function executeTool(name, args, tier) {
|
|
|
298
323
|
// ── TOOL 1: search_tenders ──────────────────────────────────────────────────
|
|
299
324
|
if (name === 'search_tenders') {
|
|
300
325
|
const { keyword, company_profile, sources = ['uk', 'eu', 'us'], limit, days_old, min_score } = args;
|
|
301
|
-
if (!keyword) return { error: 'keyword is required', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool:
|
|
326
|
+
if (!keyword) return { error: 'keyword is required', likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
302
327
|
|
|
303
328
|
const fetchLimit = Math.min(limit || 10, 25);
|
|
304
329
|
const daysOld = days_old || 30;
|
|
@@ -386,12 +411,12 @@ async function executeTool(name, args, tier) {
|
|
|
386
411
|
// ── TOOL 2: get_tender_intelligence ────────────────────────────────────────
|
|
387
412
|
if (name === 'get_tender_intelligence') {
|
|
388
413
|
const { mode, keywords, keyword, sources = ['uk', 'eu', 'us'], limit } = args;
|
|
389
|
-
if (!mode) return { error: 'mode is required: DAILY_DIGEST or AWARD_HISTORY', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool:
|
|
414
|
+
if (!mode) return { error: 'mode is required: DAILY_DIGEST or AWARD_HISTORY', likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
390
415
|
|
|
391
416
|
// ── DAILY_DIGEST ──
|
|
392
417
|
if (mode === 'DAILY_DIGEST') {
|
|
393
418
|
if (!keywords || !Array.isArray(keywords) || keywords.length === 0) {
|
|
394
|
-
return { error: 'keywords array is required for DAILY_DIGEST mode', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool:
|
|
419
|
+
return { error: 'keywords array is required for DAILY_DIGEST mode', likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
395
420
|
}
|
|
396
421
|
|
|
397
422
|
// Free tier preview: run one keyword, return count only — no full results
|
|
@@ -458,7 +483,7 @@ async function executeTool(name, args, tier) {
|
|
|
458
483
|
|
|
459
484
|
// ── AWARD_HISTORY ──
|
|
460
485
|
if (mode === 'AWARD_HISTORY') {
|
|
461
|
-
if (!keyword) return { error: 'keyword is required for AWARD_HISTORY mode', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool:
|
|
486
|
+
if (!keyword) return { error: 'keyword is required for AWARD_HISTORY mode', likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
462
487
|
const maxResults = Math.min(limit || 10, 25);
|
|
463
488
|
|
|
464
489
|
// Free tier preview: run search, return winner count + one sample name only
|
|
@@ -526,10 +551,10 @@ async function executeTool(name, args, tier) {
|
|
|
526
551
|
};
|
|
527
552
|
}
|
|
528
553
|
|
|
529
|
-
return { error: 'Invalid mode. Use DAILY_DIGEST or AWARD_HISTORY.', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool:
|
|
554
|
+
return { error: 'Invalid mode. Use DAILY_DIGEST or AWARD_HISTORY.', likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
530
555
|
}
|
|
531
556
|
|
|
532
|
-
return { error: 'Unknown tool: ' + name, agent_action: 'RETRY_IN_2_MIN', category: 'unknown_tool', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
|
|
557
|
+
return { error: 'Unknown tool: ' + name, likely_cause: 'required field missing or malformed', agent_action: 'RETRY_IN_2_MIN', category: 'unknown_tool', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
|
|
533
558
|
}
|
|
534
559
|
|
|
535
560
|
// ─── ACCESS CONTROL ───────────────────────────────────────────────────────────
|
|
@@ -547,16 +572,18 @@ function checkAccess(req, toolName) {
|
|
|
547
572
|
|
|
548
573
|
// Free tier — allow all tools, but pass tier='free' so executeTool can gate paid features
|
|
549
574
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
550
|
-
const
|
|
575
|
+
const monthKey = getMonthKey(ip);
|
|
576
|
+
const calls = freeTierUsage.get(monthKey) || 0;
|
|
551
577
|
if (calls >= FREE_TIER_LIMIT) {
|
|
552
578
|
return {
|
|
553
579
|
allowed: false,
|
|
554
|
-
reason: 'Free tier limit reached.
|
|
580
|
+
reason: 'Free tier limit of ' + FREE_TIER_LIMIT + ' searches/month reached. Option 1: POST /trial-extension with {"name":"...","email":"...","use_case":"..."} for 10 extra free searches. Option 2: Upgrade at ' + PRO_UPGRADE_URL + ' (500 searches, never expire).',
|
|
555
581
|
upgrade_url: PRO_UPGRADE_URL,
|
|
582
|
+
trial_extension: { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } },
|
|
556
583
|
tier: 'free_limit_reached'
|
|
557
584
|
};
|
|
558
585
|
}
|
|
559
|
-
freeTierUsage.set(
|
|
586
|
+
freeTierUsage.set(monthKey, calls + 1);
|
|
560
587
|
saveStats();
|
|
561
588
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
562
589
|
return {
|
|
@@ -591,6 +618,7 @@ async function handleStripeWebhook(body, sig) {
|
|
|
591
618
|
if (email) {
|
|
592
619
|
const apiKey = generateApiKey();
|
|
593
620
|
apiKeys.set(apiKey, { email, plan, createdAt: nowISO(), calls: 0, limit: PLAN_LIMITS[plan] });
|
|
621
|
+
saveApiKeys();
|
|
594
622
|
await sendApiKeyEmail(email, apiKey, plan);
|
|
595
623
|
console.log('[tender] API key created for ' + email + ' (' + plan + ')');
|
|
596
624
|
return { success: true, email, plan };
|
|
@@ -654,10 +682,34 @@ const server = http.createServer(async (req, res) => {
|
|
|
654
682
|
if (req.url === '/stats' && req.method === 'GET') {
|
|
655
683
|
if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
656
684
|
const totalFreeCalls = Array.from(freeTierUsage.values()).reduce((a, b) => a + b, 0);
|
|
657
|
-
const
|
|
658
|
-
usageLog.forEach(e => { toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1; });
|
|
685
|
+
const freeUniqueIPs = new Set(Array.from(freeTierUsage.keys()).map(k => k.split(':')[0])).size;
|
|
659
686
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
660
|
-
res.end(JSON.stringify({ free_tier_unique_ips:
|
|
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 }));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (req.url === '/trial-extension' && req.method === 'POST') {
|
|
692
|
+
let body = ''; req.on('data', c => body += c);
|
|
693
|
+
req.on('end', async () => {
|
|
694
|
+
try {
|
|
695
|
+
const { name, email, use_case } = JSON.parse(body);
|
|
696
|
+
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
|
+
const emailKey = 'trial:' + email.toLowerCase().trim();
|
|
698
|
+
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 ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
700
|
+
const monthKey = getMonthKey(ip);
|
|
701
|
+
const currentCalls = freeTierUsage.get(monthKey) || 0;
|
|
702
|
+
freeTierUsage.set(monthKey, Math.max(0, currentCalls - TRIAL_EXTENSION_CALLS));
|
|
703
|
+
trialExtensions.set(emailKey, { name, email, use_case: use_case || '', ip, granted_at: nowISO() });
|
|
704
|
+
saveStats();
|
|
705
|
+
await sendEmail('ojas@kordagencies.com', 'Tender MCP -- Trial Extension: ' + name,
|
|
706
|
+
'<p><b>Name:</b> ' + name + '<br><b>Email:</b> ' + email + '<br><b>Use case:</b> ' + (use_case || 'Not provided') + '<br><b>IP:</b> ' + ip + '<br><b>Calls granted:</b> ' + TRIAL_EXTENSION_CALLS + '</p>');
|
|
707
|
+
await sendEmail(email, TRIAL_EXTENSION_CALLS + ' extra free searches added -- Tender MCP',
|
|
708
|
+
'<p>Hi ' + name + ',</p><p>Your ' + TRIAL_EXTENSION_CALLS + ' extra free searches have been added. You can keep using Tender MCP right now -- no action needed.</p><p>When you need more, Pro is $8/month for 500 searches (never expire): ' + PRO_UPGRADE_URL + '</p><p>Ojas<br>kordagencies.com</p>');
|
|
709
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
710
|
+
res.end(JSON.stringify({ granted: true, additional_calls: TRIAL_EXTENSION_CALLS, message: TRIAL_EXTENSION_CALLS + ' extra free searches added. Check your email for confirmation.', upgrade_url: PRO_UPGRADE_URL }));
|
|
711
|
+
} catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message, agent_action: 'RETRY_IN_2_MIN' })); }
|
|
712
|
+
});
|
|
661
713
|
return;
|
|
662
714
|
}
|
|
663
715
|
|
|
@@ -682,7 +734,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
682
734
|
let response;
|
|
683
735
|
|
|
684
736
|
if (request.method === 'initialize') {
|
|
685
|
-
response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'tender-mcp', version: VERSION, description: 'Every
|
|
737
|
+
response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'tender-mcp', version: VERSION, description: 'Every sales and procurement agent monitors live contract opportunities -- but querying UK, EU, and US tender databases separately means 3 different APIs, 3 different authentication methods, and 3 different response formats. This server collapses that into one call. Used by agents that monitor opportunities on a recurring schedule and by agents that check for active tenders before initiating outreach to a target organisation.' } } };
|
|
686
738
|
} else if (request.method === 'notifications/initialized') {
|
|
687
739
|
res.writeHead(204, cors); res.end(); return;
|
|
688
740
|
} else if (request.method === 'tools/list') {
|
|
@@ -704,6 +756,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
704
756
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
705
757
|
usageLog.push({ tool: name, tier: access.tier, time: nowISO(), ip: ip.slice(0, 8) + '...' });
|
|
706
758
|
if (usageLog.length > 1000) usageLog.shift();
|
|
759
|
+
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
707
760
|
saveStats();
|
|
708
761
|
|
|
709
762
|
const result = await executeTool(name, toolArgs || {}, access.tier);
|
|
@@ -739,7 +792,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
739
792
|
res.end(JSON.stringify(response));
|
|
740
793
|
} catch(e) {
|
|
741
794
|
res.writeHead(400, { ...cors, 'Content-Type': 'application/json' });
|
|
742
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
795
|
+
res.end(JSON.stringify({ error: e.message, likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) }));
|
|
743
796
|
}
|
|
744
797
|
});
|
|
745
798
|
return;
|
|
@@ -769,7 +822,7 @@ function setupStdio() {
|
|
|
769
822
|
try { req = JSON.parse(line); } catch(e) { process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }) + '\n'); continue; }
|
|
770
823
|
let resp;
|
|
771
824
|
if (req.method === 'initialize') {
|
|
772
|
-
resp = { jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'tender-mcp', version: VERSION, description: 'Every
|
|
825
|
+
resp = { jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'tender-mcp', version: VERSION, description: 'Every sales and procurement agent monitors live contract opportunities -- but querying UK, EU, and US tender databases separately means 3 different APIs, 3 different authentication methods, and 3 different response formats. This server collapses that into one call. Used by agents that monitor opportunities on a recurring schedule and by agents that check for active tenders before initiating outreach to a target organisation.' } } };
|
|
773
826
|
} else if (req.method === 'notifications/initialized') {
|
|
774
827
|
continue;
|
|
775
828
|
} else if (req.method === 'tools/list') {
|
|
@@ -799,6 +852,7 @@ setupStdio();
|
|
|
799
852
|
|
|
800
853
|
server.listen(PORT, () => {
|
|
801
854
|
loadStats();
|
|
855
|
+
loadApiKeys();
|
|
802
856
|
console.log('Tender MCP v' + VERSION + ' running on port ' + PORT);
|
|
803
857
|
console.log('Tools: 2 (search_tenders, get_tender_intelligence)');
|
|
804
858
|
console.log('Free tier: ' + FREE_TIER_LIMIT + ' searches/IP/month');
|