1use std::sync::Arc;
7
8use synwire_core::error::{SynwireError, ToolError};
9use synwire_core::tools::{StructuredTool, Tool, ToolOutput, ToolSchema};
10
11use crate::client::DapClient;
12
13#[allow(clippy::needless_pass_by_value)] fn dap_err(e: crate::error::DapError) -> SynwireError {
16 SynwireError::Tool(ToolError::InvocationFailed {
17 message: e.to_string(),
18 })
19}
20
21#[allow(clippy::needless_pass_by_value)] fn json_err(e: serde_json::Error) -> SynwireError {
24 SynwireError::Tool(ToolError::InvocationFailed {
25 message: e.to_string(),
26 })
27}
28
29pub fn create_tools(client: Arc<DapClient>) -> Result<Vec<Arc<dyn Tool>>, SynwireError> {
36 let tools: Vec<Arc<dyn Tool>> = vec![
37 Arc::new(dap_status_tool(Arc::clone(&client))?),
38 Arc::new(dap_launch_tool(Arc::clone(&client))?),
39 Arc::new(dap_attach_tool(Arc::clone(&client))?),
40 Arc::new(dap_set_breakpoints_tool(Arc::clone(&client))?),
41 Arc::new(dap_continue_tool(Arc::clone(&client))?),
42 Arc::new(dap_step_over_tool(Arc::clone(&client))?),
43 Arc::new(dap_step_in_tool(Arc::clone(&client))?),
44 Arc::new(dap_step_out_tool(Arc::clone(&client))?),
45 Arc::new(dap_pause_tool(Arc::clone(&client))?),
46 Arc::new(dap_threads_tool(Arc::clone(&client))?),
47 Arc::new(dap_stack_trace_tool(Arc::clone(&client))?),
48 Arc::new(dap_variables_tool(Arc::clone(&client))?),
49 Arc::new(dap_evaluate_tool(Arc::clone(&client))?),
50 Arc::new(dap_disconnect_tool(client)?),
51 ];
52 Ok(tools)
53}
54
55fn dap_status_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
56 StructuredTool::builder()
57 .name("debug.status")
58 .description("Show the current debug session state, capabilities, and active breakpoints")
59 .schema(ToolSchema {
60 name: "debug.status".into(),
61 description:
62 "Show the current debug session state, capabilities, and active breakpoints".into(),
63 parameters: serde_json::json!({
64 "type": "object",
65 "properties": {},
66 "additionalProperties": false,
67 }),
68 })
69 .func(move |_input| {
70 let client = Arc::clone(&client);
71 Box::pin(async move {
72 let state = client.status().await;
73 let caps = client.capabilities().await;
74 let bps = client.active_breakpoints().await;
75
76 let result = serde_json::json!({
77 "state": format!("{state}"),
78 "capabilities": caps,
79 "active_breakpoints": bps,
80 });
81
82 let content = serde_json::to_string_pretty(&result).map_err(json_err)?;
83 Ok(ToolOutput {
84 content,
85 ..Default::default()
86 })
87 })
88 })
89 .build()
90}
91
92fn dap_launch_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
93 StructuredTool::builder()
94 .name("debug.launch")
95 .description(
96 "Launch a program under the debugger. Pass launch configuration as JSON arguments.",
97 )
98 .schema(ToolSchema {
99 name: "debug.launch".into(),
100 description: "Launch a program under the debugger".into(),
101 parameters: serde_json::json!({
102 "type": "object",
103 "properties": {
104 "program": {
105 "type": "string",
106 "description": "Path to the program to debug"
107 },
108 "args": {
109 "type": "array",
110 "items": { "type": "string" },
111 "description": "Command-line arguments for the program"
112 },
113 "cwd": {
114 "type": "string",
115 "description": "Working directory for the program"
116 },
117 "env": {
118 "type": "object",
119 "description": "Environment variables for the program"
120 },
121 "stopOnEntry": {
122 "type": "boolean",
123 "description": "Whether to stop at the program entry point"
124 }
125 },
126 "required": ["program"],
127 }),
128 })
129 .func(move |input| {
130 let client = Arc::clone(&client);
131 Box::pin(async move {
132 client.launch(input).await.map_err(dap_err)?;
133 Ok(ToolOutput {
134 content: "Program launched successfully under debugger.".into(),
135 ..Default::default()
136 })
137 })
138 })
139 .build()
140}
141
142fn dap_attach_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
143 StructuredTool::builder()
144 .name("debug.attach")
145 .description("Attach to a running process for debugging")
146 .schema(ToolSchema {
147 name: "debug.attach".into(),
148 description: "Attach to a running process for debugging".into(),
149 parameters: serde_json::json!({
150 "type": "object",
151 "properties": {
152 "processId": {
153 "type": "integer",
154 "description": "Process ID to attach to"
155 },
156 "port": {
157 "type": "integer",
158 "description": "Port to connect to (for remote debugging)"
159 },
160 "host": {
161 "type": "string",
162 "description": "Host for remote debugging"
163 }
164 },
165 }),
166 })
167 .func(move |input| {
168 let client = Arc::clone(&client);
169 Box::pin(async move {
170 client.attach(input).await.map_err(dap_err)?;
171 Ok(ToolOutput {
172 content: "Attached to process successfully.".into(),
173 ..Default::default()
174 })
175 })
176 })
177 .build()
178}
179
180fn dap_set_breakpoints_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
181 StructuredTool::builder()
182 .name("debug.set_breakpoints")
183 .description("Set breakpoints in a source file at specified line numbers")
184 .schema(ToolSchema {
185 name: "debug.set_breakpoints".into(),
186 description: "Set breakpoints in a source file at specified line numbers".into(),
187 parameters: serde_json::json!({
188 "type": "object",
189 "properties": {
190 "source_path": {
191 "type": "string",
192 "description": "Absolute path to the source file"
193 },
194 "lines": {
195 "type": "array",
196 "items": { "type": "integer" },
197 "description": "Line numbers to set breakpoints on"
198 }
199 },
200 "required": ["source_path", "lines"],
201 }),
202 })
203 .func(move |input| {
204 let client = Arc::clone(&client);
205 Box::pin(async move {
206 let source_path = input
207 .get("source_path")
208 .and_then(serde_json::Value::as_str)
209 .ok_or_else(|| {
210 dap_err(crate::error::DapError::Transport(
211 "missing source_path parameter".into(),
212 ))
213 })?;
214
215 let lines: Vec<i64> = input
216 .get("lines")
217 .and_then(serde_json::Value::as_array)
218 .map(|arr| arr.iter().filter_map(serde_json::Value::as_i64).collect())
219 .unwrap_or_default();
220
221 let result = client
222 .set_breakpoints(source_path, &lines)
223 .await
224 .map_err(dap_err)?;
225
226 let content = serde_json::to_string_pretty(&serde_json::json!({
227 "breakpoints": result,
228 }))
229 .map_err(json_err)?;
230
231 Ok(ToolOutput {
232 content,
233 ..Default::default()
234 })
235 })
236 })
237 .build()
238}
239
240fn dap_continue_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
241 StructuredTool::builder()
242 .name("debug.continue")
243 .description("Continue execution of a paused thread")
244 .schema(ToolSchema {
245 name: "debug.continue".into(),
246 description: "Continue execution of a paused thread".into(),
247 parameters: serde_json::json!({
248 "type": "object",
249 "properties": {
250 "thread_id": {
251 "type": "integer",
252 "description": "Thread ID to continue (use debug.threads to list)"
253 }
254 },
255 "required": ["thread_id"],
256 }),
257 })
258 .func(move |input| {
259 let client = Arc::clone(&client);
260 Box::pin(async move {
261 let thread_id = input
262 .get("thread_id")
263 .and_then(serde_json::Value::as_i64)
264 .ok_or_else(|| {
265 dap_err(crate::error::DapError::Transport(
266 "missing thread_id parameter".into(),
267 ))
268 })?;
269
270 client
271 .continue_execution(thread_id)
272 .await
273 .map_err(dap_err)?;
274 Ok(ToolOutput {
275 content: format!("Thread {thread_id} resumed."),
276 ..Default::default()
277 })
278 })
279 })
280 .build()
281}
282
283fn dap_step_over_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
284 StructuredTool::builder()
285 .name("debug.step_over")
286 .description("Step over the current line (next) for a thread")
287 .schema(ToolSchema {
288 name: "debug.step_over".into(),
289 description: "Step over the current line (next) for a thread".into(),
290 parameters: serde_json::json!({
291 "type": "object",
292 "properties": {
293 "thread_id": {
294 "type": "integer",
295 "description": "Thread ID to step over"
296 }
297 },
298 "required": ["thread_id"],
299 }),
300 })
301 .func(move |input| {
302 let client = Arc::clone(&client);
303 Box::pin(async move {
304 let thread_id = input
305 .get("thread_id")
306 .and_then(serde_json::Value::as_i64)
307 .ok_or_else(|| {
308 dap_err(crate::error::DapError::Transport(
309 "missing thread_id parameter".into(),
310 ))
311 })?;
312
313 client.next(thread_id).await.map_err(dap_err)?;
314 Ok(ToolOutput {
315 content: format!("Thread {thread_id} stepped over."),
316 ..Default::default()
317 })
318 })
319 })
320 .build()
321}
322
323fn dap_step_in_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
324 StructuredTool::builder()
325 .name("debug.step_in")
326 .description("Step into the current function call for a thread")
327 .schema(ToolSchema {
328 name: "debug.step_in".into(),
329 description: "Step into the current function call for a thread".into(),
330 parameters: serde_json::json!({
331 "type": "object",
332 "properties": {
333 "thread_id": {
334 "type": "integer",
335 "description": "Thread ID to step into"
336 }
337 },
338 "required": ["thread_id"],
339 }),
340 })
341 .func(move |input| {
342 let client = Arc::clone(&client);
343 Box::pin(async move {
344 let thread_id = input
345 .get("thread_id")
346 .and_then(serde_json::Value::as_i64)
347 .ok_or_else(|| {
348 dap_err(crate::error::DapError::Transport(
349 "missing thread_id parameter".into(),
350 ))
351 })?;
352
353 client.step_in(thread_id).await.map_err(dap_err)?;
354 Ok(ToolOutput {
355 content: format!("Thread {thread_id} stepped in."),
356 ..Default::default()
357 })
358 })
359 })
360 .build()
361}
362
363fn dap_step_out_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
364 StructuredTool::builder()
365 .name("debug.step_out")
366 .description("Step out of the current function for a thread")
367 .schema(ToolSchema {
368 name: "debug.step_out".into(),
369 description: "Step out of the current function for a thread".into(),
370 parameters: serde_json::json!({
371 "type": "object",
372 "properties": {
373 "thread_id": {
374 "type": "integer",
375 "description": "Thread ID to step out of"
376 }
377 },
378 "required": ["thread_id"],
379 }),
380 })
381 .func(move |input| {
382 let client = Arc::clone(&client);
383 Box::pin(async move {
384 let thread_id = input
385 .get("thread_id")
386 .and_then(serde_json::Value::as_i64)
387 .ok_or_else(|| {
388 dap_err(crate::error::DapError::Transport(
389 "missing thread_id parameter".into(),
390 ))
391 })?;
392
393 client.step_out(thread_id).await.map_err(dap_err)?;
394 Ok(ToolOutput {
395 content: format!("Thread {thread_id} stepped out."),
396 ..Default::default()
397 })
398 })
399 })
400 .build()
401}
402
403fn dap_pause_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
404 StructuredTool::builder()
405 .name("debug.pause")
406 .description("Pause execution of a running thread")
407 .schema(ToolSchema {
408 name: "debug.pause".into(),
409 description: "Pause execution of a running thread".into(),
410 parameters: serde_json::json!({
411 "type": "object",
412 "properties": {
413 "thread_id": {
414 "type": "integer",
415 "description": "Thread ID to pause"
416 }
417 },
418 "required": ["thread_id"],
419 }),
420 })
421 .func(move |input| {
422 let client = Arc::clone(&client);
423 Box::pin(async move {
424 let thread_id = input
425 .get("thread_id")
426 .and_then(serde_json::Value::as_i64)
427 .ok_or_else(|| {
428 dap_err(crate::error::DapError::Transport(
429 "missing thread_id parameter".into(),
430 ))
431 })?;
432
433 client.pause(thread_id).await.map_err(dap_err)?;
434 Ok(ToolOutput {
435 content: format!("Thread {thread_id} paused."),
436 ..Default::default()
437 })
438 })
439 })
440 .build()
441}
442
443fn dap_threads_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
444 StructuredTool::builder()
445 .name("debug.threads")
446 .description("List all threads in the debuggee process")
447 .schema(ToolSchema {
448 name: "debug.threads".into(),
449 description: "List all threads in the debuggee process".into(),
450 parameters: serde_json::json!({
451 "type": "object",
452 "properties": {},
453 "additionalProperties": false,
454 }),
455 })
456 .func(move |_input| {
457 let client = Arc::clone(&client);
458 Box::pin(async move {
459 let threads = client.threads().await.map_err(dap_err)?;
460
461 let content = serde_json::to_string_pretty(&serde_json::json!({
462 "threads": threads,
463 }))
464 .map_err(json_err)?;
465
466 Ok(ToolOutput {
467 content,
468 ..Default::default()
469 })
470 })
471 })
472 .build()
473}
474
475fn dap_stack_trace_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
476 StructuredTool::builder()
477 .name("debug.stack_trace")
478 .description("Get the stack trace for a specific thread")
479 .schema(ToolSchema {
480 name: "debug.stack_trace".into(),
481 description: "Get the stack trace for a specific thread".into(),
482 parameters: serde_json::json!({
483 "type": "object",
484 "properties": {
485 "thread_id": {
486 "type": "integer",
487 "description": "Thread ID to get stack trace for"
488 }
489 },
490 "required": ["thread_id"],
491 }),
492 })
493 .func(move |input| {
494 let client = Arc::clone(&client);
495 Box::pin(async move {
496 let thread_id = input
497 .get("thread_id")
498 .and_then(serde_json::Value::as_i64)
499 .ok_or_else(|| {
500 dap_err(crate::error::DapError::Transport(
501 "missing thread_id parameter".into(),
502 ))
503 })?;
504
505 let frames = client.stack_trace(thread_id).await.map_err(dap_err)?;
506
507 let content = serde_json::to_string_pretty(&serde_json::json!({
508 "stack_frames": frames,
509 }))
510 .map_err(json_err)?;
511
512 Ok(ToolOutput {
513 content,
514 ..Default::default()
515 })
516 })
517 })
518 .build()
519}
520
521fn dap_variables_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
522 StructuredTool::builder()
523 .name("debug.variables")
524 .description("Get variables for a scope or structured variable reference. Use debug.stack_trace and then debug.scopes to get variable references.")
525 .schema(ToolSchema {
526 name: "debug.variables".into(),
527 description: "Get variables for a scope or structured variable reference".into(),
528 parameters: serde_json::json!({
529 "type": "object",
530 "properties": {
531 "variables_reference": {
532 "type": "integer",
533 "description": "Variables reference ID (from scopes or structured variables)"
534 }
535 },
536 "required": ["variables_reference"],
537 }),
538 })
539 .func(move |input| {
540 let client = Arc::clone(&client);
541 Box::pin(async move {
542 let variables_ref = input
543 .get("variables_reference")
544 .and_then(serde_json::Value::as_i64)
545 .ok_or_else(|| dap_err(crate::error::DapError::Transport(
546 "missing variables_reference parameter".into(),
547 )))?;
548
549 let variables = client.variables(variables_ref).await.map_err(dap_err)?;
550
551 let content = serde_json::to_string_pretty(&serde_json::json!({
552 "variables": variables,
553 }))
554 .map_err(json_err)?;
555
556 Ok(ToolOutput {
557 content,
558 ..Default::default()
559 })
560 })
561 })
562 .build()
563}
564
565fn dap_evaluate_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
566 StructuredTool::builder()
567 .name("debug.evaluate")
568 .description("Evaluate an expression in the debuggee context (REPL mode)")
569 .schema(ToolSchema {
570 name: "debug.evaluate".into(),
571 description: "Evaluate an expression in the debuggee context".into(),
572 parameters: serde_json::json!({
573 "type": "object",
574 "properties": {
575 "expression": {
576 "type": "string",
577 "description": "Expression to evaluate"
578 },
579 "frame_id": {
580 "type": "integer",
581 "description": "Optional stack frame ID for context"
582 }
583 },
584 "required": ["expression"],
585 }),
586 })
587 .func(move |input| {
588 let client = Arc::clone(&client);
589 Box::pin(async move {
590 let expression = input
591 .get("expression")
592 .and_then(serde_json::Value::as_str)
593 .ok_or_else(|| {
594 dap_err(crate::error::DapError::Transport(
595 "missing expression parameter".into(),
596 ))
597 })?;
598
599 let frame_id = input.get("frame_id").and_then(serde_json::Value::as_i64);
600
601 let result = client
602 .evaluate(expression, frame_id)
603 .await
604 .map_err(dap_err)?;
605
606 let content = serde_json::to_string_pretty(&result).map_err(json_err)?;
607
608 Ok(ToolOutput {
609 content,
610 ..Default::default()
611 })
612 })
613 })
614 .build()
615}
616
617fn dap_disconnect_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
618 StructuredTool::builder()
619 .name("debug.disconnect")
620 .description("Disconnect from the debug session and terminate the debuggee")
621 .schema(ToolSchema {
622 name: "debug.disconnect".into(),
623 description: "Disconnect from the debug session and terminate the debuggee".into(),
624 parameters: serde_json::json!({
625 "type": "object",
626 "properties": {},
627 "additionalProperties": false,
628 }),
629 })
630 .func(move |_input| {
631 let client = Arc::clone(&client);
632 Box::pin(async move {
633 client.disconnect().await.map_err(dap_err)?;
634 Ok(ToolOutput {
635 content: "Debug session disconnected.".into(),
636 ..Default::default()
637 })
638 })
639 })
640 .build()
641}