feat: mcp server?

This commit is contained in:
hyper
2026-03-17 20:01:04 +08:00
parent b099d5695d
commit 834db72ec4
5 changed files with 710 additions and 0 deletions
+284
View File
@@ -0,0 +1,284 @@
import { Command } from 'commander';
import { generateCardDeck, type GenerateCardDeckParams, type CardField } from '../tools/generate-card-deck.js';
/**
* MCP 服务器命令
*
* 提供 MCP (Model Context Protocol) 服务器功能,用于与 AI 助手集成
*/
export interface MCPOptions {
port?: string;
}
export const mcpCommand = new Command('mcp')
.description('MCP 服务器 - 用于 AI 助手集成的工具协议')
.addCommand(
new Command('serve')
.description('启动 MCP 服务器')
.argument('[host]', '服务器地址', 'stdio')
.option('-p, --port <port>', 'HTTP 端口(仅 HTTP 传输)', '3001')
.action(mcpServeAction)
)
.addCommand(
new Command('generate-card-deck')
.description('生成卡牌组(快速命令)')
.requiredOption('--name <name>', '卡牌组名称')
.requiredOption('--output <dir>', '输出目录')
.option('-c, --count <number>', '卡牌数量', '10')
.option('--fields <fields>', '字段列表(逗号分隔)', 'name,type,cost,description')
.option('--description <desc>', '卡牌组描述')
.option('--size <size>', '卡牌尺寸', '54x86')
.option('--grid <grid>', '网格布局', '5x8')
.action(generateCardDeckAction)
);
/**
* MCP 服务器启动处理函数
*/
async function mcpServeAction(host: string, options: MCPOptions) {
// 动态导入 MCP SDK
const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
const {
CallToolRequestSchema,
ListToolsRequestSchema,
} = await import('@modelcontextprotocol/sdk/types.js');
const server = new Server(
{
name: 'ttrpg-card-generator',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
// 处理工具列表请求
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'generate_card_deck',
description: '生成 TTRPG 卡牌组内容,包括 Markdown 介绍文件、CSV 数据文件和 md-deck 组件配置',
inputSchema: {
type: 'object',
properties: {
deck_name: {
type: 'string',
description: '卡牌组名称,将用于生成文件名',
},
output_dir: {
type: 'string',
description: '输出目录路径',
},
card_count: {
type: 'number',
description: '卡牌数量',
default: 10,
},
card_template: {
type: 'object',
description: '卡牌模板定义',
properties: {
fields: {
type: 'array',
description: '卡牌字段列表',
items: {
type: 'object',
properties: {
name: {
type: 'string',
description: '字段名称(英文,用于 CSV 列名)',
},
description: {
type: 'string',
description: '字段描述',
},
examples: {
type: 'array',
description: '示例值列表',
items: { type: 'string' },
},
},
required: ['name'],
},
},
examples: {
type: 'array',
description: '完整的卡牌示例数据',
items: {
type: 'object',
additionalProperties: { type: 'string' },
},
},
},
required: ['fields'],
},
deck_config: {
type: 'object',
description: 'md-deck 组件配置',
properties: {
size: {
type: 'string',
description: '卡牌尺寸,格式 "宽 x 高"(单位 mm',
default: '54x86',
},
grid: {
type: 'string',
description: '网格布局,格式 "列 x 行"',
default: '5x8',
},
bleed: {
type: 'number',
description: '出血边距(mm',
default: 1,
},
padding: {
type: 'number',
description: '内边距(mm',
default: 2,
},
shape: {
type: 'string',
description: '卡牌形状',
enum: ['rectangle', 'circle', 'hex', 'diamond'],
default: 'rectangle',
},
layers: {
type: 'string',
description: '正面图层配置,格式 "字段:行,列范围,字体大小"',
},
back_layers: {
type: 'string',
description: '背面图层配置',
},
},
},
description: {
type: 'string',
description: '卡牌组的介绍描述(可选)',
},
},
required: ['deck_name', 'output_dir'],
},
},
],
};
});
// 处理工具调用请求
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'generate_card_deck') {
try {
const params = args as unknown as GenerateCardDeckParams;
// 验证必需参数
if (!params.deck_name || !params.output_dir) {
return {
content: [
{
type: 'text',
text: '错误:缺少必需参数 deck_name 或 output_dir',
},
],
isError: true,
};
}
// 生成卡牌组
const result = generateCardDeck(params);
return {
content: [
{
type: 'text',
text: result.message,
},
{
type: 'text',
text: `\n## 生成的组件代码\n\n\`\`\`markdown\n${result.deckComponent}\n\`\`\``,
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `生成失败:${error instanceof Error ? error.message : '未知错误'}`,
},
],
isError: true,
};
}
}
return {
content: [
{
type: 'text',
text: `未知工具:${name}`,
},
],
isError: true,
};
});
// 启动服务器
if (host === 'stdio') {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('TTRPG 卡牌生成 MCP 服务器已启动(stdio');
} else {
// TODO: 支持 HTTP 传输
console.error('HTTP 传输模式尚未实现,请使用 stdio 模式');
process.exit(1);
}
}
/**
* 快速生成卡牌组命令处理函数
*/
async function generateCardDeckAction(options: {
name: string;
output: string;
count: string;
fields: string;
description?: string;
size: string;
grid: string;
}) {
const fieldNames = options.fields.split(',').map(f => f.trim());
const template = {
fields: fieldNames.map((name): CardField => ({
name,
examples: ['示例 1', '示例 2']
}))
};
const params: GenerateCardDeckParams = {
deck_name: options.name,
output_dir: options.output,
card_count: parseInt(options.count, 10),
card_template: template,
deck_config: {
size: options.size,
grid: options.grid
},
description: options.description
};
const result = generateCardDeck(params);
console.log('\n✅ 卡牌组生成成功!\n');
console.log(result.message);
console.log('\n📦 组件代码:');
console.log(` ${result.deckComponent}\n`);
}
+4
View File
@@ -2,6 +2,7 @@
import { Command } from 'commander';
import { serveCommand } from './commands/serve.js';
import { compileCommand } from './commands/compile.js';
import { mcpCommand } from './commands/mcp.js';
import type { ServeOptions, CompileOptions } from './types.js';
const program = new Command();
@@ -25,4 +26,7 @@ program
.option('-o, --output <dir>', '输出目录', './dist/output')
.action(compileCommand);
program
.addCommand(mcpCommand);
program.parse();
+289
View File
@@ -0,0 +1,289 @@
import { writeFileSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';
/**
* 卡牌字段定义
*/
export interface CardField {
name: string;
description?: string;
examples?: string[];
}
/**
* 卡牌模板配置
*/
export interface CardTemplate {
fields: CardField[];
examples?: Record<string, string>[];
}
/**
* Deck 配置
*/
export interface DeckConfig {
size?: string;
grid?: string;
bleed?: number;
padding?: number;
shape?: 'rectangle' | 'circle' | 'hex' | 'diamond';
layers?: string;
backLayers?: string;
}
/**
* 生成卡牌组的参数
*/
export interface GenerateCardDeckParams {
deck_name: string;
output_dir: string;
card_count?: number;
card_template?: CardTemplate;
deck_config?: DeckConfig;
description?: string;
}
/**
* 生成卡牌数据 CSV
*/
function generateCardCSV(
template: CardTemplate,
cardCount: number
): string {
const fields = template.fields;
// 构建 CSV 表头
const headers = ['label', ...fields.map(f => f.name), 'body'];
// 生成示例数据
const rows: string[][] = [];
const examples = template.examples || [];
for (let i = 0; i < cardCount; i++) {
const row: string[] = [(i + 1).toString()];
// 为每个字段生成值
for (const field of fields) {
let value = '';
if (examples.length > 0) {
// 从示例中循环取值
const exampleIndex = i % examples.length;
const example = examples[exampleIndex];
value = example[field.name] || field.examples?.[i % (field.examples?.length || 1)] || '';
} else if (field.examples && field.examples.length > 0) {
// 从字段的示例中取值
value = field.examples[i % field.examples.length];
} else {
// 默认占位符
value = `{{${field.name}_${i + 1}}}`;
}
row.push(value);
}
// body 列使用模板语法
const bodyParts: string[] = [];
for (const field of fields) {
bodyParts.push(`**${field.name}:** {{${field.name}}}`);
}
row.push(bodyParts.join('\n\n'));
rows.push(row);
}
// 组合 CSV 内容
const csvLines = [headers.join(',')];
for (const row of rows) {
csvLines.push(row.map(cell => {
// 处理包含逗号或换行的单元格
if (cell.includes(',') || cell.includes('\n') || cell.includes('"')) {
return `"${cell.replace(/"/g, '""')}"`;
}
return cell;
}).join(','));
}
return csvLines.join('\n');
}
/**
* 生成卡牌介绍的 Markdown 文件
*/
function generateDeckMarkdown(
deckName: string,
csvFileName: string,
deckConfig: DeckConfig,
description?: string
): string {
const mdLines: string[] = [];
// 标题
mdLines.push(`# ${deckName}`);
mdLines.push('');
// 描述
if (description) {
mdLines.push(description);
mdLines.push('');
}
// 卡牌预览组件
mdLines.push('## 卡牌预览');
mdLines.push('');
// 构建 :md-deck 组件代码
const deckComponent = buildDeckComponent(csvFileName, deckConfig);
mdLines.push(deckComponent);
mdLines.push('');
// 使用说明
mdLines.push('## 使用说明');
mdLines.push('');
mdLines.push('- 点击卡牌可以查看详情');
mdLines.push('- 使用右上角的按钮可以随机抽取卡牌');
mdLines.push('- 可以通过编辑面板调整卡牌样式和布局');
mdLines.push('');
return mdLines.join('\n');
}
/**
* 构建 :md-deck 组件代码
*/
function buildDeckComponent(
csvFileName: string,
config: DeckConfig
): string {
const parts = [`:md-deck[${csvFileName}]`];
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.backLayers) {
attrs.push(`back-layers="${config.backLayers}"`);
}
if (attrs.length > 0) {
parts.push(`{${attrs.join(' ')}}`);
}
return parts.join('');
}
/**
* 自动生成图层配置
*/
function autoGenerateLayers(fields: CardField[]): string {
if (fields.length === 0) return '';
const layers: string[] = [];
const totalHeight = 8;
const heightPerField = Math.floor((totalHeight - 2) / fields.length);
for (let i = 0; i < fields.length; i++) {
const field = fields[i];
const y1 = 2 + i * heightPerField;
const y2 = y1 + heightPerField - 1;
const fontSize = Math.min(12, Math.floor(80 / fields.length));
layers.push(`${field.name}:1,${y1}-${y2},${fontSize}`);
}
return layers.join(' ');
}
/**
* 生成卡牌组的主函数
*/
export function generateCardDeck(params: GenerateCardDeckParams): {
mdFile: string;
csvFile: string;
deckComponent: string;
message: string;
} {
const {
deck_name,
output_dir,
card_count = 10,
card_template,
deck_config = {},
description
} = params;
// 确保输出目录存在
if (!existsSync(output_dir)) {
mkdirSync(output_dir, { recursive: true });
}
// 生成文件名
const safeName = deck_name.toLowerCase().replace(/\s+/g, '-');
const csvFileName = `${safeName}.csv`;
const mdFileName = `${safeName}.md`;
// 创建默认模板(如果没有提供)
const template: CardTemplate = card_template || {
fields: [
{ name: 'name', description: '卡牌名称', examples: ['示例卡牌 1', '示例卡牌 2'] },
{ name: 'type', description: '卡牌类型', examples: ['物品', '法术'] },
{ name: 'cost', description: '费用', examples: ['1', '2'] },
{ name: 'description', description: '效果描述', examples: ['这是一个效果描述', '这是另一个效果'] }
]
};
// 创建默认配置(如果没有提供)
const config: DeckConfig = {
size: deck_config.size || '54x86',
grid: deck_config.grid || '5x8',
bleed: deck_config.bleed ?? 1,
padding: deck_config.padding ?? 2,
shape: deck_config.shape || 'rectangle',
layers: deck_config.layers || autoGenerateLayers(template.fields)
};
// 生成 CSV 内容
const csvContent = generateCardCSV(template, card_count);
const csvPath = join(output_dir, csvFileName);
writeFileSync(csvPath, csvContent, 'utf-8');
// 生成 Markdown 内容
const mdContent = generateDeckMarkdown(
deck_name,
`./${csvFileName}`,
config,
description
);
const mdPath = join(output_dir, mdFileName);
writeFileSync(mdPath, mdContent, 'utf-8');
// 构建完整的 deck 组件代码
const deckComponent = buildDeckComponent(`./${csvFileName}`, config);
return {
mdFile: mdPath,
csvFile: csvPath,
deckComponent,
message: `已生成卡牌组 "${deck_name}":\n- Markdown 文件:${mdPath}\n- CSV 数据:${csvPath}\n- 组件代码:${deckComponent}`
};
}