freshcontext-mcp 0.1.8 β†’ 0.2.0

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 CHANGED
@@ -1,257 +1,212 @@
1
- # freshcontext-mcp
2
-
3
- > Timestamped web intelligence for AI agents. Every result is wrapped in a **FreshContext envelope** β€” so your agent always knows *when* it's looking at data, not just *what*.
4
-
5
- [![npm version](https://img.shields.io/npm/v/freshcontext-mcp)](https://www.npmjs.com/package/freshcontext-mcp)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
-
8
- ---
9
-
10
- ## The Problem
11
-
12
- LLMs hallucinate recency. They'll cite a 2022 job posting as "current", recall outdated API docs as if they're live, or tell you a project is active when it hasn't been touched in two years. This happens because they have no reliable signal for *when* data was retrieved vs. when it was published.
13
-
14
- Existing MCP servers return raw content. No timestamp. No confidence signal. No way for the agent to know if it's looking at something from this morning or three years ago.
15
-
16
- ## The Fix: FreshContext Envelope
17
-
18
- Every piece of data extracted by `freshcontext-mcp` is wrapped in a structured envelope:
19
-
20
- ```
21
- [FRESHCONTEXT]
22
- Source: https://github.com/owner/repo
23
- Published: 2024-11-03
24
- Retrieved: 2026-03-04T10:14:00Z
25
- Confidence: high
26
- ---
27
- ... content ...
28
- [/FRESHCONTEXT]
29
- ```
30
-
31
- The AI agent always knows **when it's looking at data**, not just what the data says.
32
-
33
- ---
34
-
35
- ## Tools
36
-
37
- ### πŸ”¬ Intelligence Tools
38
-
39
- | Tool | Description |
40
- |---|---|
41
- | `extract_github` | README, stars, forks, language, topics, last commit from any GitHub repo |
42
- | `extract_hackernews` | Top stories or search results from HN with scores and timestamps |
43
- | `extract_scholar` | Research paper titles, authors, years, and snippets from Google Scholar |
44
- | `extract_reddit` | Posts and community sentiment from any subreddit or Reddit search |
45
-
46
- ### πŸš€ Competitive Intelligence Tools
47
-
48
- | Tool | Description |
49
- |---|---|
50
- | `extract_yc` | Scrape YC company listings by keyword β€” find who's funded in your space |
51
- | `extract_producthunt` | Recent Product Hunt launches by keyword or topic |
52
- | `search_repos` | Search GitHub for similar/competing repos, ranked by stars with activity signals |
53
- | `package_trends` | npm and PyPI package metadata β€” version history, release cadence, last updated |
54
-
55
- ### πŸ“ˆ Market Data
56
-
57
- | Tool | Description |
58
- |---|---|
59
- | `extract_finance` | Live stock data via Yahoo Finance β€” price, market cap, P/E, 52w range, sector, company summary |
60
-
61
- ### πŸ—ΊοΈ Composite Tool
62
-
63
- | Tool | Description |
64
- |---|---|
65
- | `extract_landscape` | **One call. Full picture.** Queries YC + GitHub + HN + npm/PyPI simultaneously. Returns a unified timestamped landscape report. |
66
-
67
- ---
68
-
69
- ## Quick Start
70
-
71
- ### Option A β€” Cloud (recommended, no install needed)
72
-
73
- Visit **[freshcontext-site.pages.dev](https://freshcontext-site.pages.dev)** for a guided 3-step install with copy-paste config. No terminal, no downloads, no antivirus alerts.
74
-
75
- Or add this manually to your Claude Desktop config and restart:
76
-
77
- **Mac:** `~/Library/Application Support/Claude/claude_desktop_config.json`
78
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
79
-
80
- ```json
81
- {
82
- "mcpServers": {
83
- "freshcontext": {
84
- "command": "npx",
85
- "args": ["-y", "mcp-remote", "https://freshcontext-mcp.gimmanuel73.workers.dev/mcp"]
86
- }
87
- }
88
- }
89
- ```
90
-
91
- Restart Claude Desktop. The freshcontext tools will appear in your session.
92
-
93
- > If `claude_desktop_config.json` doesn't exist yet, create it with the content above.
94
-
95
- ---
96
-
97
- ### Option B β€” Local (full Playwright, for heavy use)
98
-
99
- **Prerequisites:** Node.js 18+ ([nodejs.org](https://nodejs.org))
100
-
101
- ```bash
102
- git clone https://github.com/PrinceGabriel-lgtm/freshcontext-mcp
103
- cd freshcontext-mcp
104
- npm install
105
- npx playwright install chromium
106
- npm run build
107
- ```
108
-
109
- Then add to your Claude Desktop config:
110
-
111
- **Mac:**
112
- ```json
113
- {
114
- "mcpServers": {
115
- "freshcontext": {
116
- "command": "node",
117
- "args": ["/Users/YOUR_USERNAME/path/to/freshcontext-mcp/dist/server.js"]
118
- }
119
- }
120
- }
121
- ```
122
-
123
- **Windows:**
124
- ```json
125
- {
126
- "mcpServers": {
127
- "freshcontext": {
128
- "command": "node",
129
- "args": ["C:\\Users\\YOUR_USERNAME\\path\\to\\freshcontext-mcp\\dist\\server.js"]
130
- }
131
- }
132
- }
133
- ```
134
-
135
- ---
136
-
137
- ### Troubleshooting (Mac)
138
-
139
- **"command not found: node"** β€” Node isn't on Claude Desktop's PATH. Use the full path:
140
- ```bash
141
- which node # copy this output
142
- ```
143
- Replace `"command": "node"` with `"command": "/usr/local/bin/node"` (or whatever `which node` returned).
144
-
145
- **"npx: command not found"** β€” Same fix. Run `which npx` and use the full path.
146
-
147
- **Config file doesn't exist** β€” Create it:
148
- ```bash
149
- mkdir -p ~/Library/Application\ Support/Claude
150
- touch ~/Library/Application\ Support/Claude/claude_desktop_config.json
151
- ```
152
-
153
- ---
154
-
155
- ## Usage Examples
156
-
157
- ### Check if anyone is already building what you're building
158
- ```
159
- Use extract_landscape with topic "cashflow prediction mcp"
160
- ```
161
- Returns a unified report: who's funded (YC), what's trending (HN), what repos exist (GitHub), what packages are active (npm/PyPI). All timestamped.
162
-
163
- ### Get community sentiment on a topic
164
- ```
165
- Use extract_reddit with url "r/MachineLearning"
166
- Use extract_hackernews with url "https://hn.algolia.com/api/v1/search?query=mcp+server&tags=story"
167
- ```
168
-
169
- ### Check a company's stock
170
- ```
171
- Use extract_finance with url "NVDA,MSFT,GOOG"
172
- ```
173
-
174
- ### Find what just launched in your space
175
- ```
176
- Use extract_producthunt with url "AI developer tools"
177
- ```
178
-
179
- ---
180
-
181
- ## Why FreshContext?
182
-
183
- Most AI agents retrieve data but don't timestamp it. This creates a silent failure mode: the agent presents stale information with the same confidence as fresh information. The user has no way to know the difference.
184
-
185
- FreshContext treats **retrieval time as first-class metadata**. Every adapter returns:
186
-
187
- - `retrieved_at` β€” exact ISO timestamp of when the data was fetched
188
- - `content_date` β€” best estimate of when the content was originally published
189
- - `freshness_confidence` β€” `high`, `medium`, or `low` based on signal quality
190
- - `adapter` β€” which source the data came from
191
-
192
- ---
193
-
194
- ## Security
195
-
196
- - Input sanitization and domain allowlists on all adapters
197
- - SSRF prevention (blocked private IP ranges)
198
- - KV-backed global rate limiting: 60 requests/minute per IP across all edge nodes
199
- - No credentials required for public data sources
200
-
201
- ---
202
-
203
- ## Project Structure
204
-
205
- ```
206
- freshcontext-mcp/
207
- β”œβ”€β”€ src/
208
- β”‚ β”œβ”€β”€ server.ts # MCP server, all tool registrations
209
- β”‚ β”œβ”€β”€ types.ts # FreshContext interfaces
210
- β”‚ β”œβ”€β”€ security.ts # Input validation, domain allowlists, SSRF prevention
211
- β”‚ β”œβ”€β”€ adapters/
212
- β”‚ β”‚ β”œβ”€β”€ github.ts
213
- β”‚ β”‚ β”œβ”€β”€ hackernews.ts
214
- β”‚ β”‚ β”œβ”€β”€ scholar.ts
215
- β”‚ β”‚ β”œβ”€β”€ yc.ts
216
- β”‚ β”‚ β”œβ”€β”€ repoSearch.ts
217
- β”‚ β”‚ β”œβ”€β”€ packageTrends.ts
218
- β”‚ β”‚ β”œβ”€β”€ reddit.ts
219
- β”‚ β”‚ β”œβ”€β”€ productHunt.ts
220
- β”‚ β”‚ └── finance.ts
221
- β”‚ └── tools/
222
- β”‚ └── freshnessStamp.ts
223
- └── worker/ # Cloudflare Workers deployment (all 10 tools)
224
- └── src/worker.ts
225
- ```
226
-
227
- ---
228
-
229
- ## Roadmap
230
-
231
- - [x] GitHub adapter
232
- - [x] Hacker News adapter
233
- - [x] Google Scholar adapter
234
- - [x] YC startup scraper
235
- - [x] GitHub repo search
236
- - [x] npm/PyPI package trends
237
- - [x] `extract_landscape` composite tool
238
- - [x] Cloudflare Workers deployment
239
- - [x] Worker auth + KV-backed global rate limiting
240
- - [x] Reddit community sentiment adapter
241
- - [x] Product Hunt launches adapter
242
- - [x] Yahoo Finance market data adapter
243
- - [ ] `extract_arxiv` β€” structured arXiv API (more reliable than Scholar)
244
- - [ ] TTL-based caching layer
245
- - [ ] `freshness_score` numeric metric
246
-
247
- ---
248
-
249
- ## Contributing
250
-
251
- PRs welcome. New adapters are the highest-value contribution β€” see `src/adapters/` for the pattern. Each adapter returns `{ raw, content_date, freshness_confidence }`.
252
-
253
- ---
254
-
255
- ## License
256
-
257
- MIT
1
+ # freshcontext-mcp
2
+
3
+ I asked Claude to help me find a job. It gave me a list of openings. I applied to three of them. Two didn't exist anymore. One had been closed for two years.
4
+
5
+ Claude had no idea. It presented everything with the same confidence.
6
+
7
+ That's the problem freshcontext fixes.
8
+
9
+ [![npm version](https://img.shields.io/npm/v/freshcontext-mcp)](https://www.npmjs.com/package/freshcontext-mcp)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
+
12
+ ---
13
+
14
+ ## What it does
15
+
16
+ Every MCP server returns data. freshcontext returns data **plus when it was retrieved and how confident that date is** β€” wrapped in a FreshContext envelope:
17
+
18
+ ```
19
+ [FRESHCONTEXT]
20
+ Source: https://github.com/owner/repo
21
+ Published: 2024-11-03
22
+ Retrieved: 2026-03-05T09:19:00Z
23
+ Confidence: high
24
+ ---
25
+ ... content ...
26
+ [/FRESHCONTEXT]
27
+ ```
28
+
29
+ Claude now knows the difference between something from this morning and something from two years ago. You do too.
30
+
31
+ ---
32
+
33
+ ## 11 tools. No API keys.
34
+
35
+ ### Intelligence
36
+ | Tool | What it gets you |
37
+ |---|---|
38
+ | `extract_github` | README, stars, forks, language, topics, last commit |
39
+ | `extract_hackernews` | Top stories or search results with scores and timestamps |
40
+ | `extract_scholar` | Research papers β€” titles, authors, years, snippets |
41
+ | `extract_arxiv` | arXiv papers via official API β€” more reliable than Scholar |
42
+ | `extract_reddit` | Posts and community sentiment from any subreddit |
43
+
44
+ ### Competitive research
45
+ | Tool | What it gets you |
46
+ |---|---|
47
+ | `extract_yc` | YC company listings by keyword β€” who's funded in your space |
48
+ | `extract_producthunt` | Recent launches by topic |
49
+ | `search_repos` | GitHub repos ranked by stars with activity signals |
50
+ | `package_trends` | npm and PyPI metadata β€” version history, release cadence |
51
+
52
+ ### Market data
53
+ | Tool | What it gets you |
54
+ |---|---|
55
+ | `extract_finance` | Live stock data β€” price, market cap, P/E, 52w range |
56
+
57
+ ### Composite
58
+ | Tool | What it gets you |
59
+ |---|---|
60
+ | `extract_landscape` | One call. YC + GitHub + HN + Reddit + Product Hunt + npm in parallel. Full timestamped picture. |
61
+
62
+ ---
63
+
64
+ ## Quick Start
65
+
66
+ ### Option A β€” Cloud (no install)
67
+
68
+ Add to your Claude Desktop config and restart:
69
+
70
+ **Mac:** `~/Library/Application Support/Claude/claude_desktop_config.json`
71
+ **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
72
+
73
+ ```json
74
+ {
75
+ "mcpServers": {
76
+ "freshcontext": {
77
+ "command": "npx",
78
+ "args": ["-y", "mcp-remote", "https://freshcontext-mcp.gimmanuel73.workers.dev/mcp"]
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ Restart Claude. Done.
85
+
86
+ > Prefer a guided setup? Visit **[freshcontext-site.pages.dev](https://freshcontext-site.pages.dev)** β€” 3 steps, no terminal.
87
+
88
+ ---
89
+
90
+ ### Option B β€” Local (full Playwright)
91
+
92
+ **Requires:** Node.js 18+ ([nodejs.org](https://nodejs.org))
93
+
94
+ ```bash
95
+ git clone https://github.com/PrinceGabriel-lgtm/freshcontext-mcp
96
+ cd freshcontext-mcp
97
+ npm install
98
+ npx playwright install chromium
99
+ npm run build
100
+ ```
101
+
102
+ Add to Claude Desktop config:
103
+
104
+ **Mac:**
105
+ ```json
106
+ {
107
+ "mcpServers": {
108
+ "freshcontext": {
109
+ "command": "node",
110
+ "args": ["/Users/YOUR_USERNAME/path/to/freshcontext-mcp/dist/server.js"]
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ **Windows:**
117
+ ```json
118
+ {
119
+ "mcpServers": {
120
+ "freshcontext": {
121
+ "command": "node",
122
+ "args": ["C:\\Users\\YOUR_USERNAME\\path\\to\\freshcontext-mcp\\dist\\server.js"]
123
+ }
124
+ }
125
+ }
126
+ ```
127
+
128
+ ---
129
+
130
+ ### Troubleshooting (Mac)
131
+
132
+ **"command not found: node"** β€” Use the full path:
133
+ ```bash
134
+ which node # copy this output, replace "node" in config
135
+ ```
136
+
137
+ **Config file doesn't exist** β€” Create it:
138
+ ```bash
139
+ mkdir -p ~/Library/Application\ Support/Claude
140
+ touch ~/Library/Application\ Support/Claude/claude_desktop_config.json
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Usage examples
146
+
147
+ **Is anyone already building what you're building?**
148
+ ```
149
+ Use extract_landscape with topic "cashflow prediction saas"
150
+ ```
151
+ Returns who's funded, what's trending, what repos exist, what packages are moving β€” all timestamped.
152
+
153
+ **What's the community actually saying right now?**
154
+ ```
155
+ Use extract_reddit on r/MachineLearning
156
+ Use extract_hackernews to search "mcp server 2026"
157
+ ```
158
+
159
+ **Did that company actually ship recently?**
160
+ ```
161
+ Use extract_github on https://github.com/some-org/some-repo
162
+ ```
163
+ Check `Published` vs `Retrieved`. If the gap is 18 months, Claude will tell you.
164
+
165
+ ---
166
+
167
+ ## How freshness works
168
+
169
+ Most AI tools retrieve data silently. No timestamp, no signal, no way for the agent to know how old it is.
170
+
171
+ freshcontext treats **retrieval time as first-class metadata**. Every adapter returns:
172
+
173
+ - `retrieved_at` β€” exact ISO timestamp of the fetch
174
+ - `content_date` β€” best estimate of when the content was originally published
175
+ - `freshness_confidence` β€” `high`, `medium`, or `low` based on signal quality
176
+ - `adapter` β€” which source the data came from
177
+
178
+ When confidence is `high`, the date came from a structured field (API, metadata). When it's `medium` or `low`, freshcontext tells you why.
179
+
180
+ ---
181
+
182
+ ## Security
183
+
184
+ - Input sanitization and domain allowlists on all adapters
185
+ - SSRF prevention (blocked private IP ranges)
186
+ - KV-backed global rate limiting: 60 req/min per IP across all edge nodes
187
+ - No credentials required β€” all public data sources
188
+
189
+ ---
190
+
191
+ ## Roadmap
192
+
193
+ - [x] GitHub, HN, Scholar, YC, Reddit, Product Hunt, Finance, arXiv adapters
194
+ - [x] `extract_landscape` β€” 6-source composite tool
195
+ - [x] Cloudflare Workers deployment
196
+ - [x] KV-backed global rate limiting
197
+ - [x] Listed on official MCP Registry
198
+ - [ ] TTL-based caching layer
199
+ - [ ] `freshness_score` numeric metric
200
+ - [ ] Job search adapter (the tool that started all this)
201
+
202
+ ---
203
+
204
+ ## Contributing
205
+
206
+ PRs welcome. New adapters are the highest-value contribution β€” see `src/adapters/` for the pattern.
207
+
208
+ ---
209
+
210
+ ## License
211
+
212
+ MIT
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Jobs adapter v2 β€” 4 sources, freshness badges, location + keyword filtering.
3
+ *
4
+ * Sources (all no-auth):
5
+ * - Remotive β€” remote jobs, location filterable
6
+ * - RemoteOK β€” pure remote, salary data, unix timestamps
7
+ * - The Muse β€” structured jobs, location + category
8
+ * - HN Who is Hiring β€” community-sourced, raw but real
9
+ *
10
+ * Every listing gets a freshness badge:
11
+ * 🟒 < 7 days β€” FRESH, apply now
12
+ * 🟑 7–30 days β€” still good
13
+ * πŸ”΄ 31–90 days β€” apply with caution
14
+ * β›” > 90 days β€” likely expired, shown last
15
+ *
16
+ * This adapter was the whole reason freshcontext exists.
17
+ */
18
+ // ─── Freshness Scoring ────────────────────────────────────────────────────────
19
+ function freshnessBadge(dateStr) {
20
+ if (!dateStr)
21
+ return { badge: "❓ Unknown date", days: 9999 };
22
+ const days = Math.floor((Date.now() - new Date(dateStr).getTime()) / 86400000);
23
+ if (days < 0)
24
+ return { badge: "🟒 Just posted β€” FRESH", days: 0 };
25
+ if (days <= 7)
26
+ return { badge: `🟒 ${days}d ago β€” FRESH`, days };
27
+ if (days <= 30)
28
+ return { badge: `🟑 ${days}d ago`, days };
29
+ if (days <= 90)
30
+ return { badge: `πŸ”΄ ${days}d ago β€” apply with caution`, days };
31
+ return { badge: `β›” ${days}d ago β€” likely expired`, days };
32
+ }
33
+ function matchesLocation(locationField, filterLocation) {
34
+ if (!filterLocation || filterLocation.toLowerCase() === "worldwide" || filterLocation.toLowerCase() === "remote")
35
+ return true;
36
+ const loc = locationField.toLowerCase();
37
+ const filter = filterLocation.toLowerCase();
38
+ return loc.includes(filter) || filter.includes(loc) || loc.includes("worldwide") || loc.includes("anywhere") || loc.includes("remote");
39
+ }
40
+ function highlightKeywords(text, keywords) {
41
+ if (!keywords.length)
42
+ return text;
43
+ let result = text;
44
+ for (const kw of keywords) {
45
+ const re = new RegExp(`(${kw})`, "gi");
46
+ result = result.replace(re, "⚑$1");
47
+ }
48
+ return result;
49
+ }
50
+ // ─── Main Adapter ─────────────────────────────────────────────────────────────
51
+ export async function jobsAdapter(options) {
52
+ const query = options.url.trim();
53
+ const maxLength = options.maxLength ?? 8000;
54
+ const location = options.location ?? "";
55
+ const remoteOnly = options.remoteOnly ?? false;
56
+ const maxAgeDays = options.maxAgeDays ?? 60;
57
+ const keywords = options.keywords ?? [];
58
+ const perSource = Math.floor(maxLength / 4);
59
+ const [remotiveRes, remoteOkRes, museRes, hnRes] = await Promise.allSettled([
60
+ fetchRemotive(query, location, maxAgeDays, keywords, perSource),
61
+ fetchRemoteOK(query, location, maxAgeDays, keywords, perSource),
62
+ remoteOnly ? Promise.reject("skipped (remote_only mode)") : fetchMuse(query, location, maxAgeDays, keywords, perSource),
63
+ fetchHNHiring(query, location, maxAgeDays, keywords, perSource),
64
+ ]);
65
+ const pool = [];
66
+ const harvest = (res, label) => {
67
+ if (res.status === "fulfilled")
68
+ pool.push(...res.value.listings);
69
+ // silently skip rejected sources β€” don't clutter output
70
+ };
71
+ harvest(remotiveRes, "Remotive");
72
+ harvest(remoteOkRes, "RemoteOK");
73
+ harvest(museRes, "The Muse");
74
+ harvest(hnRes, "HN");
75
+ if (!pool.length) {
76
+ return {
77
+ raw: `No job listings found for "${query}"${location ? ` in ${location}` : ""}.\n\nTips:\nβ€’ Try broader terms e.g. "engineer" instead of "senior TypeScript engineer"\nβ€’ Set location to "remote" for worldwide results\nβ€’ Increase max_age_days`,
78
+ content_date: null,
79
+ freshness_confidence: "low",
80
+ };
81
+ }
82
+ // Sort: freshest first, expired listings last
83
+ pool.sort((a, b) => a.days - b.days);
84
+ const freshCount = pool.filter(l => l.days <= 7).length;
85
+ const goodCount = pool.filter(l => l.days > 7 && l.days <= 30).length;
86
+ const staleCount = pool.filter(l => l.days > 30).length;
87
+ const header = [
88
+ `# Job Search: "${query}"${location ? ` Β· ${location}` : ""}${remoteOnly ? " Β· remote only" : ""}`,
89
+ `Retrieved: ${new Date().toISOString()}`,
90
+ `Found: ${pool.length} listings β€” 🟒 ${freshCount} fresh 🟑 ${goodCount} recent πŸ”΄ ${staleCount} older`,
91
+ `⚠️ Listings sorted freshest first. Check the date badge before you apply.`,
92
+ keywords.length ? `πŸ” Watching for: ${keywords.map(k => `⚑${k}`).join(", ")}` : null,
93
+ "",
94
+ ].filter(Boolean).join("\n");
95
+ const body = pool
96
+ .map(l => l.text)
97
+ .join("\n\n─────────────────────────────\n\n");
98
+ const raw = (header + "\n\n" + body).slice(0, maxLength);
99
+ const freshestDays = pool[0]?.days ?? 9999;
100
+ const newestDate = freshestDays < 9999
101
+ ? new Date(Date.now() - freshestDays * 86400000).toISOString().slice(0, 10)
102
+ : null;
103
+ return {
104
+ raw,
105
+ content_date: newestDate,
106
+ freshness_confidence: freshestDays <= 7 ? "high" : freshestDays <= 30 ? "medium" : "low",
107
+ };
108
+ }
109
+ // ─── Source: Remotive ─────────────────────────────────────────────────────────
110
+ async function fetchRemotive(query, location, maxAgeDays, keywords, maxLength) {
111
+ const url = `https://remotive.com/api/remote-jobs?search=${encodeURIComponent(query)}&limit=15`;
112
+ const res = await fetch(url, {
113
+ headers: { "User-Agent": "freshcontext-mcp/0.2.0", "Accept": "application/json" },
114
+ });
115
+ if (!res.ok)
116
+ throw new Error(`Remotive ${res.status}`);
117
+ const data = await res.json();
118
+ const listings = (data.jobs ?? [])
119
+ .filter(j => matchesLocation(j.candidate_required_location ?? "", location))
120
+ .map(j => {
121
+ const { badge, days } = freshnessBadge(j.publication_date);
122
+ if (days > maxAgeDays)
123
+ return null;
124
+ const text = highlightKeywords([
125
+ `[Remotive] ${j.title} β€” ${j.company_name}`,
126
+ `${badge}`,
127
+ `Location: ${j.candidate_required_location || "Remote"} | Type: ${j.job_type || "N/A"}`,
128
+ j.salary ? `Salary: ${j.salary}` : null,
129
+ j.tags?.length ? `Tags: ${j.tags.slice(0, 6).join(", ")}` : null,
130
+ `Apply: ${j.url}`,
131
+ ].filter(Boolean).join("\n"), keywords);
132
+ return { text, days, source: "remotive" };
133
+ })
134
+ .filter((l) => l !== null)
135
+ .slice(0, 8);
136
+ return { listings };
137
+ }
138
+ // ─── Source: RemoteOK ─────────────────────────────────────────────────────────
139
+ async function fetchRemoteOK(query, location, maxAgeDays, keywords, maxLength) {
140
+ const tag = query.toLowerCase().replace(/\s+/g, "-");
141
+ const url = `https://remoteok.com/api?tag=${encodeURIComponent(tag)}`;
142
+ const res = await fetch(url, {
143
+ headers: { "User-Agent": "freshcontext-mcp/0.2.0", "Accept": "application/json" },
144
+ });
145
+ if (!res.ok)
146
+ throw new Error(`RemoteOK ${res.status}`);
147
+ const raw = await res.json();
148
+ // First element is a legal notice object, skip it
149
+ const jobs = raw.filter(j => j.id && j.position);
150
+ const listings = jobs
151
+ .filter(j => matchesLocation(j.location ?? "Remote", location))
152
+ .map(j => {
153
+ const dateStr = j.date ?? (j.epoch ? new Date(j.epoch * 1000).toISOString() : null);
154
+ const { badge, days } = freshnessDate(dateStr);
155
+ if (days > maxAgeDays)
156
+ return null;
157
+ const salary = j.salary_min && j.salary_max
158
+ ? `$${(j.salary_min / 1000).toFixed(0)}k–$${(j.salary_max / 1000).toFixed(0)}k`
159
+ : null;
160
+ const text = highlightKeywords([
161
+ `[RemoteOK] ${j.position} β€” ${j.company ?? "Unknown"}`,
162
+ `${badge}`,
163
+ `Location: ${j.location || "Remote Worldwide"}`,
164
+ salary ? `Salary: ${salary}` : null,
165
+ j.tags?.length ? `Tags: ${j.tags.slice(0, 6).join(", ")}` : null,
166
+ j.url ? `Apply: ${j.url}` : null,
167
+ ].filter(Boolean).join("\n"), keywords);
168
+ return { text, days, source: "remoteok" };
169
+ })
170
+ .filter((l) => l !== null)
171
+ .slice(0, 8);
172
+ return { listings };
173
+ }
174
+ // ─── Source: The Muse ─────────────────────────────────────────────────────────
175
+ async function fetchMuse(query, location, maxAgeDays, keywords, maxLength) {
176
+ const locParam = location && location.toLowerCase() !== "remote"
177
+ ? `&location=${encodeURIComponent(location)}`
178
+ : "&location=Flexible%20%2F%20Remote";
179
+ const url = `https://www.themuse.com/api/public/jobs?category=${encodeURIComponent(query)}${locParam}&page=0&descending=true`;
180
+ const res = await fetch(url, {
181
+ headers: { "User-Agent": "freshcontext-mcp/0.2.0", "Accept": "application/json" },
182
+ });
183
+ if (!res.ok)
184
+ throw new Error(`The Muse ${res.status}`);
185
+ const data = await res.json();
186
+ const listings = (data.results ?? [])
187
+ .map(j => {
188
+ const locationStr = j.locations?.map(l => l.name).join(", ") || "Flexible/Remote";
189
+ const { badge, days } = freshnessDate(j.publication_date);
190
+ if (days > maxAgeDays)
191
+ return null;
192
+ const level = j.levels?.map(l => l.name).join(", ") || null;
193
+ const text = highlightKeywords([
194
+ `[The Muse] ${j.name} β€” ${j.company?.name ?? "Unknown"}`,
195
+ `${badge}`,
196
+ `Location: ${locationStr}`,
197
+ level ? `Level: ${level}` : null,
198
+ `Apply: ${j.refs?.landing_page ?? "N/A"}`,
199
+ ].filter(Boolean).join("\n"), keywords);
200
+ return { text, days, source: "themuse" };
201
+ })
202
+ .filter((l) => l !== null)
203
+ .slice(0, 8);
204
+ return { listings };
205
+ }
206
+ // ─── Source: HN Who is Hiring ─────────────────────────────────────────────────
207
+ async function fetchHNHiring(query, location, maxAgeDays, keywords, maxLength) {
208
+ const searchQ = [query, location, "hiring"].filter(Boolean).join(" ");
209
+ const url = `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(searchQ)}&tags=comment&hitsPerPage=10`;
210
+ const res = await fetch(url, { headers: { "User-Agent": "freshcontext-mcp/0.2.0" } });
211
+ if (!res.ok)
212
+ throw new Error(`HN ${res.status}`);
213
+ const data = await res.json();
214
+ const listings = (data.hits ?? [])
215
+ .filter(h => {
216
+ const t = (h.comment_text ?? "").toLowerCase();
217
+ return t.includes("hiring") || t.includes("remote") || t.includes("full-time") || t.includes("apply");
218
+ })
219
+ .map(h => {
220
+ const { badge, days } = freshnessDate(h.created_at);
221
+ if (days > maxAgeDays)
222
+ return null;
223
+ const excerpt = (h.comment_text ?? "")
224
+ .replace(/<[^>]+>/g, " ")
225
+ .replace(/\s+/g, " ")
226
+ .trim()
227
+ .slice(0, 350);
228
+ const text = highlightKeywords([
229
+ `[HN Hiring] Posted by ${h.author}`,
230
+ `${badge}`,
231
+ excerpt + (excerpt.length >= 350 ? "…" : ""),
232
+ `Source: https://news.ycombinator.com/item?id=${h.objectID}`,
233
+ ].join("\n"), keywords);
234
+ return { text, days, source: "hn" };
235
+ })
236
+ .filter((l) => l !== null)
237
+ .slice(0, 6);
238
+ return { listings };
239
+ }
240
+ // ─── Shared date helper ───────────────────────────────────────────────────────
241
+ function freshnessDate(dateStr) {
242
+ return freshnessBadge(dateStr ?? null);
243
+ }
package/dist/server.js CHANGED
@@ -8,6 +8,7 @@ import { hackerNewsAdapter } from "./adapters/hackernews.js";
8
8
  import { ycAdapter } from "./adapters/yc.js";
9
9
  import { repoSearchAdapter } from "./adapters/repoSearch.js";
10
10
  import { packageTrendsAdapter } from "./adapters/packageTrends.js";
11
+ import { jobsAdapter } from "./adapters/jobs.js";
11
12
  import { stampFreshness, formatForLLM } from "./tools/freshnessStamp.js";
12
13
  import { formatSecurityError } from "./security.js";
13
14
  const server = new McpServer({
@@ -152,6 +153,35 @@ server.registerTool("extract_landscape", {
152
153
  ].join("\n\n");
153
154
  return { content: [{ type: "text", text: combined }] };
154
155
  });
156
+ // ─── Tool: search_jobs ───────────────────────────────────────────────────────
157
+ server.registerTool("search_jobs", {
158
+ description: "Search for real-time job listings with freshness badges on every result β€” so you never apply to a role that closed months ago. Sources: Remotive + RemoteOK + The Muse + HN 'Who is Hiring'. Supports location filtering, remote-only mode, keyword spotting (e.g. FIFO), and max age filtering. Returns timestamped freshcontext sorted freshest first.",
159
+ inputSchema: z.object({
160
+ query: z.string().describe("Job search query e.g. 'typescript', 'mining engineer', 'FIFO operator', 'data analyst'"),
161
+ location: z.string().optional().describe("Country, city, or 'remote' / 'worldwide' e.g. 'South Africa', 'Australia', 'remote'"),
162
+ remote_only: z.boolean().optional().default(false).describe("Only return remote-friendly listings"),
163
+ max_age_days: z.number().optional().default(60).describe("Hide listings older than N days (default 60, use 7 for very fresh only)"),
164
+ keywords: z.array(z.string()).optional().default([]).describe("Keywords to highlight in results e.g. ['FIFO', 'underground', 'contract']"),
165
+ max_length: z.number().optional().default(8000),
166
+ }),
167
+ annotations: { readOnlyHint: true, openWorldHint: true },
168
+ }, async ({ query, location, remote_only, max_age_days, keywords, max_length }) => {
169
+ try {
170
+ const result = await jobsAdapter({
171
+ url: query,
172
+ maxLength: max_length,
173
+ location: location ?? "",
174
+ remoteOnly: remote_only,
175
+ maxAgeDays: max_age_days,
176
+ keywords: keywords ?? [],
177
+ });
178
+ const ctx = stampFreshness(result, { url: query, maxLength: max_length }, "jobs");
179
+ return { content: [{ type: "text", text: formatForLLM(ctx) }] };
180
+ }
181
+ catch (err) {
182
+ return { content: [{ type: "text", text: formatSecurityError(err) }] };
183
+ }
184
+ });
155
185
  // ─── Start ───────────────────────────────────────────────────────────────────
156
186
  async function main() {
157
187
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,54 +1,54 @@
1
- {
2
- "name": "freshcontext-mcp",
3
- "mcpName": "io.github.PrinceGabriel-lgtm/freshcontext",
4
- "version": "0.1.8",
5
- "description": "Real-time web extraction MCP server with freshness timestamps for AI agents",
6
- "keywords": [
7
- "mcp",
8
- "mcp-server",
9
- "ai-agents",
10
- "llm",
11
- "freshness",
12
- "web-scraping",
13
- "github-analytics",
14
- "hackernews",
15
- "yc",
16
- "typescript",
17
- "context",
18
- "model-context-protocol"
19
- ],
20
- "homepage": "https://github.com/PrinceGabriel-lgtm/freshcontext-mcp",
21
- "repository": {
22
- "type": "git",
23
- "url": "https://github.com/PrinceGabriel-lgtm/freshcontext-mcp.git"
24
- },
25
- "license": "MIT",
26
- "type": "module",
27
- "main": "dist/server.js",
28
- "bin": {
29
- "freshcontext-mcp": "dist/server.js"
30
- },
31
- "scripts": {
32
- "build": "tsc",
33
- "dev": "tsx watch src/server.ts",
34
- "start": "node dist/server.js",
35
- "inspect": "npx @modelcontextprotocol/inspector tsx src/server.ts",
36
- "test": "jest"
37
- },
38
- "dependencies": {
39
- "@modelcontextprotocol/sdk": "^1.0.0",
40
- "playwright": "^1.44.0",
41
- "zod": "^3.23.0",
42
- "dotenv": "^16.4.0"
43
- },
44
- "devDependencies": {
45
- "@types/node": "^20.0.0",
46
- "tsx": "^4.0.0",
47
- "typescript": "^5.4.0",
48
- "jest": "^29.0.0",
49
- "@types/jest": "^29.0.0"
50
- }
51
- }
1
+ {
2
+ "name": "freshcontext-mcp",
3
+ "mcpName": "io.github.PrinceGabriel-lgtm/freshcontext",
4
+ "version": "0.2.0",
5
+ "description": "Real-time web extraction MCP server with freshness timestamps for AI agents",
6
+ "keywords": [
7
+ "mcp",
8
+ "mcp-server",
9
+ "ai-agents",
10
+ "llm",
11
+ "freshness",
12
+ "web-scraping",
13
+ "github-analytics",
14
+ "hackernews",
15
+ "yc",
16
+ "typescript",
17
+ "context",
18
+ "model-context-protocol"
19
+ ],
20
+ "homepage": "https://github.com/PrinceGabriel-lgtm/freshcontext-mcp",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/PrinceGabriel-lgtm/freshcontext-mcp.git"
24
+ },
25
+ "license": "MIT",
26
+ "type": "module",
27
+ "main": "dist/server.js",
28
+ "bin": {
29
+ "freshcontext-mcp": "dist/server.js"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "dev": "tsx watch src/server.ts",
34
+ "start": "node dist/server.js",
35
+ "inspect": "npx @modelcontextprotocol/inspector tsx src/server.ts",
36
+ "test": "jest"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.0.0",
40
+ "playwright": "^1.44.0",
41
+ "zod": "^3.23.0",
42
+ "dotenv": "^16.4.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20.0.0",
46
+ "tsx": "^4.0.0",
47
+ "typescript": "^5.4.0",
48
+ "jest": "^29.0.0",
49
+ "@types/jest": "^29.0.0"
50
+ }
51
+ }
52
52
 
53
53
 
54
54