@jtalk22/slack-mcp 3.0.0 → 3.1.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 +25 -22
- package/docs/INDEX.md +6 -5
- package/docs/LAUNCH-COPY-v3.0.0.md +43 -15
- package/docs/LAUNCH-OPS.md +1 -1
- package/docs/RELEASE-HEALTH.md +42 -55
- 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-poster.png +0 -0
- package/docs/images/social-preview-v3.png +0 -0
- package/package.json +21 -29
- package/public/demo-claude.html +19 -3
- package/public/demo-video.html +16 -7
- package/public/demo.html +2 -2
- package/public/share.html +127 -0
- package/scripts/build-mobile-demo.js +168 -0
- package/scripts/build-release-health-delta.js +2 -2
- package/scripts/build-social-preview.js +189 -0
- package/scripts/capture-screenshots.js +6 -0
- package/scripts/check-owner-attribution.sh +53 -2
- package/scripts/check-public-language.sh +1 -0
- package/scripts/check-version-parity.js +45 -3
- package/scripts/collect-release-health.js +13 -1
- package/scripts/impact-push-v3.js +781 -0
- package/scripts/record-demo.js +4 -3
- package/scripts/release-preflight.js +6 -2
- package/scripts/update-github-social-preview.js +208 -0
- package/scripts/verify-web.js +23 -19
- package/server.json +2 -2
- package/src/web-server.js +2 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Slack MCP Server v3.0.0</title>
|
|
7
|
+
<meta name="description" content="Session-based Slack MCP for Claude. Local-first stdio/web with secure-default hosted HTTP in v3.">
|
|
8
|
+
<meta property="og:type" content="website">
|
|
9
|
+
<meta property="og:title" content="Slack MCP Server v3.0.0">
|
|
10
|
+
<meta property="og:description" content="Session-based Slack MCP for Claude. Local-first stdio/web with secure-default hosted HTTP in v3.">
|
|
11
|
+
<meta property="og:url" content="https://jtalk22.github.io/slack-mcp-server/public/share.html">
|
|
12
|
+
<meta property="og:image" content="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png">
|
|
13
|
+
<meta property="og:image:width" content="1280">
|
|
14
|
+
<meta property="og:image:height" content="640">
|
|
15
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
16
|
+
<meta name="twitter:title" content="Slack MCP Server v3.0.0">
|
|
17
|
+
<meta name="twitter:description" content="Session-based Slack MCP for Claude. Local-first stdio/web with secure-default hosted HTTP in v3.">
|
|
18
|
+
<meta name="twitter:image" content="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png">
|
|
19
|
+
<style>
|
|
20
|
+
:root {
|
|
21
|
+
--bg-1: #0b1436;
|
|
22
|
+
--bg-2: #0e1d49;
|
|
23
|
+
--line: rgba(131, 161, 224, 0.36);
|
|
24
|
+
--text: #edf4ff;
|
|
25
|
+
--muted: #b4c4e8;
|
|
26
|
+
--link-bg: rgba(17, 57, 120, 0.7);
|
|
27
|
+
--link-bg-hover: rgba(22, 71, 148, 0.85);
|
|
28
|
+
--accent: #54d8cf;
|
|
29
|
+
}
|
|
30
|
+
* { box-sizing: border-box; }
|
|
31
|
+
body {
|
|
32
|
+
margin: 0;
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
color: var(--text);
|
|
35
|
+
background:
|
|
36
|
+
radial-gradient(900px 430px at 12% 0%, #2b4f98 0%, transparent 60%),
|
|
37
|
+
radial-gradient(980px 480px at 100% 100%, #124b8c 0%, transparent 64%),
|
|
38
|
+
linear-gradient(145deg, var(--bg-1), var(--bg-2));
|
|
39
|
+
font-family: "Space Grotesk", "IBM Plex Sans", "Segoe UI", Arial, sans-serif;
|
|
40
|
+
display: grid;
|
|
41
|
+
place-items: center;
|
|
42
|
+
padding: 20px;
|
|
43
|
+
}
|
|
44
|
+
.wrap {
|
|
45
|
+
width: min(980px, 100%);
|
|
46
|
+
border: 1px solid var(--line);
|
|
47
|
+
border-radius: 16px;
|
|
48
|
+
background: linear-gradient(165deg, rgba(17, 41, 92, 0.72), rgba(10, 22, 56, 0.9));
|
|
49
|
+
box-shadow: 0 18px 38px rgba(0, 0, 0, 0.28);
|
|
50
|
+
padding: 16px;
|
|
51
|
+
}
|
|
52
|
+
h1 {
|
|
53
|
+
margin: 0;
|
|
54
|
+
line-height: 1.08;
|
|
55
|
+
letter-spacing: -0.02em;
|
|
56
|
+
font-size: clamp(30px, 5vw, 48px);
|
|
57
|
+
}
|
|
58
|
+
.sub {
|
|
59
|
+
margin: 8px 0 14px;
|
|
60
|
+
color: var(--muted);
|
|
61
|
+
font-size: clamp(16px, 2.4vw, 22px);
|
|
62
|
+
line-height: 1.25;
|
|
63
|
+
}
|
|
64
|
+
.preview {
|
|
65
|
+
display: block;
|
|
66
|
+
border-radius: 12px;
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
border: 1px solid rgba(135, 163, 225, 0.4);
|
|
69
|
+
text-decoration: none;
|
|
70
|
+
margin-bottom: 14px;
|
|
71
|
+
}
|
|
72
|
+
.preview img {
|
|
73
|
+
width: 100%;
|
|
74
|
+
display: block;
|
|
75
|
+
}
|
|
76
|
+
.links {
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-wrap: wrap;
|
|
79
|
+
gap: 10px;
|
|
80
|
+
margin-bottom: 8px;
|
|
81
|
+
}
|
|
82
|
+
.links a {
|
|
83
|
+
display: inline-block;
|
|
84
|
+
text-decoration: none;
|
|
85
|
+
border: 1px solid rgba(131, 161, 224, 0.5);
|
|
86
|
+
border-radius: 10px;
|
|
87
|
+
padding: 9px 12px;
|
|
88
|
+
color: var(--text);
|
|
89
|
+
background: var(--link-bg);
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
font-size: 14px;
|
|
92
|
+
}
|
|
93
|
+
.links a:hover { background: var(--link-bg-hover); }
|
|
94
|
+
.note {
|
|
95
|
+
color: #9fb4de;
|
|
96
|
+
font-size: 13px;
|
|
97
|
+
line-height: 1.45;
|
|
98
|
+
}
|
|
99
|
+
.note strong { color: var(--accent); }
|
|
100
|
+
@media (max-width: 640px) {
|
|
101
|
+
body { padding: 10px; }
|
|
102
|
+
.wrap { padding: 12px; }
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
105
|
+
</head>
|
|
106
|
+
<body>
|
|
107
|
+
<main class="wrap">
|
|
108
|
+
<h1>Slack MCP Server v3.0.0</h1>
|
|
109
|
+
<p class="sub">Session-based Slack MCP for Claude and MCP clients. Local-first stdio/web, secure-default hosted HTTP in v3.</p>
|
|
110
|
+
|
|
111
|
+
<a class="preview" href="https://github.com/jtalk22/slack-mcp-server" rel="noopener">
|
|
112
|
+
<img src="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png" alt="Slack MCP Server social preview card">
|
|
113
|
+
</a>
|
|
114
|
+
|
|
115
|
+
<div class="links">
|
|
116
|
+
<a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/SETUP.md" rel="noopener">Install (`--setup`)</a>
|
|
117
|
+
<a href="https://github.com/jtalk22/slack-mcp-server/blob/main/README.md#install--verify" rel="noopener">Verify (`--version/--doctor/--status`)</a>
|
|
118
|
+
<a href="https://github.com/jtalk22/slack-mcp-server/releases/latest" rel="noopener">Latest Release</a>
|
|
119
|
+
<a href="https://jtalk22.github.io/slack-mcp-server/" rel="noopener">Autoplay Demo Landing</a>
|
|
120
|
+
<a href="https://jtalk22.github.io/slack-mcp-server/docs/videos/demo-claude-mobile-20s.mp4" rel="noopener">20s Mobile Clip</a>
|
|
121
|
+
<a href="https://www.npmjs.com/package/@jtalk22/slack-mcp" rel="noopener">npm Package</a>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<p class="note"><strong>Verify in 30 seconds:</strong> <code>--version</code>, <code>--doctor</code>, <code>--status</code>. Maintainer/operator: <code>jtalk22</code> (<code>james@revasser.nyc</code>).</p>
|
|
125
|
+
</main>
|
|
126
|
+
</body>
|
|
127
|
+
</html>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdirSync, statSync } from "node:fs";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const ROOT = resolve(__dirname, "..");
|
|
10
|
+
|
|
11
|
+
const argv = process.argv.slice(2);
|
|
12
|
+
const hasArg = (flag) => argv.includes(flag);
|
|
13
|
+
const argValue = (flag) => {
|
|
14
|
+
const idx = argv.indexOf(flag);
|
|
15
|
+
return idx >= 0 && idx + 1 < argv.length ? argv[idx + 1] : null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const input = resolve(argValue("--in") || join(ROOT, "docs", "videos", "demo-claude.webm"));
|
|
19
|
+
const outputVideo = resolve(
|
|
20
|
+
argValue("--out-video") || join(ROOT, "docs", "videos", "demo-claude-mobile-20s.mp4")
|
|
21
|
+
);
|
|
22
|
+
const outputPoster = resolve(
|
|
23
|
+
argValue("--out-poster") || join(ROOT, "docs", "images", "demo-claude-mobile-poster.png")
|
|
24
|
+
);
|
|
25
|
+
const outputGif = resolve(
|
|
26
|
+
argValue("--out-gif") || join(ROOT, "docs", "images", "demo-claude-mobile-20s.gif")
|
|
27
|
+
);
|
|
28
|
+
const validationDir = resolve(
|
|
29
|
+
argValue("--validation-dir") || join(ROOT, "output", "release-health", "mobile-first3-frames")
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Start from a frame where tool execution is already visible.
|
|
33
|
+
const startSeconds = Number(argValue("--start") || 8);
|
|
34
|
+
const durationSeconds = Number(argValue("--duration") || 20);
|
|
35
|
+
|
|
36
|
+
function run(label, args) {
|
|
37
|
+
console.log(`\n▶ ${label}`);
|
|
38
|
+
const result = spawnSync("ffmpeg", args, { stdio: "inherit" });
|
|
39
|
+
if (result.status !== 0) {
|
|
40
|
+
throw new Error(`ffmpeg failed during: ${label}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasCommand(name, args = ["--version"]) {
|
|
45
|
+
const probe = spawnSync(name, args, { stdio: "ignore" });
|
|
46
|
+
return probe.status === 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ensureFfmpeg() {
|
|
50
|
+
const probe = spawnSync("ffmpeg", ["-version"], { stdio: "ignore" });
|
|
51
|
+
if (probe.status !== 0) {
|
|
52
|
+
throw new Error("ffmpeg is required but not available in PATH");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatSize(path) {
|
|
57
|
+
const bytes = statSync(path).size;
|
|
58
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!existsSync(input)) {
|
|
62
|
+
console.error(`Missing input video: ${input}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ensureFfmpeg();
|
|
67
|
+
mkdirSync(dirname(outputVideo), { recursive: true });
|
|
68
|
+
mkdirSync(dirname(outputPoster), { recursive: true });
|
|
69
|
+
mkdirSync(dirname(outputGif), { recursive: true });
|
|
70
|
+
|
|
71
|
+
const verticalComposite =
|
|
72
|
+
"[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,boxblur=22:10[bg];" +
|
|
73
|
+
"[0:v]scale=1040:-2:force_original_aspect_ratio=decrease[fg];" +
|
|
74
|
+
"[bg][fg]overlay=(W-w)/2:(H-h)/2[vout]";
|
|
75
|
+
|
|
76
|
+
run("Build 9:16 mobile clip", [
|
|
77
|
+
"-y",
|
|
78
|
+
"-ss",
|
|
79
|
+
String(startSeconds),
|
|
80
|
+
"-t",
|
|
81
|
+
String(durationSeconds),
|
|
82
|
+
"-i",
|
|
83
|
+
input,
|
|
84
|
+
"-filter_complex",
|
|
85
|
+
verticalComposite,
|
|
86
|
+
"-map",
|
|
87
|
+
"[vout]",
|
|
88
|
+
"-c:v",
|
|
89
|
+
"libx264",
|
|
90
|
+
"-preset",
|
|
91
|
+
"medium",
|
|
92
|
+
"-crf",
|
|
93
|
+
"20",
|
|
94
|
+
"-pix_fmt",
|
|
95
|
+
"yuv420p",
|
|
96
|
+
"-movflags",
|
|
97
|
+
"+faststart",
|
|
98
|
+
"-an",
|
|
99
|
+
outputVideo,
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
run("Build mobile poster", [
|
|
103
|
+
"-y",
|
|
104
|
+
"-ss",
|
|
105
|
+
String(startSeconds + 2),
|
|
106
|
+
"-i",
|
|
107
|
+
input,
|
|
108
|
+
"-vframes",
|
|
109
|
+
"1",
|
|
110
|
+
"-update",
|
|
111
|
+
"1",
|
|
112
|
+
"-filter_complex",
|
|
113
|
+
verticalComposite,
|
|
114
|
+
"-map",
|
|
115
|
+
"[vout]",
|
|
116
|
+
outputPoster,
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
if (hasArg("--gif")) {
|
|
120
|
+
if (hasCommand("gifski")) {
|
|
121
|
+
console.log("\n▶ Build optional mobile GIF preview (gifski)");
|
|
122
|
+
const gifResult = spawnSync(
|
|
123
|
+
"gifski",
|
|
124
|
+
["--fps", "12", "--width", "540", "--quality", "88", "--output", outputGif, outputVideo],
|
|
125
|
+
{ stdio: "inherit" }
|
|
126
|
+
);
|
|
127
|
+
if (gifResult.status !== 0) {
|
|
128
|
+
throw new Error("gifski failed while building optional mobile GIF preview");
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
run("Build optional mobile GIF preview (ffmpeg fallback)", [
|
|
132
|
+
"-y",
|
|
133
|
+
"-i",
|
|
134
|
+
outputVideo,
|
|
135
|
+
"-vf",
|
|
136
|
+
"fps=12,scale=540:-2:flags=lanczos,split[s0][s1];[s0]palettegen=stats_mode=diff[p];[s1][p]paletteuse=dither=sierra2_4a",
|
|
137
|
+
outputGif,
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (hasArg("--validate-first3")) {
|
|
143
|
+
mkdirSync(validationDir, { recursive: true });
|
|
144
|
+
for (let second = 0; second < 3; second += 1) {
|
|
145
|
+
run(`Capture validation frame @ +${second}s`, [
|
|
146
|
+
"-y",
|
|
147
|
+
"-ss",
|
|
148
|
+
String(second),
|
|
149
|
+
"-i",
|
|
150
|
+
outputVideo,
|
|
151
|
+
"-frames:v",
|
|
152
|
+
"1",
|
|
153
|
+
"-update",
|
|
154
|
+
"1",
|
|
155
|
+
join(validationDir, `frame-${second}s.png`),
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log("\n✅ Mobile demo artifacts ready");
|
|
161
|
+
console.log(`- Video: ${outputVideo} (${formatSize(outputVideo)})`);
|
|
162
|
+
console.log(`- Poster: ${outputPoster} (${formatSize(outputPoster)})`);
|
|
163
|
+
if (hasArg("--gif") && existsSync(outputGif)) {
|
|
164
|
+
console.log(`- GIF: ${outputGif} (${formatSize(outputGif)})`);
|
|
165
|
+
}
|
|
166
|
+
if (hasArg("--validate-first3")) {
|
|
167
|
+
console.log(`- Validation frames: ${validationDir}`);
|
|
168
|
+
}
|
|
@@ -16,8 +16,8 @@ const METRICS = [
|
|
|
16
16
|
"deployment-intake submissions (all-time)",
|
|
17
17
|
];
|
|
18
18
|
|
|
19
|
-
const DEFAULT_AFTER = resolve("
|
|
20
|
-
const DEFAULT_OUT = resolve("
|
|
19
|
+
const DEFAULT_AFTER = resolve("output", "release-health", "latest.md");
|
|
20
|
+
const DEFAULT_OUT = resolve("output", "release-health", "automation-delta.md");
|
|
21
21
|
const TARGETS = {
|
|
22
22
|
"npm downloads (last week)": { operator: ">=", value: 180 },
|
|
23
23
|
"deployment-intake submissions (all-time)": { operator: ">=", value: 2 },
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { chromium } from "playwright";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const ROOT = resolve(__dirname, "..");
|
|
10
|
+
|
|
11
|
+
const WIDTH = 1280;
|
|
12
|
+
const HEIGHT = 640;
|
|
13
|
+
const MAX_BYTES = 1_000_000;
|
|
14
|
+
|
|
15
|
+
function parseArg(flag) {
|
|
16
|
+
const idx = process.argv.indexOf(flag);
|
|
17
|
+
return idx >= 0 && idx + 1 < process.argv.length ? process.argv[idx + 1] : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const outputPath = resolve(parseArg("--out") || join(ROOT, "docs", "images", "social-preview-v3.png"));
|
|
21
|
+
const sourcePath = resolve(parseArg("--source") || join(ROOT, "docs", "images", "demo-poster.png"));
|
|
22
|
+
|
|
23
|
+
if (!existsSync(sourcePath)) {
|
|
24
|
+
console.error(`Missing source image: ${sourcePath}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
29
|
+
|
|
30
|
+
const sourceDataUri = `data:image/png;base64,${readFileSync(sourcePath).toString("base64")}`;
|
|
31
|
+
|
|
32
|
+
const html = `<!doctype html>
|
|
33
|
+
<html lang="en">
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="utf-8">
|
|
36
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
37
|
+
<title>Slack MCP Server v3.0.0 Social Preview</title>
|
|
38
|
+
<style>
|
|
39
|
+
:root {
|
|
40
|
+
--bg-a: #0f1f4c;
|
|
41
|
+
--bg-b: #0a1538;
|
|
42
|
+
--line: rgba(141, 168, 233, 0.3);
|
|
43
|
+
--text: #ecf4ff;
|
|
44
|
+
--muted: #b3c4e8;
|
|
45
|
+
}
|
|
46
|
+
* {
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
margin: 0;
|
|
49
|
+
padding: 0;
|
|
50
|
+
font-family: "Space Grotesk", "IBM Plex Sans", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
|
51
|
+
}
|
|
52
|
+
body {
|
|
53
|
+
width: ${WIDTH}px;
|
|
54
|
+
height: ${HEIGHT}px;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
color: var(--text);
|
|
57
|
+
background:
|
|
58
|
+
radial-gradient(920px 460px at 8% 0%, #2a4f98 0%, transparent 58%),
|
|
59
|
+
radial-gradient(980px 500px at 100% 100%, #0f4686 0%, transparent 63%),
|
|
60
|
+
linear-gradient(130deg, var(--bg-a), var(--bg-b));
|
|
61
|
+
padding: 24px 28px;
|
|
62
|
+
}
|
|
63
|
+
.card {
|
|
64
|
+
width: 100%;
|
|
65
|
+
height: 100%;
|
|
66
|
+
border-radius: 16px;
|
|
67
|
+
border: 1px solid var(--line);
|
|
68
|
+
background: linear-gradient(165deg, rgba(16, 34, 82, 0.76), rgba(9, 20, 54, 0.92));
|
|
69
|
+
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.3);
|
|
70
|
+
padding: 14px 16px;
|
|
71
|
+
display: grid;
|
|
72
|
+
grid-template-rows: auto 1fr auto;
|
|
73
|
+
gap: 10px;
|
|
74
|
+
}
|
|
75
|
+
.top {
|
|
76
|
+
display: flex;
|
|
77
|
+
justify-content: space-between;
|
|
78
|
+
align-items: center;
|
|
79
|
+
font-size: 19px;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
letter-spacing: -0.01em;
|
|
82
|
+
color: #e9f2ff;
|
|
83
|
+
}
|
|
84
|
+
.pill {
|
|
85
|
+
font-size: 17px;
|
|
86
|
+
background: rgba(88, 121, 191, 0.24);
|
|
87
|
+
border: 1px solid rgba(137, 167, 227, 0.5);
|
|
88
|
+
color: #dce9ff;
|
|
89
|
+
border-radius: 999px;
|
|
90
|
+
padding: 4px 10px 5px;
|
|
91
|
+
font-weight: 600;
|
|
92
|
+
}
|
|
93
|
+
.image-frame {
|
|
94
|
+
border-radius: 13px;
|
|
95
|
+
border: 1px solid rgba(147, 173, 240, 0.32);
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
background: #0a1438;
|
|
98
|
+
height: 474px;
|
|
99
|
+
}
|
|
100
|
+
.image-frame img {
|
|
101
|
+
display: block;
|
|
102
|
+
width: 100%;
|
|
103
|
+
height: 100%;
|
|
104
|
+
object-fit: cover;
|
|
105
|
+
object-position: top center;
|
|
106
|
+
filter: saturate(1.03);
|
|
107
|
+
}
|
|
108
|
+
.bottom {
|
|
109
|
+
border-top: 1px solid rgba(130, 156, 220, 0.24);
|
|
110
|
+
padding-top: 8px;
|
|
111
|
+
display: grid;
|
|
112
|
+
grid-template-columns: 1fr auto;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: 12px;
|
|
115
|
+
}
|
|
116
|
+
.subhead {
|
|
117
|
+
font-size: 28px;
|
|
118
|
+
font-weight: 620;
|
|
119
|
+
line-height: 1.08;
|
|
120
|
+
letter-spacing: -0.02em;
|
|
121
|
+
max-width: 930px;
|
|
122
|
+
}
|
|
123
|
+
.detail {
|
|
124
|
+
margin-top: 5px;
|
|
125
|
+
font-size: 18px;
|
|
126
|
+
color: var(--muted);
|
|
127
|
+
letter-spacing: -0.012em;
|
|
128
|
+
font-weight: 500;
|
|
129
|
+
}
|
|
130
|
+
.attribution {
|
|
131
|
+
text-align: right;
|
|
132
|
+
font-size: 17px;
|
|
133
|
+
color: #d6e3ff;
|
|
134
|
+
font-weight: 560;
|
|
135
|
+
line-height: 1.2;
|
|
136
|
+
}
|
|
137
|
+
.attribution .mail {
|
|
138
|
+
font-size: 14px;
|
|
139
|
+
color: #a8bbe6;
|
|
140
|
+
font-weight: 500;
|
|
141
|
+
}
|
|
142
|
+
</style>
|
|
143
|
+
</head>
|
|
144
|
+
<body>
|
|
145
|
+
<main class="card">
|
|
146
|
+
<header class="top">
|
|
147
|
+
<span>Slack MCP Server</span>
|
|
148
|
+
<span class="pill">v3.0.0</span>
|
|
149
|
+
</header>
|
|
150
|
+
<section class="image-frame">
|
|
151
|
+
<img src="${sourceDataUri}" alt="Slack MCP live demo frame">
|
|
152
|
+
</section>
|
|
153
|
+
<footer class="bottom">
|
|
154
|
+
<div>
|
|
155
|
+
<div class="subhead">Session-based Slack MCP for Claude and MCP clients.</div>
|
|
156
|
+
<div class="detail">Local-first stdio/web. Secure-default hosted HTTP in v3.</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="attribution">
|
|
159
|
+
<div>jtalk22</div>
|
|
160
|
+
<div class="mail">james@revasser.nyc</div>
|
|
161
|
+
</div>
|
|
162
|
+
</footer>
|
|
163
|
+
</main>
|
|
164
|
+
</body>
|
|
165
|
+
</html>`;
|
|
166
|
+
|
|
167
|
+
const browser = await chromium.launch({ headless: true });
|
|
168
|
+
const context = await browser.newContext({
|
|
169
|
+
viewport: { width: WIDTH, height: HEIGHT },
|
|
170
|
+
deviceScaleFactor: 1,
|
|
171
|
+
colorScheme: "dark",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const page = await context.newPage();
|
|
175
|
+
await page.setContent(html, { waitUntil: "networkidle" });
|
|
176
|
+
await page.waitForTimeout(300);
|
|
177
|
+
await page.screenshot({ path: outputPath, type: "png" });
|
|
178
|
+
|
|
179
|
+
await context.close();
|
|
180
|
+
await browser.close();
|
|
181
|
+
|
|
182
|
+
const size = statSync(outputPath).size;
|
|
183
|
+
console.log(`Wrote ${outputPath}`);
|
|
184
|
+
console.log(`Size: ${size} bytes`);
|
|
185
|
+
|
|
186
|
+
if (size > MAX_BYTES) {
|
|
187
|
+
console.error(`Image exceeds ${MAX_BYTES} bytes target.`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
@@ -86,6 +86,12 @@ async function captureScreenshots() {
|
|
|
86
86
|
{
|
|
87
87
|
const { context, page } = await openPage(browser, demoClaudePath, { width: 1280, height: 800 });
|
|
88
88
|
console.log('Capturing poster image...');
|
|
89
|
+
// Wait for transient scenario caption animation to settle.
|
|
90
|
+
await page.waitForTimeout(2600);
|
|
91
|
+
await page.evaluate(() => {
|
|
92
|
+
const caption = document.getElementById('scenarioCaption');
|
|
93
|
+
if (caption) caption.classList.remove('visible');
|
|
94
|
+
});
|
|
89
95
|
await page.screenshot({
|
|
90
96
|
path: join(imagesDir, 'demo-poster.png'),
|
|
91
97
|
clip: { x: 0, y: 0, width: 1280, height: 800 }
|
|
@@ -3,6 +3,7 @@ set -euo pipefail
|
|
|
3
3
|
|
|
4
4
|
EXPECTED_NAME="${EXPECTED_GIT_NAME:-jtalk22}"
|
|
5
5
|
EXPECTED_EMAIL="${EXPECTED_GIT_EMAIL:-james@revasser.nyc}"
|
|
6
|
+
ALLOW_GITHUB_WEB_COMMITTER="${ALLOW_GITHUB_WEB_COMMITTER:-0}"
|
|
6
7
|
BANNED_REGEX='(?i)(co-authored-by|generated with|\bclaude\b|\bgpt\b|\bcopilot\b|\bgemini\b|\bai\b)'
|
|
7
8
|
|
|
8
9
|
die() {
|
|
@@ -20,6 +21,56 @@ contains_banned_markers() {
|
|
|
20
21
|
fi
|
|
21
22
|
}
|
|
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
|
+
|
|
23
74
|
if [[ "${SKIP_LOCAL_CONFIG_CHECK:-0}" != "1" ]]; then
|
|
24
75
|
local_name="$(git config --get user.name || true)"
|
|
25
76
|
local_email="$(git config --get user.email || true)"
|
|
@@ -57,12 +108,12 @@ while IFS= read -r sha; do
|
|
|
57
108
|
committer_email="$(git show -s --format=%ce "$sha")"
|
|
58
109
|
body="$(git show -s --format=%B "$sha")"
|
|
59
110
|
|
|
60
|
-
if
|
|
111
|
+
if ! is_allowed_owner_author "$author_name" "$author_email"; then
|
|
61
112
|
echo "Commit $sha has author '$author_name <$author_email>' (expected '$EXPECTED_NAME <$EXPECTED_EMAIL>')." >&2
|
|
62
113
|
errors=1
|
|
63
114
|
fi
|
|
64
115
|
|
|
65
|
-
if
|
|
116
|
+
if ! is_allowed_owner_committer "$committer_name" "$committer_email" "$author_name" "$author_email"; then
|
|
66
117
|
echo "Commit $sha has committer '$committer_name <$committer_email>' (expected '$EXPECTED_NAME <$EXPECTED_EMAIL>')." >&2
|
|
67
118
|
errors=1
|
|
68
119
|
fi
|
|
@@ -12,10 +12,14 @@ const serverMeta = JSON.parse(readFileSync(join(repoRoot, "server.json"), "utf8"
|
|
|
12
12
|
|
|
13
13
|
const outputArg = process.argv.includes("--out")
|
|
14
14
|
? process.argv[process.argv.indexOf("--out") + 1]
|
|
15
|
-
:
|
|
15
|
+
: process.argv.includes("--public")
|
|
16
|
+
? "docs/release-health/version-parity.md"
|
|
17
|
+
: "output/release-health/version-parity.md";
|
|
16
18
|
const allowPropagation = process.argv.includes("--allow-propagation");
|
|
17
19
|
|
|
18
20
|
const mcpServerName = serverMeta.name;
|
|
21
|
+
const expectedWebsiteUrl = serverMeta.websiteUrl;
|
|
22
|
+
const expectedDescriptionPrefix = serverMeta.description;
|
|
19
23
|
const smitheryEndpoint = "https://server.smithery.ai/jtalk22/slack-mcp-server";
|
|
20
24
|
const smitheryListingUrl = "https://smithery.ai/server/jtalk22/slack-mcp-server";
|
|
21
25
|
|
|
@@ -44,6 +48,8 @@ async function main() {
|
|
|
44
48
|
|
|
45
49
|
let npmVersion = null;
|
|
46
50
|
let mcpRegistryVersion = null;
|
|
51
|
+
let mcpRegistryWebsiteUrl = null;
|
|
52
|
+
let mcpRegistryDescription = null;
|
|
47
53
|
let smitheryReachable = null;
|
|
48
54
|
let smitheryStatus = null;
|
|
49
55
|
let npmError = null;
|
|
@@ -62,6 +68,8 @@ async function main() {
|
|
|
62
68
|
`https://registry.modelcontextprotocol.io/v0/servers/${encodeURIComponent(mcpServerName)}/versions/latest`
|
|
63
69
|
);
|
|
64
70
|
mcpRegistryVersion = registry?.server?.version || null;
|
|
71
|
+
mcpRegistryWebsiteUrl = registry?.server?.websiteUrl || null;
|
|
72
|
+
mcpRegistryDescription = registry?.server?.description || null;
|
|
65
73
|
} catch (error) {
|
|
66
74
|
mcpError = String(error?.message || error);
|
|
67
75
|
}
|
|
@@ -88,12 +96,23 @@ async function main() {
|
|
|
88
96
|
{ name: "package.json vs server.json package", ok: localVersion === localServerPkgVersion },
|
|
89
97
|
{ name: "npm latest", ok: npmVersion === localVersion },
|
|
90
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
|
+
},
|
|
91
104
|
];
|
|
92
105
|
|
|
106
|
+
const externalMismatchNames = new Set([
|
|
107
|
+
"npm latest",
|
|
108
|
+
"MCP registry latest",
|
|
109
|
+
"MCP registry websiteUrl",
|
|
110
|
+
"MCP registry description prefix",
|
|
111
|
+
]);
|
|
93
112
|
const externalMismatches = parityChecks
|
|
94
|
-
.filter((check) => !check.ok && (check.name
|
|
113
|
+
.filter((check) => !check.ok && externalMismatchNames.has(check.name));
|
|
95
114
|
const hardFailures = parityChecks
|
|
96
|
-
.filter((check) => !check.ok &&
|
|
115
|
+
.filter((check) => !check.ok && !externalMismatchNames.has(check.name));
|
|
97
116
|
|
|
98
117
|
const now = new Date().toISOString();
|
|
99
118
|
const lines = [
|
|
@@ -133,6 +152,20 @@ async function main() {
|
|
|
133
152
|
mcpRegistryVersion === localVersion ? "ok" : "mismatch",
|
|
134
153
|
mcpError ? `fetch_error: ${mcpError}` : ""
|
|
135
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
|
+
),
|
|
136
169
|
row(
|
|
137
170
|
"Smithery endpoint",
|
|
138
171
|
"n/a",
|
|
@@ -150,6 +183,15 @@ async function main() {
|
|
|
150
183
|
externalMismatches.length === 0
|
|
151
184
|
? "- External parity: pass."
|
|
152
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.",
|
|
153
195
|
externalMismatches.length === 0
|
|
154
196
|
? "- Propagation mode: not needed (external parity is already aligned)."
|
|
155
197
|
: (allowPropagation
|