Skip to main content

ai_crew_sync/tools/
notes.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    model::{Ack, NoteInfo, NoteList, NoteRef},
11    store::notes,
12};
13
14fn default_limit() -> i64 {
15    50
16}
17
18#[derive(Debug, Deserialize, JsonSchema)]
19pub struct SetNoteArgs {
20    /// Namespace for the note, typically a repository or project name.
21    /// Defaults to "global".
22    #[serde(default)]
23    pub scope: Option<String>,
24    /// Short identifier, e.g. "deploy-runbook" or "why-we-dropped-redis".
25    pub key: String,
26    /// The content. Write it for a teammate's agent reading it cold, with no
27    /// memory of this conversation.
28    pub value: String,
29    /// Optional tags for filtering, e.g. ["infra", "postmortem"].
30    #[serde(default)]
31    pub tags: Option<Vec<String>>,
32}
33
34#[derive(Debug, Deserialize, JsonSchema)]
35pub struct GetNoteArgs {
36    /// Namespace. Defaults to "global".
37    #[serde(default)]
38    pub scope: Option<String>,
39    /// The note key.
40    pub key: String,
41}
42
43#[derive(Debug, Deserialize, JsonSchema)]
44pub struct ListNotesArgs {
45    /// Restrict to one namespace. Omit to list every scope.
46    #[serde(default)]
47    pub scope: Option<String>,
48    /// Only return notes carrying this tag.
49    #[serde(default)]
50    pub tag: Option<String>,
51    /// Maximum notes to return (1-200).
52    #[serde(default = "default_limit")]
53    pub limit: i64,
54}
55
56#[derive(Debug, Deserialize, JsonSchema)]
57pub struct SearchNotesArgs {
58    /// Full-text search terms, matched against note keys and content.
59    pub query: String,
60    /// Restrict to one namespace.
61    #[serde(default)]
62    pub scope: Option<String>,
63    /// Maximum notes to return (1-200).
64    #[serde(default = "default_limit")]
65    pub limit: i64,
66}
67
68#[tool_router(router = notes_router, vis = "pub")]
69impl Bus {
70    #[tool(
71        description = "Write or overwrite a shared note: a decision, a gotcha, the state \
72                       of a deploy. This is the team's durable memory, readable by every \
73                       teammate's agent. Previous versions are kept."
74    )]
75    async fn set_note(
76        &self,
77        ctx: RequestContext<rmcp::RoleServer>,
78        Parameters(args): Parameters<SetNoteArgs>,
79    ) -> Result<Json<NoteInfo>, ErrorData> {
80        let auth = auth_of(&ctx)?;
81        let input = notes::SetInput {
82            scope: args.scope,
83            key: args.key,
84            value: args.value,
85            tags: args.tags,
86        };
87        Ok(Json(notes::set_note(&self.db, &auth, input).await?))
88    }
89
90    #[tool(
91        description = "Read one shared note by scope and key. Returns found=false rather \
92                       than erroring when the note does not exist."
93    )]
94    async fn get_note(
95        &self,
96        ctx: RequestContext<rmcp::RoleServer>,
97        Parameters(args): Parameters<GetNoteArgs>,
98    ) -> Result<Json<NoteRef>, ErrorData> {
99        let auth = auth_of(&ctx)?;
100        Ok(Json(
101            notes::get_note(&self.db, &auth, args.scope, &args.key).await?,
102        ))
103    }
104
105    #[tool(
106        description = "List shared notes, optionally filtered by scope or tag. Use this \
107                       to discover what the team already wrote down."
108    )]
109    async fn list_notes(
110        &self,
111        ctx: RequestContext<rmcp::RoleServer>,
112        Parameters(args): Parameters<ListNotesArgs>,
113    ) -> Result<Json<NoteList>, ErrorData> {
114        let auth = auth_of(&ctx)?;
115        Ok(Json(
116            notes::list_notes(&self.db, &auth, args.scope, args.tag, args.limit).await?,
117        ))
118    }
119
120    #[tool(description = "Full-text search the team's shared notes.")]
121    async fn search_notes(
122        &self,
123        ctx: RequestContext<rmcp::RoleServer>,
124        Parameters(args): Parameters<SearchNotesArgs>,
125    ) -> Result<Json<NoteList>, ErrorData> {
126        let auth = auth_of(&ctx)?;
127        Ok(Json(
128            notes::search_notes(&self.db, &auth, &args.query, args.scope, args.limit).await?,
129        ))
130    }
131
132    #[tool(description = "Delete a shared note. Its revision history goes with it.")]
133    async fn delete_note(
134        &self,
135        ctx: RequestContext<rmcp::RoleServer>,
136        Parameters(args): Parameters<GetNoteArgs>,
137    ) -> Result<Json<Ack>, ErrorData> {
138        let auth = auth_of(&ctx)?;
139        Ok(Json(
140            notes::delete_note(&self.db, &auth, args.scope, &args.key).await?,
141        ))
142    }
143}