mirror of
https://github.com/punkpeye/awesome-mcp-servers.git
synced 2026-03-12 04:05:17 +00:00
chore: automate parts of curation
This commit is contained in:
356
.github/workflows/check-glama.yml
vendored
Normal file
356
.github/workflows/check-glama.yml
vendored
Normal file
@@ -0,0 +1,356 @@
|
||||
name: Check Glama Link
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, synchronize, closed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
# Post-merge welcome comment
|
||||
welcome:
|
||||
if: github.event.action == 'closed' && github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Post welcome comment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.payload.pull_request.number;
|
||||
const marker = '<!-- welcome-comment -->';
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
if (!comments.some(c => c.body.includes(marker))) {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body: `${marker}\nThank you for your contribution! Your server has been merged.
|
||||
|
||||
Are you in the MCP [Discord](https://glama.ai/mcp/discord)? Let me know your Discord username and I will give you a **server-author** flair.
|
||||
|
||||
Also, make sure to claim your server on https://glama.ai/mcp/servers (click "Add Server" if it is not already there). Then update Dockerfile settings — this will make it available for anyone to use.
|
||||
|
||||
If you already have a remote server, you can list it under https://glama.ai/mcp/connectors`
|
||||
});
|
||||
}
|
||||
|
||||
# Validation checks (only on open PRs)
|
||||
check-submission:
|
||||
if: github.event.action != 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout base branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
|
||||
- name: Validate PR submission
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.payload.pull_request.number;
|
||||
|
||||
// Read existing README to check for duplicates
|
||||
const readme = fs.readFileSync('README.md', 'utf8');
|
||||
const existingUrls = new Set();
|
||||
const urlRegex = /\(https:\/\/github\.com\/[^)]+\)/gi;
|
||||
for (const match of readme.matchAll(urlRegex)) {
|
||||
existingUrls.add(match[0].toLowerCase());
|
||||
}
|
||||
|
||||
// Get the PR diff
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
// Permitted emojis
|
||||
const permittedEmojis = [
|
||||
'\u{1F396}\uFE0F', // 🎖️ official
|
||||
'\u{1F40D}', // 🐍 Python
|
||||
'\u{1F4C7}', // 📇 TypeScript/JS
|
||||
'\u{1F3CE}\uFE0F', // 🏎️ Go
|
||||
'\u{1F980}', // 🦀 Rust
|
||||
'#\uFE0F\u20E3', // #️⃣ C#
|
||||
'\u2615', // ☕ Java
|
||||
'\u{1F30A}', // 🌊 C/C++
|
||||
'\u{1F48E}', // 💎 Ruby
|
||||
'\u2601\uFE0F', // ☁️ Cloud
|
||||
'\u{1F3E0}', // 🏠 Local
|
||||
'\u{1F4DF}', // 📟 Embedded
|
||||
'\u{1F34E}', // 🍎 macOS
|
||||
'\u{1FAA9}', // 🪟 Windows
|
||||
'\u{1F427}', // 🐧 Linux
|
||||
];
|
||||
|
||||
// Only check added lines (starting with +) for glama link
|
||||
const hasGlama = files.some(file =>
|
||||
file.patch && file.patch.split('\n')
|
||||
.filter(line => line.startsWith('+'))
|
||||
.some(line => line.includes('glama.ai/mcp/servers'))
|
||||
);
|
||||
|
||||
// Check added lines for emoji usage
|
||||
const addedLines = files
|
||||
.filter(f => f.patch)
|
||||
.flatMap(f => f.patch.split('\n').filter(line => line.startsWith('+')))
|
||||
.filter(line => line.includes('](https://github.com/'));
|
||||
|
||||
let hasValidEmoji = false;
|
||||
let hasInvalidEmoji = false;
|
||||
const invalidLines = [];
|
||||
const badNameLines = [];
|
||||
const duplicateUrls = [];
|
||||
const nonGithubUrls = [];
|
||||
|
||||
// Check for non-GitHub URLs in added entry lines (list items with markdown links)
|
||||
const allAddedEntryLines = files
|
||||
.filter(f => f.patch)
|
||||
.flatMap(f => f.patch.split('\n').filter(line => line.startsWith('+')))
|
||||
.map(line => line.replace(/^\+/, ''))
|
||||
.filter(line => /^\s*-\s*\[/.test(line));
|
||||
|
||||
for (const line of allAddedEntryLines) {
|
||||
// Extract the primary link URL (first markdown link)
|
||||
const linkMatch = line.match(/\]\((https?:\/\/[^)]+)\)/);
|
||||
if (linkMatch) {
|
||||
const url = linkMatch[1];
|
||||
if (!url.startsWith('https://github.com/')) {
|
||||
nonGithubUrls.push(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
for (const line of addedLines) {
|
||||
const ghMatch = line.match(/\(https:\/\/github\.com\/[^)]+\)/i);
|
||||
if (ghMatch && existingUrls.has(ghMatch[0].toLowerCase())) {
|
||||
duplicateUrls.push(ghMatch[0].replace(/[()]/g, ''));
|
||||
}
|
||||
}
|
||||
|
||||
for (const line of addedLines) {
|
||||
const usedPermitted = permittedEmojis.filter(e => line.includes(e));
|
||||
if (usedPermitted.length > 0) {
|
||||
hasValidEmoji = true;
|
||||
} else {
|
||||
// Line with a GitHub link but no permitted emoji
|
||||
invalidLines.push(line.replace(/^\+/, '').trim());
|
||||
}
|
||||
|
||||
// Check for emojis that aren't in the permitted list
|
||||
const emojiRegex = /\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu;
|
||||
const allEmojis = line.match(emojiRegex) || [];
|
||||
const unknownEmojis = allEmojis.filter(e => !permittedEmojis.some(p => p.includes(e) || e.includes(p)));
|
||||
if (unknownEmojis.length > 0) {
|
||||
hasInvalidEmoji = true;
|
||||
}
|
||||
|
||||
// Check that the link text uses full owner/repo format
|
||||
// Pattern: [link-text](https://github.com/owner/repo)
|
||||
const entryRegex = /\[([^\]]+)\]\(https:\/\/github\.com\/([^/]+)\/([^/)]+)\)/;
|
||||
const match = line.match(entryRegex);
|
||||
if (match) {
|
||||
const linkText = match[1];
|
||||
const expectedName = `${match[2]}/${match[3]}`;
|
||||
// Link text should contain owner/repo (case-insensitive)
|
||||
if (!linkText.toLowerCase().includes('/')) {
|
||||
badNameLines.push({ linkText, expectedName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const emojiOk = addedLines.length === 0 || (hasValidEmoji && !hasInvalidEmoji && invalidLines.length === 0);
|
||||
const nameOk = badNameLines.length === 0;
|
||||
const noDuplicates = duplicateUrls.length === 0;
|
||||
const allGithub = nonGithubUrls.length === 0;
|
||||
|
||||
// Apply glama labels
|
||||
const glamaLabel = hasGlama ? 'has-glama' : 'missing-glama';
|
||||
const glamaLabelRemove = hasGlama ? 'missing-glama' : 'has-glama';
|
||||
|
||||
// Apply emoji labels
|
||||
const emojiLabel = emojiOk ? 'has-emoji' : 'missing-emoji';
|
||||
const emojiLabelRemove = emojiOk ? 'missing-emoji' : 'has-emoji';
|
||||
|
||||
// Apply name labels
|
||||
const nameLabel = nameOk ? 'valid-name' : 'invalid-name';
|
||||
const nameLabelRemove = nameOk ? 'invalid-name' : 'valid-name';
|
||||
|
||||
const labelsToAdd = [glamaLabel, emojiLabel, nameLabel];
|
||||
const labelsToRemove = [glamaLabelRemove, emojiLabelRemove, nameLabelRemove];
|
||||
|
||||
if (!noDuplicates) {
|
||||
labelsToAdd.push('duplicate');
|
||||
} else {
|
||||
labelsToRemove.push('duplicate');
|
||||
}
|
||||
|
||||
if (!allGithub) {
|
||||
labelsToAdd.push('non-github-url');
|
||||
} else {
|
||||
labelsToRemove.push('non-github-url');
|
||||
}
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
labels: labelsToAdd,
|
||||
});
|
||||
|
||||
for (const label of labelsToRemove) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
name: label,
|
||||
});
|
||||
} catch (e) {
|
||||
// Label wasn't present, ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Post comments for issues, avoiding duplicates
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
// Glama comment
|
||||
if (!hasGlama) {
|
||||
const marker = '<!-- glama-check -->';
|
||||
if (!comments.some(c => c.body.includes(marker))) {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body: `${marker}\nHey,
|
||||
|
||||
To ensure that only working servers are listed, we're updating our listing requirements.
|
||||
|
||||
Please complete the following steps:
|
||||
|
||||
1. **Ensure your server is listed on Glama.** If it isn't already, submit it at https://glama.ai/mcp/servers and verify that it passes all checks (including a successfully built Docker image and a release).
|
||||
|
||||
2. **Update your PR** by adding a \`[glama](https://glama.ai/mcp/servers/...)\` link immediately after the GitHub repository link, pointing to your server's Glama listing page.
|
||||
|
||||
If you need any assistance, feel free to ask questions here or on [Discord](https://glama.ai/discord).
|
||||
|
||||
P.S. If your server already has a hosted endpoint, you can also list it under https://glama.ai/mcp/connectors.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Emoji comment
|
||||
if (!emojiOk && addedLines.length > 0) {
|
||||
const marker = '<!-- emoji-check -->';
|
||||
if (!comments.some(c => c.body.includes(marker))) {
|
||||
const emojiList = [
|
||||
'🎖️ – official implementation',
|
||||
'🐍 – Python',
|
||||
'📇 – TypeScript / JavaScript',
|
||||
'🏎️ – Go',
|
||||
'🦀 – Rust',
|
||||
'#️⃣ – C#',
|
||||
'☕ – Java',
|
||||
'🌊 – C/C++',
|
||||
'💎 – Ruby',
|
||||
'☁️ – Cloud Service',
|
||||
'🏠 – Local Service',
|
||||
'📟 – Embedded Systems',
|
||||
'🍎 – macOS',
|
||||
'🪟 – Windows',
|
||||
'🐧 – Linux',
|
||||
].map(e => `- ${e}`).join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body: `${marker}\nYour submission is missing a required emoji tag or uses an unrecognized one. Each entry must include at least one of the permitted emojis after the repository link.
|
||||
|
||||
**Permitted emojis:**
|
||||
${emojiList}
|
||||
|
||||
Please update your PR to include the appropriate emoji(s). See existing entries for examples.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Duplicate comment
|
||||
if (!noDuplicates) {
|
||||
const marker = '<!-- duplicate-check -->';
|
||||
if (!comments.some(c => c.body.includes(marker))) {
|
||||
const dupes = duplicateUrls.map(u => `- ${u}`).join('\n');
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body: `${marker}\nThe following server(s) are already listed in the repository:
|
||||
|
||||
${dupes}
|
||||
|
||||
Please remove the duplicate entries from your PR.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Non-GitHub URL comment
|
||||
if (!allGithub) {
|
||||
const marker = '<!-- url-check -->';
|
||||
if (!comments.some(c => c.body.includes(marker))) {
|
||||
const urls = nonGithubUrls.map(u => `- ${u}`).join('\n');
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body: `${marker}\nWe only accept servers hosted on GitHub. The following URLs are not GitHub links:
|
||||
|
||||
${urls}
|
||||
|
||||
Please update your PR to use a \`https://github.com/...\` repository link.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Name format comment
|
||||
if (!nameOk) {
|
||||
const marker = '<!-- name-check -->';
|
||||
if (!comments.some(c => c.body.includes(marker))) {
|
||||
const examples = badNameLines.map(
|
||||
b => `- \`${b.linkText}\` should be \`${b.expectedName}\``
|
||||
).join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body: `${marker}\nThe entry name must use the full \`owner/repo\` format (not just the repo name).
|
||||
|
||||
${examples}
|
||||
|
||||
For example: \`[user/mcp-server-example](https://github.com/user/mcp-server-example)\`
|
||||
|
||||
Please update your PR to use the full repository name.`
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user