1use std::convert::Infallible;
8
9use codoseo_mcp::cloud::types::{
10 AuditIssueUrls, ChangesPage, CrawlQueued, IssueUrlsPage, MonitoringRequested, PageInfo,
11 QuickAuditState, SiteHealth, SiteInfo,
12};
13use codoseo_mcp::cloud::{AnonBackend, CloudBackend};
14
15use super::anon::{AnonCaller, AnonService};
16use super::auth::ApiCaller;
17use super::error::AgentError;
18use super::service::{AgentService, Reply};
19use crate::metrics::{self, Surface, Tier};
20use crate::state::AppState;
21
22#[derive(Clone)]
23pub struct AgentBackend {
24 state: AppState,
25}
26
27impl AgentBackend {
28 pub fn new(state: AppState) -> AgentBackend {
29 AgentBackend { state }
30 }
31
32 fn service(&self) -> AgentService<'_> {
33 AgentService::new(&self.state)
34 }
35}
36
37fn tool_error(e: AgentError) -> String {
40 if let AgentError::Internal(detail) = &e {
41 tracing::error!(%detail, "mcp tool call failed");
42 }
43 e.message()
44}
45
46fn outcome<T>(reply: Reply<T>) -> Result<T, String> {
47 let result = reply.into_result();
48 metrics::api_request(
49 Surface::Mcp,
50 Tier::Key,
51 result.as_ref().map_or_else(AgentError::code, |_| "ok"),
52 );
53 result.map_err(tool_error)
54}
55
56fn anon<T>(result: Result<T, String>) -> Result<T, String> {
58 let label = if result.is_ok() { "ok" } else { "error" };
59 metrics::api_request(Surface::Mcp, Tier::Anon, label);
60 result
61}
62
63impl CloudBackend for AgentBackend {
64 type Keyed = ApiCaller;
65 type Anon = AnonCaller;
66
67 async fn list_sites(&self, who: &ApiCaller) -> Result<Vec<SiteInfo>, String> {
68 outcome(self.service().list_sites(who).await)
69 }
70
71 async fn site_health(&self, who: &ApiCaller, site_id: &str) -> Result<SiteHealth, String> {
72 outcome(self.service().site_health(who, site_id).await)
73 }
74
75 async fn issue_urls(
76 &self,
77 who: &ApiCaller,
78 site_id: &str,
79 check: &str,
80 limit: Option<u32>,
81 offset: Option<u32>,
82 ) -> Result<IssueUrlsPage, String> {
83 outcome(
84 self.service()
85 .issue_urls(who, site_id, check, limit, offset)
86 .await,
87 )
88 }
89
90 async fn page(&self, who: &ApiCaller, site_id: &str, url: &str) -> Result<PageInfo, String> {
91 outcome(self.service().page(who, site_id, url).await)
92 }
93
94 async fn changes(
95 &self,
96 who: &ApiCaller,
97 site_id: &str,
98 severity: Option<&str>,
99 limit: Option<u32>,
100 offset: Option<u32>,
101 ) -> Result<ChangesPage, String> {
102 outcome(
103 self.service()
104 .changes(who, site_id, severity, limit, offset)
105 .await,
106 )
107 }
108
109 async fn run_crawl(&self, who: &ApiCaller, site_id: &str) -> Result<CrawlQueued, String> {
110 outcome(self.service().run_crawl(who, site_id).await)
111 }
112
113 async fn reject(&self, who: &ApiCaller, message: String) -> Result<Infallible, String> {
114 let refused = self
115 .service()
116 .refuse::<Infallible>(who, AgentError::BadRequest(message))
117 .await;
118 outcome(refused)
119 }
120}
121
122impl AnonBackend for AgentBackend {
123 async fn quick_audit(&self, who: &AnonCaller, url: &str) -> Result<QuickAuditState, String> {
124 anon(AnonService::new(&self.state).quick_audit(who, url).await)
125 }
126
127 async fn get_audit(&self, who: &AnonCaller, audit_id: &str) -> Result<QuickAuditState, String> {
128 anon(AnonService::new(&self.state).get_audit(who, audit_id).await)
129 }
130
131 async fn poll_audit(&self, _: &AnonCaller, audit_id: &str) -> Result<QuickAuditState, String> {
134 AnonService::new(&self.state).poll_audit(audit_id).await
135 }
136
137 async fn audit_issue_urls(
138 &self,
139 who: &AnonCaller,
140 audit_id: &str,
141 check: &str,
142 limit: Option<u32>,
143 offset: Option<u32>,
144 ) -> Result<AuditIssueUrls, String> {
145 anon(
146 AnonService::new(&self.state)
147 .audit_issue_urls(who, audit_id, check, limit, offset)
148 .await,
149 )
150 }
151
152 async fn start_monitoring(
153 &self,
154 who: &AnonCaller,
155 url: &str,
156 email: &str,
157 ) -> Result<MonitoringRequested, String> {
158 anon(
159 AnonService::new(&self.state)
160 .start_monitoring(who, url, email)
161 .await,
162 )
163 }
164}