Iris MCP Server

  • src
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode, } from '@modelcontextprotocol/sdk/types.js'; import { simpleGit, SimpleGit } from 'simple-git'; import * as fs from 'fs-extra'; import * as path from 'path'; interface ReleaseNoteInput { startTag: string; endTag: string; title?: string; features?: string[]; improvements?: string[]; bugfixes?: string[]; breaking?: string[]; } class IrisServer { private server: Server; private git: SimpleGit; constructor() { this.server = new Server( { name: 'iris-mcp-server', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); this.git = simpleGit({ baseDir: process.cwd(), binary: 'git', maxConcurrentProcesses: 1, }); this.setupToolHandlers(); } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'generate_release_note', description: 'タグ間の差分からリリースノートを生成します', inputSchema: { type: 'object', properties: { startTag: { type: 'string', description: '開始タグ', }, endTag: { type: 'string', description: '終了タグ', }, title: { type: 'string', description: 'リリースノートのタイトル(オプション)', }, features: { type: 'array', items: { type: 'string' }, description: '新機能の一覧(オプション)', }, improvements: { type: 'array', items: { type: 'string' }, description: '改善項目の一覧(オプション)', }, bugfixes: { type: 'array', items: { type: 'string' }, description: 'バグ修正の一覧(オプション)', }, breaking: { type: 'array', items: { type: 'string' }, description: '破壊的変更の一覧(オプション)', }, }, required: ['startTag', 'endTag'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'generate_release_note') { const input = request.params.arguments as unknown as ReleaseNoteInput; if (!input.startTag || !input.endTag) { throw new McpError( ErrorCode.InvalidParams, 'startTagとendTagは必須パラメータです' ); } return await this.handleGenerateReleaseNote(input); } throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); }); } private async handleGenerateReleaseNote(input: ReleaseNoteInput) { try { // .irisディレクトリの作成 const irisDir = path.join(process.cwd(), '.iris'); await fs.ensureDir(irisDir); // タグ間の差分を取得 const diff = await this.git.diff([input.startTag, input.endTag]); const files = diff.split('diff --git').slice(1); // リリースノートの内容を生成 const content = this.generateReleaseNoteContent(input, files); // ファイル名を生成(タグ名とタイムスタンプを使用) const filename = `release-note-${input.endTag}-${Date.now()}.md`; const filePath = path.join(irisDir, filename); // リリースノートを保存 await fs.writeFile(filePath, content, 'utf-8'); return { content: [ { type: 'text', text: `リリースノートを生成しました: ${filePath}\n\n${content}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : '不明なエラーが発生しました'; throw new McpError( ErrorCode.InternalError, `リリースノートの生成に失敗しました: ${errorMessage}` ); } } private generateReleaseNoteContent(input: ReleaseNoteInput, files: string[]): string { let content = ''; const title = input.title || `Release ${input.endTag}`; const date = new Date().toISOString().split('T')[0]; content += `# ${title}\n\n`; content += `リリース日: ${date}\n\n`; // 破壊的変更 if (input.breaking && input.breaking.length > 0) { content += '## 💥 破壊的変更\n\n'; input.breaking.forEach(item => { content += `- ${item}\n`; }); content += '\n'; } // 新機能 if (input.features && input.features.length > 0) { content += '## ✨ 新機能\n\n'; input.features.forEach(feature => { content += `- ${feature}\n`; }); content += '\n'; } // 改善項目 if (input.improvements && input.improvements.length > 0) { content += '## 🔧 改善項目\n\n'; input.improvements.forEach(improvement => { content += `- ${improvement}\n`; }); content += '\n'; } // バグ修正 if (input.bugfixes && input.bugfixes.length > 0) { content += '## 🐛 バグ修正\n\n'; input.bugfixes.forEach(bugfix => { content += `- ${bugfix}\n`; }); content += '\n'; } // 変更されたファイル if (files.length > 0) { content += '## 📝 変更されたファイル\n\n'; files.forEach(file => { const match = file.match(/a\/(.*) b\//); if (match) { content += `- \`${match[1]}\`\n`; } }); content += '\n'; } return content; } public async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Iris MCP server running on stdio'); } } const server = new IrisServer(); server.run().catch(console.error);