freshcontext-mcp 0.1.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/.env.example +8 -0
- package/README.md +71 -0
- package/dist/adapters/github.js +41 -0
- package/dist/adapters/hackernews.js +65 -0
- package/dist/adapters/packageTrends.js +75 -0
- package/dist/adapters/repoSearch.js +54 -0
- package/dist/adapters/scholar.js +51 -0
- package/dist/adapters/yc.js +81 -0
- package/dist/server.js +129 -0
- package/dist/tools/freshnessStamp.js +25 -0
- package/dist/types.js +2 -0
- package/package.json +27 -0
- package/src/adapters/github.ts +50 -0
- package/src/adapters/hackernews.ts +93 -0
- package/src/adapters/packageTrends.ts +104 -0
- package/src/adapters/repoSearch.ts +78 -0
- package/src/adapters/scholar.ts +65 -0
- package/src/adapters/yc.ts +99 -0
- package/src/server.ts +179 -0
- package/src/tools/freshnessStamp.ts +33 -0
- package/src/types.ts +22 -0
- package/start-server.bat +2 -0
- package/tsconfig.json +17 -0
- package/worker/package-lock.json +3578 -0
- package/worker/package.json +19 -0
- package/worker/src/worker.ts +162 -0
- package/worker/tsconfig.json +12 -0
- package/worker/wrangler.jsonc +10 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "freshcontext-mcp-worker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "wrangler dev",
|
|
7
|
+
"deploy": "wrangler deploy"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@cloudflare/puppeteer": "^1.0.4",
|
|
11
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
12
|
+
"zod": "^3.23.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@cloudflare/workers-types": "^4.0.0",
|
|
16
|
+
"typescript": "^5.4.0",
|
|
17
|
+
"wrangler": "^3.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import puppeteer from "@cloudflare/puppeteer";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
interface Env {
|
|
9
|
+
BROWSER: Fetcher;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface FreshContext {
|
|
13
|
+
content: string;
|
|
14
|
+
source_url: string;
|
|
15
|
+
content_date: string | null;
|
|
16
|
+
retrieved_at: string;
|
|
17
|
+
freshness_confidence: "high" | "medium" | "low";
|
|
18
|
+
adapter: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─── Freshness Stamp ─────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function stamp(content: string, url: string, date: string | null, confidence: "high" | "medium" | "low", adapter: string): string {
|
|
24
|
+
const ctx: FreshContext = {
|
|
25
|
+
content: content.slice(0, 6000),
|
|
26
|
+
source_url: url,
|
|
27
|
+
content_date: date,
|
|
28
|
+
retrieved_at: new Date().toISOString(),
|
|
29
|
+
freshness_confidence: confidence,
|
|
30
|
+
adapter,
|
|
31
|
+
};
|
|
32
|
+
return [
|
|
33
|
+
"[FRESHCONTEXT]",
|
|
34
|
+
`Source: ${ctx.source_url}`,
|
|
35
|
+
`Published: ${ctx.content_date ?? "unknown"}`,
|
|
36
|
+
`Retrieved: ${ctx.retrieved_at}`,
|
|
37
|
+
`Confidence: ${ctx.freshness_confidence}`,
|
|
38
|
+
"---",
|
|
39
|
+
ctx.content,
|
|
40
|
+
"[/FRESHCONTEXT]",
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Server Factory ───────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function createServer(env: Env): McpServer {
|
|
47
|
+
const server = new McpServer({ name: "freshcontext-mcp", version: "0.1.0" });
|
|
48
|
+
|
|
49
|
+
// ── extract_github ──────────────────────────────────────────────────────────
|
|
50
|
+
server.registerTool("extract_github", {
|
|
51
|
+
description: "Extract real-time data from a GitHub repository — README, stars, forks, last commit, topics. Returns timestamped freshcontext.",
|
|
52
|
+
inputSchema: z.object({
|
|
53
|
+
url: z.string().url().describe("Full GitHub repo URL"),
|
|
54
|
+
}),
|
|
55
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
56
|
+
}, async ({ url }) => {
|
|
57
|
+
const browser = await puppeteer.launch(env.BROWSER);
|
|
58
|
+
const page = await browser.newPage();
|
|
59
|
+
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36");
|
|
60
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
61
|
+
|
|
62
|
+
const data = await page.evaluate(`(function() {
|
|
63
|
+
var readme = (document.querySelector('[data-target="readme-toc.content"]') || document.querySelector('.markdown-body') || {}).textContent || null;
|
|
64
|
+
var starsEl = document.querySelector('[id="repo-stars-counter-star"]') || document.querySelector('.Counter.js-social-count');
|
|
65
|
+
var stars = starsEl ? starsEl.textContent.trim() : null;
|
|
66
|
+
var forksEl = document.querySelector('[id="repo-network-counter"]');
|
|
67
|
+
var forks = forksEl ? forksEl.textContent.trim() : null;
|
|
68
|
+
var commitEl = document.querySelector('relative-time');
|
|
69
|
+
var lastCommit = commitEl ? commitEl.getAttribute('datetime') : null;
|
|
70
|
+
var descEl = document.querySelector('.f4.my-3');
|
|
71
|
+
var description = descEl ? descEl.textContent.trim() : null;
|
|
72
|
+
var topics = Array.from(document.querySelectorAll('.topic-tag')).map(function(t) { return t.textContent.trim(); });
|
|
73
|
+
var langEl = document.querySelector('.color-fg-default.text-bold.mr-1');
|
|
74
|
+
var language = langEl ? langEl.textContent.trim() : null;
|
|
75
|
+
return { readme, stars, forks, lastCommit, description, topics, language };
|
|
76
|
+
})()`);
|
|
77
|
+
|
|
78
|
+
await browser.close();
|
|
79
|
+
const d = data as any;
|
|
80
|
+
const raw = [`Description: ${d.description ?? "N/A"}`, `Stars: ${d.stars ?? "N/A"} | Forks: ${d.forks ?? "N/A"}`, `Language: ${d.language ?? "N/A"}`, `Last commit: ${d.lastCommit ?? "N/A"}`, `Topics: ${d.topics?.join(", ") ?? "none"}`, `\n--- README ---\n${d.readme ?? "No README"}`].join("\n");
|
|
81
|
+
return { content: [{ type: "text", text: stamp(raw, url, d.lastCommit ?? null, d.lastCommit ? "high" : "medium", "github") }] };
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ── extract_hackernews ──────────────────────────────────────────────────────
|
|
85
|
+
server.registerTool("extract_hackernews", {
|
|
86
|
+
description: "Extract top stories from Hacker News with real-time timestamps.",
|
|
87
|
+
inputSchema: z.object({ url: z.string().url().describe("HN URL") }),
|
|
88
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
89
|
+
}, async ({ url }) => {
|
|
90
|
+
const browser = await puppeteer.launch(env.BROWSER);
|
|
91
|
+
const page = await browser.newPage();
|
|
92
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
93
|
+
|
|
94
|
+
const data = await page.evaluate(`(function() {
|
|
95
|
+
var items = Array.from(document.querySelectorAll('.athing')).slice(0, 20);
|
|
96
|
+
return items.map(function(el) {
|
|
97
|
+
var titleLineEl = el.querySelector('.titleline > a');
|
|
98
|
+
var title = titleLineEl ? titleLineEl.textContent.trim() : null;
|
|
99
|
+
var link = titleLineEl ? titleLineEl.getAttribute('href') : null;
|
|
100
|
+
var subtext = el.nextElementSibling;
|
|
101
|
+
var scoreEl = subtext ? subtext.querySelector('.score') : null;
|
|
102
|
+
var score = scoreEl ? scoreEl.textContent.trim() : null;
|
|
103
|
+
var ageEl = subtext ? subtext.querySelector('.age') : null;
|
|
104
|
+
var age = ageEl ? ageEl.getAttribute('title') : null;
|
|
105
|
+
return { title, link, score, age };
|
|
106
|
+
});
|
|
107
|
+
})()`);
|
|
108
|
+
|
|
109
|
+
await browser.close();
|
|
110
|
+
const items = data as any[];
|
|
111
|
+
const raw = items.map((r, i) => `[${i + 1}] ${r.title}\nURL: ${r.link}\nScore: ${r.score ?? "N/A"}\nPosted: ${r.age ?? "unknown"}`).join("\n\n");
|
|
112
|
+
const newest = items.map(r => r.age).filter(Boolean).sort().reverse()[0] ?? null;
|
|
113
|
+
return { content: [{ type: "text", text: stamp(raw, url, newest, newest ? "high" : "medium", "hackernews") }] };
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ── extract_scholar ─────────────────────────────────────────────────────────
|
|
117
|
+
server.registerTool("extract_scholar", {
|
|
118
|
+
description: "Extract research results from Google Scholar with publication dates.",
|
|
119
|
+
inputSchema: z.object({ url: z.string().url().describe("Google Scholar URL") }),
|
|
120
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
121
|
+
}, async ({ url }) => {
|
|
122
|
+
const browser = await puppeteer.launch(env.BROWSER);
|
|
123
|
+
const page = await browser.newPage();
|
|
124
|
+
await page.setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36");
|
|
125
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
126
|
+
|
|
127
|
+
const data = await page.evaluate(`(function() {
|
|
128
|
+
var items = Array.from(document.querySelectorAll('.gs_r.gs_or.gs_scl'));
|
|
129
|
+
return items.map(function(el) {
|
|
130
|
+
var titleEl = el.querySelector('.gs_rt');
|
|
131
|
+
var title = titleEl ? titleEl.textContent.trim() : null;
|
|
132
|
+
var authorsEl = el.querySelector('.gs_a');
|
|
133
|
+
var authors = authorsEl ? authorsEl.textContent.trim() : null;
|
|
134
|
+
var snippetEl = el.querySelector('.gs_rs');
|
|
135
|
+
var snippet = snippetEl ? snippetEl.textContent.trim() : null;
|
|
136
|
+
var yearMatch = authors ? authors.match(/\\b(19|20)\\d{2}\\b/) : null;
|
|
137
|
+
var year = yearMatch ? yearMatch[0] : null;
|
|
138
|
+
return { title, authors, snippet, year };
|
|
139
|
+
});
|
|
140
|
+
})()`);
|
|
141
|
+
|
|
142
|
+
await browser.close();
|
|
143
|
+
const items = data as any[];
|
|
144
|
+
const raw = items.map((r, i) => `[${i + 1}] ${r.title ?? "Untitled"}\nAuthors: ${r.authors ?? "Unknown"}\nYear: ${r.year ?? "Unknown"}\nSnippet: ${r.snippet ?? "N/A"}`).join("\n\n");
|
|
145
|
+
const years = items.map(r => r.year).filter(Boolean).sort().reverse();
|
|
146
|
+
const newest = years[0] ?? null;
|
|
147
|
+
return { content: [{ type: "text", text: stamp(raw, url, newest ? `${newest}-01-01` : null, newest ? "high" : "low", "google_scholar") }] };
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return server;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Worker Export ────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
export default {
|
|
156
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
157
|
+
const transport = new WebStandardStreamableHTTPServerTransport();
|
|
158
|
+
const server = createServer(env);
|
|
159
|
+
await server.connect(transport);
|
|
160
|
+
return transport.handleRequest(request);
|
|
161
|
+
},
|
|
162
|
+
} satisfies ExportedHandler<Env>;
|