feat: MCP ecosystem tracking + Slack/Discord notifications#122
feat: MCP ecosystem tracking + Slack/Discord notifications#122howardpen9 wants to merge 1 commit intoduanyytop:masterfrom
Conversation
- Track 6 MCP repos (spec, TypeScript/Python SDKs, official servers, GitHub MCP, Context7) with daily ai-mcp.md reports in ZH + EN - Add Slack and Discord webhook support to notifications alongside existing Telegram (opt-in via SLACK_WEBHOOK_URL, DISCORD_WEBHOOK_URL) - New prompt builder for MCP cross-project analysis - Update manifest, RSS feed, Web UI labels, and GitHub issue labels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
duanyytop
left a comment
There was a problem hiding this comment.
Review
Clean PR — follows existing pipeline patterns well, and the Slack/Discord notifications are nicely done (opt-in, zero dependencies). A few things to address:
Action items
1. MCP summaries reuse buildPeerPrompt (src/index.ts:458)
MCP repos call buildPeerPrompt directly. If that prompt contains OpenClaw-specific framing or terminology, the generated MCP summaries may be subtly off. Either confirm buildPeerPrompt is generic enough, or create a dedicated buildMcpPrompt.
2. MCP summaries run sequentially after CLI/OpenClaw (src/index.ts:441-471)
generateMcpDigests waits for generateSummaries to finish, but they're independent. Running them in parallel saves ~30s of latency:
const [zhSummaries, enSummaries, zhMcpDigests, enMcpDigests] = await Promise.all([
generateSummaries(..., "zh"),
generateSummaries(..., "en"),
generateMcpDigests("zh"),
generateMcpDigests("en"),
]);3. Missing documentation updates
Per the "Adding a new report type" checklist in CLAUDE.md, still need:
- Update the report outputs table and tracked sources in CLAUDE.md
- Update README
Suggestions
buildSlackMessageandbuildDiscordDescriptioninnotify.tsduplicate the report ordering logic — consider extracting a sharedorderedBaseReports(reports)helper.generateMcpDigestsreimplements the same pattern asgenerateSummaries(check activity → call LLM → handle error). Consider reusing the existing infrastructure.REPORT_EMOJISis only used by Slack/Discord — Telegram'sbuildMessagecould use it too for consistency.
Looks good
- Purple label colors — nice visual distinction
-ensuffix naming consistent withai-cli-en/ai-agents-enPromise.allSettled+ catch-log for notifications is the right call — one channel failure shouldn't block others
|
@claude review this pr |
There was a problem hiding this comment.
Pull request overview
Adds a new “MCP ecosystem” report to the existing daily digest pipeline and extends notifications beyond Telegram to Slack and Discord, wiring both into the daily workflow and UI/manifest metadata.
Changes:
- Track 6 MCP-related repos and generate new
ai-mcp.md/ai-mcp-en.mdreports (including cross-project comparison). - Extend
notify.tsto optionally send Slack + Discord webhooks in addition to Telegram. - Register MCP report IDs across manifest generation, GitHub issue labels/colors, and the Web UI sidebar.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/config.ts |
Adds default MCP repo list + config parsing (mcp_repos → mcpRepos). |
src/index.ts |
Integrates MCP repos into fetch/summarize/compare/save + GitHub issue creation. |
src/prompts.ts |
Adds buildMcpComparisonPrompt() for cross-project MCP analysis. |
src/report-builders.ts |
Adds buildMcpReportContent() to render the MCP digest report. |
src/notify.ts |
Adds Slack + Discord sending and new message builders. |
src/generate-manifest.ts |
Registers ai-mcp and ai-mcp-en report IDs and labels. |
src/github.ts |
Adds label colors for mcp and mcp-en. |
index.html |
Adds MCP labels to the sidebar label map. |
.github/workflows/daily-digest.yml |
Passes Slack/Discord webhook env vars to notification step. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| export function loadConfig(configPath = "config.yml"): RadarConfig { | ||
| const resolved = path.resolve(configPath); | ||
|
|
||
| if (!fs.existsSync(resolved)) { | ||
| console.log(`[config] ${configPath} not found — using built-in defaults.`); | ||
| return { | ||
| cliRepos: DEFAULT_CLI_REPOS, | ||
| mcpRepos: DEFAULT_MCP_REPOS, | ||
| skillsRepo: DEFAULT_SKILLS_REPO, | ||
| openclaw: DEFAULT_OPENCLAW, | ||
| openclawPeers: DEFAULT_OPENCLAW_PEERS, | ||
| }; | ||
| } | ||
|
|
||
| const raw = yaml.load(fs.readFileSync(resolved, "utf-8")) as RawConfig; | ||
|
|
||
| const cliRepos = | ||
| Array.isArray(raw?.cli_repos) && raw.cli_repos.length > 0 | ||
| ? raw.cli_repos.map(toRepoConfig) | ||
| : DEFAULT_CLI_REPOS; | ||
|
|
||
| const skillsRepo = | ||
| typeof raw?.skills_repo === "string" && raw.skills_repo.trim() | ||
| ? raw.skills_repo.trim() | ||
| : DEFAULT_SKILLS_REPO; | ||
|
|
||
| const openclaw = raw?.openclaw?.id && raw.openclaw.repo ? toRepoConfig(raw.openclaw) : DEFAULT_OPENCLAW; | ||
|
|
||
| const mcpRepos = | ||
| Array.isArray(raw?.mcp_repos) && raw.mcp_repos.length > 0 | ||
| ? raw.mcp_repos.map(toRepoConfig) | ||
| : DEFAULT_MCP_REPOS; | ||
|
|
||
| const openclawPeers = | ||
| Array.isArray(raw?.openclaw_peers) && raw.openclaw_peers.length > 0 | ||
| ? raw.openclaw_peers.map(toRepoConfig) | ||
| : DEFAULT_OPENCLAW_PEERS; | ||
|
|
||
| console.log( | ||
| `[config] Loaded from ${configPath}: ` + | ||
| `${cliRepos.length} CLI repos, ${openclawPeers.length} OpenClaw peers`, | ||
| `${cliRepos.length} CLI repos, ${mcpRepos.length} MCP repos, ${openclawPeers.length} OpenClaw peers`, | ||
| ); | ||
|
|
||
| return { cliRepos, skillsRepo, openclaw, openclawPeers }; | ||
| return { cliRepos, mcpRepos, skillsRepo, openclaw, openclawPeers }; | ||
| } |
There was a problem hiding this comment.
loadConfig() now supports mcp_repos and returns mcpRepos, but the config tests don’t assert default MCP repos are present or that YAML mcp_repos overrides are parsed/fallback correctly. Add test cases similar to cli_repos coverage to lock in this new config behavior.
| console.log(` [${cfg.id}] Calling LLM for MCP summary (${lang})...`); | ||
| try { | ||
| const summary = await callLlm( | ||
| buildPeerPrompt(cfg, issues, prs, releases, dateStr, undefined, undefined, lang), | ||
| ); | ||
| return { config: cfg, issues, prs, releases, summary }; |
There was a problem hiding this comment.
MCP per-repo summaries are generated with buildPeerPrompt(), which is explicitly tailored to “AI agent and personal AI assistant open-source projects”. For MCP spec/SDK/server repos this mismatches the domain and will likely produce irrelevant/incorrect summaries. Introduce an MCP-specific per-repo prompt (or parameterize buildPeerPrompt wording) and use that here instead.
| for (const r of ordered) { | ||
| const emoji = REPORT_EMOJIS[r] ?? "📄"; | ||
| const label = EN_LABELS[r] ?? r; | ||
| const url = `${pagesUrl}/#${date}/${r}`; | ||
| lines.push(`${emoji} <${url}|${label}>`); |
There was a problem hiding this comment.
Slack notifications use EN_LABELS but always link to the base (non -en) report (/#${date}/${r}). When English reports exist in the manifest (e.g. ai-cli-en), this sends users to the Chinese page despite an English label. Consider including both zh/en links (like Telegram) or linking to ${r}-en when present.
| .map((r) => { | ||
| const emoji = REPORT_EMOJIS[r] ?? "📄"; | ||
| const label = EN_LABELS[r] ?? r; | ||
| const url = `${pagesUrl}/#${date}/${r}`; |
There was a problem hiding this comment.
Discord embeds are built from baseReports only and link to /#${date}/${r} regardless of whether an English report exists. Since the embed uses EN_LABELS, this can send users to Chinese content even when *-en pages are available. Adjust the description builder to prefer ${r}-en when present or include both links.
| const url = `${pagesUrl}/#${date}/${r}`; | |
| const englishId = `${r}-en`; | |
| const targetId = reports.includes(englishId) ? englishId : r; | |
| const url = `${pagesUrl}/#${date}/${targetId}`; |
| function buildSlackMessage(date: string, reports: string[], pagesUrl: string): string { | ||
| const baseReports = reports.filter((r) => !r.endsWith("-en")); | ||
| const isWeekly = baseReports.includes("ai-weekly"); | ||
| const isMonthly = baseReports.includes("ai-monthly"); | ||
|
|
||
| const icon = isMonthly ? "📆" : isWeekly ? "📅" : "📡"; | ||
| const suffix = isMonthly ? " Monthly" : isWeekly ? " Weekly" : ""; | ||
| const lines: string[] = [`${icon} *agents-radar${suffix} · ${date}*\n`]; | ||
|
|
||
| const ordered = [ | ||
| ...baseReports.filter((r) => !r.includes("weekly") && !r.includes("monthly")), | ||
| ...baseReports.filter((r) => r.includes("weekly") || r.includes("monthly")), | ||
| ]; | ||
|
|
||
| for (const r of ordered) { | ||
| const emoji = REPORT_EMOJIS[r] ?? "📄"; | ||
| const label = EN_LABELS[r] ?? r; | ||
| const url = `${pagesUrl}/#${date}/${r}`; | ||
| lines.push(`${emoji} <${url}|${label}>`); | ||
| } | ||
|
|
||
| lines.push(`\n<${pagesUrl}|🌐 Web UI> · <${pagesUrl}/feed.xml|⊕ RSS>`); | ||
| return lines.join("\n"); | ||
| } | ||
|
|
||
| function buildDiscordDescription(date: string, reports: string[], pagesUrl: string): string { | ||
| const baseReports = reports.filter((r) => !r.endsWith("-en")); | ||
| const ordered = [ | ||
| ...baseReports.filter((r) => !r.includes("weekly") && !r.includes("monthly")), | ||
| ...baseReports.filter((r) => r.includes("weekly") || r.includes("monthly")), | ||
| ]; | ||
|
|
||
| return ordered | ||
| .map((r) => { | ||
| const emoji = REPORT_EMOJIS[r] ?? "📄"; | ||
| const label = EN_LABELS[r] ?? r; | ||
| const url = `${pagesUrl}/#${date}/${r}`; | ||
| return `${emoji} [${label}](${url})`; | ||
| }) | ||
| .join("\n"); |
There was a problem hiding this comment.
New Slack/Discord message builders were added but existing tests only cover buildMessage(). Please add unit tests for buildSlackMessage/buildDiscordDescription (ordering, weekly/monthly suffix/icon, link selection when -en exists) to keep notification formatting stable.
| export function buildMcpReportContent( | ||
| mcpDigests: RepoDigest[], | ||
| comparison: string, | ||
| utcStr: string, | ||
| dateStr: string, | ||
| footer: string, | ||
| lang: "zh" | "en" = "zh", | ||
| ): string { | ||
| const repoLinks = mcpDigests | ||
| .map((d) => `- [${d.config.name}](https://github.com/${d.config.repo})`) | ||
| .join("\n"); | ||
|
|
||
| const t = | ||
| lang === "en" | ||
| ? { | ||
| title: `# MCP Ecosystem Digest ${dateStr}\n\n`, | ||
| meta: `> Generated: ${utcStr} UTC | Projects covered: ${mcpDigests.length}\n\n`, | ||
| comparison: `## Cross-Project Analysis\n\n`, | ||
| detail: `## Per-Project Reports\n\n`, | ||
| } | ||
| : { | ||
| title: `# MCP 生态日报 ${dateStr}\n\n`, | ||
| meta: `> 生成时间: ${utcStr} UTC | 覆盖项目: ${mcpDigests.length} 个\n\n`, | ||
| comparison: `## 横向对比\n\n`, | ||
| detail: `## 各项目详细报告\n\n`, | ||
| }; | ||
|
|
||
| const sections = mcpDigests | ||
| .map((d) => | ||
| [ | ||
| `<details>`, | ||
| `<summary><strong>${d.config.name}</strong> — <a href="https://github.com/${d.config.repo}">${d.config.repo}</a></summary>`, | ||
| ``, | ||
| d.summary, | ||
| ``, | ||
| `</details>`, | ||
| ].join("\n"), | ||
| ) | ||
| .join("\n\n"); | ||
|
|
||
| return ( | ||
| t.title + | ||
| t.meta + | ||
| `${repoLinks}\n\n` + | ||
| `---\n\n` + | ||
| t.comparison + | ||
| comparison + | ||
| `\n\n---\n\n` + | ||
| t.detail + | ||
| sections + | ||
| footer | ||
| ); | ||
| } |
There was a problem hiding this comment.
buildMcpReportContent() is new behavior but report-builders tests currently cover only buildCliReportContent/buildOpenclawReportContent. Add tests asserting MCP title/meta, repo link list, cross-project section, and per-project
Details
rendering (zh + en) to prevent regressions.| export function buildMcpComparisonPrompt( | ||
| digests: RepoDigest[], | ||
| dateStr: string, | ||
| lang: "zh" | "en" = "zh", | ||
| ): string { | ||
| const summaries = digests | ||
| .map((d) => `### ${d.config.name} (${d.config.repo})\n${d.summary}`) | ||
| .join("\n\n---\n\n"); | ||
|
|
||
| if (lang === "en") { | ||
| return `You are an expert on the Model Context Protocol (MCP) ecosystem. Based on the following per-repo summaries for ${dateStr}, produce a cross-project comparison: | ||
|
|
||
| ${summaries} | ||
|
|
||
| --- | ||
|
|
||
| Generate a structured English analysis: | ||
|
|
||
| 1. **Ecosystem Highlights** — 2-3 sentences on the most important MCP developments today | ||
| 2. **Spec & SDK Progress** — Key changes to the specification and official SDKs | ||
| 3. **Server Ecosystem** — Notable new servers, integrations, or community contributions | ||
| 4. **Cross-Project Themes** — Common patterns, recurring issues, or coordinated efforts | ||
| 5. **Developer Impact** — What these changes mean for MCP adopters and tool builders | ||
|
|
||
| Style: concise and professional, include GitHub links where possible. | ||
| `; | ||
| } | ||
|
|
||
| return `你是 Model Context Protocol (MCP) 生态的技术分析师。请根据以下 ${dateStr} 各仓库的总结,生成一份横向对比分析: | ||
|
|
||
| ${summaries} | ||
|
|
||
| --- | ||
|
|
||
| 请生成结构清晰的中文分析报告: | ||
|
|
||
| 1. **今日速览** — 2-3 句话概括今日 MCP 生态最重要的动态 | ||
| 2. **规范与 SDK 进展** — 规范和官方 SDK 的重要变更 | ||
| 3. **Server 生态** — 值得关注的新 Server、集成或社区贡献 | ||
| 4. **跨项目主题** — 各项目间的共同模式、重复出现的问题或协同工作 | ||
| 5. **开发者影响** — 这些变更对 MCP 使用者和工具开发者意味着什么 | ||
|
|
||
| 语言要求:中文,简洁专业,保留所有 GitHub 链接。 | ||
| `; | ||
| } |
There was a problem hiding this comment.
buildMcpComparisonPrompt() adds a new prompt surface, but prompts/prompt-builders tests don’t cover it yet. Add a unit test to assert both zh/en variants include the repo summary blocks and the expected section headings so prompt structure stays stable.
|
what's your TG or X.com? @duanyytop |
Summary
Split from #41 as suggested — this PR contains only the three features approved for merge:
ai-mcp.md+ai-mcp-en.mdreports with cross-project comparison.notify.tsto support Slack (incoming webhook) and Discord (embed webhook). All channels are opt-in via env vars (SLACK_WEBHOOK_URL,DISCORD_WEBHOOK_URL). Telegram behavior is unchanged.index.ts.What's NOT in this PR
The config-driven YAML refactor (
config/sources.yaml, generalized pipeline groups) is intentionally excluded per reviewer feedback — that deserves a separate discussion.Changes
src/config.tsDEFAULT_MCP_REPOS(6 repos) +mcpRepostoRadarConfigsrc/index.tsai-mcp.mdreportssrc/prompts.tsbuildMcpComparisonPromptfor cross-project MCP analysissrc/report-builders.tsbuildMcpReportContentsrc/notify.tssrc/generate-manifest.tsai-mcp/ai-mcp-ento report files + labelssrc/github.tsmcp/mcp-enlabel colorsindex.html.github/workflows/daily-digest.ymlSLACK_WEBHOOK_URL+DISCORD_WEBHOOK_URLenv varsNo new dependencies
All notification channels use native
fetch()— zero new runtime dependencies added.Test plan
pnpm typecheckpassespnpm lintpassespnpm format:checkpassespnpm startgeneratesai-mcp.md+ai-mcp-en.mdalongside existing reports🤖 Generated with Claude Code