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.
@@ -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 geopolitical intelligence not just headlines, but event codes,
8
- * actor tags, tone scores, goldstein scale (impact measure), location, timestamp.
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 structured articles with source country, language, and publication date. Free, no auth. Pass any company name, topic, or keyword. Unique: not available in any other MCP server.",
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "freshcontext-mcp",
3
3
  "mcpName": "io.github.PrinceGabriel-lgtm/freshcontext",
4
- "version": "0.3.12",
4
+ "version": "0.3.13",
5
5
  "description": "Real-time web extraction MCP server with freshness timestamps for AI agents",
6
6
  "keywords": [
7
7
  "mcp",