diff --git a/assets/css/mcp-guide.css b/assets/css/mcp-guide.css
new file mode 100644
index 000000000..c253b808e
--- /dev/null
+++ b/assets/css/mcp-guide.css
@@ -0,0 +1,635 @@
+/* MCP 配置指南页面样式。仅放本页专属布局,通用 token 来自 tool-base.css。 */
+
+html {
+ color-scheme: dark;
+}
+
+html[data-theme='light'] {
+ color-scheme: light;
+}
+
+body {
+ min-height: 100vh;
+}
+
+.skip-link {
+ position: fixed;
+ top: var(--space-3);
+ left: 50%;
+ z-index: 2147483001;
+ padding: 10px 16px;
+ color: #071018;
+ background: var(--accent-cyan);
+ border-radius: var(--radius-md);
+ font-weight: 700;
+ text-decoration: none;
+ transform: translate(-50%, -160%);
+ transition: transform var(--transition-fast);
+}
+
+.skip-link:focus-visible {
+ outline: 2px solid var(--text-primary);
+ outline-offset: 3px;
+ transform: translate(-50%, 0);
+}
+
+.tool-main {
+ min-height: 100vh;
+ padding: max(var(--space-6), env(safe-area-inset-top))
+ max(var(--space-6), env(safe-area-inset-right))
+ max(88px, calc(env(safe-area-inset-bottom) + 72px))
+ max(var(--space-6), env(safe-area-inset-left));
+ background:
+ radial-gradient(circle at 12% 6%, var(--glow-cyan), transparent 30%),
+ radial-gradient(circle at 88% 4%, var(--glow-magenta), transparent 28%), var(--bg-deep);
+}
+
+.tool-main .container {
+ display: flex;
+ flex-direction: column;
+ width: min(100%, 1000px);
+ margin: 0 auto;
+}
+
+/* ---------- 页头 ---------- */
+
+.header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--space-5);
+ margin-bottom: var(--space-6);
+}
+
+.back-link {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ gap: var(--space-2);
+ min-height: 40px;
+ padding: 8px 14px;
+ color: var(--text-secondary);
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ font-family: var(--font-mono);
+ font-size: 0.82rem;
+ text-decoration: none;
+ transition:
+ color var(--transition-fast),
+ border-color var(--transition-fast);
+}
+
+.back-link:hover {
+ color: var(--accent-magenta);
+ border-color: var(--accent-magenta);
+}
+
+.theme-toggle {
+ position: fixed;
+ top: var(--space-4);
+ right: var(--space-4);
+ z-index: 90;
+ width: 44px;
+ height: 44px;
+ color: var(--text-primary);
+ background: var(--bg-card);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-full);
+ cursor: pointer;
+ font-size: 1.1rem;
+ line-height: 1;
+ transition:
+ border-color var(--transition-fast),
+ transform var(--transition-fast);
+}
+
+.theme-toggle:hover {
+ border-color: var(--accent-cyan);
+ transform: scale(1.06);
+}
+
+.title-area {
+ flex: 1 1 220px;
+ min-width: 0;
+}
+
+.title-area h1 {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ margin: 0;
+ color: var(--text-primary);
+ font-family: var(--font-mono);
+ font-size: clamp(1.25rem, 4.4vw, 1.6rem);
+ font-weight: 600;
+ line-height: 1.3;
+}
+
+.title-area h1 .icon {
+ font-size: 1.25rem;
+}
+
+.title-area p {
+ margin: var(--space-1) 0 0;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+}
+
+/* ---------- 信息横幅 ---------- */
+
+.info-banner {
+ padding: var(--space-5);
+ margin-bottom: var(--space-6);
+ background: var(--bg-card);
+ border: 1px solid var(--border-subtle);
+ border-left: 3px solid var(--accent-blue);
+ border-radius: var(--radius-lg);
+}
+
+.info-banner h2 {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ margin: 0 0 var(--space-3);
+ color: var(--accent-blue);
+ font-family: var(--font-mono);
+ font-size: 1rem;
+}
+
+.info-banner p {
+ margin: 0 0 var(--space-3);
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ line-height: 1.65;
+}
+
+.info-banner p:last-child {
+ margin-bottom: 0;
+}
+
+.info-banner a {
+ color: var(--accent-cyan);
+ text-decoration: none;
+ overflow-wrap: anywhere;
+}
+
+.info-banner a:hover {
+ text-decoration: underline;
+}
+
+/* ---------- 内容分区 ---------- */
+
+.section {
+ padding: var(--space-5);
+ margin-bottom: var(--space-5);
+ background: var(--bg-card);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+}
+
+.section h3 {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ margin: 0 0 var(--space-4);
+ color: var(--accent-purple);
+ font-family: var(--font-mono);
+ font-size: 1rem;
+}
+
+.section p {
+ margin: 0 0 var(--space-3);
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ line-height: 1.65;
+}
+
+.section code {
+ padding: 2px 6px;
+ color: var(--accent-cyan);
+ background: var(--bg-input);
+ border-radius: var(--radius-sm);
+ font-family: var(--font-mono);
+ font-size: 0.82rem;
+}
+
+.resource-link {
+ color: var(--accent-cyan);
+ text-decoration: none;
+}
+
+.resource-link.alt {
+ color: var(--accent-magenta);
+}
+
+.resource-link:hover {
+ text-decoration: underline;
+}
+
+/* ---------- 代码块 ---------- */
+
+.code-block {
+ margin: var(--space-3) 0;
+ overflow: hidden;
+ background: var(--bg-deep);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+}
+
+.code-header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-2);
+ padding: var(--space-2) var(--space-3);
+ background: var(--bg-surface);
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+.code-header span {
+ min-width: 0;
+ color: var(--text-muted);
+ font-family: var(--font-mono);
+ font-size: 0.75rem;
+ overflow-wrap: anywhere;
+}
+
+.code-header button {
+ min-height: 32px;
+ padding: 4px 12px;
+ color: var(--text-secondary);
+ background: transparent;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ font-family: var(--font-mono);
+ font-size: 0.72rem;
+ transition:
+ color var(--transition-fast),
+ border-color var(--transition-fast);
+}
+
+.code-header button:hover {
+ color: var(--accent-purple);
+ border-color: var(--accent-purple);
+}
+
+.code-content {
+ padding: var(--space-3) var(--space-4);
+ overflow-x: auto;
+}
+
+.code-content pre {
+ margin: 0;
+ color: var(--text-primary);
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ line-height: 1.6;
+}
+
+.code-content .key,
+.config-content .key {
+ color: var(--accent-purple);
+}
+
+.code-content .string,
+.config-content .string {
+ color: var(--accent-cyan);
+}
+
+.code-content .comment {
+ color: var(--text-muted);
+}
+
+/* ---------- 服务器卡片 ---------- */
+
+.server-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
+ gap: var(--space-4);
+ margin-top: var(--space-4);
+}
+
+.server-card {
+ min-width: 0;
+ padding: var(--space-4);
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ transition:
+ border-color var(--transition-fast),
+ transform var(--transition-fast);
+}
+
+.server-card:hover {
+ border-color: var(--accent-blue);
+ transform: translateY(-2px);
+}
+
+.server-card-header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ margin-bottom: var(--space-3);
+}
+
+.server-icon {
+ display: flex;
+ flex: 0 0 auto;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-cyan));
+ border-radius: var(--radius-sm);
+ font-size: 1.1rem;
+}
+
+.server-name {
+ min-width: 0;
+ color: var(--text-primary);
+ font-family: var(--font-mono);
+ font-size: 0.95rem;
+ font-weight: 600;
+ overflow-wrap: anywhere;
+}
+
+.server-desc {
+ margin-bottom: var(--space-3);
+ color: var(--text-secondary);
+ font-size: 0.85rem;
+ line-height: 1.55;
+}
+
+.server-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: var(--space-3);
+}
+
+.server-tag {
+ padding: 3px 8px;
+ color: var(--text-muted);
+ background: var(--bg-deep);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
+}
+
+.server-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-2);
+}
+
+.server-links a {
+ display: inline-flex;
+ align-items: center;
+ min-height: 36px;
+ padding: 4px 12px;
+ color: var(--accent-blue);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ font-family: var(--font-mono);
+ font-size: 0.75rem;
+ text-decoration: none;
+ transition:
+ color var(--transition-fast),
+ border-color var(--transition-fast);
+}
+
+.server-links a:hover {
+ border-color: var(--accent-blue);
+}
+
+/* ---------- 安装命令 ---------- */
+
+.server-install {
+ margin: var(--space-3) 0;
+ overflow: hidden;
+ background: var(--bg-deep);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+}
+
+.install-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-2);
+ padding: 4px 6px 4px 10px;
+ background: var(--bg-card);
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+.install-header span {
+ color: var(--text-muted);
+ font-family: var(--font-mono);
+ font-size: 0.68rem;
+}
+
+.install-header button {
+ min-height: 32px;
+ padding: 4px 10px;
+ color: var(--text-muted);
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
+ transition:
+ color var(--transition-fast),
+ border-color var(--transition-fast);
+}
+
+.install-header button:hover {
+ color: var(--accent-purple);
+ border-color: var(--border-subtle);
+}
+
+.install-cmd {
+ padding: var(--space-2) 10px;
+ overflow-x: auto;
+ color: var(--accent-purple);
+ font-family: var(--font-mono);
+ font-size: 0.75rem;
+ white-space: nowrap;
+}
+
+/* ---------- 配置折叠 ---------- */
+
+.server-config {
+ margin-top: var(--space-2);
+}
+
+.config-toggle {
+ width: 100%;
+ min-height: 36px;
+ padding: 6px 10px;
+ color: var(--text-muted);
+ background: transparent;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ font-family: var(--font-mono);
+ font-size: 0.72rem;
+ text-align: left;
+ transition:
+ color var(--transition-fast),
+ border-color var(--transition-fast);
+}
+
+.config-toggle:hover {
+ color: var(--accent-cyan);
+ border-color: var(--accent-cyan);
+}
+
+.config-content {
+ display: block;
+ padding: 10px;
+ margin-top: var(--space-2);
+ overflow-x: auto;
+ background: var(--bg-deep);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ font-family: var(--font-mono);
+ font-size: 0.72rem;
+}
+
+.config-content[hidden] {
+ display: none;
+}
+
+.config-content pre {
+ margin: 0;
+ color: var(--text-primary);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
+
+.env-note {
+ margin-top: 6px;
+ color: var(--accent-yellow);
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
+ line-height: 1.5;
+}
+
+/* ---------- 提示与页脚 ---------- */
+
+.tip {
+ padding: var(--space-3) var(--space-4);
+ margin: var(--space-3) 0;
+ color: var(--text-secondary);
+ background: var(--bg-input);
+ border: 1px solid var(--border-subtle);
+ border-left: 3px solid var(--accent-yellow);
+ border-radius: var(--radius-md);
+ font-size: 0.85rem;
+ line-height: 1.6;
+}
+
+.tip strong {
+ color: var(--accent-yellow);
+}
+
+footer {
+ padding-top: var(--space-6);
+ margin-top: auto;
+ border-top: 1px solid var(--border-subtle);
+ text-align: center;
+}
+
+footer p {
+ margin: 0;
+ color: var(--text-muted);
+ font-family: var(--font-mono);
+ font-size: 0.78rem;
+}
+
+footer a {
+ color: var(--text-secondary);
+ text-decoration: none;
+ transition: color var(--transition-fast);
+}
+
+footer a:hover {
+ color: var(--accent-purple);
+}
+
+/* ---------- 焦点可见性 ---------- */
+
+.back-link:focus-visible,
+.theme-toggle:focus-visible,
+.info-banner a:focus-visible,
+.resource-link:focus-visible,
+.code-header button:focus-visible,
+.install-header button:focus-visible,
+.config-toggle:focus-visible,
+.server-links a:focus-visible,
+footer a:focus-visible {
+ outline: 2px solid var(--accent-cyan);
+ outline-offset: 2px;
+}
+
+/* 复制成功的临时反馈 */
+.is-copied {
+ color: var(--accent-green);
+ border-color: var(--accent-green);
+}
+
+/* ---------- 响应式 ---------- */
+
+@media (max-width: 720px) {
+ .tool-main {
+ padding: max(var(--space-4), env(safe-area-inset-top))
+ max(var(--space-4), env(safe-area-inset-right))
+ max(80px, calc(env(safe-area-inset-bottom) + 68px))
+ max(var(--space-4), env(safe-area-inset-left));
+ }
+
+ .header {
+ gap: var(--space-3);
+ }
+
+ .info-banner,
+ .section {
+ padding: var(--space-4);
+ }
+
+ .server-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 420px) {
+ .header {
+ align-items: flex-start;
+ flex-direction: column;
+ padding-right: 52px;
+ }
+
+ .title-area {
+ flex: 0 1 auto;
+ }
+
+ .code-content {
+ padding: var(--space-3);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .skip-link:focus-visible {
+ transform: translate(-50%, 0);
+ }
+
+ .server-card:hover,
+ .theme-toggle:hover {
+ transform: none;
+ }
+}
diff --git a/package.json b/package.json
index 499159471..1260732c1 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"lint:html": "htmlhint \"tools/**/*.html\" --ignore \"**/ByteDanceVerify.html,**/baidu_verify_*.html,**/js-playground.html,**/code-screenshot.html,**/generator/wave-bg.html\"",
"lint:css": "npm run lint:css:tools && npm run lint:css:shared",
"lint:css:tools": "stylelint \"tools/**/*.html\" --custom-syntax postcss-html",
- "lint:css:shared": "stylelint \"assets/css/{tool-base,claude-skills}.css\"",
+ "lint:css:shared": "stylelint \"assets/css/{tool-base,claude-skills,mcp-guide}.css\"",
"lint:js": "eslint \"tools/**/*.html\" \"assets/js/**/*.js\" \"tests/e2e/**/*.js\" playwright.config.js",
"lint:fix": "npm run lint:css:tools -- --fix && npm run lint:css:shared -- --fix",
"format": "prettier --write \"**/*.{html,js,json,md}\"",
diff --git a/tests/e2e/mcp-guide-presentation.spec.js b/tests/e2e/mcp-guide-presentation.spec.js
new file mode 100644
index 000000000..b3e014b81
--- /dev/null
+++ b/tests/e2e/mcp-guide-presentation.spec.js
@@ -0,0 +1,140 @@
+import { expect, test } from '@playwright/test';
+
+function collectPageErrors(page) {
+ const errors = [];
+
+ page.on('console', (message) => {
+ if (message.type() === 'error') errors.push(message.text());
+ });
+ page.on('pageerror', (error) => errors.push(error.message));
+
+ return errors;
+}
+
+test.beforeEach(async ({ context }) => {
+ await context.addInitScript(() => localStorage.clear());
+ await context.grantPermissions(['clipboard-read', 'clipboard-write'], {
+ origin: 'http://127.0.0.1:3000'
+ });
+ await context.route(/^https:\/\//, (route) =>
+ route.fulfill({ status: 204, contentType: 'text/plain', body: '' })
+ );
+});
+
+test('MCP guide preserves card/code presentation and config disclosure at 390px and 1440px', async ({
+ page
+}) => {
+ const errors = collectPageErrors(page);
+
+ for (const viewport of [
+ { width: 390, height: 844 },
+ { width: 1440, height: 900 }
+ ]) {
+ await page.setViewportSize(viewport);
+ await page.goto('/tools/ai/mcp-guide.html');
+
+ await expect(page.getByRole('heading', { name: 'MCP 配置指南' })).toBeVisible();
+ await expect(page.locator('.server-grid')).toHaveCSS('display', 'grid');
+ await expect(page.locator('.server-card').first()).toBeVisible();
+ await expect(page.locator('.code-block').first()).toBeVisible();
+ await expect(page.locator('.server-install').first()).toBeVisible();
+
+ if (viewport.width === 390) {
+ const titleAreaGeometry = await page.locator('.title-area').evaluate((titleArea) => {
+ const lastContent = titleArea.lastElementChild;
+ if (!lastContent) return { hasContent: false, extraHeight: 0 };
+ return {
+ hasContent: true,
+ extraHeight:
+ titleArea.getBoundingClientRect().bottom - lastContent.getBoundingClientRect().bottom
+ };
+ });
+ expect(titleAreaGeometry.hasContent).toBe(true);
+ expect(titleAreaGeometry.extraHeight).toBeLessThanOrEqual(1);
+ }
+
+ const copyStatus = page.getByRole('status');
+ await expect(copyStatus).toHaveAttribute('aria-live', 'polite');
+ await page.locator('.code-header button').first().click();
+ await expect(copyStatus).toHaveText('代码已复制');
+ await page.locator('.install-header button').first().click();
+ await expect(copyStatus).toHaveText('安装命令已复制');
+
+ const layout = await page.locator('.server-grid').evaluate((grid) => {
+ const documentElement = grid.ownerDocument.documentElement;
+ const body = grid.ownerDocument.body;
+ const cards = Array.from(grid.querySelectorAll('.server-card'), (card) => {
+ const rect = card.getBoundingClientRect();
+ return { top: rect.top, left: rect.left, right: rect.right };
+ });
+ return {
+ cards,
+ gridWidth: grid.getBoundingClientRect().width,
+ viewportWidth: documentElement.clientWidth,
+ documentWidth: Math.max(documentElement.scrollWidth, body.scrollWidth)
+ };
+ });
+
+ expect(layout.documentWidth - layout.viewportWidth).toBeLessThanOrEqual(1);
+ expect(layout.gridWidth).toBeLessThanOrEqual(layout.viewportWidth);
+ const firstRowTop = layout.cards[0].top;
+ const firstRowCards = layout.cards.filter((card) => Math.abs(card.top - firstRowTop) < 1);
+ expect(firstRowCards).toHaveLength(viewport.width === 390 ? 1 : 3);
+
+ const toggle = page.locator('.config-toggle').first();
+ const panel = page.locator('#config-filesystem');
+ await expect(toggle).toHaveAttribute('aria-expanded', 'false');
+ await expect(toggle).toHaveAttribute('aria-controls', 'config-filesystem');
+ await expect(panel).toBeHidden();
+
+ await toggle.focus();
+ await expect(toggle).toBeFocused();
+ await page.keyboard.press('Enter');
+ await expect(toggle).toHaveAttribute('aria-expanded', 'true');
+ await expect(panel).toBeVisible();
+ await expect(panel).not.toHaveAttribute('hidden', '');
+
+ await page.keyboard.press('Space');
+ await expect(toggle).toHaveAttribute('aria-expanded', 'false');
+ await expect(panel).toBeHidden();
+ }
+
+ const unsafeLinks = await page
+ .locator('a[target="_blank"]')
+ .evaluateAll((links) =>
+ links.filter((link) => !link.rel.split(/\s+/).includes('noopener')).map((link) => link.href)
+ );
+ expect(unsafeLinks).toEqual([]);
+ expect(errors).toEqual([]);
+});
+
+test('MCP guide keeps reduced-motion styles and skip link keyboard focus', async ({ page }) => {
+ const errors = collectPageErrors(page);
+
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto('/tools/ai/mcp-guide.html');
+
+ const skipLink = page.getByRole('link', { name: '跳到主要内容' });
+ await skipLink.focus();
+ await expect(skipLink).toBeFocused();
+ await expect(skipLink).toHaveCSS('outline-style', 'solid');
+ await page.keyboard.press('Enter');
+ await expect(page.locator('#main-content')).toBeFocused();
+
+ const transitionDurationMs = await page
+ .locator('.server-card')
+ .first()
+ .evaluate((card) =>
+ card.ownerDocument.defaultView
+ .getComputedStyle(card)
+ .transitionDuration.split(',')
+ .reduce((total, duration) => {
+ const value = Number.parseFloat(duration);
+ if (!Number.isFinite(value)) return total;
+ return total + (duration.trim().endsWith('ms') ? value : value * 1000);
+ }, 0)
+ );
+ expect(transitionDurationMs).toBeLessThan(1);
+ expect(errors).toEqual([]);
+});
diff --git a/tools/ai/mcp-guide.html b/tools/ai/mcp-guide.html
index 368f32c6f..a5374cfcf 100644
--- a/tools/ai/mcp-guide.html
+++ b/tools/ai/mcp-guide.html
@@ -89,18 +89,24 @@
/>
-
-
+
-