Skip to main content
Glama
grareco-server.ts14.3 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema, CompleteRequestSchema, ListToolsRequestSchema, LoggingLevel, SetLevelRequestSchema, Tool, ToolSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; // import { fetchAndConvertToMarkdown, limitMarkdownSize } from "./utils/web-to-markdown.js"; // import { generateGrareco } from "./utils/anthropic-client.js"; import { PromptType, getPromptTemplate, combineContentWithPrompt, getAvailablePromptTypes } from "./prompts/index.js"; import { processGrareco, InputType } from "./utils/grareco-processor.js"; import { writeFile, access, mkdir } from 'fs/promises'; import path from 'path'; import { constants } from 'fs'; import { fileURLToPath } from 'url'; const ToolInputSchema = ToolSchema.shape.inputSchema; type ToolInput = z.infer<typeof ToolInputSchema>; /* Input schemas for tools implemented in this server */ const EchoSchema = z.object({ message: z.string().describe("Message to echo"), }); const PrintEnvSchema = z.object({}); const WebToGrarecoSchema = z.object({ url: z.string().describe("変換対象のWebサイトURL"), promptType: z.enum([PromptType.STANDARD, PromptType.ELEMENTARY, PromptType.TIMELINE]) .default(PromptType.STANDARD) .describe("使用するプロンプトタイプ(標準: standard、小学生向け: elementary、タイムライン: timeline)"), }); // WebToGrarecoSchemaの型定義 type WebToGrarecoInput = z.infer<typeof WebToGrarecoSchema>; const TextToGrarecoSchema = z.object({ text: z.string().describe("変換対象のテキスト"), promptType: z.enum([PromptType.STANDARD, PromptType.ELEMENTARY, PromptType.TIMELINE]) .default(PromptType.STANDARD) .describe("使用するプロンプトタイプ(標準: standard、小学生向け: elementary、タイムライン: timeline)"), }); // TextToGrarecoSchemaの型定義 type TextToGrarecoInput = z.infer<typeof TextToGrarecoSchema>; enum ToolName { ECHO = "echo", PRINT_ENV = "printEnv", WEB_TO_GRARECO = "webToGrareco", TEXT_TO_GRARECO = "textToGrareco", } // ESモジュールで__dirnameを取得するための設定 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * 相対パスを絶対パスに変換する * @param relativePath 相対パス * @returns 絶対パス */ function resolveOutputPath(outputDir: string): string { if (path.isAbsolute(outputDir)) { return outputDir; } // プロジェクトのルートディレクトリ(package.jsonがある場所)を起点とする // __dirnameは現在のファイルのディレクトリを指す // src/grareco-server.tsの場合、__dirnameは'src'ディレクトリを指す // そこから一つ上のディレクトリに移動することでプロジェクトルートを取得 const projectRoot = path.resolve(__dirname, '..'); return path.resolve(projectRoot, outputDir); } /** * HTMLをファイルに保存する * @param html 保存するHTML * @returns 保存結果のメッセージとファイルパス */ async function saveHtmlToFile(html: string): Promise<{ message: string, filePath: string }> { try { // 環境変数から出力先ディレクトリを取得 const outputDir = process.env.GRARECO_OUTPUT_DIR || './output'; // 相対パスを絶対パスに変換 const absoluteOutputDir = resolveOutputPath(outputDir); // 現在の日時からファイル名を生成 const now = new Date(); const fileName = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}.html`; const fullPath = path.join(absoluteOutputDir, fileName); // ファイルが既に存在するかチェック try { await access(fullPath, constants.F_OK); // ファイルが存在する場合はエラー throw new Error(`ファイルが既に存在します: ${fullPath}`); } catch (error) { // ファイルが存在しない場合(ENOENT)は続行、それ以外のエラーは再スロー if (error instanceof Error && !error.message.includes('既に存在します')) { // ディレクトリを作成 await mkdir(absoluteOutputDir, { recursive: true }); // ファイルに保存 await writeFile(fullPath, html, 'utf8'); return { message: `HTMLをファイルに保存しました`, filePath: fullPath }; } throw error; } } catch (error) { throw new Error(`ファイル保存中にエラーが発生しました: ${error instanceof Error ? error.message : String(error)}`); } } /** * HTMLの保存処理を行い、結果をレスポンスに含める * @param html 保存するHTML * @returns 保存結果と更新されたcontent配列 */ async function handleHtmlSave(html: string): Promise<{ saveResult: string, content: { type: string, text: string }[] }> { // 基本のcontent配列 const content = [ ]; // content.push({ // type: "text", // text: "-----", // }); // content.push( // { // type: "text", // text: html, // }); // content.push({ // type: "text", // text: "-----", // }); // ファイル保存処理 let saveResult = ''; try { console.log(`HTMLをファイルに保存します`); const result = await saveHtmlToFile(html); saveResult = `${result.message}: ${result.filePath}`; console.log(saveResult); // 保存結果をcontent配列に追加 content.push({ type: "text", text: saveResult, }); } catch (saveError) { saveResult = `ファイル保存エラー: ${saveError instanceof Error ? saveError.message : String(saveError)}`; console.error(saveResult); // エラー結果をcontent配列に追加 content.push({ type: "text", text: saveResult, }); } return { saveResult, content }; } export const createServer = () => { console.log("サーバー作成開始"); const server = new Server( { name: "mcp_grareco", version: "1.0.0", }, { capabilities: { tools: {}, logging: {}, }, }, ); // let subscriptions: Set<string> = new Set(); // let subsUpdateInterval: NodeJS.Timeout | undefined; // // Set up update interval for subscribed resources // subsUpdateInterval = setInterval(() => { // for (const uri of subscriptions) { // server.notification({ // method: "notifications/resources/updated", // params: { uri }, // }); // } // }, 5000); let logLevel: LoggingLevel = "debug"; // let logsUpdateInterval: NodeJS.Timeout | undefined; const messages = [ { level: "debug", data: "Debug-level message" }, { level: "info", data: "Info-level message" }, { level: "notice", data: "Notice-level message" }, { level: "warning", data: "Warning-level message" }, { level: "error", data: "Error-level message" }, { level: "critical", data: "Critical-level message" }, { level: "alert", data: "Alert level-message" }, { level: "emergency", data: "Emergency-level message" } ] const isMessageIgnored = (level: LoggingLevel): boolean => { const currentLevel = messages.findIndex((msg) => logLevel === msg.level); const messageLevel = messages.findIndex((msg) => level === msg.level); return messageLevel < currentLevel; } // Set up update interval for random log messages // logsUpdateInterval = setInterval(() => { // let message = { // method: "notifications/message", // params: messages[Math.floor(Math.random() * messages.length)], // } // if (!isMessageIgnored(message.params.level as LoggingLevel)) server.notification(message); // }, 15000); server.setRequestHandler(ListToolsRequestSchema, async () => { console.log("ツール一覧リクエスト受信"); const tools: Tool[] = [ { name: ToolName.ECHO, description: "Echoes back the input", inputSchema: zodToJsonSchema(EchoSchema) as ToolInput, }, { name: ToolName.PRINT_ENV, description: "Prints all environment variables, helpful for debugging MCP server configuration", inputSchema: zodToJsonSchema(PrintEnvSchema) as ToolInput, }, { name: ToolName.WEB_TO_GRARECO, description: "URLからWebサイトを取得し、グラフィックレコーディング形式のHTMLに変換します", inputSchema: zodToJsonSchema(WebToGrarecoSchema) as ToolInput, }, { name: ToolName.TEXT_TO_GRARECO, description: "テキストをグラフィックレコーディング形式のHTMLに変換します", inputSchema: zodToJsonSchema(TextToGrarecoSchema) as ToolInput, }, ]; return { tools }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; console.log(`ツール呼び出し: ${name}`, args); if (name === ToolName.ECHO) { const validatedArgs = EchoSchema.parse(args); return { content: [{ type: "text", text: `Echo: ${validatedArgs.message}` }], }; } if (name === ToolName.PRINT_ENV) { return { content: [ { type: "text", text: JSON.stringify(process.env, null, 2), }, ], }; } if (name === ToolName.WEB_TO_GRARECO) { const validatedArgs = WebToGrarecoSchema.parse(args) as WebToGrarecoInput; const { url, promptType } = validatedArgs; const progressToken = request.params._meta?.progressToken; console.log("webToGrareco処理開始", validatedArgs); try { // 進捗コールバック関数 const progressCallback = progressToken ? async (progress: number, total: number) => { await server.notification({ method: "notifications/progress", params: { progress, total, progressToken }, }); } : undefined; // 共通処理を使用 console.log(`Webサイト処理開始: ${url}`); const html = await processGrareco({ inputType: InputType.WEB, content: url as string, promptType: promptType as PromptType, progressCallback }); console.log("グラレコ生成完了\n", html.substring(0, 100) + "..."); // 共通のファイル保存処理を使用 const { content } = await handleHtmlSave(html); return { content }; } catch (error) { console.error("webToGrarecoでエラーが発生しました:", error); return { content: [ { type: "text", text: `エラーが発生しました: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } if (name === ToolName.TEXT_TO_GRARECO) { const validatedArgs = TextToGrarecoSchema.parse(args) as TextToGrarecoInput; const { text, promptType } = validatedArgs; const progressToken = request.params._meta?.progressToken; console.log("textToGrareco処理開始", validatedArgs); try { // 進捗コールバック関数 const progressCallback = progressToken ? async (progress: number, total: number) => { await server.notification({ method: "notifications/progress", params: { progress, total, progressToken }, }); } : undefined; // 共通処理を使用 console.log(`テキスト処理開始: ${(text as string).substring(0, 50)}...`); const html = await processGrareco({ inputType: InputType.TEXT, content: text as string, promptType: promptType as PromptType, progressCallback }); console.log("グラレコ生成完了\n", html.substring(0, 100) + "..."); // 共通のファイル保存処理を使用 const { content } = await handleHtmlSave(html); return { content }; } catch (error) { console.error("textToGrarecoでエラーが発生しました:", error); return { content: [ { type: "text", text: `エラーが発生しました: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } throw new Error(`Unknown tool: ${name}`); }); server.setRequestHandler(CompleteRequestSchema, async (request) => { console.log("補完リクエスト受信", request.params); const { ref, argument } = request.params; if (ref.type === "ref/resource") { const resourceId = ref.uri.split("/").pop(); if (!resourceId) return { completion: { values: [] } }; // EXAMPLE_COMPLETIONSを削除したため、空の配列を返す return { completion: { values: [], hasMore: false, total: 0 } }; } if (ref.type === "ref/prompt") { // EXAMPLE_COMPLETIONSを削除したため、空の配列を返す return { completion: { values: [], hasMore: false, total: 0 } }; } throw new Error(`Unknown reference type`); }); server.setRequestHandler(SetLevelRequestSchema, async (request) => { console.log("ログレベル設定リクエスト受信", request.params); const { level } = request.params; logLevel = level; // Demonstrate different log levels await server.notification({ method: "notifications/message", params: { level: "debug", logger: "test-server", data: `Logging level set to: ${logLevel}`, }, }); return {}; }); const cleanup = async () => { console.log("サーバークリーンアップ実行"); // if (subsUpdateInterval) clearInterval(subsUpdateInterval); // if (logsUpdateInterval) clearInterval(logsUpdateInterval); console.log("サーバークリーンアップ終了"); }; return { server, cleanup }; };

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/iuill/mcp_grareco'

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