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