Skip to main content

ai_crew_sync/tools/
attachments.rs

1use rmcp::{
2    ErrorData, Json, handler::server::wrapper::Parameters, service::RequestContext, tool,
3    tool_router,
4};
5use schemars::JsonSchema;
6use serde::Deserialize;
7
8use super::{Bus, auth_of};
9use crate::{
10    error::BusError,
11    model::{AttachmentContent, AttachmentMeta},
12    store::attachments,
13};
14
15#[derive(Debug, Deserialize, JsonSchema)]
16pub struct AttachFileArgs {
17    /// Task key to attach to (any teammate may attach to a task).
18    #[serde(default)]
19    pub task: Option<String>,
20    /// Message id to attach to (only your own messages; prefer attaching at
21    /// post_message time so readers never see the message without its files).
22    #[serde(default)]
23    pub message_id: Option<i64>,
24    pub filename: String,
25    /// MIME type; defaults to application/octet-stream.
26    #[serde(default)]
27    pub content_type: Option<String>,
28    /// File content, base64-encoded. Decoded size is limited to 256 KiB.
29    pub data_base64: String,
30}
31
32#[derive(Debug, Deserialize, JsonSchema)]
33pub struct GetAttachmentArgs {
34    /// Attachment id, as listed in a message's or task's `attachments`.
35    pub id: i64,
36}
37
38#[tool_router(router = attachments_router, vis = "pub")]
39impl Bus {
40    #[tool(
41        description = "Attach a small file (diff, log, config — max 256 KiB) to a task or to \
42                       a message you sent. Teammates see it listed on the task/message and \
43                       download it with get_attachment. To share a file with a new message, \
44                       pass `attachments` to post_message instead."
45    )]
46    async fn attach_file(
47        &self,
48        ctx: RequestContext<rmcp::RoleServer>,
49        Parameters(args): Parameters<AttachFileArgs>,
50    ) -> Result<Json<AttachmentMeta>, ErrorData> {
51        let auth = auth_of(&ctx)?;
52        let att = attachments::decode_input(&args.filename, args.content_type, &args.data_base64)?;
53        let meta = match (&args.task, args.message_id) {
54            (Some(_), Some(_)) | (None, None) => {
55                return Err(BusError::invalid(
56                    "set either `task` (a task key) or `message_id`, not both",
57                )
58                .into());
59            }
60            (Some(task), None) => attachments::attach_to_task(&self.db, &auth, task, &att).await?,
61            (None, Some(message_id)) => {
62                attachments::attach_to_message(&self.db, &auth, message_id, &att).await?
63            }
64        };
65        Ok(Json(meta))
66    }
67
68    #[tool(
69        description = "Download an attachment by id (returns metadata plus base64 content). \
70                       Attachments on direct messages are only visible to the DM's two \
71                       parties; everything else is team-visible."
72    )]
73    async fn get_attachment(
74        &self,
75        ctx: RequestContext<rmcp::RoleServer>,
76        Parameters(args): Parameters<GetAttachmentArgs>,
77    ) -> Result<Json<AttachmentContent>, ErrorData> {
78        let auth = auth_of(&ctx)?;
79        Ok(Json(
80            attachments::get_attachment(&self.db, &auth, args.id).await?,
81        ))
82    }
83}