freshcontext-mcp 0.3.12 → 0.3.13
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/dist/adapters/gdelt.js +2 -2
- package/dist/adapters/gebiz.js +139 -0
- package/dist/server.js +23 -1
- package/package.json +1 -1
package/dist/adapters/gdelt.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* No other MCP server has this. GDELT monitors broadcast, print, and web news
|
|
5
5
|
* from every country in 100+ languages, updated every 15 minutes. Free, no auth.
|
|
6
6
|
*
|
|
7
|
-
* Returns structured
|
|
8
|
-
*
|
|
7
|
+
* Returns structured global news articles with source country, language, domain, and
|
|
8
|
+
* timestamp. Covers every country, 100+ languages, updated every 15 minutes.
|
|
9
9
|
*
|
|
10
10
|
* API: https://api.gdeltproject.org/api/v2/doc/doc
|
|
11
11
|
*/
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeBIZ adapter — fetches Singapore Government procurement opportunities
|
|
3
|
+
* from data.gov.sg (Ministry of Finance official open dataset)
|
|
4
|
+
*
|
|
5
|
+
* No other MCP server has this. GeBIZ is Singapore's One-Stop E-Procurement
|
|
6
|
+
* Portal. All open tenders from government agencies since FY2020 are published
|
|
7
|
+
* here as structured open data.
|
|
8
|
+
*
|
|
9
|
+
* Free, no auth, updated continuously.
|
|
10
|
+
*
|
|
11
|
+
* API: https://data.gov.sg/api/action/datastore_search
|
|
12
|
+
* Dataset: d_acde1106003906a75c3fa052592f2fcb
|
|
13
|
+
*
|
|
14
|
+
* Accepts:
|
|
15
|
+
* - Keyword search: "AI", "software", "data analytics"
|
|
16
|
+
* - Agency name: "GovTech", "MOH", "MAS"
|
|
17
|
+
* - Empty string: returns latest tenders across all agencies
|
|
18
|
+
*/
|
|
19
|
+
const DATASET_ID = "d_acde1106003906a75c3fa052592f2fcb";
|
|
20
|
+
const BASE_URL = "https://data.gov.sg/api/action/datastore_search";
|
|
21
|
+
const HEADERS = {
|
|
22
|
+
"Accept": "application/json",
|
|
23
|
+
"User-Agent": "freshcontext-mcp/1.0 contact@freshcontext.dev",
|
|
24
|
+
};
|
|
25
|
+
function formatDate(raw) {
|
|
26
|
+
if (!raw)
|
|
27
|
+
return "N/A";
|
|
28
|
+
// Dates come as DD/MM/YYYY or ISO
|
|
29
|
+
return raw.slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
function formatAmt(raw) {
|
|
32
|
+
if (!raw || raw === "NA" || raw === "")
|
|
33
|
+
return "N/A";
|
|
34
|
+
const n = parseFloat(raw.replace(/[^0-9.]/g, ""));
|
|
35
|
+
if (isNaN(n))
|
|
36
|
+
return raw;
|
|
37
|
+
if (n >= 1_000_000)
|
|
38
|
+
return `S$${(n / 1_000_000).toFixed(2)}M`;
|
|
39
|
+
if (n >= 1_000)
|
|
40
|
+
return `S$${(n / 1_000).toFixed(1)}K`;
|
|
41
|
+
return `S$${n.toFixed(0)}`;
|
|
42
|
+
}
|
|
43
|
+
async function fetchGeBIZ(query, limit = 15) {
|
|
44
|
+
const params = new URLSearchParams({
|
|
45
|
+
resource_id: DATASET_ID,
|
|
46
|
+
limit: String(limit),
|
|
47
|
+
sort: "_id desc", // most recent first
|
|
48
|
+
});
|
|
49
|
+
// Add full-text search if query provided
|
|
50
|
+
if (query.trim()) {
|
|
51
|
+
params.set("q", query.trim());
|
|
52
|
+
}
|
|
53
|
+
const url = `${BASE_URL}?${params}`;
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timeout = setTimeout(() => controller.abort(), 20000);
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(url, {
|
|
58
|
+
headers: HEADERS,
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const text = await res.text().catch(() => "");
|
|
63
|
+
throw new Error(`GeBIZ API HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
64
|
+
}
|
|
65
|
+
return await res.json();
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function formatRecords(data, query, maxLength) {
|
|
72
|
+
const records = data.result?.records ?? [];
|
|
73
|
+
const total = data.result?.total ?? 0;
|
|
74
|
+
if (!records.length) {
|
|
75
|
+
return {
|
|
76
|
+
raw: `No GeBIZ tenders found for "${query}".\n\nTips:\n- Try a broader keyword: "software" or "data"\n- Try an agency name: "GovTech" or "MOH"\n- Leave query empty to see all recent tenders`,
|
|
77
|
+
content_date: null,
|
|
78
|
+
freshness_confidence: "high",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const lines = [
|
|
82
|
+
`GeBIZ Singapore Government Procurement — ${query || "All Recent Tenders"}`,
|
|
83
|
+
`${total.toLocaleString()} total records found (showing ${records.length})`,
|
|
84
|
+
`Source: data.gov.sg — Ministry of Finance open dataset`,
|
|
85
|
+
"",
|
|
86
|
+
];
|
|
87
|
+
let latestDate = null;
|
|
88
|
+
records.forEach((r, i) => {
|
|
89
|
+
const awardDate = formatDate(r.awarded_date);
|
|
90
|
+
const closeDate = formatDate(r.tender_close_date);
|
|
91
|
+
const dateStr = r.awarded_date ?? r.tender_close_date ?? null;
|
|
92
|
+
if (dateStr && dateStr !== "NA") {
|
|
93
|
+
// Parse DD/MM/YYYY
|
|
94
|
+
const parts = dateStr.split("/");
|
|
95
|
+
let iso = null;
|
|
96
|
+
if (parts.length === 3) {
|
|
97
|
+
iso = `${parts[2]}-${parts[1]}-${parts[0]}`;
|
|
98
|
+
}
|
|
99
|
+
else if (dateStr.length >= 10) {
|
|
100
|
+
iso = dateStr.slice(0, 10);
|
|
101
|
+
}
|
|
102
|
+
if (iso && (!latestDate || iso > latestDate))
|
|
103
|
+
latestDate = iso;
|
|
104
|
+
}
|
|
105
|
+
const desc = (r.description ?? "No description").slice(0, 300);
|
|
106
|
+
const agency = r.agency ?? "N/A";
|
|
107
|
+
const tenderNo = r.tender_no ?? "N/A";
|
|
108
|
+
const status = r.tender_detail_status ?? "N/A";
|
|
109
|
+
const category = r.procurement_category ?? "N/A";
|
|
110
|
+
const supplier = r.supplier_name ?? "N/A";
|
|
111
|
+
const amount = formatAmt(r.awarded_amt);
|
|
112
|
+
lines.push(`[${i + 1}] ${desc}`);
|
|
113
|
+
lines.push(` Agency: ${agency}`);
|
|
114
|
+
lines.push(` Tender No: ${tenderNo}`);
|
|
115
|
+
lines.push(` Category: ${category}`);
|
|
116
|
+
lines.push(` Status: ${status}`);
|
|
117
|
+
if (supplier !== "N/A")
|
|
118
|
+
lines.push(` Supplier: ${supplier}`);
|
|
119
|
+
if (amount !== "N/A")
|
|
120
|
+
lines.push(` Amount: ${amount}`);
|
|
121
|
+
lines.push(` Close Date: ${closeDate}`);
|
|
122
|
+
if (awardDate !== "N/A")
|
|
123
|
+
lines.push(` Awarded: ${awardDate}`);
|
|
124
|
+
lines.push("");
|
|
125
|
+
});
|
|
126
|
+
lines.push(`Full dataset: https://data.gov.sg/datasets/d_acde1106003906a75c3fa052592f2fcb/view`);
|
|
127
|
+
lines.push(`Register as supplier: https://www.gebiz.gov.sg/cmw/content/getstart.html`);
|
|
128
|
+
return {
|
|
129
|
+
raw: lines.join("\n").slice(0, maxLength),
|
|
130
|
+
content_date: latestDate,
|
|
131
|
+
freshness_confidence: "high",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export async function gebizAdapter(options) {
|
|
135
|
+
const query = (options.url ?? "").trim();
|
|
136
|
+
const maxLength = options.maxLength ?? 6000;
|
|
137
|
+
const data = await fetchGeBIZ(query);
|
|
138
|
+
return formatRecords(data, query, maxLength);
|
|
139
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -15,6 +15,7 @@ import { changelogAdapter } from "./adapters/changelog.js";
|
|
|
15
15
|
import { govContractsAdapter } from "./adapters/govcontracts.js";
|
|
16
16
|
import { secFilingsAdapter } from "./adapters/secFilings.js";
|
|
17
17
|
import { gdeltAdapter } from "./adapters/gdelt.js";
|
|
18
|
+
import { gebizAdapter } from "./adapters/gebiz.js";
|
|
18
19
|
import { stampFreshness, formatForLLM } from "./tools/freshnessStamp.js";
|
|
19
20
|
import { formatSecurityError } from "./security.js";
|
|
20
21
|
const server = new McpServer({
|
|
@@ -347,7 +348,7 @@ server.registerTool("extract_sec_filings", {
|
|
|
347
348
|
// codes, actor tags, tone scores, goldstein scale (impact), location, timestamp.
|
|
348
349
|
// Unique: no other MCP server has this.
|
|
349
350
|
server.registerTool("extract_gdelt", {
|
|
350
|
-
description: "Fetch global news intelligence from the GDELT Project. GDELT monitors broadcast, print, and web news from every country in 100+ languages, updated every 15 minutes. Returns
|
|
351
|
+
description: "Fetch global news intelligence from the GDELT Project. GDELT monitors broadcast, print, and web news from every country in 100+ languages, updated every 15 minutes. Returns articles with title, source domain, country of origin, language, and publication date — covering news worldwide that Western sources miss. Free, no auth. Pass any company name, topic, or keyword. Unique: not available in any other MCP server.",
|
|
351
352
|
inputSchema: z.object({
|
|
352
353
|
url: z.string().describe("Query: company name, topic, or keyword e.g. 'Palantir', 'artificial intelligence', 'MCP server'"),
|
|
353
354
|
max_length: z.number().optional().default(6000),
|
|
@@ -408,6 +409,27 @@ server.registerTool("extract_company_landscape", {
|
|
|
408
409
|
].join("\n\n");
|
|
409
410
|
return { content: [{ type: "text", text: combined }] };
|
|
410
411
|
});
|
|
412
|
+
// ─── Tool: extract_gebiz ────────────────────────────────────────────────────
|
|
413
|
+
// Singapore Government procurement tenders via data.gov.sg open API.
|
|
414
|
+
// Ministry of Finance official dataset — all open tenders since FY2020.
|
|
415
|
+
// Free, no auth, structured. Unique: no other MCP server has this.
|
|
416
|
+
server.registerTool("extract_gebiz", {
|
|
417
|
+
description: "Fetch Singapore Government procurement opportunities from GeBIZ via the data.gov.sg open API (Ministry of Finance official dataset). Returns open tenders, awarded contracts, agencies, amounts, and closing dates. Search by keyword (e.g. 'software', 'AI', 'data analytics'), agency name (e.g. 'GovTech', 'MOH'), or leave blank for all recent tenders. Free, no auth. Unique: not available in any other MCP server.",
|
|
418
|
+
inputSchema: z.object({
|
|
419
|
+
url: z.string().describe("Search keyword, agency name, or leave empty for all recent tenders. E.g. 'artificial intelligence', 'GovTech', 'cybersecurity'"),
|
|
420
|
+
max_length: z.number().optional().default(6000),
|
|
421
|
+
}),
|
|
422
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
423
|
+
}, async ({ url, max_length }) => {
|
|
424
|
+
try {
|
|
425
|
+
const result = await gebizAdapter({ url, maxLength: max_length });
|
|
426
|
+
const ctx = stampFreshness(result, { url, maxLength: max_length }, "gebiz");
|
|
427
|
+
return { content: [{ type: "text", text: formatForLLM(ctx) }] };
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
return { content: [{ type: "text", text: formatSecurityError(err) }] };
|
|
431
|
+
}
|
|
432
|
+
});
|
|
411
433
|
// ─── Start ───────────────────────────────────────────────────────────────────
|
|
412
434
|
async function main() {
|
|
413
435
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED