feat: mcp server?
This commit is contained in:
@@ -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`);
|
||||
}
|
||||
Reference in New Issue
Block a user