Skip to main content
Glama

360 AI Cloud Drive MCP Server

by Qihoo360
file-save.ts9.63 kB
import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { getAuthInfo, AuthInfo } from "../utils/auth.js"; import { getConfig } from "../utils/const.js"; import { gethttpContext } from "../utils/transport.js"; // 辅助函数:将字节转换为可读的文件大小格式 function formatBytes(bytes: number, decimals = 2): string { if (bytes === 0) return '0 Bytes'; if (!bytes || bytes < 0) return '未知大小'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } // 调用云盘API保存文件 async function saveFile(authInfo: AuthInfo, params: { url?: string, content?: string, upload_path?: string }): Promise<any> { try { const url = new URL(authInfo.request_url || ''); // 构建请求头 const headers = { 'Access-Token': authInfo.access_token || '', 'Content-Type': 'application/x-www-form-urlencoded' }; // 构建请求参数 const baseParams: Record<string, string> = { 'method': 'MCP.saveFile', 'access_token': authInfo.access_token || '', 'qid': authInfo.qid || '', 'sign': authInfo.sign || '' }; // 添加所有参数到URL Object.entries(baseParams).forEach(([key, value]) => { url.searchParams.append(key, String(value)); }); // 构建表单数据 const body = new URLSearchParams(); body.append('upload_path', params.upload_path || ''); if (params.url) { body.append('url', params.url); } else if (params.content) { body.append('content', params.content); } const response = await fetch(url.toString(), { method: 'POST', headers: headers, body }); if (!response.ok) { throw new Error(`API 请求失败,状态码: ${response.status}`); } return await response.json(); } catch (error) { console.error('保存文件失败:', error); throw error; } } // 查询任务状态 async function queryTaskStatus(authInfo: AuthInfo, taskId: string): Promise<any> { try { const url = new URL(authInfo.request_url || ''); // 构建请求头 const headers = { 'Access-Token': authInfo.access_token || '' }; // 构建GET参数 url.searchParams.append('method', 'MCP.query'); url.searchParams.append('qid', authInfo.qid || ''); url.searchParams.append('access_token', authInfo.access_token || ''); url.searchParams.append('sign', authInfo.sign || ''); // 构建POST参数 const body = new URLSearchParams(); body.append('task_id', taskId); const response = await fetch(url.toString(), { method: 'POST', headers: headers, body: body }); if (!response.ok) { throw new Error(`API 请求失败,状态码: ${response.status}`); } return await response.json(); } catch (error) { console.error('查询任务状态失败:', error); throw error; } } // 轮询任务状态 async function pollTaskStatus(authInfo: AuthInfo, taskId: string, interval = 1000, maxAttempts = 120): Promise<any> { let attempts = 0; let result; while (attempts < maxAttempts) { attempts++; result = await queryTaskStatus(authInfo, taskId); if (result.errno !== 0) { throw new Error(result.errmsg || '查询任务状态失败'); } const status = result.data?.status; if (status === 2) { // 处理完成 return result; } else if (status === 3) { // 处理失败 throw new Error(result.data?.error || '文件保存失败'); } // 等待下一次轮询 await new Promise(resolve => setTimeout(resolve, interval)); } // throw new Error('轮询超时,文件保存未完成'); return { content: [ { type: "text", text: `🚀 正在保存文件到云盘中,请稍后在"${result.data?.file_path}"目录查看\n` + `📦 文件大小:${result.data?.file_size}` } ] }; } export function registerFileSaveTool(server: McpServer) { server.tool( "file-save", "通过URL或文本内容保存文件到云盘", { url: z.string().optional().describe("文件下载地址,url或content必传1个"), content: z.string().optional().describe("文件内容(md格式),url或content必传1个,需要传用户指定的完整内容,不能省略任何部分"), // upload_path: z.string() // .default('/来自mcp_server/') // .describe("云盘存储路径,必须以/开头和结尾。如不指定,默认为'/来自mcp_server/'。\n- 支持自动创建不存在的一级目录\n- 不支持不存在的多级目录") // .refine((path) => path.endsWith('/'), { // message: "路径必须以/结尾" // }) // .refine((path) => path.startsWith('/'), { // message: "路径必须以/开头" // }) }, async ({ url, content }, mcpReq: any) => { // 参数验证 if (!url && !content) { return { content: [{ type: "text", text: "❌ 参数错误: 必须提供url或content参数" }] }; } const httpContext = gethttpContext(mcpReq, server); const transportAuthInfo = httpContext.authInfo; try { let authInfo: AuthInfo; try { // 获取鉴权信息 authInfo = await getAuthInfo({ method: 'MCP.saveFile' }, transportAuthInfo); authInfo.request_url = getConfig(transportAuthInfo?.ecsEnv).request_url; } catch (authError) { console.error("获取鉴权信息失败:", authError); throw new Error("获取鉴权信息失败"); } // 调用保存文件API const saveResult = await saveFile(authInfo, { url, content }); if (saveResult && saveResult.errno === 0) { const taskId = saveResult.data?.task_id; if (!taskId) { return { content: [{ type: "text", text: "❌ 保存文件失败: 未获取到任务ID" }] }; } // 轮询任务状态 try { const finalResult = await pollTaskStatus(authInfo, taskId); const resultData = finalResult.data; let resultText = `✅ 文件保存成功!\n\n`; resultText += `🆔 任务ID: ${taskId}\n`; const fileInfo: any = { taskId }; if (resultData) { const qid = resultData.qid || authInfo.qid; const path = resultData.file_path; const nid = resultData.nid; const size = resultData.file_size; fileInfo.path = path; fileInfo.nid = nid; fileInfo.size = size; fileInfo.qid = qid; if (path) { resultText += `📂 云盘路径: ${path}\n`; } if (size) { resultText += `📦 文件大小: ${formatBytes(size)}\n`; } if (path && nid && qid) { const dirPath = path.substring(0, path.lastIndexOf('/') + 1); const href = `https://www.yunpan.com/file/index#/fileManage/my/file/${encodeURIComponent(dirPath)}?focus_nid=${nid}&owner_qid=${qid}`; resultText += `🔗 [点击查看云盘文件](${href})\n`; fileInfo.href = href; } } return { content: [ { type: "text", name: "x-save-result-json", text: JSON.stringify({ status: 'success', file: fileInfo }) }, { type: "text", name: "x-save-result-display", text: resultText } ] }; } catch (pollError: any) { const isTimeout = pollError.message.includes('轮询超时'); const status = isTimeout ? 'timeout' : 'failed'; const resultText = isTimeout ? `⏳ 文件保存轮询超时\n\n- 任务ID: ${taskId}\n- 请稍后检查云盘或使用任务ID查询状态。` : `❌ 文件保存失败\n\n- 任务ID: ${taskId}\n- 错误信息: ${pollError.message}`; return { content: [ { type: "text", name: "x-save-result-json", text: JSON.stringify({ status: status, taskId: taskId, error: pollError.message }) }, { type: "text", name: "x-save-result-display", text: resultText } ] }; } } else { throw new Error(saveResult?.errmsg || "API请求失败"); } } catch (error: any) { return { content: [ { type: "text", text: `保存文件时发生错误: ${error.message}`, } ], }; } }, ); }

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/Qihoo360/ecs_mcp_server'

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