chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:57 +08:00
commit e30f8ba47c
533 changed files with 115926 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# Build the MCP App UIs
set -e
cd "$(dirname "$0")/../ui"
# Install dependencies if needed
if [ ! -d "node_modules" ]; then
echo "Installing UI dependencies..."
npm install
fi
echo "Building UI..."
npm run build
echo "UI build complete. Output:"
ls -la ../pkg/github/ui_dist/*.html
+348
View File
@@ -0,0 +1,348 @@
#!/bin/bash
set -e
# Conformance test script for comparing MCP server behavior between branches
# Builds both main and current branch, runs various flag combinations,
# and produces a conformance report with timing and diffs.
#
# Output:
# - Progress/status messages go to stderr (for visibility in CI)
# - Final report summary goes to stdout (for piping/capture)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
REPORT_DIR="$PROJECT_DIR/conformance-report"
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Colors for output (only used on stderr)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper to print to stderr
log() {
echo -e "$@" >&2
}
log "${BLUE}=== MCP Server Conformance Test ===${NC}"
log "Current branch: $CURRENT_BRANCH"
log "Report directory: $REPORT_DIR"
# Find the common ancestor
MERGE_BASE=$(git merge-base HEAD origin/main)
log "Comparing against merge-base: $MERGE_BASE"
log ""
# Create report directory
rm -rf "$REPORT_DIR"
mkdir -p "$REPORT_DIR"/{main,branch,diffs}
# Build binaries
log "${YELLOW}Building binaries...${NC}"
log "Building current branch ($CURRENT_BRANCH)..."
go build -o "$REPORT_DIR/branch/github-mcp-server" ./cmd/github-mcp-server
BRANCH_BUILD_OK=$?
log "Building main branch (using temp worktree at merge-base)..."
TEMP_WORKTREE=$(mktemp -d)
git worktree add --quiet "$TEMP_WORKTREE" "$MERGE_BASE"
(cd "$TEMP_WORKTREE" && go build -o "$REPORT_DIR/main/github-mcp-server" ./cmd/github-mcp-server)
MAIN_BUILD_OK=$?
git worktree remove --force "$TEMP_WORKTREE"
if [ $BRANCH_BUILD_OK -ne 0 ] || [ $MAIN_BUILD_OK -ne 0 ]; then
log "${RED}Build failed!${NC}"
exit 1
fi
log "${GREEN}Both binaries built successfully${NC}"
log ""
# MCP JSON-RPC messages
INIT_MSG='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"conformance-test","version":"1.0.0"}}}'
INITIALIZED_MSG='{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'
LIST_TOOLS_MSG='{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
LIST_RESOURCES_MSG='{"jsonrpc":"2.0","id":3,"method":"resources/listTemplates","params":{}}'
LIST_PROMPTS_MSG='{"jsonrpc":"2.0","id":4,"method":"prompts/list","params":{}}'
# Function to normalize JSON for comparison
# Sorts all arrays (including nested ones) and formats consistently
# Also handles embedded JSON strings in "text" fields (from tool call responses)
normalize_json() {
local file="$1"
if [ -s "$file" ]; then
# First, try to parse and re-serialize any JSON embedded in text fields
# This handles tool call responses where the result is JSON-in-a-string
jq -S '
# Function to sort arrays recursively
def deep_sort:
if type == "array" then
[.[] | deep_sort] | sort_by(tostring)
elif type == "object" then
to_entries | map(.value |= deep_sort) | from_entries
else
.
end;
# Walk the structure, and for any "text" field that looks like JSON array/object, parse and sort it
walk(
if type == "object" and .text and (.text | type == "string") and ((.text | startswith("[")) or (.text | startswith("{"))) then
.text = ((.text | fromjson | deep_sort) | tojson)
else
.
end
) | deep_sort
' "$file" 2>/dev/null > "${file}.tmp" && mv "${file}.tmp" "$file"
fi
}
# Function to run MCP server and capture output with timing
run_mcp_test() {
local binary="$1"
local name="$2"
local flags="$3"
local output_prefix="$4"
local start_time end_time duration
start_time=$(date +%s.%N)
# Run the server with all list commands - each response is on its own line
output=$(
(
echo "$INIT_MSG"
echo "$INITIALIZED_MSG"
echo "$LIST_TOOLS_MSG"
echo "$LIST_RESOURCES_MSG"
echo "$LIST_PROMPTS_MSG"
sleep 0.5
) | GITHUB_PERSONAL_ACCESS_TOKEN=1 $binary stdio $flags 2>/dev/null
)
end_time=$(date +%s.%N)
duration=$(echo "$end_time - $start_time" | bc)
# Parse and save each response by matching JSON-RPC id
# Each line is a separate JSON response
echo "$output" | while IFS= read -r line; do
id=$(echo "$line" | jq -r '.id // empty' 2>/dev/null)
case "$id" in
1) echo "$line" | jq -S '.' > "${output_prefix}_initialize.json" 2>/dev/null ;;
2) echo "$line" | jq -S '.' > "${output_prefix}_tools.json" 2>/dev/null ;;
3) echo "$line" | jq -S '.' > "${output_prefix}_resources.json" 2>/dev/null ;;
4) echo "$line" | jq -S '.' > "${output_prefix}_prompts.json" 2>/dev/null ;;
esac
done
# Create empty files if not created (in case of errors or missing responses)
touch "${output_prefix}_initialize.json" "${output_prefix}_tools.json" \
"${output_prefix}_resources.json" "${output_prefix}_prompts.json"
# Normalize all JSON files for consistent comparison (sorts arrays, keys)
for endpoint in initialize tools resources prompts; do
normalize_json "${output_prefix}_${endpoint}.json"
done
echo "$duration"
}
# Test configurations - array of "name|flags"
declare -a TEST_CONFIGS=(
"default|"
"read-only|--read-only"
"toolsets-repos|--toolsets=repos"
"toolsets-issues|--toolsets=issues"
"toolsets-pull_requests|--toolsets=pull_requests"
"toolsets-repos,issues|--toolsets=repos,issues"
"toolsets-all|--toolsets=all"
"tools-get_me|--tools=get_me"
"tools-get_me,list_issues|--tools=get_me,list_issues"
"toolsets-repos+read-only|--toolsets=repos --read-only"
)
# Summary arrays
declare -a TEST_NAMES
declare -a MAIN_TIMES
declare -a BRANCH_TIMES
declare -a DIFF_STATUS
log "${YELLOW}Running conformance tests...${NC}"
log ""
for config in "${TEST_CONFIGS[@]}"; do
IFS='|' read -r test_name flags <<< "$config"
log "${BLUE}Test: ${test_name}${NC}"
log " Flags: ${flags:-<none>}"
# Create output directories
mkdir -p "$REPORT_DIR/main/$test_name"
mkdir -p "$REPORT_DIR/branch/$test_name"
mkdir -p "$REPORT_DIR/diffs/$test_name"
# Run standard test
main_time=$(run_mcp_test "$REPORT_DIR/main/github-mcp-server" "main" "$flags" "$REPORT_DIR/main/$test_name/output")
log " Main: ${main_time}s"
branch_time=$(run_mcp_test "$REPORT_DIR/branch/github-mcp-server" "branch" "$flags" "$REPORT_DIR/branch/$test_name/output")
log " Branch: ${branch_time}s"
endpoints="initialize tools resources prompts"
# Calculate time difference
time_diff=$(echo "$branch_time - $main_time" | bc)
if (( $(echo "$time_diff > 0" | bc -l) )); then
log " Δ Time: ${RED}+${time_diff}s (slower)${NC}"
else
log " Δ Time: ${GREEN}${time_diff}s (faster)${NC}"
fi
# Generate diffs for each endpoint
has_diff=false
for endpoint in $endpoints; do
main_file="$REPORT_DIR/main/$test_name/output_${endpoint}.json"
branch_file="$REPORT_DIR/branch/$test_name/output_${endpoint}.json"
diff_file="$REPORT_DIR/diffs/$test_name/${endpoint}.diff"
if ! diff -u "$main_file" "$branch_file" > "$diff_file" 2>/dev/null; then
has_diff=true
lines=$(wc -l < "$diff_file" | tr -d ' ')
log " ${YELLOW}${endpoint}: DIFF (${lines} lines)${NC}"
else
rm -f "$diff_file" # No diff, remove empty file
log " ${GREEN}${endpoint}: OK${NC}"
fi
done
# Store results
TEST_NAMES+=("$test_name")
MAIN_TIMES+=("$main_time")
BRANCH_TIMES+=("$branch_time")
if [ "$has_diff" = true ]; then
DIFF_STATUS+=("DIFF")
else
DIFF_STATUS+=("OK")
fi
log ""
done
# Generate summary report
REPORT_FILE="$REPORT_DIR/CONFORMANCE_REPORT.md"
cat > "$REPORT_FILE" << EOF
# MCP Server Conformance Report
Generated: $(date)
Current Branch: $CURRENT_BRANCH
Compared Against: merge-base ($MERGE_BASE)
## Summary
| Test | Main Time | Branch Time | Δ Time | Status |
|------|-----------|-------------|--------|--------|
EOF
total_main=0
total_branch=0
diff_count=0
ok_count=0
for i in "${!TEST_NAMES[@]}"; do
name="${TEST_NAMES[$i]}"
main_t="${MAIN_TIMES[$i]}"
branch_t="${BRANCH_TIMES[$i]}"
status="${DIFF_STATUS[$i]}"
delta=$(echo "$branch_t - $main_t" | bc)
if (( $(echo "$delta > 0" | bc -l) )); then
delta_str="+${delta}s"
else
delta_str="${delta}s"
fi
if [ "$status" = "DIFF" ]; then
status_str="⚠️ DIFF"
((diff_count++)) || true
else
status_str="✅ OK"
((ok_count++)) || true
fi
total_main=$(echo "$total_main + $main_t" | bc)
total_branch=$(echo "$total_branch + $branch_t" | bc)
echo "| $name | ${main_t}s | ${branch_t}s | $delta_str | $status_str |" >> "$REPORT_FILE"
done
total_delta=$(echo "$total_branch - $total_main" | bc)
if (( $(echo "$total_delta > 0" | bc -l) )); then
total_delta_str="+${total_delta}s"
else
total_delta_str="${total_delta}s"
fi
cat >> "$REPORT_FILE" << EOF
| **TOTAL** | **${total_main}s** | **${total_branch}s** | **$total_delta_str** | **$ok_count OK / $diff_count DIFF** |
## Statistics
- **Tests Passed (no diff):** $ok_count
- **Tests with Differences:** $diff_count
- **Total Main Time:** ${total_main}s
- **Total Branch Time:** ${total_branch}s
- **Overall Time Delta:** $total_delta_str
## Detailed Diffs
EOF
# Add diff details to report
for i in "${!TEST_NAMES[@]}"; do
name="${TEST_NAMES[$i]}"
status="${DIFF_STATUS[$i]}"
if [ "$status" = "DIFF" ]; then
echo "### $name" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
# Check all possible endpoints
for endpoint in initialize tools resources prompts; do
diff_file="$REPORT_DIR/diffs/$name/${endpoint}.diff"
if [ -f "$diff_file" ] && [ -s "$diff_file" ]; then
echo "#### ${endpoint}" >> "$REPORT_FILE"
echo '```diff' >> "$REPORT_FILE"
cat "$diff_file" >> "$REPORT_FILE"
echo '```' >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
fi
done
fi
done
log "${BLUE}=== Conformance Test Complete ===${NC}"
log ""
log "Report: ${GREEN}$REPORT_FILE${NC}"
log ""
# Output summary to stdout (for CI capture)
echo "=== Conformance Test Summary ==="
echo "Tests passed: $ok_count"
echo "Tests with diffs: $diff_count"
echo "Total main time: ${total_main}s"
echo "Total branch time: ${total_branch}s"
echo "Time delta: $total_delta_str"
if [ $diff_count -gt 0 ]; then
log ""
log "${YELLOW}⚠️ Some tests have differences. Review the diffs in:${NC}"
log " $REPORT_DIR/diffs/"
echo ""
echo "RESULT: DIFFERENCES FOUND"
# Don't exit with error - diffs may be intentional improvements
else
echo ""
echo "RESULT: ALL TESTS PASSED"
fi
+72
View File
@@ -0,0 +1,72 @@
#!/bin/bash
# Fetch Octicon icons and convert them to PNG for embedding in the MCP server.
# Generates both light theme (dark icons) and dark theme (white icons) variants.
# Uses sed to modify SVG fill color before converting to PNG.
# Requires: rsvg-convert (from librsvg2-bin on Ubuntu/Debian)
#
# Usage:
# script/fetch-icons # Fetch all required icons
# script/fetch-icons icon1 icon2 # Fetch specific icons
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ICONS_DIR="$REPO_ROOT/pkg/octicons/icons"
REQUIRED_ICONS_FILE="$REPO_ROOT/pkg/octicons/required_icons.txt"
OCTICONS_BASE="https://raw.githubusercontent.com/primer/octicons/main/icons"
# Check for rsvg-convert
if ! command -v rsvg-convert &> /dev/null; then
echo "Error: rsvg-convert not found. Install with:"
echo " Ubuntu/Debian: sudo apt-get install librsvg2-bin"
echo " macOS: brew install librsvg"
exit 1
fi
# Load icons from required_icons.txt or use command-line arguments
if [ $# -gt 0 ]; then
ICONS=("$@")
else
if [ ! -f "$REQUIRED_ICONS_FILE" ]; then
echo "Error: Required icons file not found: $REQUIRED_ICONS_FILE"
exit 1
fi
# Read icons from file, skipping comments and empty lines
mapfile -t ICONS < <(grep -v '^#' "$REQUIRED_ICONS_FILE" | grep -v '^$')
fi
# Ensure icons directory exists
mkdir -p "$ICONS_DIR"
echo "Fetching ${#ICONS[@]} icons (24px, light + dark themes)..."
for icon in "${ICONS[@]}"; do
svg_url="${OCTICONS_BASE}/${icon}-24.svg"
light_file="${ICONS_DIR}/${icon}-light.png"
dark_file="${ICONS_DIR}/${icon}-dark.png"
echo " ${icon} (light + dark)"
# Download SVG
svg_content=$(curl -sfL "$svg_url" 2>/dev/null) || {
echo " Warning: Failed to fetch ${icon}-24.svg (may not exist)"
continue
}
# Light theme: dark icons (#24292f) for light backgrounds
# Add fill attribute to the svg tag
light_svg=$(echo "$svg_content" | sed 's/<svg /<svg fill="#24292f" /')
echo "$light_svg" | rsvg-convert -o "$light_file"
# Dark theme: white icons (#ffffff) for dark backgrounds
dark_svg=$(echo "$svg_content" | sed 's/<svg /<svg fill="#ffffff" /')
echo "$dark_svg" | rsvg-convert -o "$dark_file"
done
echo "Done. Icons saved to $ICONS_DIR"
echo ""
echo "Next steps:"
echo " 1. Run 'go test ./pkg/octicons/...' to verify icons are embedded"
echo " 2. Run 'go test ./pkg/github/...' to verify toolset icons are valid"
echo " 3. Commit the new icon files"
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# This script generates documentation for the GitHub MCP server.
# It needs to be run after tool updates to ensure the latest changes are reflected in the documentation.
go run ./cmd/github-mcp-server generate-docs
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# echo '{"jsonrpc":"2.0","id":3,"params":{"name":"list_discussions","arguments": {"owner": "github", "repo": "securitylab", "first": 10, "since": "2025-04-01T00:00:00Z"}},"method":"tools/call"}' | go run cmd/github-mcp-server/main.go stdio | jq .
echo '{"jsonrpc":"2.0","id":3,"params":{"name":"list_discussions","arguments": {"owner": "github", "repo": "securitylab", "first": 10, "since": "2025-04-01T00:00:00Z", "sort": "CREATED_AT", "direction": "DESC"}},"method":"tools/call"}' | go run cmd/github-mcp-server/main.go stdio | jq .
Executable
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# MCP requires initialize -> notifications/initialized -> tools/call
output=$(
(
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"get-me-script","version":"1.0.0"}}}'
echo '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_me","arguments":{}}}'
sleep 3
) | go run cmd/github-mcp-server/main.go stdio "$@" 2>/dev/null | grep '"id":2'
)
if command -v jq &> /dev/null; then
echo "$output" | jq '{_meta: .result._meta, content: (.result.content[0].text | fromjson)}'
else
echo "$output"
fi
+172
View File
@@ -0,0 +1,172 @@
#!/bin/bash
#
# Generate license files for all platform/arch combinations.
# This script handles architecture-specific dependency differences by:
# 1. Generating separate license reports per GOOS/GOARCH combination
# 2. Grouping identical reports together (comma-separated arch names)
# 3. Creating an index at the top of each platform file
# 4. Copying all license files to third-party/
#
# Note: third-party/ is a union of all license files across all architectures.
# This means that license files for dependencies present in only some architectures
# may still appear in third-party/. This is intentional and ensures compliance.
#
# Note: we ignore warnings because we want the command to succeed, however the output should be checked
# for any new warnings, and potentially we may need to add license information.
#
# Normally these warnings are packages containing non go code, which may or may not require explicit attribution,
# depending on the license.
set -e
# Pinned version for reproducibility
# See: https://github.com/cli/cli/pull/11161
go install github.com/google/go-licenses/v2@v2.0.1
# actions/setup-go does not setup the installed toolchain to be preferred over the system install,
# which causes go-licenses to raise "Package ... does not have module info" errors in CI.
# For more information, https://github.com/google/go-licenses/issues/244#issuecomment-1885098633
if [ "$CI" = "true" ]; then
export GOROOT=$(go env GOROOT)
export PATH=${GOROOT}/bin:$PATH
fi
# actions/setup-go does not setup the installed toolchain to be preferred over the system install,
# which causes go-licenses to raise "Package ... does not have module info" errors in CI.
# For more information, https://github.com/google/go-licenses/issues/244#issuecomment-1885098633
if [ "$CI" = "true" ]; then
export GOROOT=$(go env GOROOT)
export PATH=${GOROOT}/bin:$PATH
fi
rm -rf third-party
mkdir -p third-party
export TEMPDIR="$(mktemp -d)"
trap "rm -fr ${TEMPDIR}" EXIT
# Cross-platform hash function (works on both Linux and macOS)
compute_hash() {
if command -v md5sum >/dev/null 2>&1; then
md5sum | cut -d' ' -f1
elif command -v md5 >/dev/null 2>&1; then
md5 -q
else
# Fallback to cksum if neither is available
cksum | cut -d' ' -f1
fi
}
# Function to get architectures for a given OS
get_archs() {
case "$1" in
linux) echo "386 amd64 arm64" ;;
darwin) echo "amd64 arm64" ;;
windows) echo "386 amd64 arm64" ;;
esac
}
# Generate reports for each platform/arch combination
for goos in darwin linux windows; do
echo "Processing ${goos}..."
archs=$(get_archs "$goos")
for goarch in $archs; do
echo " Generating for ${goos}/${goarch}..."
# Generate the license report for this arch
report_file="${TEMPDIR}/${goos}_${goarch}_report.md"
GOOS="${goos}" GOARCH="${goarch}" GOFLAGS=-mod=mod go-licenses report ./... --template .github/licenses.tmpl > "${report_file}" 2>/dev/null || echo " (warnings ignored for ${goos}/${goarch})"
# Save licenses to temp directory
GOOS="${goos}" GOARCH="${goarch}" GOFLAGS=-mod=mod go-licenses save ./... --save_path="${TEMPDIR}/${goos}_${goarch}" --force 2>/dev/null || echo " (warnings ignored for ${goos}/${goarch})"
# Copy to third-party (accumulate all - union of all architectures for compliance)
if [ -d "${TEMPDIR}/${goos}_${goarch}" ]; then
cp -fR "${TEMPDIR}/${goos}_${goarch}"/* third-party/ 2>/dev/null || true
fi
# Extract just the package list (skip header), sort it, and hash it
# Use LC_ALL=C for consistent sorting across different systems
packages_file="${TEMPDIR}/${goos}_${goarch}_packages.txt"
if [ -s "${report_file}" ] && grep -qE '^ - \[' "${report_file}" 2>/dev/null; then
grep -E '^ - \[' "${report_file}" | LC_ALL=C sort > "${packages_file}"
hash=$(cat "${packages_file}" | compute_hash)
else
echo "(FAILED TO GENERATE LICENSE REPORT FOR ${goos}/${goarch})" > "${packages_file}"
hash="FAILED_${goos}_${goarch}"
fi
# Store hash for grouping
echo "${hash}" > "${TEMPDIR}/${goos}_${goarch}_hash.txt"
done
# Group architectures with identical reports (deterministic order)
# Create groups file: hash -> comma-separated archs
groups_file="${TEMPDIR}/${goos}_groups.txt"
rm -f "${groups_file}"
# Process architectures in order to build groups
for goarch in $archs; do
hash=$(cat "${TEMPDIR}/${goos}_${goarch}_hash.txt")
# Check if we've seen this hash before
if grep -q "^${hash}:" "${groups_file}" 2>/dev/null; then
# Append to existing group
existing=$(grep "^${hash}:" "${groups_file}" | cut -d: -f2)
sed -i.bak "s/^${hash}:.*/${hash}:${existing}, ${goarch}/" "${groups_file}"
rm -f "${groups_file}.bak"
else
# New group
echo "${hash}:${goarch}" >> "${groups_file}"
fi
done
# Generate the combined report for this platform
output_file="third-party-licenses.${goos}.md"
cat > "${output_file}" << 'EOF'
# GitHub MCP Server dependencies
The following open source dependencies are used to build the [github/github-mcp-server][] GitHub Model Context Protocol Server.
## Table of Contents
EOF
# Build table of contents (sorted for determinism)
# Use LC_ALL=C for consistent sorting across different systems
LC_ALL=C sort "${groups_file}" | while IFS=: read -r hash group_archs; do
# Create anchor-friendly name
anchor=$(echo "${group_archs}" | tr ', ' '-' | tr -s '-')
echo "- [${group_archs}](#${anchor})" >> "${output_file}"
done
echo "" >> "${output_file}"
echo "---" >> "${output_file}"
echo "" >> "${output_file}"
# Add each unique report section (sorted for determinism)
# Use LC_ALL=C for consistent sorting across different systems
LC_ALL=C sort "${groups_file}" | while IFS=: read -r hash group_archs; do
# Get the packages from the first arch in this group
first_arch=$(echo "${group_archs}" | cut -d',' -f1 | tr -d ' ')
packages=$(cat "${TEMPDIR}/${goos}_${first_arch}_packages.txt")
cat >> "${output_file}" << EOF
## ${group_archs}
The following packages are included for the ${group_archs} architectures.
${packages}
EOF
done
# Add footer
echo "[github/github-mcp-server]: https://github.com/github/github-mcp-server" >> "${output_file}"
echo "Generated ${output_file}"
done
echo "Done! License files generated."
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
#
# Check that license files are up to date.
# This script regenerates the license files and compares them with the committed versions.
# If there are differences, it exits with an error.
set -e
# Store original files for comparison
TEMPDIR="$(mktemp -d)"
trap "rm -fr ${TEMPDIR}" EXIT
# Save original license markdown files
for goos in darwin linux windows; do
cp "third-party-licenses.${goos}.md" "${TEMPDIR}/"
done
# Save the state of third-party directory
cp -r third-party "${TEMPDIR}/third-party.orig"
# Regenerate using the same script
./script/licenses
# Check for any differences in workspace
if ! git diff --exit-code --quiet third-party-licenses.*.md third-party/; then
echo "License files are out of date:"
git diff third-party-licenses.*.md third-party/
echo ""
printf "\nLicense check failed.\n\nPlease update the license files by running \`./script/licenses\` and committing the output.\n"
exit 1
fi
echo "License check passed for all platforms."
Executable
+15
View File
@@ -0,0 +1,15 @@
set -eu
# first run go fmt
gofmt -s -w .
BINDIR="$(git rev-parse --show-toplevel)"/bin
BINARY=$BINDIR/golangci-lint
# sync with .github/workflows/lint.yml
GOLANGCI_LINT_VERSION=v2.9.0
if [ ! -f "$BINARY" ]; then
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b "$BINDIR" "$GOLANGCI_LINT_VERSION"
fi
$BINARY run
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
#
# List required OAuth scopes for enabled tools.
#
# Usage:
# script/list-scopes [--toolsets=...] [--output=text|json|summary]
#
# Examples:
# script/list-scopes
# script/list-scopes --toolsets=all --output=json
# script/list-scopes --toolsets=repos,issues --output=summary
#
set -e
cd "$(dirname "$0")/.."
# Build the server if it doesn't exist or is outdated
if [ ! -f github-mcp-server ] || [ cmd/github-mcp-server/list_scopes.go -nt github-mcp-server ]; then
echo "Building github-mcp-server..." >&2
go build -o github-mcp-server ./cmd/github-mcp-server
fi
exec ./github-mcp-server list-scopes "$@"
+78
View File
@@ -0,0 +1,78 @@
#!/bin/bash
# Script to pretty print the output of the github-mcp-server
# log.
#
# It uses colored output when running on a terminal.
# show script help
show_help() {
cat <<EOF
Usage: $(basename "$0") [file]
If [file] is provided, input is read from that file.
If no argument is given, input is read from stdin.
Options:
-h, --help Show this help message and exit
EOF
}
# choose color for stdin or stdout if we are printing to
# an actual terminal
color(){
io="$1"
if [[ "$io" == "stdin" ]]; then
color="\033[0;32m" # green
else
color="\033[0;36m" # cyan
fi
if [ ! $is_terminal = "1" ]; then
color=""
fi
echo -e "${color}[$io]"
}
# reset code if we are printing to an actual terminal
reset(){
if [ ! $is_terminal = "1" ]; then
return
fi
echo -e "\033[0m"
}
# Handle -h or --help
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
show_help
exit 0
fi
# Determine input source
if [[ -n "$1" ]]; then
if [[ ! -r "$1" ]]; then
echo "Error: File '$1' not found or not readable." >&2
exit 1
fi
input="$1"
else
input="/dev/stdin"
fi
# check if we are in a terminal for showing colors
if test -t 1; then
is_terminal="1"
else
is_terminal="0"
fi
# Processs each log line, print whether is stdin or stdout, using different
# colors if we output to a terminal, and pretty print json data using jq
sed -nE 's/^.*\[(stdin|stdout)\]:.* ([0-9]+) bytes: (.*)\\n"$/\1 \2 \3/p' $input |
while read -r io bytes json; do
# Unescape the JSON string safely
unescaped=$(echo "$json" | awk '{ print "echo -e \"" $0 "\" | jq ." }' | bash)
echo "$(color $io)($bytes bytes):$(reset)"
echo "$unescaped" | jq .
echo
done
+217
View File
@@ -0,0 +1,217 @@
// Command print-mcp-diff-configs emits the configuration matrix consumed by
// the mcp-server-diff GitHub Action. The matrix is composed of three parts:
//
// 1. Hand-curated baseline configs (default, read-only, common toolset combos)
// 2. Insiders configs (--insiders, --insiders --read-only) — meta flag that
// expands to the curated insiders feature set
// 3. One config per entry in github.AllowedFeatureFlags — automatically kept
// in sync with the Go source so any new user-controllable feature flag
// gets diffed without touching the workflow
//
// The same logical matrix is rendered for two transports, selected by
// -transport:
//
// stdio Default. Args are appended to the action's top-level
//
// start_command (one stdio process per config).
//
// http-headers streamable-http transport against a shared HTTP server. The
//
// server is started once with no extra flags and every config
// provides its settings via X-MCP-* request headers, mirroring
// how the remote server is invoked in production (server-side
// defaults + per-user header overrides).
//
// Usage:
//
// go run ./script/print-mcp-diff-configs
// go run ./script/print-mcp-diff-configs -transport http-headers
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"sort"
"strings"
"github.com/github/github-mcp-server/pkg/github"
mcphdr "github.com/github/github-mcp-server/pkg/http/headers"
)
type config struct {
Name string `json:"name"`
Args string `json:"args,omitempty"`
Transport string `json:"transport,omitempty"`
ServerURL string `json:"server_url,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
}
// baseEntry describes one logical configuration in transport-agnostic form.
// settings are translated to either CLI flags or X-MCP-* headers depending on
// the target transport.
type baseEntry struct {
name string
settings settings
}
type settings struct {
toolsets string // comma-separated, "" for defaults
tools string
excludeTools string
features string
readOnly bool
insiders bool
lockdown bool
}
const httpServerURL = "http://localhost:8082/mcp"
func main() {
transport := flag.String("transport", "stdio", "Transport to target: stdio or http-headers")
flag.Parse()
entries := baseEntries()
var out []config
switch *transport {
case "stdio":
for _, e := range entries {
out = append(out, config{Name: e.name, Args: e.settings.toArgs()})
}
case "http-headers":
for _, e := range entries {
h := e.settings.toHeaders()
if h == nil {
h = map[string]string{}
}
// The action's top-level headers may be replaced (not merged) by
// per-config headers, so always include the bearer token here.
// The token must match a recognized GitHub prefix so the server's
// Authorization parser accepts it without contacting the API.
h[mcphdr.AuthorizationHeader] = "Bearer ghp_test"
out = append(out, config{
Name: e.name,
Transport: "streamable-http",
ServerURL: httpServerURL,
Headers: h,
})
}
default:
fmt.Fprintf(os.Stderr, "unknown transport %q (want stdio or http-headers)\n", *transport)
os.Exit(2)
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(out); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func baseEntries() []baseEntry {
entries := []baseEntry{
{name: "default"},
{name: "read-only", settings: settings{readOnly: true}},
{name: "toolsets-repos", settings: settings{toolsets: "repos"}},
{name: "toolsets-issues", settings: settings{toolsets: "issues"}},
{name: "toolsets-context", settings: settings{toolsets: "context"}},
{name: "toolsets-pull_requests", settings: settings{toolsets: "pull_requests"}},
{name: "toolsets-repos,issues", settings: settings{toolsets: "repos,issues"}},
{name: "toolsets-issues,context", settings: settings{toolsets: "issues,context"}},
{name: "toolsets-all", settings: settings{toolsets: "all"}},
{name: "tools-get_me", settings: settings{tools: "get_me"}},
{name: "tools-get_me,list_issues", settings: settings{tools: "get_me,list_issues"}},
{name: "toolsets-repos+read-only", settings: settings{toolsets: "repos", readOnly: true}},
{name: "insiders", settings: settings{insiders: true}},
{name: "insiders+read-only", settings: settings{insiders: true, readOnly: true}},
// Combined entries: exercise multiple settings together so we catch
// regressions when several X-MCP-* headers (or CLI flags) are merged.
{name: "combined-toolsets+exclude+readonly", settings: settings{
toolsets: "repos,issues",
excludeTools: "delete_file",
readOnly: true,
}},
{name: "combined-insiders+toolsets+features", settings: settings{
insiders: true,
toolsets: "repos",
features: firstFeatureFlag(),
}},
}
flags := append([]string(nil), github.AllowedFeatureFlags...)
sort.Strings(flags)
for _, f := range flags {
entries = append(entries, baseEntry{
name: "feature-" + f,
settings: settings{features: f},
})
}
return entries
}
func (s settings) toArgs() string {
var parts []string
if s.toolsets != "" {
parts = append(parts, "--toolsets="+s.toolsets)
}
if s.tools != "" {
parts = append(parts, "--tools="+s.tools)
}
if s.excludeTools != "" {
parts = append(parts, "--exclude-tools="+s.excludeTools)
}
if s.features != "" {
parts = append(parts, "--features="+s.features)
}
if s.readOnly {
parts = append(parts, "--read-only")
}
if s.insiders {
parts = append(parts, "--insiders")
}
if s.lockdown {
parts = append(parts, "--lockdown-mode")
}
return strings.Join(parts, " ")
}
func (s settings) toHeaders() map[string]string {
h := map[string]string{}
if s.toolsets != "" {
h[mcphdr.MCPToolsetsHeader] = s.toolsets
}
if s.tools != "" {
h[mcphdr.MCPToolsHeader] = s.tools
}
if s.excludeTools != "" {
h[mcphdr.MCPExcludeToolsHeader] = s.excludeTools
}
if s.features != "" {
h[mcphdr.MCPFeaturesHeader] = s.features
}
if s.readOnly {
h[mcphdr.MCPReadOnlyHeader] = "true"
}
if s.insiders {
h[mcphdr.MCPInsidersHeader] = "true"
}
if s.lockdown {
h[mcphdr.MCPLockdownHeader] = "true"
}
if len(h) == 0 {
return nil
}
return h
}
func firstFeatureFlag() string {
flags := append([]string(nil), github.AllowedFeatureFlags...)
if len(flags) == 0 {
return ""
}
sort.Strings(flags)
return flags[0]
}
+151
View File
@@ -0,0 +1,151 @@
#!/bin/bash
# Exit immediately if a command exits with a non-zero status.
set -e
# Initialize variables
TAG=""
DRY_RUN=false
# Parse arguments
for arg in "$@"; do
case $arg in
--dry-run)
DRY_RUN=true
;;
*)
# The first non-flag argument is the tag
if [[ ! $arg == --* ]]; then
if [ -z "$TAG" ]; then
TAG=$arg
fi
fi
;;
esac
done
if [ "$DRY_RUN" = true ]; then
echo "DRY RUN: No changes will be pushed to the remote repository."
echo
fi
# 1. Validate input
if [ -z "$TAG" ]; then
echo "Error: No tag specified."
echo "Usage: ./script/tag-release vX.Y.Z [--dry-run]"
exit 1
fi
# Regular expression for semantic versioning (vX.Y.Z or vX.Y.Z-suffix)
if [[ ! $TAG =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-.*)?$ ]]; then
echo "Error: Tag must be in format vX.Y.Z or vX.Y.Z-suffix (e.g., v1.0.0 or v1.0.0-rc1)"
exit 1
fi
# 2. Check current branch
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$CURRENT_BRANCH" != "main" ]; then
echo "Error: You must be on the 'main' branch to create a release."
echo "Current branch is '$CURRENT_BRANCH'."
exit 1
fi
# 3. Fetch latest from origin
echo "Fetching latest changes from origin..."
git fetch origin main
# 4. Check if the working directory is clean
if ! git diff-index --quiet HEAD --; then
echo "Error: Working directory is not clean. Please commit or stash your changes."
exit 1
fi
# 5. Check if main is up-to-date with origin/main
LOCAL_SHA=$(git rev-parse @)
REMOTE_SHA=$(git rev-parse @{u})
if [ "$LOCAL_SHA" != "$REMOTE_SHA" ]; then
echo "Error: Your local 'main' branch is not up-to-date with 'origin/main'. Please pull the latest changes."
exit 1
fi
echo "✅ Local 'main' branch is up-to-date with 'origin/main'."
# 6. Check if tag already exists
if git tag -l | grep -q "^${TAG}$"; then
echo "Error: Tag ${TAG} already exists locally."
exit 1
fi
if git ls-remote --tags origin | grep -q "refs/tags/${TAG}$"; then
echo "Error: Tag ${TAG} already exists on remote 'origin'."
exit 1
fi
# 7. Confirm release with user
echo
LATEST_TAG=$(git tag --sort=-version:refname | head -n 1)
if [ -n "$LATEST_TAG" ]; then
echo "Current latest release: $LATEST_TAG"
fi
echo "Proposed new release: $TAG"
echo
read -p "Do you want to proceed with the release? (y/n) " -n 1 -r
echo # Move to a new line
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Release cancelled."
exit 1
fi
echo
# 8. Create the new release tag
if [ "$DRY_RUN" = true ]; then
echo "DRY RUN: Skipping creation of tag $TAG."
else
echo "Creating new release tag: $TAG"
git tag -a "$TAG" -m "Release $TAG"
fi
# 9. Push the new tag to the remote repository
if [ "$DRY_RUN" = true ]; then
echo "DRY RUN: Skipping push of tag $TAG to origin."
else
echo "Pushing tag $TAG to origin..."
git push origin "$TAG"
fi
# 10. Update and push the 'latest-release' tag
if [ "$DRY_RUN" = true ]; then
echo "DRY RUN: Skipping update and push of 'latest-release' tag."
else
echo "Updating 'latest-release' tag to point to $TAG..."
git tag -f latest-release "$TAG"
echo "Pushing 'latest-release' tag to origin..."
git push origin latest-release --force
fi
if [ "$DRY_RUN" = true ]; then
echo "✅ DRY RUN complete. No tags were created or pushed."
else
echo "✅ Successfully tagged and pushed release $TAG."
echo "✅ 'latest-release' tag has been updated."
fi
# 11. Post-release instructions
REPO_URL=$(git remote get-url origin)
REPO_SLUG=$(echo "$REPO_URL" | sed -e 's/.*github.com[:\/]//' -e 's/\.git$//')
cat << EOF
## 🎉 Release $TAG has been initiated!
### Next steps:
1. 📋 Check https://github.com/$REPO_SLUG/releases and wait for the draft release to show up (after the goreleaser workflow completes)
2. ✏️ Edit the new release, delete the existing notes and click the auto-generate button GitHub provides
3. ✨ Add a section at the top calling out the main features
4. 🚀 Publish the release
5. 📢 Post message in #gh-mcp-releases channel in Slack and then share to the other mcp channels
### Resources:
- 📦 Draft Release: https://github.com/$REPO_SLUG/releases/tag/$TAG
The release process is now ready for your review and completion!
EOF
Executable
+3
View File
@@ -0,0 +1,3 @@
set -eu
go test -race ./...