Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "mrscraper",
"owner": {
"name": "MrScraper",
"email": "support@mrscraper.com"
},
"metadata": {
"description": "MrScraper plugin for GitHub Copilot CLI.",
"version": "0.1.0"
},
"plugins": [
{
"name": "mrscraper",
"description": "Retrieve public web pages as HTML, Markdown, or screenshots and discover pages through Google search with MrScraper's hosted MCP server. Includes skills that keep the raw page as the source for local extraction and check account usage.",
"version": "0.1.0",
"source": "./plugins/mrscraper"
}
]
}
99 changes: 99 additions & 0 deletions .github/scripts/check-plugin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env node
// Repository checks for the MrScraper Copilot CLI plugin. No dependencies.
// Usage: node .github/scripts/check-plugin.mjs [repository root]

import fs from "node:fs";
import path from "node:path";

const root = path.resolve(process.argv[2] ?? ".");
const pluginDir = path.join(root, "plugins", "mrscraper");
const errors = [];

function readJson(relativePath) {
const file = path.join(root, relativePath);
try {
return JSON.parse(fs.readFileSync(file, "utf8"));
} catch (error) {
errors.push(`${relativePath}: ${error.message}`);
return {};
}
}

function check(condition, message) {
if (!condition) {
errors.push(message);
}
}

const manifest = readJson("plugins/mrscraper/plugin.json");
const mcp = readJson("plugins/mrscraper/mcp.json");
const marketplace = readJson(".github/plugin/marketplace.json");

// Agent Plugins 1.0 manifest: only plugin.json at the plugin root, and the
// non-empty version and description that the awesome-copilot intake requires.
check(manifest.name === "mrscraper", 'plugin.json: name must be "mrscraper"');
for (const field of ["version", "description"]) {
check(typeof manifest[field] === "string" && manifest[field].trim() !== "", `plugin.json: ${field} must be a non-empty string`);
}
check(/^\d+\.\d+\.\d+$/.test(manifest.version ?? ""), "plugin.json: version must be MAJOR.MINOR.PATCH");
check((manifest.description ?? "").length <= 500, "plugin.json: description must be 500 characters or fewer");
check(Array.isArray(manifest.keywords) && manifest.keywords.length > 0 && manifest.keywords.length <= 10, "plugin.json: keywords must have 1-10 entries");
for (const keyword of manifest.keywords ?? []) {
check(/^[a-z0-9-]{1,30}$/.test(keyword), `plugin.json: keyword "${keyword}" must be lowercase letters, digits, and hyphens`);
}
for (const legacy of [".plugin/plugin.json", ".claude-plugin/plugin.json", ".github/plugin/plugin.json", ".mcp.json"]) {
check(!fs.existsSync(path.join(pluginDir, legacy)), `plugins/mrscraper/${legacy}: remove it; Agent Plugins 1.0 uses plugin.json and mcp.json at the plugin root`);
}

// Hosted MCP server: OAuth only, so the configuration must not carry headers.
const servers = Object.entries(mcp.mcpServers ?? {});
check(servers.length === 1, "mcp.json: expected exactly one MCP server");
const server = mcp.mcpServers?.mrscraper ?? {};
check(server.type === "streamable-http", 'mcp.json: mrscraper.type must be "streamable-http"');
check(server.url === "https://mcp.mrscraper.com/mcp", "mcp.json: mrscraper.url must be https://mcp.mrscraper.com/mcp");
check(server.headers === undefined, "mcp.json: mrscraper must not define headers; authentication uses OAuth");

// Marketplace entry must describe the same plugin and version.
const entries = marketplace.plugins ?? [];
check(entries.length === 1, "marketplace.json: expected exactly one plugin entry");
const entry = entries[0] ?? {};
check(entry.name === manifest.name, "marketplace.json: plugin name must match plugin.json");
check(entry.version === manifest.version, "marketplace.json: plugin version must match plugin.json");
check(entry.description === manifest.description, "marketplace.json: plugin description must match plugin.json");
check(marketplace.metadata?.version === manifest.version, "marketplace.json: metadata.version must match plugin.json");
check(path.resolve(root, entry.source ?? "") === pluginDir, "marketplace.json: source must be ./plugins/mrscraper");

// Skills: each directory under skills/ has a SKILL.md whose name matches it.
const expectedSkills = ["mrscraper", "mrscraper-fetch", "mrscraper-serp"];
const skillsDir = path.join(pluginDir, "skills");
const foundSkills = fs.existsSync(skillsDir)
? fs.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort()
: [];
check(JSON.stringify(foundSkills) === JSON.stringify(expectedSkills), `skills: expected ${expectedSkills.join(", ")}; found ${foundSkills.join(", ") || "none"}`);
for (const skill of foundSkills) {
const file = path.join(skillsDir, skill, "SKILL.md");
if (!fs.existsSync(file)) {
errors.push(`skills/${skill}: SKILL.md is missing`);
continue;
}
const text = fs.readFileSync(file, "utf8");
const frontmatter = text.match(/^---\n([\s\S]*?)\n---\n/);
check(Boolean(frontmatter), `skills/${skill}/SKILL.md: missing YAML frontmatter`);
const name = frontmatter?.[1].match(/^name:\s*(.+)$/m)?.[1].trim();
check(name === skill, `skills/${skill}/SKILL.md: frontmatter name "${name}" must equal the directory name`);
check(/^description:\s*\S/m.test(frontmatter?.[1] ?? ""), `skills/${skill}/SKILL.md: frontmatter description is required`);
check(!/\]\(\.\.\//.test(text), `skills/${skill}/SKILL.md: links outside the skill directory fail vally lint`);
}

// Release notes for the manifest version.
const changelog = fs.readFileSync(path.join(root, "CHANGELOG.md"), "utf8");
check(changelog.includes(`## ${manifest.version} `), `CHANGELOG.md: add a "## ${manifest.version}" entry`);

if (errors.length > 0) {
for (const error of errors) {
console.error(`::error::${error}`);
}
process.exit(1);
}

console.log(`ok: mrscraper ${manifest.version}, ${foundSkills.length} skills, MCP ${server.url}`);
112 changes: 112 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Validate Copilot CLI plugin

on:
pull_request:
push:
branches:
- main
tags:
- "v*"
workflow_dispatch:

permissions:
contents: read

env:
COPILOT_CLI_VERSION: 1.0.98
# github/awesome-copilot commit whose external plugin quality gates this job runs.
AWESOME_COPILOT_SHA: cb64ad12e32737ae764becd6885462e9b4c7cb00
COPILOT_AUTO_UPDATE: "false"

jobs:
validate:
name: Schema, repository checks, and install
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"

- name: Validate against the Agent Plugins 1.0 schemas
run: |
mkdir -p "$RUNNER_TEMP/schemas"
for name in plugin mcp; do
curl -fsSL -o "$RUNNER_TEMP/schemas/$name.schema.json" \
"https://agent-plugins.org/schemas/1.0.0/$name.schema.json"
npx --yes ajv-cli@5.0.0 validate --spec=draft2020 \
-s "$RUNNER_TEMP/schemas/$name.schema.json" \
-d "plugins/mrscraper/$name.json"
done

- name: Check manifest, marketplace, skills, and changelog
run: node .github/scripts/check-plugin.mjs .

- name: Install Copilot CLI
run: npm install --global "@github/copilot@${COPILOT_CLI_VERSION}"

- name: Install the plugin and confirm what it loads
env:
HOME: ${{ runner.temp }}/copilot-home
run: |
mkdir -p "$HOME"
copilot --version
copilot plugin marketplace add "$GITHUB_WORKSPACE"
copilot plugin install mrscraper@mrscraper

copilot plugin list --json | tee "$RUNNER_TEMP/plugins.json"
version="$(jq -r .version plugins/mrscraper/plugin.json)"
jq -e --arg version "$version" \
'any(.[]; .name == "mrscraper" and .marketplace == "mrscraper" and .version == $version and .enabled)' \
"$RUNNER_TEMP/plugins.json"

copilot mcp get mrscraper --json | tee "$RUNNER_TEMP/mcp.json"
jq -e '.mrscraper | .url == "https://mcp.mrscraper.com/mcp" and .source == "plugin" and (has("headers") | not)' \
"$RUNNER_TEMP/mcp.json"

copilot skill list | tee "$RUNNER_TEMP/skills.txt"
for skill in mrscraper mrscraper-fetch mrscraper-serp; do
grep -qE "^ ${skill} - " "$RUNNER_TEMP/skills.txt" || { echo "::error::missing skill ${skill}"; exit 1; }
done

awesome-copilot-gates:
name: awesome-copilot external plugin quality gates
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Check out awesome-copilot quality gates
uses: actions/checkout@v4
with:
repository: github/awesome-copilot
ref: ${{ env.AWESOME_COPILOT_SHA }}
path: awesome-copilot
sparse-checkout: eng

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"

- name: Install gate dependencies and Copilot CLI
run: |
npm ci --prefix awesome-copilot --ignore-scripts --no-audit --no-fund
npm install --global "@github/copilot@${COPILOT_CLI_VERSION}"

- name: Run spec compliance, vally lint, install smoke test, and version match
env:
SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
plugin_json="$(jq -c --arg repo "$GITHUB_REPOSITORY" --arg sha "$SOURCE_SHA" \
'{name, description, version, author, repository, homepage, license, keywords,
source: {source: "github", repo: $repo, path: "plugins/mrscraper", sha: $sha}}' \
plugins/mrscraper/plugin.json)"
cd awesome-copilot
node eng/external-plugin-quality-gates.mjs --plugin-json "$plugin_json" \
--log-file "$RUNNER_TEMP/quality-gates.log" > "$RUNNER_TEMP/quality-gates.json"
cat "$RUNNER_TEMP/quality-gates.log"
jq -e '.overall_status == "pass" and .spec_compliance_status == "pass"' "$RUNNER_TEMP/quality-gates.json"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.mrscraper/
.DS_Store
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Changelog

## 0.1.0 - 2026-10-16

- First release of the MrScraper plugin for GitHub Copilot CLI, in the Agent
Plugins 1.0 format.
- Hosted MCP server `https://mcp.mrscraper.com/mcp` (Streamable HTTP, OAuth 2.1
with dynamic client registration) exposing `scraperAPI`, `webSearch`, and
`status`.
- Skills: `mrscraper`, `mrscraper-fetch`, and `mrscraper-serp`.
- Marketplace file at `.github/plugin/marketplace.json` for
`copilot plugin install mrscraper@mrscraper`.
- CI that validates the manifests, installs the plugin with Copilot CLI, and
runs the awesome-copilot external plugin quality gates.
Loading
Loading