@jtalk22/slack-mcp 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -13
- package/docs/SETUP.md +64 -29
- package/docs/TROUBLESHOOTING.md +28 -0
- package/lib/handlers.js +156 -0
- package/lib/slack-client.js +11 -3
- package/lib/token-store.js +6 -5
- package/lib/tools.js +131 -0
- package/package.json +15 -8
- package/public/index.html +10 -6
- package/public/share.html +6 -5
- package/scripts/setup-wizard.js +1 -1
- package/server.json +8 -2
- package/src/server-http.js +16 -1
- package/src/server.js +31 -7
- package/src/web-server.js +117 -4
- package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +0 -67
- package/docs/COMMUNICATION-STYLE.md +0 -66
- package/docs/COMPATIBILITY.md +0 -19
- package/docs/DEPLOYMENT-MODES.md +0 -55
- package/docs/HN-LAUNCH.md +0 -72
- package/docs/INDEX.md +0 -41
- package/docs/INSTALL-PROOF.md +0 -18
- package/docs/LAUNCH-COPY-v3.0.0.md +0 -101
- package/docs/LAUNCH-MATRIX.md +0 -22
- package/docs/LAUNCH-OPS.md +0 -71
- package/docs/RELEASE-HEALTH.md +0 -77
- package/docs/SUPPORT-BOUNDARIES.md +0 -49
- package/docs/USE_CASE_RECIPES.md +0 -69
- package/docs/WEB-API.md +0 -303
- package/docs/images/demo-channel-messages.png +0 -0
- package/docs/images/demo-channels.png +0 -0
- package/docs/images/demo-claude-mobile-360x800.png +0 -0
- package/docs/images/demo-claude-mobile-390x844.png +0 -0
- package/docs/images/demo-claude-mobile-poster.png +0 -0
- package/docs/images/demo-main-mobile-360x800.png +0 -0
- package/docs/images/demo-main-mobile-390x844.png +0 -0
- package/docs/images/demo-main.png +0 -0
- package/docs/images/demo-messages.png +0 -0
- package/docs/images/demo-poster.png +0 -0
- package/docs/images/demo-sidebar.png +0 -0
- package/docs/images/diagram-oauth-comparison.svg +0 -80
- package/docs/images/diagram-session-flow.svg +0 -105
- package/docs/images/social-preview-v3.png +0 -0
- package/docs/images/web-api-mobile-360x800.png +0 -0
- package/docs/images/web-api-mobile-390x844.png +0 -0
- package/public/demo-claude.html +0 -1974
- package/public/demo-video.html +0 -244
- package/public/demo.html +0 -1196
- package/scripts/build-mobile-demo.js +0 -168
- package/scripts/build-release-health-delta.js +0 -201
- package/scripts/build-social-preview.js +0 -189
- package/scripts/capture-screenshots.js +0 -152
- package/scripts/check-owner-attribution.sh +0 -131
- package/scripts/check-public-language.sh +0 -26
- package/scripts/check-version-parity.js +0 -218
- package/scripts/cloudflare-browser-tool.js +0 -237
- package/scripts/collect-release-health.js +0 -162
- package/scripts/impact-push-v3.js +0 -781
- package/scripts/record-demo.js +0 -163
- package/scripts/release-preflight.js +0 -247
- package/scripts/setup-git-hooks.sh +0 -15
- package/scripts/update-github-social-preview.js +0 -208
- package/scripts/verify-core.js +0 -159
- package/scripts/verify-install-flow.js +0 -193
- package/scripts/verify-web.js +0 -273
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
EXPECTED_NAME="${EXPECTED_GIT_NAME:-jtalk22}"
|
|
5
|
-
EXPECTED_EMAIL="${EXPECTED_GIT_EMAIL:-james@revasser.nyc}"
|
|
6
|
-
ALLOW_GITHUB_WEB_COMMITTER="${ALLOW_GITHUB_WEB_COMMITTER:-0}"
|
|
7
|
-
BANNED_REGEX='(?i)(co-authored-by|generated with|\bclaude\b|\bgpt\b|\bcopilot\b|\bgemini\b|\bai\b)'
|
|
8
|
-
|
|
9
|
-
die() {
|
|
10
|
-
echo "ERROR: $*" >&2
|
|
11
|
-
exit 1
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
contains_banned_markers() {
|
|
15
|
-
local text="$1"
|
|
16
|
-
if command -v rg >/dev/null 2>&1; then
|
|
17
|
-
rg -Niq "$BANNED_REGEX" <<<"$text"
|
|
18
|
-
else
|
|
19
|
-
grep -Eiq '(Co-authored-by|Generated with|Claude|GPT|Copilot|Gemini)' <<<"$text" \
|
|
20
|
-
|| grep -Eiq '(^|[^[:alnum:]_])[Aa][Ii]([^[:alnum:]_]|$)' <<<"$text"
|
|
21
|
-
fi
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
is_allowed_owner_author() {
|
|
25
|
-
local name="$1"
|
|
26
|
-
local email="$2"
|
|
27
|
-
|
|
28
|
-
if [[ "$name" != "$EXPECTED_NAME" ]]; then
|
|
29
|
-
return 1
|
|
30
|
-
fi
|
|
31
|
-
|
|
32
|
-
if [[ "$email" == "$EXPECTED_EMAIL" ]]; then
|
|
33
|
-
return 0
|
|
34
|
-
fi
|
|
35
|
-
|
|
36
|
-
if [[ "$ALLOW_GITHUB_WEB_COMMITTER" == "1" ]]; then
|
|
37
|
-
if [[ "$email" == "${EXPECTED_NAME}@users.noreply.github.com" || "$email" =~ ^[0-9]+\+${EXPECTED_NAME}@users\.noreply\.github\.com$ ]]; then
|
|
38
|
-
return 0
|
|
39
|
-
fi
|
|
40
|
-
fi
|
|
41
|
-
|
|
42
|
-
return 1
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
is_allowed_owner_committer() {
|
|
46
|
-
local committer_name="$1"
|
|
47
|
-
local committer_email="$2"
|
|
48
|
-
local author_name="$3"
|
|
49
|
-
local author_email="$4"
|
|
50
|
-
|
|
51
|
-
if [[ "$committer_name" == "$EXPECTED_NAME" && "$committer_email" == "$EXPECTED_EMAIL" ]]; then
|
|
52
|
-
return 0
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
if [[ "$ALLOW_GITHUB_WEB_COMMITTER" == "1" ]]; then
|
|
56
|
-
if [[ "$committer_name" == "$EXPECTED_NAME" ]]; then
|
|
57
|
-
if [[ "$committer_email" == "${EXPECTED_NAME}@users.noreply.github.com" || "$committer_email" =~ ^[0-9]+\+${EXPECTED_NAME}@users\.noreply\.github\.com$ ]]; then
|
|
58
|
-
if is_allowed_owner_author "$author_name" "$author_email"; then
|
|
59
|
-
return 0
|
|
60
|
-
fi
|
|
61
|
-
fi
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
if [[ "$committer_name" == "GitHub" && "$committer_email" == "noreply@github.com" ]]; then
|
|
65
|
-
if is_allowed_owner_author "$author_name" "$author_email"; then
|
|
66
|
-
return 0
|
|
67
|
-
fi
|
|
68
|
-
fi
|
|
69
|
-
fi
|
|
70
|
-
|
|
71
|
-
return 1
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if [[ "${SKIP_LOCAL_CONFIG_CHECK:-0}" != "1" ]]; then
|
|
75
|
-
local_name="$(git config --get user.name || true)"
|
|
76
|
-
local_email="$(git config --get user.email || true)"
|
|
77
|
-
|
|
78
|
-
[[ -n "$local_name" ]] || die "Missing repo-local git user.name"
|
|
79
|
-
[[ -n "$local_email" ]] || die "Missing repo-local git user.email"
|
|
80
|
-
|
|
81
|
-
[[ "$local_name" == "$EXPECTED_NAME" ]] \
|
|
82
|
-
|| die "Repo-local user.name is '$local_name' (expected '$EXPECTED_NAME')"
|
|
83
|
-
[[ "$local_email" == "$EXPECTED_EMAIL" ]] \
|
|
84
|
-
|| die "Repo-local user.email is '$local_email' (expected '$EXPECTED_EMAIL')"
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
|
-
default_range="HEAD"
|
|
88
|
-
if git rev-parse --verify origin/main >/dev/null 2>&1; then
|
|
89
|
-
default_range="origin/main..HEAD"
|
|
90
|
-
fi
|
|
91
|
-
|
|
92
|
-
range="${1:-${GIT_CHECK_RANGE:-$default_range}}"
|
|
93
|
-
|
|
94
|
-
git rev-list --count "$range" >/dev/null 2>&1 || die "Invalid commit range: $range"
|
|
95
|
-
commit_count="$(git rev-list --count "$range")"
|
|
96
|
-
|
|
97
|
-
if [[ "$commit_count" -eq 0 ]]; then
|
|
98
|
-
echo "No commits to validate in range '$range'."
|
|
99
|
-
exit 0
|
|
100
|
-
fi
|
|
101
|
-
|
|
102
|
-
errors=0
|
|
103
|
-
|
|
104
|
-
while IFS= read -r sha; do
|
|
105
|
-
author_name="$(git show -s --format=%an "$sha")"
|
|
106
|
-
author_email="$(git show -s --format=%ae "$sha")"
|
|
107
|
-
committer_name="$(git show -s --format=%cn "$sha")"
|
|
108
|
-
committer_email="$(git show -s --format=%ce "$sha")"
|
|
109
|
-
body="$(git show -s --format=%B "$sha")"
|
|
110
|
-
|
|
111
|
-
if ! is_allowed_owner_author "$author_name" "$author_email"; then
|
|
112
|
-
echo "Commit $sha has author '$author_name <$author_email>' (expected '$EXPECTED_NAME <$EXPECTED_EMAIL>')." >&2
|
|
113
|
-
errors=1
|
|
114
|
-
fi
|
|
115
|
-
|
|
116
|
-
if ! is_allowed_owner_committer "$committer_name" "$committer_email" "$author_name" "$author_email"; then
|
|
117
|
-
echo "Commit $sha has committer '$committer_name <$committer_email>' (expected '$EXPECTED_NAME <$EXPECTED_EMAIL>')." >&2
|
|
118
|
-
errors=1
|
|
119
|
-
fi
|
|
120
|
-
|
|
121
|
-
if contains_banned_markers "$body"; then
|
|
122
|
-
echo "Commit $sha contains disallowed attribution markers in commit message." >&2
|
|
123
|
-
errors=1
|
|
124
|
-
fi
|
|
125
|
-
done < <(git rev-list "$range")
|
|
126
|
-
|
|
127
|
-
if [[ "$errors" -ne 0 ]]; then
|
|
128
|
-
exit 1
|
|
129
|
-
fi
|
|
130
|
-
|
|
131
|
-
echo "Owner-only attribution check passed for $commit_count commit(s) in '$range'."
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
-
|
|
6
|
-
# Terms that tend to read as hype/manipulative for technical audiences.
|
|
7
|
-
DISALLOWED='(?i)(\bstealth\b|\bgrowth\b|social-proof|social proof|share kit|\bviral\b|growth loop|\bgrindset\b|\bdominate\b|hack growth|edge line|launch-ready)'
|
|
8
|
-
|
|
9
|
-
SCAN_PATHS=(
|
|
10
|
-
"$ROOT/README.md"
|
|
11
|
-
"$ROOT/index.html"
|
|
12
|
-
"$ROOT/docs"
|
|
13
|
-
"$ROOT/public"
|
|
14
|
-
"$ROOT/.github/ISSUE_REPLY_TEMPLATE.md"
|
|
15
|
-
"$ROOT/.github/RELEASE_NOTES_TEMPLATE.md"
|
|
16
|
-
"$ROOT/docs/COMMUNICATION-STYLE.md"
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
echo "Scanning public-facing text for disallowed wording..."
|
|
20
|
-
|
|
21
|
-
if rg -Nni "$DISALLOWED" "${SCAN_PATHS[@]}"; then
|
|
22
|
-
echo "Disallowed public wording found. Use neutral reliability/compatibility language."
|
|
23
|
-
exit 1
|
|
24
|
-
fi
|
|
25
|
-
|
|
26
|
-
echo "Public wording check passed."
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const repoRoot = join(__dirname, "..");
|
|
9
|
-
|
|
10
|
-
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
|
|
11
|
-
const serverMeta = JSON.parse(readFileSync(join(repoRoot, "server.json"), "utf8"));
|
|
12
|
-
|
|
13
|
-
const outputArg = process.argv.includes("--out")
|
|
14
|
-
? process.argv[process.argv.indexOf("--out") + 1]
|
|
15
|
-
: process.argv.includes("--public")
|
|
16
|
-
? "docs/release-health/version-parity.md"
|
|
17
|
-
: "output/release-health/version-parity.md";
|
|
18
|
-
const allowPropagation = process.argv.includes("--allow-propagation");
|
|
19
|
-
|
|
20
|
-
const mcpServerName = serverMeta.name;
|
|
21
|
-
const expectedWebsiteUrl = serverMeta.websiteUrl;
|
|
22
|
-
const expectedDescriptionPrefix = serverMeta.description;
|
|
23
|
-
const smitheryEndpoint = "https://server.smithery.ai/jtalk22/slack-mcp-server";
|
|
24
|
-
const smitheryListingUrl = "https://smithery.ai/server/jtalk22/slack-mcp-server";
|
|
25
|
-
|
|
26
|
-
async function fetchJson(url) {
|
|
27
|
-
const res = await fetch(url);
|
|
28
|
-
if (!res.ok) {
|
|
29
|
-
throw new Error(`HTTP ${res.status} for ${url}`);
|
|
30
|
-
}
|
|
31
|
-
return res.json();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function fetchStatus(url) {
|
|
35
|
-
const res = await fetch(url);
|
|
36
|
-
const text = await res.text().catch(() => "");
|
|
37
|
-
return { ok: res.ok, status: res.status, text };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function row(surface, version, status, note = "") {
|
|
41
|
-
return `| ${surface} | ${version || "n/a"} | ${status} | ${note} |`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function main() {
|
|
45
|
-
const localVersion = pkg.version;
|
|
46
|
-
const localServerVersion = serverMeta.version;
|
|
47
|
-
const localServerPkgVersion = serverMeta.packages?.[0]?.version || null;
|
|
48
|
-
|
|
49
|
-
let npmVersion = null;
|
|
50
|
-
let mcpRegistryVersion = null;
|
|
51
|
-
let mcpRegistryWebsiteUrl = null;
|
|
52
|
-
let mcpRegistryDescription = null;
|
|
53
|
-
let smitheryReachable = null;
|
|
54
|
-
let smitheryStatus = null;
|
|
55
|
-
let npmError = null;
|
|
56
|
-
let mcpError = null;
|
|
57
|
-
let smitheryError = null;
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const npmMeta = await fetchJson(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`);
|
|
61
|
-
npmVersion = npmMeta?.["dist-tags"]?.latest || null;
|
|
62
|
-
} catch (error) {
|
|
63
|
-
npmError = String(error?.message || error);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const registry = await fetchJson(
|
|
68
|
-
`https://registry.modelcontextprotocol.io/v0/servers/${encodeURIComponent(mcpServerName)}/versions/latest`
|
|
69
|
-
);
|
|
70
|
-
mcpRegistryVersion = registry?.server?.version || null;
|
|
71
|
-
mcpRegistryWebsiteUrl = registry?.server?.websiteUrl || null;
|
|
72
|
-
mcpRegistryDescription = registry?.server?.description || null;
|
|
73
|
-
} catch (error) {
|
|
74
|
-
mcpError = String(error?.message || error);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const apiResult = await fetchStatus(smitheryEndpoint);
|
|
79
|
-
smitheryStatus = apiResult.status;
|
|
80
|
-
if (apiResult.ok) {
|
|
81
|
-
smitheryReachable = true;
|
|
82
|
-
} else if (apiResult.status === 401 || apiResult.status === 403) {
|
|
83
|
-
// Auth-gated endpoint still indicates the listing endpoint is live.
|
|
84
|
-
smitheryReachable = true;
|
|
85
|
-
} else {
|
|
86
|
-
const listingResult = await fetchStatus(smitheryListingUrl);
|
|
87
|
-
smitheryStatus = `${apiResult.status} (api), ${listingResult.status} (listing)`;
|
|
88
|
-
smitheryReachable = listingResult.ok && listingResult.text.length > 0;
|
|
89
|
-
}
|
|
90
|
-
} catch (error) {
|
|
91
|
-
smitheryError = String(error?.message || error);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const parityChecks = [
|
|
95
|
-
{ name: "package.json vs server.json", ok: localVersion === localServerVersion },
|
|
96
|
-
{ name: "package.json vs server.json package", ok: localVersion === localServerPkgVersion },
|
|
97
|
-
{ name: "npm latest", ok: npmVersion === localVersion },
|
|
98
|
-
{ name: "MCP registry latest", ok: mcpRegistryVersion === localVersion },
|
|
99
|
-
{ name: "MCP registry websiteUrl", ok: mcpRegistryWebsiteUrl === expectedWebsiteUrl },
|
|
100
|
-
{
|
|
101
|
-
name: "MCP registry description prefix",
|
|
102
|
-
ok: typeof mcpRegistryDescription === "string" && mcpRegistryDescription.startsWith(expectedDescriptionPrefix),
|
|
103
|
-
},
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
const externalMismatchNames = new Set([
|
|
107
|
-
"npm latest",
|
|
108
|
-
"MCP registry latest",
|
|
109
|
-
"MCP registry websiteUrl",
|
|
110
|
-
"MCP registry description prefix",
|
|
111
|
-
]);
|
|
112
|
-
const externalMismatches = parityChecks
|
|
113
|
-
.filter((check) => !check.ok && externalMismatchNames.has(check.name));
|
|
114
|
-
const hardFailures = parityChecks
|
|
115
|
-
.filter((check) => !check.ok && !externalMismatchNames.has(check.name));
|
|
116
|
-
|
|
117
|
-
const now = new Date().toISOString();
|
|
118
|
-
const lines = [
|
|
119
|
-
"# Version Parity Report",
|
|
120
|
-
"",
|
|
121
|
-
`- Generated: ${now}`,
|
|
122
|
-
`- Local target version: ${localVersion}`,
|
|
123
|
-
"",
|
|
124
|
-
"## Surface Matrix",
|
|
125
|
-
"",
|
|
126
|
-
"| Surface | Version | Status | Notes |",
|
|
127
|
-
"|---|---|---|---|",
|
|
128
|
-
row(
|
|
129
|
-
"package.json",
|
|
130
|
-
localVersion,
|
|
131
|
-
"ok"
|
|
132
|
-
),
|
|
133
|
-
row(
|
|
134
|
-
"server.json (root)",
|
|
135
|
-
localServerVersion,
|
|
136
|
-
localServerVersion === localVersion ? "ok" : "mismatch"
|
|
137
|
-
),
|
|
138
|
-
row(
|
|
139
|
-
"server.json (package entry)",
|
|
140
|
-
localServerPkgVersion,
|
|
141
|
-
localServerPkgVersion === localVersion ? "ok" : "mismatch"
|
|
142
|
-
),
|
|
143
|
-
row(
|
|
144
|
-
"npm dist-tag latest",
|
|
145
|
-
npmVersion,
|
|
146
|
-
npmVersion === localVersion ? "ok" : "mismatch",
|
|
147
|
-
npmError ? `fetch_error: ${npmError}` : ""
|
|
148
|
-
),
|
|
149
|
-
row(
|
|
150
|
-
"MCP Registry latest",
|
|
151
|
-
mcpRegistryVersion,
|
|
152
|
-
mcpRegistryVersion === localVersion ? "ok" : "mismatch",
|
|
153
|
-
mcpError ? `fetch_error: ${mcpError}` : ""
|
|
154
|
-
),
|
|
155
|
-
row(
|
|
156
|
-
"MCP Registry websiteUrl",
|
|
157
|
-
mcpRegistryWebsiteUrl,
|
|
158
|
-
mcpRegistryWebsiteUrl === expectedWebsiteUrl ? "ok" : "mismatch",
|
|
159
|
-
`expected: ${expectedWebsiteUrl}`
|
|
160
|
-
),
|
|
161
|
-
row(
|
|
162
|
-
"MCP Registry description",
|
|
163
|
-
mcpRegistryDescription,
|
|
164
|
-
typeof mcpRegistryDescription === "string" && mcpRegistryDescription.startsWith(expectedDescriptionPrefix)
|
|
165
|
-
? "ok"
|
|
166
|
-
: "mismatch",
|
|
167
|
-
`expected_prefix: ${expectedDescriptionPrefix}`
|
|
168
|
-
),
|
|
169
|
-
row(
|
|
170
|
-
"Smithery endpoint",
|
|
171
|
-
"n/a",
|
|
172
|
-
smitheryReachable ? "reachable" : "unreachable",
|
|
173
|
-
smitheryError
|
|
174
|
-
? `check_error: ${smitheryError}`
|
|
175
|
-
: `status: ${smitheryStatus ?? "unknown"}; version check is manual.`
|
|
176
|
-
),
|
|
177
|
-
"",
|
|
178
|
-
"## Interpretation",
|
|
179
|
-
"",
|
|
180
|
-
hardFailures.length === 0
|
|
181
|
-
? "- Local metadata parity: pass."
|
|
182
|
-
: `- Local metadata parity: fail (${hardFailures.map((f) => f.name).join(", ")}).`,
|
|
183
|
-
externalMismatches.length === 0
|
|
184
|
-
? "- External parity: pass."
|
|
185
|
-
: `- External parity mismatch: ${externalMismatches.map((f) => f.name).join(", ")}.`,
|
|
186
|
-
"",
|
|
187
|
-
"## Actionable Drift Notes",
|
|
188
|
-
"",
|
|
189
|
-
mcpRegistryWebsiteUrl === expectedWebsiteUrl
|
|
190
|
-
? "- MCP registry `websiteUrl` matches local metadata."
|
|
191
|
-
: "- MCP registry `websiteUrl` drift detected. Update registry metadata or re-publish metadata-bearing release to align canonical install landing URL.",
|
|
192
|
-
typeof mcpRegistryDescription === "string" && mcpRegistryDescription.startsWith(expectedDescriptionPrefix)
|
|
193
|
-
? "- MCP registry description prefix matches local metadata."
|
|
194
|
-
: "- MCP registry description drift detected. Align registry listing description with local `server.json` wording.",
|
|
195
|
-
externalMismatches.length === 0
|
|
196
|
-
? "- Propagation mode: not needed (external parity is already aligned)."
|
|
197
|
-
: (allowPropagation
|
|
198
|
-
? "- Propagation mode enabled: external mismatch accepted temporarily."
|
|
199
|
-
: "- Propagation mode disabled: external mismatch is a release gate failure."),
|
|
200
|
-
];
|
|
201
|
-
|
|
202
|
-
const outPath = join(repoRoot, outputArg);
|
|
203
|
-
mkdirSync(dirname(outPath), { recursive: true });
|
|
204
|
-
writeFileSync(outPath, `${lines.join("\n")}\n`, "utf8");
|
|
205
|
-
console.log(`Wrote ${outputArg}`);
|
|
206
|
-
|
|
207
|
-
if (hardFailures.length > 0) {
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
if (!allowPropagation && externalMismatches.length > 0) {
|
|
211
|
-
process.exit(1);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
main().catch((error) => {
|
|
216
|
-
console.error(error);
|
|
217
|
-
process.exit(1);
|
|
218
|
-
});
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { writeFileSync } from "node:fs";
|
|
4
|
-
import { resolve } from "node:path";
|
|
5
|
-
|
|
6
|
-
const ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
7
|
-
const API_TOKEN = process.env.CLOUDFLARE_API_TOKEN || process.env.CF_TERRAFORM_TOKEN;
|
|
8
|
-
|
|
9
|
-
const ENDPOINTS = new Map([
|
|
10
|
-
["content", "content"],
|
|
11
|
-
["markdown", "markdown"],
|
|
12
|
-
["links", "links"],
|
|
13
|
-
["snapshot", "snapshot"],
|
|
14
|
-
["scrape", "scrape"],
|
|
15
|
-
["json", "json"],
|
|
16
|
-
["screenshot", "screenshot"],
|
|
17
|
-
["pdf", "pdf"],
|
|
18
|
-
]);
|
|
19
|
-
|
|
20
|
-
function usage() {
|
|
21
|
-
console.error(`Usage:
|
|
22
|
-
node scripts/cloudflare-browser-tool.js verify
|
|
23
|
-
node scripts/cloudflare-browser-tool.js <mode> <url> [options]
|
|
24
|
-
|
|
25
|
-
Modes:
|
|
26
|
-
content Rendered HTML
|
|
27
|
-
markdown Rendered markdown
|
|
28
|
-
links Extract links
|
|
29
|
-
snapshot HTML with inlined resources
|
|
30
|
-
scrape Extract CSS selectors (use --selectors "h1,.card")
|
|
31
|
-
json AI-structured extraction (use --schema '{"title":"string"}')
|
|
32
|
-
screenshot Capture PNG/JPEG (use --out ./page.png)
|
|
33
|
-
pdf Capture PDF (use --out ./page.pdf)
|
|
34
|
-
|
|
35
|
-
Options:
|
|
36
|
-
--wait-until <value> load|domcontentloaded|networkidle0|networkidle2
|
|
37
|
-
--selectors <csv> CSS selectors for scrape mode
|
|
38
|
-
--schema <json> JSON schema object for json mode
|
|
39
|
-
--out <path> Output path for screenshot/pdf
|
|
40
|
-
--full-page fullPage screenshot (default true)
|
|
41
|
-
--type <png|jpeg> screenshot type (default png)
|
|
42
|
-
`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function getOption(args, name) {
|
|
46
|
-
const idx = args.indexOf(name);
|
|
47
|
-
if (idx === -1 || idx + 1 >= args.length) return null;
|
|
48
|
-
return args[idx + 1];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function hasFlag(args, name) {
|
|
52
|
-
return args.includes(name);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function verifyToken() {
|
|
56
|
-
if (!API_TOKEN) {
|
|
57
|
-
throw new Error("Missing API token. Set CLOUDFLARE_API_TOKEN or CF_TERRAFORM_TOKEN.");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const checks = [];
|
|
61
|
-
if (ACCOUNT_ID) {
|
|
62
|
-
checks.push({
|
|
63
|
-
source: "account",
|
|
64
|
-
url: `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/tokens/verify`,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
checks.push({
|
|
68
|
-
source: "user",
|
|
69
|
-
url: "https://api.cloudflare.com/client/v4/user/tokens/verify",
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const failures = [];
|
|
73
|
-
for (const check of checks) {
|
|
74
|
-
const res = await fetch(check.url, {
|
|
75
|
-
headers: {
|
|
76
|
-
Authorization: `Bearer ${API_TOKEN}`,
|
|
77
|
-
"Content-Type": "application/json",
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const raw = await res.text();
|
|
82
|
-
let data = null;
|
|
83
|
-
try {
|
|
84
|
-
data = raw ? JSON.parse(raw) : null;
|
|
85
|
-
} catch {
|
|
86
|
-
failures.push({
|
|
87
|
-
source: check.source,
|
|
88
|
-
status: res.status,
|
|
89
|
-
message: `Non-JSON response: ${raw.slice(0, 200)}`,
|
|
90
|
-
});
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (res.ok && data?.success) {
|
|
95
|
-
return {
|
|
96
|
-
source: check.source,
|
|
97
|
-
result: data.result,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
failures.push({
|
|
102
|
-
source: check.source,
|
|
103
|
-
status: res.status,
|
|
104
|
-
message: JSON.stringify(data?.errors || data || raw),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
throw new Error(`Token verify failed: ${JSON.stringify(failures)}`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function callBrowserApi(mode, url, options) {
|
|
112
|
-
if (!ACCOUNT_ID) {
|
|
113
|
-
throw new Error("Missing CLOUDFLARE_ACCOUNT_ID.");
|
|
114
|
-
}
|
|
115
|
-
if (!API_TOKEN) {
|
|
116
|
-
throw new Error("Missing API token. Set CLOUDFLARE_API_TOKEN or CF_TERRAFORM_TOKEN.");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const endpoint = ENDPOINTS.get(mode);
|
|
120
|
-
if (!endpoint) {
|
|
121
|
-
throw new Error(`Unsupported mode: ${mode}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const body = {
|
|
125
|
-
url,
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
if (options.waitUntil) {
|
|
129
|
-
body.waitUntil = options.waitUntil;
|
|
130
|
-
}
|
|
131
|
-
if (mode === "scrape" && options.selectors?.length) {
|
|
132
|
-
body.selectors = options.selectors;
|
|
133
|
-
}
|
|
134
|
-
if (mode === "json" && options.schema) {
|
|
135
|
-
body.schema = options.schema;
|
|
136
|
-
}
|
|
137
|
-
if (mode === "screenshot") {
|
|
138
|
-
body.screenshotOptions = {
|
|
139
|
-
type: options.type || "png",
|
|
140
|
-
fullPage: options.fullPage,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
if (mode === "pdf") {
|
|
144
|
-
body.pdfOptions = {
|
|
145
|
-
printBackground: true,
|
|
146
|
-
format: "A4",
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const res = await fetch(
|
|
151
|
-
`https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/browser-rendering/${endpoint}`,
|
|
152
|
-
{
|
|
153
|
-
method: "POST",
|
|
154
|
-
headers: {
|
|
155
|
-
Authorization: `Bearer ${API_TOKEN}`,
|
|
156
|
-
"Content-Type": "application/json",
|
|
157
|
-
},
|
|
158
|
-
body: JSON.stringify(body),
|
|
159
|
-
}
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
if (!res.ok) {
|
|
163
|
-
const text = await res.text().catch(() => "");
|
|
164
|
-
throw new Error(`Browser Rendering API failed (${res.status}): ${text || res.statusText}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return res;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function main() {
|
|
171
|
-
const [, , mode, url, ...rest] = process.argv;
|
|
172
|
-
|
|
173
|
-
if (!mode) {
|
|
174
|
-
usage();
|
|
175
|
-
process.exit(1);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (mode === "verify") {
|
|
179
|
-
const tokenInfo = await verifyToken();
|
|
180
|
-
console.log(
|
|
181
|
-
JSON.stringify(
|
|
182
|
-
{
|
|
183
|
-
status: "ok",
|
|
184
|
-
verify_source: tokenInfo.source,
|
|
185
|
-
token_status: tokenInfo.result?.status || "unknown",
|
|
186
|
-
token_id: tokenInfo.result?.id || null,
|
|
187
|
-
account_id_present: Boolean(ACCOUNT_ID),
|
|
188
|
-
},
|
|
189
|
-
null,
|
|
190
|
-
2
|
|
191
|
-
)
|
|
192
|
-
);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (!url) {
|
|
197
|
-
usage();
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const options = {
|
|
202
|
-
waitUntil: getOption(rest, "--wait-until") || undefined,
|
|
203
|
-
selectors: (getOption(rest, "--selectors") || "")
|
|
204
|
-
.split(",")
|
|
205
|
-
.map((v) => v.trim())
|
|
206
|
-
.filter(Boolean),
|
|
207
|
-
schema: getOption(rest, "--schema") ? JSON.parse(getOption(rest, "--schema")) : undefined,
|
|
208
|
-
out: getOption(rest, "--out") || undefined,
|
|
209
|
-
type: getOption(rest, "--type") || "png",
|
|
210
|
-
fullPage: !hasFlag(rest, "--no-full-page"),
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const res = await callBrowserApi(mode, url, options);
|
|
214
|
-
|
|
215
|
-
if (mode === "screenshot" || mode === "pdf") {
|
|
216
|
-
const outputPath = resolve(options.out || (mode === "pdf" ? "./cloudflare-page.pdf" : "./cloudflare-page.png"));
|
|
217
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
218
|
-
writeFileSync(outputPath, buffer);
|
|
219
|
-
console.log(JSON.stringify({ status: "ok", mode, output: outputPath, bytes: buffer.length }, null, 2));
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const contentType = res.headers.get("content-type") || "";
|
|
224
|
-
if (contentType.includes("application/json")) {
|
|
225
|
-
const json = await res.json();
|
|
226
|
-
console.log(JSON.stringify(json, null, 2));
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const text = await res.text();
|
|
231
|
-
console.log(text);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
main().catch((error) => {
|
|
235
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
236
|
-
process.exit(1);
|
|
237
|
-
});
|