freshcontext-mcp 0.1.9 β 0.3.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 +212 -257
- package/dist/adapters/jobs.js +276 -106
- package/dist/server.js +16 -5
- package/package.json +51 -55
package/README.md
CHANGED
|
@@ -1,257 +1,212 @@
|
|
|
1
|
-
# freshcontext-mcp
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
|
40
|
-
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
|
49
|
-
|
|
50
|
-
| `
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
**
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
-
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/freshcontext-mcp)
|
|
10
|
+
[](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
|
package/dist/adapters/jobs.js
CHANGED
|
@@ -1,139 +1,309 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Jobs adapter β
|
|
2
|
+
* Jobs adapter v3 β 5 sources, freshness badges, location + keyword filtering.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Sources (all no-auth):
|
|
5
|
+
* - Remotive β remote tech jobs, salary data
|
|
6
|
+
* - RemoteOK β pure remote, unix timestamps
|
|
7
|
+
* - Arbeitnow β broad jobs API (tech + non-tech, location-aware)
|
|
8
|
+
* - The Muse β structured listings, level info
|
|
9
|
+
* - HN Who is Hiring β monthly thread, community-sourced
|
|
6
10
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* Freshness badges on every listing:
|
|
12
|
+
* π’ < 7 days β FRESH, apply now
|
|
13
|
+
* π‘ 7β30 days β still good
|
|
14
|
+
* π΄ 31β90 days β apply with caution
|
|
15
|
+
* β > 90 days β likely expired, shown last
|
|
12
16
|
*/
|
|
17
|
+
// βββ Freshness Scoring ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
18
|
+
function freshnessBadge(dateStr) {
|
|
19
|
+
if (!dateStr)
|
|
20
|
+
return { badge: "β Unknown date", days: 9999 };
|
|
21
|
+
const days = Math.floor((Date.now() - new Date(dateStr).getTime()) / 86400000);
|
|
22
|
+
if (days < 0)
|
|
23
|
+
return { badge: "π’ Just posted β FRESH", days: 0 };
|
|
24
|
+
if (days <= 7)
|
|
25
|
+
return { badge: `π’ ${days}d ago β FRESH`, days };
|
|
26
|
+
if (days <= 30)
|
|
27
|
+
return { badge: `π‘ ${days}d ago`, days };
|
|
28
|
+
if (days <= 90)
|
|
29
|
+
return { badge: `π΄ ${days}d ago β apply with caution`, days };
|
|
30
|
+
return { badge: `β ${days}d ago β likely expired`, days };
|
|
31
|
+
}
|
|
32
|
+
function matchesLocation(locationField, filterLocation) {
|
|
33
|
+
if (!filterLocation || filterLocation.toLowerCase() === "worldwide" || filterLocation.toLowerCase() === "remote")
|
|
34
|
+
return true;
|
|
35
|
+
const loc = locationField.toLowerCase();
|
|
36
|
+
const filter = filterLocation.toLowerCase();
|
|
37
|
+
return (loc.includes(filter) ||
|
|
38
|
+
filter.includes(loc) ||
|
|
39
|
+
loc.includes("worldwide") ||
|
|
40
|
+
loc.includes("anywhere") ||
|
|
41
|
+
loc.includes("remote"));
|
|
42
|
+
}
|
|
43
|
+
function highlightKeywords(text, keywords) {
|
|
44
|
+
if (!keywords.length)
|
|
45
|
+
return text;
|
|
46
|
+
let result = text;
|
|
47
|
+
for (const kw of keywords) {
|
|
48
|
+
const re = new RegExp(`(${kw})`, "gi");
|
|
49
|
+
result = result.replace(re, "β‘$1");
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
// βββ Main Adapter βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
13
54
|
export async function jobsAdapter(options) {
|
|
14
55
|
const query = options.url.trim();
|
|
15
|
-
const maxLength = options.maxLength ??
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
56
|
+
const maxLength = options.maxLength ?? 8000;
|
|
57
|
+
const location = options.location ?? "";
|
|
58
|
+
const remoteOnly = options.remoteOnly ?? false;
|
|
59
|
+
const maxAgeDays = options.maxAgeDays ?? 60;
|
|
60
|
+
const keywords = options.keywords ?? [];
|
|
61
|
+
const [remotiveRes, remoteOkRes, arbeitnowRes, museRes, hnRes] = await Promise.allSettled([
|
|
62
|
+
fetchRemotive(query, location, maxAgeDays, keywords),
|
|
63
|
+
fetchRemoteOK(query, location, maxAgeDays, keywords),
|
|
64
|
+
fetchArbeitnow(query, location, maxAgeDays, keywords, remoteOnly),
|
|
65
|
+
remoteOnly ? Promise.reject("skipped") : fetchMuse(query, location, maxAgeDays, keywords),
|
|
66
|
+
fetchHNHiring(query, location, maxAgeDays, keywords),
|
|
20
67
|
]);
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
|
|
68
|
+
const pool = [];
|
|
69
|
+
const sourceStats = {};
|
|
70
|
+
const harvest = (res, label) => {
|
|
71
|
+
if (res.status === "fulfilled") {
|
|
72
|
+
pool.push(...res.value.listings);
|
|
73
|
+
sourceStats[label] = res.value.listings.length;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
sourceStats[label] = 0;
|
|
77
|
+
}
|
|
26
78
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
sections.push(`## π Remote Jobs (Remotive)\n[Unavailable: ${remotiveResult.reason}]`);
|
|
34
|
-
}
|
|
35
|
-
// ββ HN Who is Hiring ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
36
|
-
if (hnResult.status === "fulfilled" && hnResult.value.raw) {
|
|
37
|
-
sections.push(`## π¬ HN "Who is Hiring" (Community)\n${hnResult.value.raw}`);
|
|
38
|
-
trackDate(hnResult.value.content_date);
|
|
39
|
-
}
|
|
40
|
-
else if (hnResult.status === "rejected") {
|
|
41
|
-
sections.push(`## π¬ HN "Who is Hiring"\n[Unavailable: ${hnResult.reason}]`);
|
|
42
|
-
}
|
|
43
|
-
if (!sections.length) {
|
|
79
|
+
harvest(remotiveRes, "Remotive");
|
|
80
|
+
harvest(remoteOkRes, "RemoteOK");
|
|
81
|
+
harvest(arbeitnowRes, "Arbeitnow");
|
|
82
|
+
harvest(museRes, "The Muse");
|
|
83
|
+
harvest(hnRes, "HN Hiring");
|
|
84
|
+
if (!pool.length) {
|
|
44
85
|
return {
|
|
45
|
-
raw:
|
|
86
|
+
raw: [
|
|
87
|
+
`No job listings found for "${query}"${location ? ` in ${location}` : ""}.`,
|
|
88
|
+
"",
|
|
89
|
+
"Tips:",
|
|
90
|
+
"β’ Try broader terms e.g. \"engineer\" instead of \"senior TypeScript engineer\"",
|
|
91
|
+
"β’ Set location to \"remote\" for worldwide results",
|
|
92
|
+
"β’ Increase max_age_days (default: 60)",
|
|
93
|
+
"β’ Note: FIFO/mining/trades jobs are on specialist boards (myJobsNamibia, SEEK, mining-specific sites) β these sources are tech/remote focused",
|
|
94
|
+
].join("\n"),
|
|
46
95
|
content_date: null,
|
|
47
96
|
freshness_confidence: "low",
|
|
48
97
|
};
|
|
49
98
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
99
|
+
// Sort: freshest first
|
|
100
|
+
pool.sort((a, b) => a.days - b.days);
|
|
101
|
+
const freshCount = pool.filter(l => l.days <= 7).length;
|
|
102
|
+
const goodCount = pool.filter(l => l.days > 7 && l.days <= 30).length;
|
|
103
|
+
const staleCount = pool.filter(l => l.days > 30).length;
|
|
104
|
+
const sourceSummary = Object.entries(sourceStats)
|
|
105
|
+
.map(([src, n]) => `${src}:${n}`)
|
|
106
|
+
.join(" ");
|
|
107
|
+
const header = [
|
|
108
|
+
`# Job Search: "${query}"${location ? ` Β· ${location}` : ""}${remoteOnly ? " Β· remote only" : ""}`,
|
|
109
|
+
`Retrieved: ${new Date().toISOString()}`,
|
|
110
|
+
`Found: ${pool.length} listings β π’ ${freshCount} fresh π‘ ${goodCount} recent π΄ ${staleCount} older`,
|
|
111
|
+
`Sources: ${sourceSummary}`,
|
|
112
|
+
`β οΈ Sorted freshest first. Check badge before applying.`,
|
|
113
|
+
keywords.length ? `π Watching for: ${keywords.map(k => `β‘${k}`).join(", ")}` : null,
|
|
53
114
|
"",
|
|
54
|
-
|
|
55
|
-
|
|
115
|
+
].filter(Boolean).join("\n");
|
|
116
|
+
const body = pool
|
|
117
|
+
.map(l => l.text)
|
|
118
|
+
.join("\n\nβββββββββββββββββββββββββββββ\n\n");
|
|
119
|
+
const raw = (header + "\n\n" + body).slice(0, maxLength);
|
|
120
|
+
const freshestDays = pool[0]?.days ?? 9999;
|
|
121
|
+
const newestDate = freshestDays < 9999
|
|
122
|
+
? new Date(Date.now() - freshestDays * 86400000).toISOString().slice(0, 10)
|
|
123
|
+
: null;
|
|
56
124
|
return {
|
|
57
|
-
raw
|
|
125
|
+
raw,
|
|
58
126
|
content_date: newestDate,
|
|
59
|
-
freshness_confidence:
|
|
127
|
+
freshness_confidence: freshestDays <= 7 ? "high" : freshestDays <= 30 ? "medium" : "low",
|
|
60
128
|
};
|
|
61
129
|
}
|
|
62
|
-
// βββ Remotive
|
|
63
|
-
async function fetchRemotive(query,
|
|
64
|
-
const url = `https://remotive.com/api/remote-jobs?search=${encodeURIComponent(query)}&limit=
|
|
130
|
+
// βββ Source: Remotive βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
131
|
+
async function fetchRemotive(query, location, maxAgeDays, keywords) {
|
|
132
|
+
const url = `https://remotive.com/api/remote-jobs?search=${encodeURIComponent(query)}&limit=15`;
|
|
65
133
|
const res = await fetch(url, {
|
|
66
|
-
headers: {
|
|
67
|
-
"User-Agent": "freshcontext-mcp/0.1.9 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
68
|
-
"Accept": "application/json",
|
|
69
|
-
},
|
|
134
|
+
headers: { "User-Agent": "freshcontext-mcp/0.3.0", "Accept": "application/json" },
|
|
70
135
|
});
|
|
71
136
|
if (!res.ok)
|
|
72
|
-
throw new Error(`Remotive
|
|
137
|
+
throw new Error(`Remotive ${res.status}`);
|
|
73
138
|
const data = await res.json();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
`
|
|
85
|
-
|
|
86
|
-
|
|
139
|
+
const listings = (data.jobs ?? [])
|
|
140
|
+
.filter(j => matchesLocation(j.candidate_required_location ?? "", location))
|
|
141
|
+
.map(j => {
|
|
142
|
+
const { badge, days } = freshnessBadge(j.publication_date);
|
|
143
|
+
if (days > maxAgeDays)
|
|
144
|
+
return null;
|
|
145
|
+
const text = highlightKeywords([
|
|
146
|
+
`[Remotive] ${j.title} β ${j.company_name}`,
|
|
147
|
+
badge,
|
|
148
|
+
`Location: ${j.candidate_required_location || "Remote"} | Type: ${j.job_type || "N/A"}`,
|
|
149
|
+
j.salary ? `Salary: ${j.salary}` : null,
|
|
150
|
+
j.tags?.length ? `Tags: ${j.tags.slice(0, 6).join(", ")}` : null,
|
|
151
|
+
`Apply: ${j.url}`,
|
|
152
|
+
].filter(Boolean).join("\n"), keywords);
|
|
153
|
+
return { text, days, source: "remotive" };
|
|
154
|
+
})
|
|
155
|
+
.filter((l) => l !== null)
|
|
156
|
+
.slice(0, 8);
|
|
157
|
+
return { listings };
|
|
158
|
+
}
|
|
159
|
+
// βββ Source: RemoteOK βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
160
|
+
async function fetchRemoteOK(query, location, maxAgeDays, keywords) {
|
|
161
|
+
const tag = query.toLowerCase().replace(/\s+/g, "-");
|
|
162
|
+
const url = `https://remoteok.com/api?tag=${encodeURIComponent(tag)}`;
|
|
163
|
+
const res = await fetch(url, {
|
|
164
|
+
headers: { "User-Agent": "freshcontext-mcp/0.3.0", "Accept": "application/json" },
|
|
87
165
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
166
|
+
if (!res.ok)
|
|
167
|
+
throw new Error(`RemoteOK ${res.status}`);
|
|
168
|
+
const raw = await res.json();
|
|
169
|
+
const jobs = raw.filter(j => j.id && j.position);
|
|
170
|
+
const listings = jobs
|
|
171
|
+
.filter(j => matchesLocation(j.location ?? "Remote", location))
|
|
172
|
+
.map(j => {
|
|
173
|
+
const dateStr = j.date ?? (j.epoch ? new Date(j.epoch * 1000).toISOString() : null);
|
|
174
|
+
const { badge, days } = freshnessBadge(dateStr);
|
|
175
|
+
if (days > maxAgeDays)
|
|
176
|
+
return null;
|
|
177
|
+
const salary = j.salary_min && j.salary_max
|
|
178
|
+
? `$${(j.salary_min / 1000).toFixed(0)}kβ$${(j.salary_max / 1000).toFixed(0)}k`
|
|
179
|
+
: null;
|
|
180
|
+
const text = highlightKeywords([
|
|
181
|
+
`[RemoteOK] ${j.position} β ${j.company ?? "Unknown"}`,
|
|
182
|
+
badge,
|
|
183
|
+
`Location: ${j.location || "Remote Worldwide"}`,
|
|
184
|
+
salary ? `Salary: ${salary}` : null,
|
|
185
|
+
j.tags?.length ? `Tags: ${j.tags.slice(0, 6).join(", ")}` : null,
|
|
186
|
+
j.url ? `Apply: ${j.url}` : null,
|
|
187
|
+
].filter(Boolean).join("\n"), keywords);
|
|
188
|
+
return { text, days, source: "remoteok" };
|
|
189
|
+
})
|
|
190
|
+
.filter((l) => l !== null)
|
|
191
|
+
.slice(0, 8);
|
|
192
|
+
return { listings };
|
|
99
193
|
}
|
|
100
|
-
// βββ
|
|
101
|
-
|
|
102
|
-
|
|
194
|
+
// βββ Source: Arbeitnow (NEW) ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
195
|
+
// Free public API β broader than tech boards. Good for non-remote, non-tech roles.
|
|
196
|
+
async function fetchArbeitnow(query, location, maxAgeDays, keywords, remoteOnly) {
|
|
197
|
+
const params = new URLSearchParams({ search: query });
|
|
198
|
+
if (remoteOnly)
|
|
199
|
+
params.set("remote", "true");
|
|
200
|
+
const url = `https://arbeitnow.com/api/job-board-api?${params.toString()}`;
|
|
103
201
|
const res = await fetch(url, {
|
|
104
|
-
headers: { "User-Agent": "freshcontext-mcp/0.
|
|
202
|
+
headers: { "User-Agent": "freshcontext-mcp/0.3.0", "Accept": "application/json" },
|
|
105
203
|
});
|
|
106
204
|
if (!res.ok)
|
|
107
|
-
throw new Error(`
|
|
205
|
+
throw new Error(`Arbeitnow ${res.status}`);
|
|
108
206
|
const data = await res.json();
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
207
|
+
const listings = (data.data ?? [])
|
|
208
|
+
.filter(j => matchesLocation(j.location ?? "", location))
|
|
209
|
+
.map(j => {
|
|
210
|
+
const dateStr = new Date(j.created_at * 1000).toISOString();
|
|
211
|
+
const { badge, days } = freshnessBadge(dateStr);
|
|
212
|
+
if (days > maxAgeDays)
|
|
213
|
+
return null;
|
|
214
|
+
const text = highlightKeywords([
|
|
215
|
+
`[Arbeitnow] ${j.title} β ${j.company_name}`,
|
|
216
|
+
badge,
|
|
217
|
+
`Location: ${j.location || "Remote"}${j.remote ? " (Remote OK)" : ""}`,
|
|
218
|
+
j.job_types?.length ? `Type: ${j.job_types.join(", ")}` : null,
|
|
219
|
+
j.tags?.length ? `Tags: ${j.tags.slice(0, 6).join(", ")}` : null,
|
|
220
|
+
`Apply: ${j.url}`,
|
|
221
|
+
].filter(Boolean).join("\n"), keywords);
|
|
222
|
+
return { text, days, source: "arbeitnow" };
|
|
223
|
+
})
|
|
224
|
+
.filter((l) => l !== null)
|
|
225
|
+
.slice(0, 8);
|
|
226
|
+
return { listings };
|
|
227
|
+
}
|
|
228
|
+
// βββ Source: The Muse βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
229
|
+
// Fixed: now uses `name` param for text search instead of `category`
|
|
230
|
+
async function fetchMuse(query, location, maxAgeDays, keywords) {
|
|
231
|
+
const locParam = location && location.toLowerCase() !== "remote"
|
|
232
|
+
? `&location=${encodeURIComponent(location)}`
|
|
233
|
+
: "&location=Flexible%20%2F%20Remote";
|
|
234
|
+
const url = `https://www.themuse.com/api/public/jobs?name=${encodeURIComponent(query)}${locParam}&page=0&descending=true`;
|
|
235
|
+
const res = await fetch(url, {
|
|
236
|
+
headers: { "User-Agent": "freshcontext-mcp/0.3.0", "Accept": "application/json" },
|
|
116
237
|
});
|
|
117
|
-
if (!
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const listings =
|
|
121
|
-
|
|
238
|
+
if (!res.ok)
|
|
239
|
+
throw new Error(`The Muse ${res.status}`);
|
|
240
|
+
const data = await res.json();
|
|
241
|
+
const listings = (data.results ?? [])
|
|
242
|
+
.map(j => {
|
|
243
|
+
const locationStr = j.locations?.map(l => l.name).join(", ") || "Flexible/Remote";
|
|
244
|
+
const { badge, days } = freshnessBadge(j.publication_date);
|
|
245
|
+
if (days > maxAgeDays)
|
|
246
|
+
return null;
|
|
247
|
+
const level = j.levels?.map(l => l.name).join(", ") || null;
|
|
248
|
+
const text = highlightKeywords([
|
|
249
|
+
`[The Muse] ${j.name} β ${j.company?.name ?? "Unknown"}`,
|
|
250
|
+
badge,
|
|
251
|
+
`Location: ${locationStr}`,
|
|
252
|
+
level ? `Level: ${level}` : null,
|
|
253
|
+
`Apply: ${j.refs?.landing_page ?? "N/A"}`,
|
|
254
|
+
].filter(Boolean).join("\n"), keywords);
|
|
255
|
+
return { text, days, source: "themuse" };
|
|
256
|
+
})
|
|
257
|
+
.filter((l) => l !== null)
|
|
258
|
+
.slice(0, 8);
|
|
259
|
+
return { listings };
|
|
260
|
+
}
|
|
261
|
+
// βββ Source: HN Who is Hiring βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
262
|
+
// Fixed: now searches within the actual monthly "Who is Hiring" thread
|
|
263
|
+
// instead of all HN comments. Uses the parent_id filter to target the thread.
|
|
264
|
+
async function fetchHNHiring(query, location, maxAgeDays, keywords) {
|
|
265
|
+
// Step 1: Find the most recent "Ask HN: Who is hiring?" thread
|
|
266
|
+
const threadRes = await fetch(`https://hn.algolia.com/api/v1/search?query=Ask+HN+Who+is+hiring&tags=story&hitsPerPage=5`, { headers: { "User-Agent": "freshcontext-mcp/0.3.0" } });
|
|
267
|
+
if (!threadRes.ok)
|
|
268
|
+
throw new Error(`HN thread search ${threadRes.status}`);
|
|
269
|
+
const threadData = await threadRes.json();
|
|
270
|
+
// Pick most recent hiring thread (they post monthly)
|
|
271
|
+
const hiringThread = threadData.hits.find(h => h.title?.toLowerCase().includes("who is hiring"));
|
|
272
|
+
if (!hiringThread)
|
|
273
|
+
throw new Error("HN hiring thread not found");
|
|
274
|
+
// Step 2: Search comments within that thread for the query
|
|
275
|
+
const searchTerms = [query, location].filter(Boolean).join(" ");
|
|
276
|
+
const commentsRes = await fetch(`https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(searchTerms)}&tags=comment,story_${hiringThread.objectID}&hitsPerPage=10`, { headers: { "User-Agent": "freshcontext-mcp/0.3.0" } });
|
|
277
|
+
if (!commentsRes.ok)
|
|
278
|
+
throw new Error(`HN comments ${commentsRes.status}`);
|
|
279
|
+
const commentsData = await commentsRes.json();
|
|
280
|
+
const listings = (commentsData.hits ?? [])
|
|
281
|
+
.filter(h => {
|
|
282
|
+
const t = (h.comment_text ?? "").toLowerCase();
|
|
283
|
+
// Must look like a job post, not a meta comment
|
|
284
|
+
return t.length > 50 && (t.includes("hiring") || t.includes("remote") ||
|
|
285
|
+
t.includes("full-time") || t.includes("apply") ||
|
|
286
|
+
t.includes("|") // common delimiter in HN job posts
|
|
287
|
+
);
|
|
288
|
+
})
|
|
289
|
+
.map(h => {
|
|
290
|
+
const { badge, days } = freshnessBadge(h.created_at);
|
|
291
|
+
if (days > maxAgeDays)
|
|
292
|
+
return null;
|
|
293
|
+
const excerpt = (h.comment_text ?? "")
|
|
122
294
|
.replace(/<[^>]+>/g, " ")
|
|
123
295
|
.replace(/\s+/g, " ")
|
|
124
296
|
.trim()
|
|
125
|
-
.slice(0,
|
|
126
|
-
|
|
127
|
-
`[
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
freshness_confidence: dates[0] ? "high" : "medium",
|
|
138
|
-
};
|
|
297
|
+
.slice(0, 400);
|
|
298
|
+
const text = highlightKeywords([
|
|
299
|
+
`[HN Hiring Β· ${hiringThread.title}] by ${h.author}`,
|
|
300
|
+
badge,
|
|
301
|
+
excerpt + (excerpt.length >= 400 ? "β¦" : ""),
|
|
302
|
+
`Source: https://news.ycombinator.com/item?id=${h.objectID}`,
|
|
303
|
+
].join("\n"), keywords);
|
|
304
|
+
return { text, days, source: "hn" };
|
|
305
|
+
})
|
|
306
|
+
.filter((l) => l !== null)
|
|
307
|
+
.slice(0, 6);
|
|
308
|
+
return { listings };
|
|
139
309
|
}
|
package/dist/server.js
CHANGED
|
@@ -155,15 +155,26 @@ server.registerTool("extract_landscape", {
|
|
|
155
155
|
});
|
|
156
156
|
// βββ Tool: search_jobs βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
157
157
|
server.registerTool("search_jobs", {
|
|
158
|
-
description: "Search for real-time job listings with
|
|
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
159
|
inputSchema: z.object({
|
|
160
|
-
query: z.string().describe("Job search query e.g. 'typescript
|
|
161
|
-
|
|
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),
|
|
162
166
|
}),
|
|
163
167
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
164
|
-
}, async ({ query, max_length }) => {
|
|
168
|
+
}, async ({ query, location, remote_only, max_age_days, keywords, max_length }) => {
|
|
165
169
|
try {
|
|
166
|
-
const result = await jobsAdapter({
|
|
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
|
+
});
|
|
167
178
|
const ctx = stampFreshness(result, { url: query, maxLength: max_length }, "jobs");
|
|
168
179
|
return { content: [{ type: "text", text: formatForLLM(ctx) }] };
|
|
169
180
|
}
|
package/package.json
CHANGED
|
@@ -1,55 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "freshcontext-mcp",
|
|
3
|
-
"mcpName": "io.github.PrinceGabriel-lgtm/freshcontext",
|
|
4
|
-
"version": "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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "freshcontext-mcp",
|
|
3
|
+
"mcpName": "io.github.PrinceGabriel-lgtm/freshcontext",
|
|
4
|
+
"version": "0.3.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
|
+
}
|