1use std::borrow::Cow;
5
6use schemars::{JsonSchema, Schema, SchemaGenerator};
7use serde::de::Error as _;
8use serde::{Deserialize, Deserializer, Serialize};
9use serde_json::Value;
10use uuid::Uuid;
11
12use crate::engine::op::PatchOp;
13use crate::engine::read::{ReadState, Selector};
14use crate::engine::structural_replace::{StructuralAction, StructuralMatch};
15use crate::guide::GuideTopic;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
19#[serde(transparent)]
20pub struct IdArgument(Uuid);
21
22impl IdArgument {
23 pub fn id(self) -> Uuid {
25 self.0
26 }
27}
28
29impl JsonSchema for IdArgument {
31 fn schema_name() -> Cow<'static, str> {
32 Uuid::schema_name()
33 }
34
35 fn inline_schema() -> bool {
36 true
37 }
38
39 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
40 Uuid::json_schema(generator)
41 }
42}
43
44impl<'de> Deserialize<'de> for IdArgument {
45 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
46 let raw = String::deserialize(deserializer)?;
47 Uuid::parse_str(raw.trim().trim_start_matches('/'))
48 .map(IdArgument)
49 .map_err(|_| D::Error::custom(format!("expected an id (a UUID), got \"{raw}\"")))
50 }
51}
52
53fn present<'de, D: Deserializer<'de>, T: Deserialize<'de>>(
55 deserializer: D,
56) -> Result<Option<Option<T>>, D::Error> {
57 Option::<T>::deserialize(deserializer).map(Some)
58}
59
60#[derive(Debug, Deserialize, JsonSchema)]
61#[serde(deny_unknown_fields)]
62pub struct ListCoursesArgs {}
63
64#[derive(Debug, Deserialize, JsonSchema)]
65#[serde(deny_unknown_fields)]
66pub struct GetCourseStructureArgs {
67 #[schemars(extend("x-mcp-header" = "Course"))]
69 pub course: IdArgument,
70 pub selectors: Option<Vec<Selector>>,
72}
73
74#[derive(Debug, Deserialize, JsonSchema)]
75#[serde(deny_unknown_fields)]
76pub struct GetPageContentArgs {
77 #[schemars(extend("x-mcp-header" = "Course"))]
78 pub course: IdArgument,
79 pub page: IdArgument,
81 pub selectors: Option<Vec<Selector>>,
83}
84
85#[derive(Debug, Deserialize, JsonSchema)]
86#[serde(deny_unknown_fields)]
87pub struct CourseArgs {
88 #[schemars(extend("x-mcp-header" = "Course"))]
89 pub course: IdArgument,
90}
91
92#[derive(Debug, Deserialize, JsonSchema)]
93#[serde(deny_unknown_fields)]
94pub struct PatchPageContentArgs {
95 #[schemars(extend("x-mcp-header" = "Course"))]
96 pub course: IdArgument,
97 pub page: IdArgument,
98 #[schemars(with = "Vec<PatchOp>")]
100 pub ops: Vec<Value>,
101 #[serde(default)]
103 pub dry_run: bool,
104 pub read_back: Option<Selector>,
106}
107
108#[derive(Debug, Deserialize, JsonSchema)]
109#[serde(deny_unknown_fields)]
110pub struct GetExerciseArgs {
111 #[schemars(extend("x-mcp-header" = "Course"))]
112 pub course: IdArgument,
113 pub exercise: IdArgument,
115 pub selectors: Option<Vec<Selector>>,
117}
118
119#[derive(Debug, Deserialize, JsonSchema)]
120#[serde(deny_unknown_fields)]
121pub struct ListExercisesArgs {
122 #[schemars(extend("x-mcp-header" = "Course"))]
123 pub course: IdArgument,
124 pub scope: Option<ContentScope>,
125 pub state: Option<ReadState>,
127 pub offset: Option<u32>,
128 pub limit: Option<u32>,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
134#[serde(rename_all = "snake_case")]
135pub enum ReviewConfigChoice {
136 EditCourseDefault,
138 Fork,
140 UseCourseDefault,
142}
143
144#[derive(Debug, Serialize, Deserialize, JsonSchema)]
146#[serde(deny_unknown_fields)]
147pub struct PatchExerciseArgs {
148 #[schemars(extend("x-mcp-header" = "Course"))]
149 #[serde(skip_serializing)]
150 pub course: IdArgument,
151 pub exercise: IdArgument,
152 pub name: Option<String>,
154 #[serde(
156 default,
157 deserialize_with = "present",
158 skip_serializing_if = "Option::is_none"
159 )]
160 #[schemars(with = "Option<String>")]
161 pub deadline: Option<Option<String>>,
162 #[schemars(with = "Option<Vec<PatchOp>>")]
164 pub ops: Option<Vec<Value>>,
165 pub review_config: Option<ReviewConfigChoice>,
166 #[serde(default)]
167 pub dry_run: bool,
168}
169
170#[derive(Debug, Deserialize, JsonSchema)]
171#[serde(deny_unknown_fields)]
172pub struct PatchCourseStructureArgs {
173 #[schemars(extend("x-mcp-header" = "Course"))]
174 pub course: IdArgument,
175 #[schemars(with = "Vec<PatchOp>")]
176 pub ops: Vec<Value>,
177 #[serde(default)]
178 pub dry_run: bool,
179 #[serde(default)]
181 pub detail: bool,
182 pub read_back: Option<Selector>,
183}
184
185#[derive(Debug, Deserialize, JsonSchema)]
187#[serde(deny_unknown_fields)]
188pub struct PatchCourseArgs {
189 #[schemars(extend("x-mcp-header" = "Course"))]
190 pub course: IdArgument,
191 #[schemars(with = "Vec<PatchOp>")]
192 pub ops: Vec<Value>,
193 #[serde(default)]
194 pub dry_run: bool,
195}
196
197#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
199#[serde(deny_unknown_fields)]
200pub struct ContentScope {
201 pub chapter: Option<IdArgument>,
202 pub page: Option<IdArgument>,
203}
204
205#[derive(Debug, Serialize, Deserialize, JsonSchema)]
207#[serde(deny_unknown_fields)]
208pub struct FindReplaceArgs {
209 #[schemars(extend("x-mcp-header" = "Course"))]
210 #[serde(skip_serializing)]
211 pub course: IdArgument,
212 pub scope: Option<ContentScope>,
213 pub find: String,
215 pub replace: Option<String>,
217 #[serde(default)]
218 pub regex: bool,
219 pub attributes: Option<Vec<String>>,
221 pub expected_count: Option<u32>,
223 pub include_exercise_instructions: Option<bool>,
225 pub include_exercise_names: Option<bool>,
227 #[serde(default)]
228 pub dry_run: bool,
229}
230
231#[derive(Debug, Serialize, Deserialize, JsonSchema)]
233#[serde(deny_unknown_fields)]
234pub struct StructuralReplaceArgs {
235 #[schemars(extend("x-mcp-header" = "Course"))]
236 #[serde(skip_serializing)]
237 pub course: IdArgument,
238 pub scope: Option<ContentScope>,
239 #[serde(rename = "match")]
240 pub pattern: StructuralMatch,
241 pub action: StructuralAction,
242 pub expected_count: Option<u32>,
243 pub include_exercise_instructions: Option<bool>,
245 #[serde(default)]
246 pub dry_run: bool,
247}
248
249#[derive(Debug, Deserialize, JsonSchema)]
250#[serde(deny_unknown_fields)]
251pub struct UploadFileArgs {
252 #[schemars(extend("x-mcp-header" = "Course"))]
253 pub course: IdArgument,
254 pub file_name: String,
256 pub content_base64: String,
258}
259
260#[derive(Debug, Deserialize, JsonSchema)]
261#[serde(deny_unknown_fields)]
262pub struct ListChangesetsArgs {
263 #[schemars(extend("x-mcp-header" = "Course"))]
265 pub course: Option<IdArgument>,
266}
267
268#[derive(Debug, Deserialize, JsonSchema)]
269#[serde(deny_unknown_fields)]
270pub struct PublishChangesetArgs {
271 #[schemars(extend("x-mcp-header" = "Course"))]
272 pub course: IdArgument,
273 pub changeset: IdArgument,
275 pub revision: i32,
277}
278
279#[derive(Debug, Deserialize, JsonSchema)]
280#[serde(deny_unknown_fields)]
281pub struct DiscardChangesetArgs {
282 #[schemars(extend("x-mcp-header" = "Course"))]
283 pub course: IdArgument,
284 pub changeset: IdArgument,
286 pub entities: Option<Vec<IdArgument>>,
288 pub revision: Option<i32>,
290}
291
292#[derive(Debug, Deserialize, JsonSchema)]
293#[serde(deny_unknown_fields)]
294pub struct GetPreviewLinkArgs {
295 #[schemars(extend("x-mcp-header" = "Course"))]
296 pub course: IdArgument,
297 pub page: Option<IdArgument>,
299 pub pages: Option<Vec<IdArgument>>,
301}
302
303#[derive(Debug, Deserialize, JsonSchema)]
304#[serde(deny_unknown_fields)]
305pub struct GetHistoryArgs {
306 #[schemars(extend("x-mcp-header" = "Course"))]
307 pub course: IdArgument,
308 pub entity: Option<IdArgument>,
310 pub limit: Option<u32>,
311}
312
313#[derive(Debug, Deserialize, JsonSchema)]
314#[serde(deny_unknown_fields)]
315pub struct RevertChangeArgs {
316 #[schemars(extend("x-mcp-header" = "Course"))]
317 pub course: IdArgument,
318 pub call: IdArgument,
320 #[serde(default)]
321 pub dry_run: bool,
322}
323
324#[derive(Debug, Deserialize, JsonSchema)]
325#[serde(deny_unknown_fields)]
326pub struct ReadPublicCourseArgs {
327 pub url: Option<String>,
329 pub course_slug: Option<String>,
331 pub path: Option<String>,
333 pub offset: Option<u32>,
335}
336
337#[derive(Debug, Deserialize, JsonSchema)]
338#[serde(deny_unknown_fields)]
339pub struct GuideArgs {
340 pub block_types: Option<Vec<String>>,
342 pub topics: Option<Vec<GuideTopic>>,
343}