feat: mcp serve preview

This commit is contained in:
hypercross 2026-03-18 14:43:54 +08:00
parent 1d2b4b3e1e
commit 5389345a00
2 changed files with 271 additions and 7 deletions

View File

@ -3,7 +3,7 @@ import { generateCardDeck, type GenerateCardDeckParams, type CardField } from '.
import { readFrontmatter, type ReadFrontmatterParams, type DeckFrontmatter } from '../tools/frontmatter/read-frontmatter.js'; import { readFrontmatter, type ReadFrontmatterParams, type DeckFrontmatter } from '../tools/frontmatter/read-frontmatter.js';
import { writeFrontmatter, type WriteFrontmatterParams } from '../tools/frontmatter/write-frontmatter.js'; import { writeFrontmatter, type WriteFrontmatterParams } from '../tools/frontmatter/write-frontmatter.js';
import { cardCrud, type CardCrudParams, type CardData } from '../tools/card/card-crud.js'; import { cardCrud, type CardCrudParams, type CardData } from '../tools/card/card-crud.js';
import { ensureDeckPreview, type EnsureDeckPreviewParams } from '../tools/ensure-deck-preview.js'; import { previewDeck, type PreviewDeckParams } from '../tools/preview-deck.js';
import { import {
designCardGame, designCardGame,
getDesignCardGamePrompt, getDesignCardGamePrompt,
@ -24,6 +24,7 @@ import {
readResource, readResource,
type DocResource type DocResource
} from '../resources/docs.js'; } from '../resources/docs.js';
import { serveCommand } from './serve.js';
/** /**
* MCP * MCP
@ -68,6 +69,13 @@ async function mcpServeAction(host: string, options: MCPOptions) {
process.chdir(cwd); process.chdir(cwd);
console.error(`MCP 服务器工作目录:${cwd}`); console.error(`MCP 服务器工作目录:${cwd}`);
// 启动 serve 服务器(在后台运行)
console.error('启动预览服务器...');
serveCommand(cwd, { port: '3000' });
// 等待服务器启动
await new Promise(resolve => setTimeout(resolve, 1000));
// 动态导入 MCP SDK // 动态导入 MCP SDK
const { Server } = await import('@modelcontextprotocol/sdk/server/index.js'); const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js'); const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
@ -196,8 +204,8 @@ async function mcpServeAction(host: string, options: MCPOptions) {
}, },
}, },
{ {
name: 'deck_ensure_preview', name: 'deck_preview',
description: '确保 CSV 对应的 Markdown 预览文件存在', description: '保存 CSV 对应的 Markdown 预览文件并打开浏览器',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@ -346,8 +354,8 @@ async function mcpServeAction(host: string, options: MCPOptions) {
}; };
} }
case 'deck_ensure_preview': { case 'deck_preview': {
const params = args as unknown as EnsureDeckPreviewParams; const params = args as unknown as PreviewDeckParams;
if (!params.csv_file) { if (!params.csv_file) {
return { return {
@ -356,10 +364,15 @@ async function mcpServeAction(host: string, options: MCPOptions) {
}; };
} }
const result = ensureDeckPreview(params); const result = previewDeck(params);
return { return {
content: [{ type: 'text', text: result.success ? `${result.message}` : `${result.message}` }], content: [{
type: 'text',
text: result.success
? `${result.message}\n\n预览 URL: ${result.preview_url}`
: `${result.message}`
}],
isError: !result.success, isError: !result.success,
}; };
} }

View File

@ -0,0 +1,251 @@
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { dirname, join, basename, extname, relative } from 'path';
import { execSync } from 'child_process';
import yaml from 'js-yaml';
import type { DeckFrontmatter, DeckConfig } from './frontmatter/read-frontmatter.js';
/**
* Deck
*/
export interface PreviewDeckParams {
/**
* CSV
*/
csv_file: string;
/**
* Markdown CSV
*/
md_file?: string;
/**
* CSV
*/
title?: string;
/**
*
*/
description?: string;
}
/**
* Deck
*/
export interface PreviewDeckResult {
success: boolean;
message: string;
md_file?: string;
created?: boolean;
preview_url?: string;
}
/**
* CSV frontmatter
*/
function parseFrontMatter(content: string): { frontmatter?: DeckFrontmatter; csvContent: string } {
const parts = content.trim().split(/(?:^|\n)---\s*\n/g);
if (parts.length !== 3 || parts[0] !== '') {
return { csvContent: content };
}
try {
const frontmatterStr = parts[1].trim();
const frontmatter = yaml.load(frontmatterStr) as DeckFrontmatter | undefined;
const csvContent = parts.slice(2).join('---\n').trimStart();
return { frontmatter, csvContent };
} catch (error) {
console.warn('Failed to parse front matter:', error);
return { csvContent: content };
}
}
/**
* md-deck
*/
function buildDeckComponent(csvPath: string, config?: DeckConfig): string {
const parts = [`:md-deck[${csvPath}]`];
const attrs: string[] = [];
if (config?.size) {
attrs.push(`size="${config.size}"`);
}
if (config?.grid) {
attrs.push(`grid="${config.grid}"`);
}
if (config?.bleed !== undefined && config.bleed !== 1) {
attrs.push(`bleed="${config.bleed}"`);
}
if (config?.padding !== undefined && config.padding !== 2) {
attrs.push(`padding="${config.padding}"`);
}
if (config?.shape && config.shape !== 'rectangle') {
attrs.push(`shape="${config.shape}"`);
}
if (config?.layers) {
attrs.push(`layers="${config.layers}"`);
}
if (config?.back_layers) {
attrs.push(`back-layers="${config.back_layers}"`);
}
if (attrs.length > 0) {
parts.push(`{${attrs.join(' ')}}`);
}
return parts.join('');
}
/**
*
*/
function openBrowser(url: string) {
try {
// Windows 使用 start 命令
if (process.platform === 'win32') {
execSync(`start "" "${url}"`, { stdio: 'ignore' });
} else if (process.platform === 'darwin') {
execSync(`open "${url}"`, { stdio: 'ignore' });
} else {
execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
}
console.error(`已打开浏览器:${url}`);
} catch (error) {
console.error('打开浏览器失败:', error);
}
}
/**
* Deck - Markdown
*/
export function previewDeck(params: PreviewDeckParams): PreviewDeckResult {
const { csv_file, md_file, title, description } = params;
// 检查 CSV 文件是否存在
if (!existsSync(csv_file)) {
return {
success: false,
message: `CSV 文件不存在:${csv_file}`
};
}
// 确定 Markdown 文件路径
let mdFilePath = md_file;
if (!mdFilePath) {
// 使用与 CSV 相同的路径和文件名,只是扩展名不同
const dir = dirname(csv_file);
const name = basename(csv_file, extname(csv_file));
mdFilePath = join(dir, `${name}.md`);
}
const created = !existsSync(mdFilePath);
// 如果文件已存在,检查是否已有 md-deck 组件
if (!created) {
try {
const existingContent = readFileSync(mdFilePath, 'utf-8');
// 如果已有 md-deck 组件引用该 CSV直接返回
if (existingContent.includes(`:md-deck[${csv_file}]`) ||
existingContent.includes(`:md-deck[./${basename(csv_file)}]`)) {
// 仍然打开浏览器
const relativeMdPath = relative(process.cwd(), mdFilePath).split('\\').join('/');
const previewUrl = `http://localhost:3000/${relativeMdPath}`;
openBrowser(previewUrl);
return {
success: true,
message: `预览文件 ${mdFilePath} 已存在`,
md_file: mdFilePath,
created: false,
preview_url: previewUrl
};
}
} catch (error) {
// 读取失败,继续创建
}
}
// 读取 CSV 的 frontmatter 获取配置
let deckConfig: DeckConfig | undefined;
try {
const csvContent = readFileSync(csv_file, 'utf-8');
const { frontmatter } = parseFrontMatter(csvContent);
deckConfig = frontmatter?.deck;
} catch (error) {
// 忽略错误,使用默认配置
}
// 确定标题
let deckTitle = title;
if (!deckTitle) {
// 从 CSV 文件名推断
const name = basename(csv_file, extname(csv_file));
deckTitle = name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
// 构建 md-deck 组件代码(使用相对路径)
const mdDir = dirname(mdFilePath);
const csvDir = dirname(csv_file);
let relativeCsvPath: string;
if (mdDir === csvDir) {
relativeCsvPath = `./${basename(csv_file)}`;
} else {
relativeCsvPath = `./${basename(csv_file)}`;
}
const deckComponent = buildDeckComponent(relativeCsvPath, deckConfig);
// 生成 Markdown 内容
const mdLines: string[] = [];
mdLines.push(`# ${deckTitle}`);
mdLines.push('');
if (description) {
mdLines.push(description);
mdLines.push('');
}
mdLines.push('## 卡牌预览');
mdLines.push('');
mdLines.push(deckComponent);
mdLines.push('');
mdLines.push('## 使用说明');
mdLines.push('');
mdLines.push('- 点击卡牌可以查看详情');
mdLines.push('- 使用右上角的按钮可以随机抽取卡牌');
mdLines.push('- 可以通过编辑面板调整卡牌样式和布局');
mdLines.push('');
// 写入文件
try {
writeFileSync(mdFilePath, mdLines.join('\n'), 'utf-8');
// 计算预览 URL
const relativeMdPath = relative(process.cwd(), mdFilePath).split('\\').join('/');
const previewUrl = `http://localhost:3000/${relativeMdPath.slice(0, -3)}`;
// 打开浏览器
openBrowser(previewUrl);
return {
success: true,
message: created
? `创建预览文件 ${mdFilePath}`
: `更新预览文件 ${mdFilePath}`,
md_file: mdFilePath,
created,
preview_url: previewUrl
};
} catch (error) {
return {
success: false,
message: `写入失败:${error instanceof Error ? error.message : '未知错误'}`
};
}
}