Skip to main content
Glama
server.mjs.with_3tools34.8 kB
import express from 'express'; import cors from 'cors'; import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import dotenv from 'dotenv'; import fetch from 'node-fetch'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js'; import fs from 'fs'; import path from 'path'; // ウィジェットHTMLの読み込み const widgetHtml = fs.readFileSync('./web/src/video-qr-widget.html', 'utf-8'); // 環境変数の読み込み dotenv.config(); // 環境変数のバリデーション const requiredEnvVars = ['MCP_API_KEY', 'OMOTENASHI_SESSION_TOKEN', 'BASE_API_URL']; for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { console.error(`Error: ${envVar} is not set in .env file`); process.exit(1); } } const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 8001; const MCP_API_KEY = process.env.MCP_API_KEY; const OMOTENASHI_SESSION_TOKEN = process.env.OMOTENASHI_SESSION_TOKEN; const BASE_API_URL = process.env.BASE_API_URL; console.log('=== MCP Server Configuration ==='); console.log(`Port: ${MCP_PORT}`); console.log(`Base API URL: ${BASE_API_URL}`); console.log(`API Key: ${MCP_API_KEY.substring(0, 10)}...`); console.log('================================\n'); /** * MCPサーバーのインスタンスを作成 */ const createMcpServer = () => { const server = new McpServer( { name: 'omotenashi-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, logging: {}, resources: {}, }, } ); /** * 音声生成ツール * おもてなしQRの既存API(/api/v2/video/generate-audio)を呼び出す */ server.registerTool( 'generate_audio', { title: '音声生成ツール', description: 'テキストから音声を生成します(おもてなしQR音声生成API)', inputSchema: z.object({ content: z.string().describe('音声化するテキスト内容'), language: z.enum(['ja', 'en', 'zh', 'ko']).default('ja').describe('言語 (ja, en, zh, ko)'), voice_speaker: z.string().default('Orus').describe('音声スピーカー名(例: Orus)'), voice_speed: z.number().min(0.5).max(2.0).default(1.0).describe('音声速度 (0.5-2.0)'), }), }, async ({ content, language, voice_speaker, voice_speed }, extra) => { try { await server.sendLoggingMessage( { level: 'info', data: `Starting audio generation: "${content.substring(0, 50)}${content.length > 50 ? '...' : ''}"`, }, extra.sessionId ); // 既存APIのエンドポイント const apiUrl = `${BASE_API_URL}/video/generate-audio`; // APIリクエストボディ const requestBody = { session_token: OMOTENASHI_SESSION_TOKEN, content: content, language: language, settings: { voice_speaker: voice_speaker, voice_speed: voice_speed, }, original_prompt: 'MCP Server経由', }; await server.sendLoggingMessage( { level: 'debug', data: `Calling API: ${apiUrl}`, }, extra.sessionId ); // リクエスト詳細をログ出力 console.log('[DEBUG] API Request:'); console.log(' URL:', apiUrl); console.log(' Body:', JSON.stringify(requestBody, null, 2)); // 既存APIを呼び出し const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText = await response.text(); console.log('[ERROR] API Response:'); console.log(' Status:', response.status); console.log(' StatusText:', response.statusText); console.log(' Body:', errorText); throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`); } const data = await response.json(); // APIレスポンスの検証 if (!data.success) { // エラーの詳細を解析 const errorDetail = data.error || data.message || 'Unknown error'; // 503エラー(Google API一時的障害)の特別処理 if (typeof errorDetail === 'string' && errorDetail.includes('503')) { throw new Error('Google音声合成APIが一時的に利用できません。数秒後に再試行してください。(503 Service Unavailable)'); } throw new Error(`音声生成に失敗しました: ${JSON.stringify(data)}`); } // プロジェクトIDを取得 const projectId = data.data?.project_id; if (!projectId) { throw new Error('No project_id in response'); } await server.sendLoggingMessage( { level: 'info', data: `Audio generation started. Project ID: ${projectId}`, }, extra.sessionId ); // 🆕 音声ファイル完成を待つ(ステータスAPIポーリング) const maxAttempts = 60; // 最大60秒 const pollInterval = 1000; // 1秒ごと let audioFileUrl = null; let audioPath = null; let finalStatus = null; let completedStatusData = null; for (let attempt = 1; attempt <= maxAttempts; attempt++) { // ステータスAPIを呼び出し const statusUrl = `${BASE_API_URL}/video/project-status/${projectId}`; const statusResponse = await fetch(statusUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (statusResponse.ok) { const statusData = await statusResponse.json(); if (statusData.success && statusData.data) { const status = statusData.data.status; finalStatus = status; // 音声完成を確認 if ((status === 'audio_completed' || status === 'completed') && statusData.data.files?.audio) { audioPath = statusData.data.files.audio; audioFileUrl = `https://omotenashiqr.com/${audioPath}`; completedStatusData = statusData; await server.sendLoggingMessage( { level: 'info', data: `Audio completed after ${attempt} seconds: ${audioFileUrl}`, }, extra.sessionId ); break; } } } // まだ完成していない場合は待つ if (attempt < maxAttempts) { await new Promise(resolve => setTimeout(resolve, pollInterval)); } } if (!audioFileUrl) { throw new Error(`Audio file not ready after ${maxAttempts} seconds (status: ${finalStatus})`); } await server.sendLoggingMessage( { level: 'info', data: `Audio generation completed. Project ID: ${projectId}, Audio URL: ${audioFileUrl}`, }, extra.sessionId ); // MCPクライアントに返すレスポンス return { content: [ { type: 'text', text: JSON.stringify( { success: true, project_id: projectId, audio_path: audioPath, audio_url: audioFileUrl, status: 'audio_completed', message: '音声生成が正常に完了しました', note: '動画を生成するには、generate_videoツールでこのaudio_pathを使用してください', }, null, 2 ), }, ], }; } catch (error) { await server.sendLoggingMessage( { level: 'error', data: `Error in generate_audio: ${error.message}`, }, extra.sessionId ); // エラーの種類を判定 const is503Error = error.message.includes('503'); const isTemporary = is503Error || error.message.includes('timeout') || error.message.includes('UNAVAILABLE'); return { content: [ { type: 'text', text: JSON.stringify( { success: false, error: error.message, error_type: isTemporary ? 'temporary' : 'permanent', retry_recommended: isTemporary, message: '音声生成中にエラーが発生しました', troubleshooting: isTemporary ? '一時的なエラーです。数秒後に再試行してください。' : 'エラーの詳細を確認して、設定を見直してください。', }, null, 2 ), }, ], isError: true, }; } } ); // 動画生成ツール server.registerTool( 'generate_video', { title: '動画生成ツール', description: '音声から動画を生成します(おもてなしQR動画生成API)', inputSchema: { project_id: z.string().describe('プロジェクトID(音声生成で取得したID)'), audio_path: z.string().describe('音声ファイルパス(音声生成で取得したパス)'), background_type: z .enum(['default', 'custom']) .default('default') .describe('背景タイプ: default(デフォルト背景)またはcustom(カスタム背景)'), custom_image: z.string().optional().describe('カスタム背景画像(Base64エンコード、background_type=customの場合のみ)'), use_bgm: z.boolean().default(false).describe('BGMを使用するか'), use_subtitles: z.boolean().default(true).describe('字幕を表示するか'), use_vertical_video: z.boolean().default(false).describe('縦動画(1080x1920)にするか'), }, }, async ({ project_id, audio_path, background_type, custom_image, use_bgm, use_subtitles, use_vertical_video }, extra) => { try { await server.sendLoggingMessage( { level: 'info', data: `Generating video for project: ${project_id}`, }, extra.sessionId ); // 背景画像の準備 let uploadedImageInfo; if (background_type === 'default') { // デフォルト画像をダウンロード await server.sendLoggingMessage( { level: 'info', data: 'Downloading default background image...', }, extra.sessionId ); const bgUrl = 'https://omotenashiqr.com/assets/backgrounds/default_generic.jpg'; const imgResp = await fetch(bgUrl); if (!imgResp.ok) { throw new Error('Failed to download default background image'); } const imgBuf = await imgResp.arrayBuffer(); // TODO: 画像保存の恒久的な解決策が必要 // 現在の実装: MCPサーバーから直接プロジェクトフォルダに保存 // 将来の改善: 本体側のAPI拡張(project_id対応のアップロードエンドポイント) // 課題: uploads/フォルダとproject/フォルダの二重管理 await server.sendLoggingMessage( { level: 'info', data: `Saving image directly to project folder...`, }, extra.sessionId ); // プロジェクトフォルダに直接保存 const timestamp = Math.floor(Date.now() / 1000); const filename = `${timestamp}_default_background.jpg`; const projectPath = `/home/ubuntu/omotenashiqr_production/outputs/${project_id}`; const filepath = `${projectPath}/${filename}`; // プロジェクトフォルダが存在することを確認 if (!fs.existsSync(projectPath)) { throw new Error(`Project directory does not exist: ${projectPath}`); } // バッファをファイルに書き込み fs.writeFileSync(filepath, Buffer.from(imgBuf)); uploadedImageInfo = { id: 'img_1', file_name: filename, original_name: 'default_background.jpg', }; await server.sendLoggingMessage( { level: 'info', data: `Image saved to project folder: ${filename}`, }, extra.sessionId ); } else if (background_type === 'custom') { // カスタム画像を使用(既にアップロード済みと仮定) if (!custom_image) { throw new Error('custom_image is required when background_type is "custom"'); } // custom_imageがファイル名の場合 uploadedImageInfo = { id: 'img_1', file_name: custom_image, original_name: custom_image, }; } else { throw new Error(`Invalid background_type: ${background_type}`); } const apiUrl = `${BASE_API_URL}/video/generate-video`; const requestBody = { session_token: OMOTENASHI_SESSION_TOKEN, project_id: project_id, audio_path: audio_path, settings: { backgroundType: 'custom', multiple_images: [uploadedImageInfo], }, use_bgm: use_bgm, use_subtitles: use_subtitles, use_vertical_video: use_vertical_video, }; await server.sendLoggingMessage( { level: 'debug', data: `Video generation request: ${JSON.stringify(requestBody, null, 2)}`, }, extra.sessionId ); const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`); } const data = await response.json(); // APIレスポンスの検証 if (!data.success) { // エラーの詳細を解析 const errorDetail = data.error || data.message || 'Unknown error'; // 503エラー(Google API一時的障害)の特別処理 if (typeof errorDetail === 'string' && errorDetail.includes('503')) { throw new Error('Google音声合成APIが一時的に利用できません。数秒後に再試行してください。(503 Service Unavailable)'); } throw new Error(`動画生成に失敗しました: ${JSON.stringify(data)}`); } await server.sendLoggingMessage( { level: 'info', data: `Video generation started. Polling for completion...`, }, extra.sessionId ); // ポーリングで動画完成を待つ(最大5分) let videoUrl = null; let shortUrl = null; const statusUrl = `${BASE_API_URL}/video/project-status/${project_id}`; for (let i = 1; i <= 300; i++) { const statusResp = await fetch(statusUrl); if (statusResp.ok) { const statusData = await statusResp.json(); if (statusData.success && statusData.data && statusData.data.status === 'completed' && statusData.data.files?.video) { videoUrl = `https://omotenashiqr.com/${statusData.data.files.video}`; shortUrl = statusData.data.short_url || null; break; } } // 1秒待機(最後のループ以外) if (i < 300) await new Promise(r => setTimeout(r, 1000)); } if (!videoUrl) { throw new Error('動画生成がタイムアウトしました(5分経過)。プロジェクトステータスを確認してください。'); } await server.sendLoggingMessage( { level: 'info', data: `Video generation completed. Video URL: ${videoUrl}`, }, extra.sessionId ); // MCPクライアントに返すレスポンス return { content: [ { type: 'text', text: JSON.stringify( { success: true, project_id: project_id, video_url: videoUrl, short_url: shortUrl, message: '動画生成が正常に完了しました', }, null, 2 ), }, ], }; } catch (error) { await server.sendLoggingMessage( { level: 'error', data: `Error in generate_video: ${error.message}`, }, extra.sessionId ); // エラーの種類を判定 const is503Error = error.message.includes('503'); const isTemporary = is503Error || error.message.includes('timeout') || error.message.includes('UNAVAILABLE'); return { content: [ { type: 'text', text: JSON.stringify( { success: false, error: error.message, error_type: isTemporary ? 'temporary' : 'permanent', retry_recommended: isTemporary, message: '動画生成中にエラーが発生しました', troubleshooting: isTemporary ? '一時的なエラーです。数秒後に再試行してください。' : 'エラーの詳細を確認して、設定を見直してください。', }, null, 2 ), }, ], isError: true, }; } } ); // 動画完全生成ツール(音声→動画→QRコード一括生成) server.registerTool( 'generate_complete_video', { _meta: { "openai/outputTemplate": "ui://widget/video-qr-display.html", "openai/toolInvocation/invoking": "動画を生成しています...", "openai/toolInvocation/invoked": "動画生成完了" }, title: '動画完全生成ツール', description: 'テキストから音声生成・動画生成・ストレージアップロード・QRコード生成まで一括実行します(15言語対応)', inputSchema: z.object({ text: z.string().describe('動画にするテキスト内容'), language: z .enum([ 'ja', 'en', 'zh', 'zh-TW', 'ko', 'th', 'es', 'it', 'fr', 'de', 'ru', 'ms', 'id', 'vi', 'fil' ]) .default('ja') .describe('言語コード(15言語対応)'), title: z.string().optional().describe('動画タイトル(省略時は自動生成)'), description: z.string().optional().describe('動画説明文(省略時はデフォルト値)'), tags: z.array(z.string()).optional().describe('動画タグ(配列)'), is_public: z.boolean().default(true).describe('動画を公開するか'), use_bgm: z.boolean().default(false).describe('BGMを使用するか'), use_subtitles: z.boolean().default(true).describe('字幕を表示するか'), use_vertical_video: z.boolean().default(false).describe('縦動画(1080x1920)にするか'), image_urls: z.array(z.string()).optional().describe('背景画像URL(複数対応・将来拡張用)'), mcp_user_id: z.union([z.string(), z.number()]).default('38').describe('MCPユーザーID'), }), outputSchema: z.object({ qrCode: z.string().describe('QRコードのBase64データURL'), shortUrl: z.string().describe('短縮URL'), videoUrl: z.string().describe('動画URL'), title: z.string().describe('動画タイトル'), description: z.string().describe('動画説明'), language: z.string().describe('言語コード'), durationSeconds: z.number().describe('音声の長さ(秒)'), projectId: z.string().describe('プロジェクトID'), }), }, async (args, extra) => { try { const { text, language, title, description, tags, is_public, use_bgm, use_subtitles, use_vertical_video, image_urls, mcp_user_id } = args; await server.sendLoggingMessage( { level: 'info', data: 'Starting complete video generation: "' + text.substring(0, 50) + (text.length > 50 ? '...' : '') + '" (' + language + ')' }, extra.sessionId ); const apiUrl = BASE_API_URL + '/mcp/generate-complete-video'; const requestBody = { mcp_api_key: MCP_API_KEY, text: text, language: language, title: title, description: description, tags: tags, is_public: is_public, use_bgm: use_bgm, use_subtitles: use_subtitles, use_vertical_video: use_vertical_video, image_urls: image_urls, mcp_user_id: String(mcp_user_id) }; console.log('[DEBUG] Complete Video API Request:', JSON.stringify(requestBody, null, 2)); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 120000); const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), signal: controller.signal, }).finally(() => clearTimeout(timeout)); if (!response.ok) { const errorText = await response.text(); console.log('[ERROR] Complete Video API:', response.status, errorText); throw new Error('API request failed: ' + response.status + ' - ' + errorText); } const data = await response.json(); if (!data.success) { throw new Error('動画生成に失敗: ' + JSON.stringify(data)); } const r = data.data; await server.sendLoggingMessage( { level: 'info', data: 'Complete video succeeded! Project: ' + r.project_id + ', Short URL: ' + r.short_url }, extra.sessionId ); const resultText = '動画生成完了!\n\n' + 'プロジェクトID: ' + r.project_id + '\n' + 'video_id: ' + r.video_id + '\n\n' + '短縮URL: ' + r.short_url + '\n' + '短縮コード: ' + r.short_code + '\n\n' + '動画URL: ' + r.video_url + '\n' + '音声URL: ' + r.audio_url + '\n\n' + 'タイトル: ' + r.title + '\n' + '説明: ' + r.description + '\n' + '言語: ' + r.language + '\n' + '音声時間: ' + r.duration_seconds + '秒\n' + 'ファイルサイズ: ' + r.file_size + ' bytes\n' + '画像枚数: ' + r.image_count + '枚\n\n' + 'QRコード: Base64データ(' + r.qr_code.length + '文字)\n\n' + 'スマートフォンでQRコードをスキャンすると動画が再生されます。'; return { structuredContent: { qrCode: r.qr_code, shortUrl: r.short_url, videoUrl: r.video_url, title: r.title, description: r.description, language: r.language, durationSeconds: r.duration_seconds, projectId: r.project_id }, content: [ { type: 'text', text: resultText } ], _meta: { "openai/outputTemplate": "ui://widget/video-qr-display.html", "openai/widgetAccessible": true }, isError: false }; } catch (error) { console.error('[ERROR] generate_complete_video failed:', error); await server.sendLoggingMessage( { level: 'error', data: 'Complete video generation failed: ' + error.message }, extra.sessionId ); if (error.name === 'AbortError') { return { content: [{ type: 'text', text: '動画生成がタイムアウトしました(120秒超過)。処理は継続している可能性があります。\n\nエラー: ' + error.message }], isError: true, }; } return { content: [{ type: 'text', text: '動画生成エラー: ' + error.message + '\n\nStack: ' + error.stack }], isError: true, }; } } ); // ウィジェットリソース登録 server.registerResource( "video-qr-widget", "ui://widget/video-qr-display.html", { "openai/widgetCSP": { "resource_domains": [ "https://cdn.omotenashiqr.com", "https://omotenashiqr.com", "https://s3.isk01.sakurastorage.jp" ] }, "openai/widgetPrefersBorder": true }, async () => ({ contents: [{ uri: "ui://widget/video-qr-display.html", mimeType: "text/html+skybridge", text: widgetHtml, _meta: { "openai/widgetDescription": "動画プレイヤーとQRコードを表示するウィジェット", "openai/widgetDomain": "https://chatgpt.com" } }] }) ); return server; }; // グローバルMCPサーバーインスタンス(全セッションで共有) const globalMcpServer = createMcpServer(); /** * Expressアプリケーションのセットアップ */ const app = express(); // ミドルウェア app.use(express.json()); app.use( cors({ origin: '*', exposedHeaders: ['Mcp-Session-Id'], }) ); // トランスポートマップ(セッションIDごとに管理) const transports = {}; /** * API KEY認証ミドルウェア */ const authenticateApiKey = (req, res, next) => { // initializeリクエストはAPI KEY不要(セッション確立のため) if (req.body && isInitializeRequest(req.body)) { return next(); } const apiKey = req.headers['x-api-key'] || req.headers['authorization']?.replace('Bearer ', ''); if (!apiKey) { return res.status(401).json({ jsonrpc: '2.0', error: { code: -32001, message: 'Unauthorized: API key is required (X-API-Key header)', }, id: null, }); } if (apiKey !== MCP_API_KEY) { return res.status(403).json({ jsonrpc: '2.0', error: { code: -32002, message: 'Forbidden: Invalid API key', }, id: null, }); } next(); }; /** * MCP POSTエンドポイント */ const mcpPostHandler = async (req, res) => { // デバッグ: リクエスト詳細をログ出力 console.log('[DEBUG] POST /mcp - Headers:', JSON.stringify(req.headers)); console.log('[DEBUG] POST /mcp - Body:', JSON.stringify(req.body)); const sessionId = req.headers['mcp-session-id']; if (sessionId) { console.log(`[MCP] Received request for session: ${sessionId}`); } else if (isInitializeRequest(req.body)) { console.log('[MCP] Received new initialization request'); } else { console.log('[MCP] Received request without session ID'); } try { let transport; if (sessionId && transports[sessionId]) { // 既存のトランスポートを再利用 transport = transports[sessionId]; console.log(`[MCP] Reusing existing transport for session: ${sessionId}`); } else if (!sessionId && isInitializeRequest(req.body)) { // 新規初期化リクエスト console.log('[MCP] Creating new transport for initialization'); const eventStore = new InMemoryEventStore(); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), eventStore, onsessioninitialized: (sessionId) => { console.log(`[MCP] Session initialized with ID: ${sessionId}`); transports[sessionId] = transport; }, }); // クローズ時のクリーンアップ transport.onclose = () => { const sid = transport.sessionId; if (sid && transports[sid]) { console.log(`[MCP] Transport closed for session ${sid}`); delete transports[sid]; } }; // グローバルMCPサーバーに接続 await globalMcpServer.connect(transport); await transport.handleRequest(req, res, req.body); return; } else if (sessionId && req.body && req.body.method === 'tools/call') { // エラー: セッションが失われている場合は再初期化が必要 console.log('[ERROR] Session lost. Client needs to reinitialize.'); return res.status(400).json({ jsonrpc: '2.0', error: { code: -32001, message: 'Session expired. Please reinitialize the connection.', }, id: req.body.id || null, }); } else if (false) { // セッションIDはあるが、サーバーが知らない場合(サーバー再起動後など) // 新しいトランスポートを自動作成してセッションを再確立 console.log('[MCP] Recreating lost transport for session: ' + sessionId); console.log('[MCP] This typically happens after server restart'); const eventStore = new InMemoryEventStore(); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => sessionId, // 既存のセッションIDを再利用 eventStore, onsessioninitialized: (sid) => { console.log('[MCP] Session re-initialized with ID: ' + sid); transports[sid] = transport; }, }); // クローズ時のクリーンアップ transport.onclose = () => { if (transports[sessionId]) { console.log('[MCP] Transport closed for session ' + sessionId); delete transports[sessionId]; } }; // MCPサーバーに接続 await server.connect(transport); await transport.handleRequest(req, res, req.body); return; } else { // 無効なリクエスト console.log('[ERROR] Invalid request - No session ID and not an initialize request'); console.log('[ERROR] Request body:', JSON.stringify(req.body)); return res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided or not an initialization request', }, id: null, }); } // 既存トランスポートでリクエストを処理 await transport.handleRequest(req, res, req.body); } catch (error) { console.error('[MCP] Error handling request:', error); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', data: error.message, }, id: null, }); } } }; /** * MCP GETエンドポイント(SSEストリーム用) */ const mcpGetHandler = async (req, res) => { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !transports[sessionId]) { return res.status(400).send('Invalid or missing session ID'); } const lastEventId = req.headers['last-event-id']; if (lastEventId) { console.log(`[MCP] Client reconnecting with Last-Event-ID: ${lastEventId}`); } else { console.log(`[MCP] Establishing new SSE stream for session ${sessionId}`); } const transport = transports[sessionId]; await transport.handleRequest(req, res); }; /** * MCP DELETEエンドポイント(セッション終了用) */ const mcpDeleteHandler = async (req, res) => { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !transports[sessionId]) { return res.status(400).send('Invalid or missing session ID'); } console.log(`[MCP] Received session termination request for session ${sessionId}`); try { const transport = transports[sessionId]; await transport.handleRequest(req, res); } catch (error) { console.error('[MCP] Error handling session termination:', error); if (!res.headersSent) { res.status(500).send('Error processing session termination'); } } }; // ルート設定(認証なし - ChatGPT Desktop App互換) app.post('/mcp', mcpPostHandler); app.get('/mcp', mcpGetHandler); app.delete('/mcp', mcpDeleteHandler); // ヘルスチェックエンドポイント app.get('/health', (req, res) => { res.json({ status: 'ok', server: 'omotenashi-mcp-server', version: '1.0.0', uptime: process.uptime(), sessions: Object.keys(transports).length, }); }); // サーバー起動 app.listen(MCP_PORT, (error) => { if (error) { console.error('[MCP] Failed to start server:', error); process.exit(1); } console.log(`\n✓ MCP Server is running on port ${MCP_PORT}`); console.log(` - POST endpoint: http://localhost:${MCP_PORT}/mcp`); console.log(` - GET endpoint: http://localhost:${MCP_PORT}/mcp (SSE stream)`); console.log(` - DELETE endpoint: http://localhost:${MCP_PORT}/mcp (session termination)`); console.log(` - Health check: http://localhost:${MCP_PORT}/health`); console.log('\nServer is ready to accept MCP connections.\n'); }); // シャットダウン処理 process.on('SIGINT', async () => { console.log('\n[MCP] Shutting down server...'); // すべてのアクティブなトランスポートをクローズ for (const sessionId in transports) { try { console.log(`[MCP] Closing transport for session ${sessionId}`); await transports[sessionId].close(); delete transports[sessionId]; } catch (error) { console.error(`[MCP] Error closing transport for session ${sessionId}:`, error); } } console.log('[MCP] Server shutdown complete'); process.exit(0); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rakuai-support/mcp-omotenashi'

If you have feedback or need assistance with the MCP directory API, please join our Discord server