yggdrasil/mcp/tools/
media.rs1#![cfg(feature = "server")]
17
18use rmcp::handler::server::tool::Extension;
19use rmcp::handler::server::wrapper::Parameters;
20use rmcp::model::CallToolResult;
21use rmcp::{schemars, tool, tool_router, ErrorData as McpError};
22use serde::Deserialize;
23
24use super::common::{internal, ok_json, require_scope};
25use crate::models::mcp_token::TokenScope;
26
27#[tool_router(router = media_router, vis = "pub")]
28impl crate::mcp::server::YggMcpServer {
29 #[tool(
35 description = "从图片 URL 抓取并入库(服务端转 WebP 若更小),返回 /uploads/... URL(可直接用于 Markdown 正文 img)。仅接受 https:// URL,支持 JPEG/PNG/GIF/WebP。二进制不经 JSON-RPC。"
36 )]
37 async fn upload_media(
38 &self,
39 Parameters(p): Parameters<UploadMediaParams>,
40 Extension(parts): Extension<http::request::Parts>,
41 ) -> Result<CallToolResult, McpError> {
42 let _principal = require_scope(&parts, "upload_media", TokenScope::Write)?;
43
44 let outcome = crate::api::url_fetch::fetch_and_ingest(&p.url)
46 .await
47 .map_err(|e| match e {
48 crate::api::url_fetch::FetchError::Invalid(msg)
49 | crate::api::url_fetch::FetchError::BadStatus(msg) => {
50 McpError::invalid_request(msg, None)
51 }
52 crate::api::url_fetch::FetchError::TooLarge => McpError::invalid_request(
53 format!(
54 "文件超过大小限制({} bytes)",
55 crate::utils::server::MAX_FILE_SIZE
56 ),
57 None,
58 ),
59 crate::api::url_fetch::FetchError::Fetch(ctx) => internal(ctx, "url fetch"),
60 })?;
61
62 tracing::info!(
63 "MCP media uploaded via URL: {} ({}x{}, reused={})",
64 outcome.url,
65 outcome.width,
66 outcome.height,
67 outcome.reused
68 );
69
70 ok_json(UploadResult {
71 success: true,
72 url: outcome.url,
73 reused: outcome.reused,
74 width: outcome.width,
75 height: outcome.height,
76 mime: outcome.mime,
77 })
78 }
79}
80
81#[derive(Debug, Deserialize, schemars::JsonSchema)]
86pub struct UploadMediaParams {
87 pub url: String,
89 #[serde(default)]
91 #[allow(dead_code)] pub alt: Option<String>,
93}
94
95#[derive(Debug, serde::Serialize)]
96struct UploadResult {
97 success: bool,
98 url: String,
99 reused: bool,
100 width: u32,
101 height: u32,
102 mime: String,
103}