freshcontext-mcp 0.3.16 → 0.3.17
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/.env.example +3 -0
- package/README.md +6 -6
- package/dist/adapters/finance.js +87 -101
- package/dist/adapters/gdelt.js +1 -1
- package/dist/adapters/gebiz.js +1 -1
- package/dist/adapters/hackernews.js +43 -13
- package/dist/adapters/productHunt.js +8 -4
- package/dist/adapters/repoSearch.js +1 -1
- package/dist/adapters/secFilings.js +1 -1
- package/dist/security.js +1 -1
- package/dist/server.js +10 -10
- package/dist/tools/freshnessStamp.js +23 -3
- package/freshcontext.schema.json +1 -1
- package/package.json +14 -7
- package/server.json +3 -3
- package/.github/workflows/publish.yml +0 -32
- package/RESEARCH.md +0 -487
- package/RISKS.md +0 -137
- package/cleanup.ps1 +0 -99
- package/demo/README.md +0 -70
- package/demo/data.json +0 -88
- package/demo/generate.mjs +0 -199
- package/demo/index.html +0 -513
- package/demo/logo-export.html +0 -61
- package/demo/logo.svg +0 -23
- package/freshcontext-validate.js +0 -196
- package/time-check.ps1 +0 -46
package/.env.example
CHANGED
|
@@ -4,5 +4,8 @@
|
|
|
4
4
|
# Optional: GitHub Personal Access Token (increases rate limits for GitHub API fallback)
|
|
5
5
|
GITHUB_TOKEN=
|
|
6
6
|
|
|
7
|
+
# Optional: Product Hunt developer token (enables GraphQL path; scraper fallback works without it)
|
|
8
|
+
PH_TOKEN=
|
|
9
|
+
|
|
7
10
|
# Optional: Proxy URL if needed for certain extractions
|
|
8
11
|
# PROXY_URL=http://user:pass@host:port
|
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ R_t = R_0 · e^(−λt)
|
|
|
41
41
|
|
|
42
42
|
That's the whole fix. No model swap. No re-embedding. No re-indexing. The layer drops onto whatever retrieval pipeline you already have.
|
|
43
43
|
|
|
44
|
-
**The layer is the product.** The
|
|
44
|
+
**The layer is the product.** The 21 tools shipped with this repo are reference implementations demonstrating compatibility — useful, but commodity. The DAR engine, the freshness envelope, and the FreshContext Specification are the moat.
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
@@ -82,7 +82,7 @@ Production endpoint: `https://freshcontext-mcp.gimmanuel73.workers.dev`
|
|
|
82
82
|
|
|
83
83
|
## Reference adapters
|
|
84
84
|
|
|
85
|
-
The repo ships
|
|
85
|
+
The repo ships 21 tools demonstrating how to make any data source FreshContext-compatible. Useful as drop-in tools, but the value is the layer above them.
|
|
86
86
|
|
|
87
87
|
### Intelligence
|
|
88
88
|
| Adapter | What it returns |
|
|
@@ -104,7 +104,7 @@ The repo ships 20 adapters demonstrating how to make any data source FreshContex
|
|
|
104
104
|
### Market data
|
|
105
105
|
| Adapter | What it returns |
|
|
106
106
|
|---|---|
|
|
107
|
-
| `extract_finance` |
|
|
107
|
+
| `extract_finance` | No-key Stooq quote data — close, OHLC, volume, quote timestamp, source. Up to 5 tickers. |
|
|
108
108
|
| `search_jobs` | Remote job listings from Remotive, RemoteOK, HN "Who is Hiring" |
|
|
109
109
|
|
|
110
110
|
### Composites — multiple sources, one call
|
|
@@ -153,7 +153,7 @@ Restart Claude. Done.
|
|
|
153
153
|
|
|
154
154
|
### Local (full Playwright)
|
|
155
155
|
|
|
156
|
-
**Requires:** Node.js
|
|
156
|
+
**Requires:** Node.js 20+ ([nodejs.org](https://nodejs.org))
|
|
157
157
|
|
|
158
158
|
```bash
|
|
159
159
|
git clone https://github.com/PrinceGabriel-lgtm/freshcontext-mcp
|
|
@@ -259,12 +259,12 @@ Production: `https://freshcontext-mcp.gimmanuel73.workers.dev`
|
|
|
259
259
|
## Roadmap
|
|
260
260
|
|
|
261
261
|
- [x] FreshContext Specification v1.1 published (MIT, open standard)
|
|
262
|
-
- [x] DAR engine with proprietary λ constants (v0.3.
|
|
262
|
+
- [x] DAR engine with proprietary λ constants (v0.3.17)
|
|
263
263
|
- [x] Ha-Pri audit signatures on every signal
|
|
264
264
|
- [x] Semantic deduplication via fingerprinting
|
|
265
265
|
- [x] Live before/after demo at `/demo`
|
|
266
266
|
- [x] METHODOLOGY.md — formal IP and engineering documentation
|
|
267
|
-
- [x]
|
|
267
|
+
- [x] 21 reference tools across intelligence, competitive research, market data, and composites
|
|
268
268
|
- [x] Cloudflare Workers deployment — global edge, KV cache, KV rate limiting
|
|
269
269
|
- [x] Listed on official MCP Registry, Apify Store, npm
|
|
270
270
|
- [x] GitHub Actions CI/CD — auto-publish on every push
|
package/dist/adapters/finance.js
CHANGED
|
@@ -1,117 +1,103 @@
|
|
|
1
|
-
function
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (cap >= 1e6)
|
|
9
|
-
return `$${(cap / 1e6).toFixed(2)}M`;
|
|
10
|
-
return `$${cap.toLocaleString()}`;
|
|
1
|
+
function toStooqSymbol(ticker) {
|
|
2
|
+
const clean = ticker.trim().toUpperCase().replace(/[^A-Z0-9.^=-]/g, "");
|
|
3
|
+
if (!clean)
|
|
4
|
+
throw new Error("Ticker cannot be empty");
|
|
5
|
+
if (clean.includes(".") || clean.startsWith("^") || clean.includes("="))
|
|
6
|
+
return clean;
|
|
7
|
+
return `${clean}.US`;
|
|
11
8
|
}
|
|
12
|
-
function
|
|
13
|
-
if (
|
|
14
|
-
return
|
|
15
|
-
const
|
|
16
|
-
return
|
|
9
|
+
function toNumber(value) {
|
|
10
|
+
if (value === undefined || value === "N/D")
|
|
11
|
+
return null;
|
|
12
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
13
|
+
return Number.isFinite(n) ? n : null;
|
|
14
|
+
}
|
|
15
|
+
function normalizeQuoteTimestamp(date, time) {
|
|
16
|
+
if (!date || date === "N/D")
|
|
17
|
+
throw new Error("Quote date unavailable");
|
|
18
|
+
const clock = time && time !== "N/D" ? time : "00:00:00";
|
|
19
|
+
const parsed = new Date(`${date}T${clock}Z`);
|
|
20
|
+
if (Number.isNaN(parsed.getTime()))
|
|
21
|
+
throw new Error(`Invalid quote timestamp: ${date} ${time}`);
|
|
22
|
+
return parsed.toISOString();
|
|
23
|
+
}
|
|
24
|
+
function formatNumber(value, prefix = "") {
|
|
25
|
+
const n = toNumber(value);
|
|
26
|
+
return n === null ? "N/A" : `${prefix}${n.toLocaleString(undefined, { maximumFractionDigits: 4 })}`;
|
|
27
|
+
}
|
|
28
|
+
async function fetchStooqQuote(ticker) {
|
|
29
|
+
const stooqSymbol = toStooqSymbol(ticker);
|
|
30
|
+
const url = `https://stooq.com/q/l/?s=${encodeURIComponent(stooqSymbol.toLowerCase())}&f=sd2t2ohlcv&h&e=json`;
|
|
31
|
+
const res = await fetch(url, {
|
|
32
|
+
headers: {
|
|
33
|
+
"User-Agent": "freshcontext-mcp/0.3.17",
|
|
34
|
+
"Accept": "application/json",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok)
|
|
38
|
+
throw new Error(`Stooq quote API error: ${res.status}`);
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
const quote = data.symbols?.[0];
|
|
41
|
+
if (!quote)
|
|
42
|
+
throw new Error(`No quote returned for ${ticker}`);
|
|
43
|
+
if (quote.close === "N/D" || quote.date === "N/D") {
|
|
44
|
+
throw new Error(`No Stooq quote data found for ${ticker}`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
requested: ticker,
|
|
48
|
+
stooqSymbol,
|
|
49
|
+
quote,
|
|
50
|
+
timestamp: normalizeQuoteTimestamp(quote.date, quote.time),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function formatQuote(result) {
|
|
54
|
+
const q = result.quote;
|
|
55
|
+
return [
|
|
56
|
+
`${result.requested.toUpperCase()} — ${q.symbol}`,
|
|
57
|
+
`source: stooq`,
|
|
58
|
+
`Quote timestamp: ${result.timestamp}`,
|
|
59
|
+
"",
|
|
60
|
+
`Close: ${formatNumber(q.close, "$")}`,
|
|
61
|
+
`Open: ${formatNumber(q.open, "$")}`,
|
|
62
|
+
`High: ${formatNumber(q.high, "$")}`,
|
|
63
|
+
`Low: ${formatNumber(q.low, "$")}`,
|
|
64
|
+
`Volume: ${formatNumber(q.volume)}`,
|
|
65
|
+
].join("\n");
|
|
17
66
|
}
|
|
18
67
|
export async function financeAdapter(options) {
|
|
19
68
|
const input = options.url.trim();
|
|
20
|
-
// Support comma-separated tickers
|
|
21
69
|
const rawTickers = input
|
|
22
70
|
.split(",")
|
|
23
|
-
.map((t) => t.trim()
|
|
71
|
+
.map((t) => t.trim())
|
|
24
72
|
.filter(Boolean)
|
|
25
|
-
.slice(0, 5);
|
|
26
|
-
|
|
27
|
-
|
|
73
|
+
.slice(0, 5);
|
|
74
|
+
if (!rawTickers.length)
|
|
75
|
+
throw new Error("At least one ticker is required");
|
|
76
|
+
const successes = [];
|
|
77
|
+
const failures = [];
|
|
28
78
|
for (const ticker of rawTickers) {
|
|
29
79
|
try {
|
|
30
|
-
|
|
31
|
-
if (quoteData) {
|
|
32
|
-
results.push(formatQuote(quoteData));
|
|
33
|
-
if (quoteData.regularMarketTime) {
|
|
34
|
-
latestTimestamp = Math.max(latestTimestamp ?? 0, quoteData.regularMarketTime);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
80
|
+
successes.push(await fetchStooqQuote(ticker));
|
|
37
81
|
}
|
|
38
82
|
catch (err) {
|
|
39
|
-
|
|
83
|
+
failures.push(`[${ticker.toUpperCase()}] ${err instanceof Error ? err.message : String(err)}`);
|
|
40
84
|
}
|
|
41
85
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
? new Date(latestTimestamp * 1000).toISOString()
|
|
45
|
-
: new Date().toISOString();
|
|
46
|
-
return { raw, content_date, freshness_confidence: "high" };
|
|
47
|
-
}
|
|
48
|
-
async function fetchQuote(ticker) {
|
|
49
|
-
// v7 quote endpoint — public, no auth
|
|
50
|
-
const quoteUrl = `https://query1.finance.yahoo.com/v7/finance/quote?symbols=${encodeURIComponent(ticker)}&fields=shortName,longName,regularMarketPrice,regularMarketChange,regularMarketChangePercent,marketCap,regularMarketVolume,fiftyTwoWeekHigh,fiftyTwoWeekLow,trailingPE,dividendYield,currency,exchangeName,regularMarketTime`;
|
|
51
|
-
const quoteRes = await fetch(quoteUrl, {
|
|
52
|
-
headers: {
|
|
53
|
-
"User-Agent": "Mozilla/5.0 (compatible; freshcontext-mcp/0.1.5)",
|
|
54
|
-
"Accept": "application/json",
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
if (!quoteRes.ok)
|
|
58
|
-
throw new Error(`Yahoo Finance API error: ${quoteRes.status}`);
|
|
59
|
-
const quoteJson = await quoteRes.json();
|
|
60
|
-
const quote = quoteJson?.quoteResponse?.result?.[0];
|
|
61
|
-
if (!quote)
|
|
62
|
-
throw new Error(`No data found for ticker: ${ticker}`);
|
|
63
|
-
// Optionally fetch company summary (v11 quoteSummary)
|
|
64
|
-
try {
|
|
65
|
-
const summaryUrl = `https://query1.finance.yahoo.com/v11/finance/quoteSummary/${encodeURIComponent(ticker)}?modules=assetProfile`;
|
|
66
|
-
const summaryRes = await fetch(summaryUrl, {
|
|
67
|
-
headers: { "User-Agent": "Mozilla/5.0 (compatible; freshcontext-mcp/0.1.5)" },
|
|
68
|
-
});
|
|
69
|
-
if (summaryRes.ok) {
|
|
70
|
-
const summaryJson = await summaryRes.json();
|
|
71
|
-
const profile = summaryJson?.quoteSummary?.result?.[0]?.assetProfile;
|
|
72
|
-
if (profile) {
|
|
73
|
-
Object.assign(quote, {
|
|
74
|
-
longBusinessSummary: profile.longBusinessSummary,
|
|
75
|
-
sector: profile.sector,
|
|
76
|
-
industry: profile.industry,
|
|
77
|
-
fullTimeEmployees: profile.fullTimeEmployees,
|
|
78
|
-
website: profile.website,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
// Summary is optional — continue without it
|
|
85
|
-
}
|
|
86
|
-
return quote;
|
|
87
|
-
}
|
|
88
|
-
function formatQuote(q) {
|
|
89
|
-
const lines = [
|
|
90
|
-
`${q.symbol} — ${q.longName ?? q.shortName ?? "Unknown"}`,
|
|
91
|
-
`Exchange: ${q.exchangeName ?? "N/A"} · Currency: ${q.currency ?? "USD"}`,
|
|
92
|
-
"",
|
|
93
|
-
`Price: ${q.regularMarketPrice !== undefined ? `$${q.regularMarketPrice.toFixed(2)}` : "N/A"}`,
|
|
94
|
-
`Change: ${formatChange(q.regularMarketChange, q.regularMarketChangePercent)}`,
|
|
95
|
-
`Market Cap: ${formatMarketCap(q.marketCap)}`,
|
|
96
|
-
`Volume: ${q.regularMarketVolume?.toLocaleString() ?? "N/A"}`,
|
|
97
|
-
`52w High: ${q.fiftyTwoWeekHigh !== undefined ? `$${q.fiftyTwoWeekHigh.toFixed(2)}` : "N/A"}`,
|
|
98
|
-
`52w Low: ${q.fiftyTwoWeekLow !== undefined ? `$${q.fiftyTwoWeekLow.toFixed(2)}` : "N/A"}`,
|
|
99
|
-
`P/E Ratio: ${q.trailingPE !== undefined ? q.trailingPE.toFixed(2) : "N/A"}`,
|
|
100
|
-
`Div Yield: ${q.dividendYield !== undefined ? `${(q.dividendYield * 100).toFixed(2)}%` : "N/A"}`,
|
|
101
|
-
];
|
|
102
|
-
if (q.sector || q.industry) {
|
|
103
|
-
lines.push("");
|
|
104
|
-
if (q.sector)
|
|
105
|
-
lines.push(`Sector: ${q.sector}`);
|
|
106
|
-
if (q.industry)
|
|
107
|
-
lines.push(`Industry: ${q.industry}`);
|
|
108
|
-
if (q.fullTimeEmployees)
|
|
109
|
-
lines.push(`Employees: ${q.fullTimeEmployees.toLocaleString()}`);
|
|
110
|
-
if (q.website)
|
|
111
|
-
lines.push(`Website: ${q.website}`);
|
|
86
|
+
if (!successes.length) {
|
|
87
|
+
throw new Error(`Finance quote lookup failed for all tickers via source=stooq. ${failures.join("; ")}`);
|
|
112
88
|
}
|
|
113
|
-
|
|
114
|
-
|
|
89
|
+
const sections = successes.map(formatQuote);
|
|
90
|
+
if (failures.length) {
|
|
91
|
+
sections.push(["Partial failures:", ...failures.map((f) => `- ${f}`)].join("\n"));
|
|
115
92
|
}
|
|
116
|
-
|
|
93
|
+
const raw = sections.join("\n\n-----------------------------\n\n").slice(0, options.maxLength ?? 5000);
|
|
94
|
+
const content_date = successes
|
|
95
|
+
.map((s) => s.timestamp)
|
|
96
|
+
.sort()
|
|
97
|
+
.reverse()[0] ?? null;
|
|
98
|
+
return {
|
|
99
|
+
raw,
|
|
100
|
+
content_date,
|
|
101
|
+
freshness_confidence: failures.length ? "medium" : "high",
|
|
102
|
+
};
|
|
117
103
|
}
|
package/dist/adapters/gdelt.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
const HEADERS = {
|
|
13
13
|
"Accept": "application/json",
|
|
14
|
-
"User-Agent": "freshcontext-mcp/
|
|
14
|
+
"User-Agent": "freshcontext-mcp/0.3.17 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
15
15
|
};
|
|
16
16
|
function parseGdeltDate(raw) {
|
|
17
17
|
if (!raw)
|
package/dist/adapters/gebiz.js
CHANGED
|
@@ -20,7 +20,7 @@ const DATASET_ID = "d_acde1106003906a75c3fa052592f2fcb";
|
|
|
20
20
|
const BASE_URL = "https://data.gov.sg/api/action/datastore_search";
|
|
21
21
|
const HEADERS = {
|
|
22
22
|
"Accept": "application/json",
|
|
23
|
-
"User-Agent": "freshcontext-mcp/
|
|
23
|
+
"User-Agent": "freshcontext-mcp/0.3.17 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
24
24
|
};
|
|
25
25
|
function formatDate(raw) {
|
|
26
26
|
if (!raw)
|
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import { chromium } from "playwright";
|
|
2
2
|
import { validateUrl } from "../security.js";
|
|
3
|
+
function isUrl(input) {
|
|
4
|
+
try {
|
|
5
|
+
new URL(input);
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function normalizeHnDate(raw) {
|
|
13
|
+
if (!raw)
|
|
14
|
+
return null;
|
|
15
|
+
const match = raw.match(/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?\b/);
|
|
16
|
+
if (!match)
|
|
17
|
+
return null;
|
|
18
|
+
const isoLike = match[0].endsWith("Z") ? match[0] : `${match[0]}Z`;
|
|
19
|
+
const parsed = new Date(isoLike);
|
|
20
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
21
|
+
}
|
|
3
22
|
export async function hackerNewsAdapter(options) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
23
|
+
const input = options.url.trim();
|
|
24
|
+
if (!input)
|
|
25
|
+
throw new Error("HN URL or search query is required");
|
|
26
|
+
const url = isUrl(input) ? validateUrl(input, "hackernews") : `hn-search:${input}`;
|
|
7
27
|
if (url.includes("hn.algolia.com/api/") || url.startsWith("hn-search:")) {
|
|
8
28
|
const query = url.startsWith("hn-search:")
|
|
9
29
|
? url.replace("hn-search:", "").trim()
|
|
@@ -20,14 +40,17 @@ export async function hackerNewsAdapter(options) {
|
|
|
20
40
|
`[${i + 1}] ${r.title ?? "Untitled"}`,
|
|
21
41
|
`URL: ${r.url ?? `https://news.ycombinator.com/item?id=${r.objectID}`}`,
|
|
22
42
|
`Score: ${r.points} points | ${r.num_comments} comments`,
|
|
23
|
-
`Author: ${r.author} | Posted: ${r.created_at}`,
|
|
43
|
+
`Author: ${r.author} | Posted: ${normalizeHnDate(r.created_at) ?? r.created_at}`,
|
|
24
44
|
].join("\n"))
|
|
25
45
|
.join("\n\n")
|
|
26
46
|
.slice(0, options.maxLength ?? 4000);
|
|
27
|
-
const newest = data.hits
|
|
47
|
+
const newest = data.hits
|
|
48
|
+
.map((r) => normalizeHnDate(r.created_at))
|
|
49
|
+
.filter((d) => Boolean(d))
|
|
50
|
+
.sort()
|
|
51
|
+
.reverse()[0] ?? null;
|
|
28
52
|
return { raw, content_date: newest, freshness_confidence: newest ? "high" : "medium" };
|
|
29
53
|
}
|
|
30
|
-
// Default: browser-based scrape for HN front page or search pages
|
|
31
54
|
const browser = await chromium.launch({ headless: true });
|
|
32
55
|
const page = await browser.newPage();
|
|
33
56
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 20000 });
|
|
@@ -51,14 +74,21 @@ export async function hackerNewsAdapter(options) {
|
|
|
51
74
|
await browser.close();
|
|
52
75
|
const typedData = data;
|
|
53
76
|
const raw = typedData
|
|
54
|
-
.map((r, i) =>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
.map((r, i) => {
|
|
78
|
+
const date = normalizeHnDate(r.age);
|
|
79
|
+
return [
|
|
80
|
+
`[${i + 1}] ${r.title ?? "Untitled"}`,
|
|
81
|
+
`URL: ${r.link ?? "N/A"}`,
|
|
82
|
+
`Score: ${r.score ?? "N/A"} | ${r.commentLink ?? ""}`,
|
|
83
|
+
`Posted: ${date ?? "unknown"}`,
|
|
84
|
+
].join("\n");
|
|
85
|
+
})
|
|
60
86
|
.join("\n\n");
|
|
61
|
-
const newestDate = typedData
|
|
87
|
+
const newestDate = typedData
|
|
88
|
+
.map((r) => normalizeHnDate(r.age))
|
|
89
|
+
.filter((d) => Boolean(d))
|
|
90
|
+
.sort()
|
|
91
|
+
.reverse()[0] ?? null;
|
|
62
92
|
return {
|
|
63
93
|
raw,
|
|
64
94
|
content_date: newestDate,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { validateUrl } from "../security.js";
|
|
1
2
|
export async function productHuntAdapter(options) {
|
|
2
|
-
|
|
3
|
+
const token = process.env.PH_TOKEN?.trim() || process.env.PRODUCTHUNT_TOKEN?.trim();
|
|
4
|
+
if (!token)
|
|
5
|
+
return scrapeProductHunt(options);
|
|
6
|
+
// Product Hunt GraphQL API requires a bearer token. Keep it in env/secrets,
|
|
7
|
+
// never in source.
|
|
3
8
|
const query = options.url.startsWith("http")
|
|
4
9
|
? null
|
|
5
10
|
: options.url;
|
|
@@ -28,8 +33,7 @@ export async function productHuntAdapter(options) {
|
|
|
28
33
|
method: "POST",
|
|
29
34
|
headers: {
|
|
30
35
|
"Content-Type": "application/json",
|
|
31
|
-
|
|
32
|
-
"Authorization": "Bearer irgTzMNAz-S-p1P8H5pFCxzU4TEF7GIJZ8vZZi0gLJg",
|
|
36
|
+
"Authorization": `Bearer ${token}`,
|
|
33
37
|
},
|
|
34
38
|
body: JSON.stringify({ query: gql }),
|
|
35
39
|
});
|
|
@@ -68,7 +72,7 @@ export async function productHuntAdapter(options) {
|
|
|
68
72
|
async function scrapeProductHunt(options) {
|
|
69
73
|
const { chromium } = await import("playwright");
|
|
70
74
|
const url = options.url.startsWith("http")
|
|
71
|
-
? options.url
|
|
75
|
+
? validateUrl(options.url, "productHunt")
|
|
72
76
|
: `https://www.producthunt.com/search?q=${encodeURIComponent(options.url)}`;
|
|
73
77
|
const browser = await chromium.launch({ headless: true });
|
|
74
78
|
const page = await browser.newPage();
|
|
@@ -22,7 +22,7 @@ export async function repoSearchAdapter(options) {
|
|
|
22
22
|
const res = await fetch(apiUrl, {
|
|
23
23
|
headers: {
|
|
24
24
|
Accept: "application/vnd.github.v3+json",
|
|
25
|
-
"User-Agent": "freshcontext-mcp/0.
|
|
25
|
+
"User-Agent": "freshcontext-mcp/0.3.17 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
26
26
|
},
|
|
27
27
|
});
|
|
28
28
|
if (!res.ok) {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
const HEADERS = {
|
|
14
14
|
"Accept": "application/json",
|
|
15
|
-
"User-Agent": "freshcontext-mcp/
|
|
15
|
+
"User-Agent": "freshcontext-mcp/0.3.17 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
16
16
|
};
|
|
17
17
|
async function fetchSecFilings(query, maxResults = 10) {
|
|
18
18
|
const today = new Date().toISOString().slice(0, 10);
|
package/dist/security.js
CHANGED
|
@@ -11,7 +11,7 @@ export const ALLOWED_DOMAINS = {
|
|
|
11
11
|
repoSearch: [], // uses GitHub API directly, no browser
|
|
12
12
|
packageTrends: [], // uses npm/PyPI APIs directly, no browser
|
|
13
13
|
reddit: [], // uses public Reddit JSON API, no browser
|
|
14
|
-
finance: [], // uses
|
|
14
|
+
finance: [], // uses Stooq quote API, no browser
|
|
15
15
|
productHunt: ["www.producthunt.com", "producthunt.com"],
|
|
16
16
|
};
|
|
17
17
|
// ─── Blocked IP ranges and internal hostnames ────────────────────────────────
|
package/dist/server.js
CHANGED
|
@@ -22,7 +22,7 @@ import { stampFreshness, formatForLLM } from "./tools/freshnessStamp.js";
|
|
|
22
22
|
import { formatSecurityError } from "./security.js";
|
|
23
23
|
const server = new McpServer({
|
|
24
24
|
name: "freshcontext-mcp",
|
|
25
|
-
version: "0.
|
|
25
|
+
version: "0.3.17",
|
|
26
26
|
});
|
|
27
27
|
// ─── Tool: extract_github ────────────────────────────────────────────────────
|
|
28
28
|
server.registerTool("extract_github", {
|
|
@@ -62,9 +62,9 @@ server.registerTool("extract_scholar", {
|
|
|
62
62
|
});
|
|
63
63
|
// ─── Tool: extract_hackernews ────────────────────────────────────────────────
|
|
64
64
|
server.registerTool("extract_hackernews", {
|
|
65
|
-
description: "Extract top stories or search results from Hacker News.
|
|
65
|
+
description: "Extract top stories or search results from Hacker News. Accepts an HN/Algolia URL or a plain search query while preserving the url field for compatibility.",
|
|
66
66
|
inputSchema: z.object({
|
|
67
|
-
url: z.string().
|
|
67
|
+
url: z.string().min(1).describe("HN URL e.g. https://news.ycombinator.com/news, Algolia API URL, or search query e.g. 'browser agents'"),
|
|
68
68
|
max_length: z.number().optional().default(4000),
|
|
69
69
|
}),
|
|
70
70
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
@@ -152,7 +152,7 @@ server.registerTool("extract_arxiv", {
|
|
|
152
152
|
});
|
|
153
153
|
// ─── Tool: extract_finance ───────────────────────────────────────────────────
|
|
154
154
|
server.registerTool("extract_finance", {
|
|
155
|
-
description: "
|
|
155
|
+
description: "No-key stock quote data via Stooq — close, open, high, low, volume, quote timestamp, and source. Accepts up to 5 comma-separated tickers. Returns timestamped freshcontext only for successful observations.",
|
|
156
156
|
inputSchema: z.object({
|
|
157
157
|
url: z.string().describe("Ticker symbol(s) e.g. 'AAPL' or 'MSFT,GOOG,PLTR'"),
|
|
158
158
|
max_length: z.number().optional().default(5000),
|
|
@@ -355,7 +355,7 @@ server.registerTool("extract_gov_landscape", {
|
|
|
355
355
|
// community sentiment, repo ecosystem size, and product release velocity.
|
|
356
356
|
// Unique: Bloomberg Terminal doesn't read commit history as a company health signal.
|
|
357
357
|
server.registerTool("extract_finance_landscape", {
|
|
358
|
-
description: "Composite financial intelligence tool for developers. Given one or more ticker symbols, simultaneously queries: (1)
|
|
358
|
+
description: "Composite financial intelligence tool for developers. Given one or more ticker symbols, simultaneously queries: (1) Stooq for no-key quote data, (2) Hacker News for developer community sentiment, (3) Reddit for investor and tech community discussion, (4) GitHub for repo ecosystem activity around the company's tech, and (5) their product changelog for release velocity as a company health signal. Answers: What's the price? What are developers saying? Is the company actually shipping? Returns a unified 5-source timestamped report.",
|
|
359
359
|
inputSchema: z.object({
|
|
360
360
|
tickers: z.string().describe("One or more ticker symbols e.g. 'PLTR' or 'PLTR,MSFT,GOOG'. Up to 5 tickers."),
|
|
361
361
|
company_name: z.string().optional().describe("Company name for HN/Reddit/GitHub searches e.g. 'Palantir'. If omitted, derived from the ticker."),
|
|
@@ -381,10 +381,10 @@ server.registerTool("extract_finance_landscape", {
|
|
|
381
381
|
const combined = [
|
|
382
382
|
`# Finance + Developer Intelligence: "${tickers}"${company_name ? ` (${company_name})` : ""}`,
|
|
383
383
|
`Generated: ${new Date().toISOString()}`,
|
|
384
|
-
`Sources:
|
|
384
|
+
`Sources: Stooq · Hacker News · Reddit · GitHub · Changelog`,
|
|
385
385
|
min_freshness_score ? `min_freshness_score: ${min_freshness_score}` : null,
|
|
386
386
|
"",
|
|
387
|
-
sectionWithFreshnessCheck("📈 Market Data (
|
|
387
|
+
sectionWithFreshnessCheck("📈 Market Data (Stooq)", priceResult, "finance", min_freshness_score),
|
|
388
388
|
sectionWithFreshnessCheck("💬 Developer Sentiment (Hacker News)", hnResult, "hackernews", min_freshness_score),
|
|
389
389
|
sectionWithFreshnessCheck("🗣️ Community Discussion (Reddit)", redditResult, "reddit", min_freshness_score),
|
|
390
390
|
sectionWithFreshnessCheck("📦 Repo Ecosystem (GitHub)", repoResult, "reposearch", min_freshness_score),
|
|
@@ -442,7 +442,7 @@ server.registerTool("extract_gdelt", {
|
|
|
442
442
|
// global news intelligence + product release velocity + market pricing.
|
|
443
443
|
// Unique: this combination exists nowhere else.
|
|
444
444
|
server.registerTool("extract_company_landscape", {
|
|
445
|
-
description: "Composite company intelligence tool. The most complete single-call company analysis available. Simultaneously queries 5 unique sources: (1) SEC EDGAR for 8-K material event filings — what the company legally just disclosed, (2) USASpending.gov for federal contract footprint — who is giving them government money, (3) GDELT for global news intelligence — what the world is saying about them right now, (4) their product changelog — are they actually shipping, (5)
|
|
445
|
+
description: "Composite company intelligence tool. The most complete single-call company analysis available. Simultaneously queries 5 unique sources: (1) SEC EDGAR for 8-K material event filings — what the company legally just disclosed, (2) USASpending.gov for federal contract footprint — who is giving them government money, (3) GDELT for global news intelligence — what the world is saying about them right now, (4) their product changelog — are they actually shipping, (5) Stooq quote data — what the market is pricing in. Returns a unified 5-source timestamped report. Unique: this combination is not available in any other MCP server.",
|
|
446
446
|
inputSchema: z.object({
|
|
447
447
|
company: z.string().describe("Company name e.g. 'Palantir', 'Anthropic', 'OpenAI'"),
|
|
448
448
|
ticker: z.string().optional().describe("Stock ticker for finance data e.g. 'PLTR'. Leave blank for private companies."),
|
|
@@ -464,14 +464,14 @@ server.registerTool("extract_company_landscape", {
|
|
|
464
464
|
const combined = [
|
|
465
465
|
`# Company Intelligence Landscape: "${company}"${ticker ? ` (${ticker})` : ""}`,
|
|
466
466
|
`Generated: ${new Date().toISOString()}`,
|
|
467
|
-
`Sources: SEC EDGAR · USASpending.gov · GDELT · Changelog ·
|
|
467
|
+
`Sources: SEC EDGAR · USASpending.gov · GDELT · Changelog · Stooq`,
|
|
468
468
|
min_freshness_score ? `min_freshness_score: ${min_freshness_score}` : null,
|
|
469
469
|
"",
|
|
470
470
|
sectionWithFreshnessCheck("📋 SEC 8-K Filings — Legal Disclosures", secResult, "sec_filings", min_freshness_score),
|
|
471
471
|
sectionWithFreshnessCheck("🏛️ Federal Contract Awards (USASpending.gov)", contractsResult, "govcontracts", min_freshness_score),
|
|
472
472
|
sectionWithFreshnessCheck("🌍 Global News Intelligence (GDELT)", gdeltResult, "gdelt", min_freshness_score),
|
|
473
473
|
sectionWithFreshnessCheck("🔄 Product Release Velocity (Changelog)", changelogResult, "changelog", min_freshness_score),
|
|
474
|
-
sectionWithFreshnessCheck("📈 Market Data (
|
|
474
|
+
sectionWithFreshnessCheck("📈 Market Data (Stooq)", financeResult, "finance", min_freshness_score),
|
|
475
475
|
].filter(Boolean).join("\n\n");
|
|
476
476
|
return { content: [{ type: "text", text: combined }] };
|
|
477
477
|
});
|
|
@@ -54,16 +54,36 @@ function scoreLabel(score) {
|
|
|
54
54
|
return "verify before acting";
|
|
55
55
|
return "use with caution";
|
|
56
56
|
}
|
|
57
|
+
function looksLikeFailedAdapterContent(raw) {
|
|
58
|
+
const trimmed = raw.trim();
|
|
59
|
+
if (!trimmed)
|
|
60
|
+
return true;
|
|
61
|
+
if (/^\[(?:error|security)\]/i.test(trimmed))
|
|
62
|
+
return true;
|
|
63
|
+
if (/^(?:error|failed|upstream|timeout)\b/i.test(trimmed))
|
|
64
|
+
return true;
|
|
65
|
+
const meaningful = trimmed
|
|
66
|
+
.split(/\r?\n/)
|
|
67
|
+
.map((line) => line.trim())
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
if (!meaningful.length)
|
|
70
|
+
return true;
|
|
71
|
+
const failureLines = meaningful.filter((line) => /\b(?:error|failed|failure|timeout|401|403|404|429|5\d\d)\b/i.test(line));
|
|
72
|
+
return failureLines.length === meaningful.length;
|
|
73
|
+
}
|
|
57
74
|
// ─── Main stamp function ──────────────────────────────────────────────────────
|
|
58
75
|
export function stampFreshness(result, options, adapter) {
|
|
59
76
|
const retrieved_at = new Date().toISOString();
|
|
60
|
-
const
|
|
77
|
+
const failedContent = looksLikeFailedAdapterContent(result.raw);
|
|
78
|
+
const content_date = failedContent ? null : result.content_date;
|
|
79
|
+
const freshness_confidence = failedContent ? "low" : result.freshness_confidence;
|
|
80
|
+
const freshness_score = calculateFreshnessScore(content_date, retrieved_at, adapter);
|
|
61
81
|
return {
|
|
62
82
|
content: result.raw.slice(0, options.maxLength ?? 8000),
|
|
63
83
|
source_url: options.url,
|
|
64
|
-
content_date
|
|
84
|
+
content_date,
|
|
65
85
|
retrieved_at,
|
|
66
|
-
freshness_confidence
|
|
86
|
+
freshness_confidence,
|
|
67
87
|
freshness_score,
|
|
68
88
|
adapter,
|
|
69
89
|
};
|
package/freshcontext.schema.json
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"type": ["number", "null"],
|
|
37
37
|
"minimum": 0,
|
|
38
38
|
"maximum": 100,
|
|
39
|
-
"description": "Optional numeric freshness score 0-100. Calculated
|
|
39
|
+
"description": "Optional numeric freshness score 0-100. Calculated with source-specific exponential temporal decay. Null if content_date is unknown.",
|
|
40
40
|
"examples": [94, 72, 45, null]
|
|
41
41
|
},
|
|
42
42
|
"adapter": {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freshcontext-mcp",
|
|
3
3
|
"mcpName": "io.github.PrinceGabriel-lgtm/freshcontext",
|
|
4
|
-
"version": "0.3.
|
|
5
|
-
"description": "Real-time web intelligence for AI agents.
|
|
4
|
+
"version": "0.3.17",
|
|
5
|
+
"description": "Real-time web intelligence for AI agents. 21 tools, no required API keys. Every result timestamped with a freshness score.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
8
8
|
"mcp-server",
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
},
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"type": "module",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
27
30
|
"main": "dist/server.js",
|
|
28
31
|
"bin": {
|
|
29
32
|
"freshcontext-mcp": "dist/server.js"
|
|
@@ -33,20 +36,24 @@
|
|
|
33
36
|
"dev": "tsx watch src/server.ts",
|
|
34
37
|
"start": "node dist/server.js",
|
|
35
38
|
"inspect": "npx @modelcontextprotocol/inspector tsx src/server.ts",
|
|
39
|
+
"smoke:stdio": "node scripts/smoke-stdio.mjs",
|
|
36
40
|
"test": "jest"
|
|
37
41
|
},
|
|
38
42
|
"dependencies": {
|
|
39
43
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
40
44
|
"apify": "^3.0.0",
|
|
45
|
+
"dotenv": "^16.4.0",
|
|
41
46
|
"playwright": "^1.44.0",
|
|
42
|
-
"zod": "^3.23.0"
|
|
43
|
-
"dotenv": "^16.4.0"
|
|
47
|
+
"zod": "^3.23.0"
|
|
44
48
|
},
|
|
45
49
|
"devDependencies": {
|
|
50
|
+
"@types/jest": "^29.0.0",
|
|
46
51
|
"@types/node": "^20.0.0",
|
|
47
|
-
"tsx": "^4.0.0",
|
|
48
|
-
"typescript": "^5.4.0",
|
|
49
52
|
"jest": "^29.0.0",
|
|
50
|
-
"
|
|
53
|
+
"tsx": "^4.0.0",
|
|
54
|
+
"typescript": "^5.4.0"
|
|
55
|
+
},
|
|
56
|
+
"overrides": {
|
|
57
|
+
"file-type": "21.3.4"
|
|
51
58
|
}
|
|
52
59
|
}
|
package/server.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
|
|
3
3
|
"name": "io.github.PrinceGabriel-lgtm/freshcontext",
|
|
4
|
-
"description": "Real-time web intelligence for AI agents.
|
|
4
|
+
"description": "Real-time web intelligence for AI agents. 21 tools, no required API keys. GitHub, HN, Reddit, arXiv, SEC filings, US gov contracts, GDELT global news, Singapore GeBIZ, changelog & more — every result timestamped with a freshness score.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/PrinceGabriel-lgtm/freshcontext-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.17",
|
|
10
10
|
"website_url": "https://freshcontext-site.pages.dev",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registry_type": "npm",
|
|
14
14
|
"registry_base_url": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "freshcontext-mcp",
|
|
16
|
-
"version": "0.3.
|
|
16
|
+
"version": "0.3.17",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|