Coverage Report

Created: 2026-03-25 23:22

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/tenet/tenet/tenet-aws/src/services/mcp/handlers/tools.rs
Line
Count
Source
1
use super::super::helpers::*;
2
use super::super::working_context::WorkingContext;
3
use super::super::*;
4
use super::tools_auth::tool_auth_scope_for_name;
5
use super::tools_handlers::*;
6
use super::tools_parse::{
7
    parse_architecture_connection_target_kind, parse_architecture_entity_kind,
8
    parse_connection_anchor_side, parse_connection_routing_mode, parse_connection_source_kind,
9
    parse_connection_target_kind,
10
};
11
use super::tools_registry::{
12
    ToolRegistryEntry, all_tool_definitions as build_all_tool_definitions,
13
    all_tool_registry_entries as build_all_tool_registry_entries,
14
    find_tool_registry_entry as lookup_tool_registry_entry,
15
};
16
use super::tools_render::{render_project_context_fallback, render_working_context};
17
use super::tools_schemas::{base_tool_definitions, crud_tools};
18
use crate::models::spec::{
19
    ExecutorType, SpecLinkRelation, SpecStatus, SpecType, TaskLinkRelation, TaskStatus,
20
};
21
use crate::observability::current_db_call_count;
22
use crate::services::context_read::{ContextProjection, WorkingContextSelector};
23
use std::time::Instant;
24
use tracing::{error, info};
25
26
25
pub(super) fn normalize_tool_name(name: &str) -> &str {
27
25
    match name.rsplit_once(':') {
28
4
        Some((_, suffix)) if !suffix.is_empty() => suffix,
29
21
        _ => name,
30
    }
31
25
}
32
33
0
fn log_get_working_context_invalid_params(
34
0
    reason: &str,
35
0
    has_arguments: bool,
36
0
    has_project: bool,
37
0
    has_path: bool,
38
0
    has_component_name: bool,
39
0
) {
40
0
    tracing::warn!(
41
        mcp_tool = "get_working_context",
42
        reason,
43
        has_arguments,
44
        has_project,
45
        has_path,
46
        has_component_name,
47
        "mcp_get_working_context_invalid_params"
48
    );
49
0
}
50
51
736
fn base_tool_handler_for_name(name: &str) -> Option<ToolHandler> {
52
736
    match name {
53
736
        "get_rules" => 
Some(handle_get_rules)23
,
54
713
        "get_project_info" => 
Some(handle_get_project_info)23
,
55
690
        "get_working_context" => 
Some(handle_get_working_context)23
,
56
667
        "get_graph_layout" => 
Some(handle_get_graph_layout)23
,
57
644
        "update_graph_layout" => 
Some(handle_update_graph_layout)23
,
58
621
        "reset_graph_layout" => 
Some(handle_reset_graph_layout)23
,
59
598
        "create_project_architecture" => 
Some(handle_create_project_architecture)23
,
60
575
        "create_skill" => 
Some(handle_create_skill)23
,
61
552
        "create_spec" => 
Some(handle_create_spec)23
,
62
529
        "get_skill" => 
Some(handle_get_skill)23
,
63
506
        "get_spec" => 
Some(handle_get_spec)23
,
64
483
        "get_task" => 
Some(handle_get_task)23
,
65
460
        "list_skills" => 
Some(handle_list_skills)23
,
66
437
        "list_specs" => 
Some(handle_list_specs)23
,
67
414
        "list_tasks" => 
Some(handle_list_tasks)23
,
68
391
        "update_skill" => 
Some(handle_update_skill)23
,
69
368
        "update_spec" => 
Some(handle_update_spec)23
,
70
345
        "update_task" => 
Some(handle_update_task)23
,
71
322
        "delete_skill" => 
Some(handle_delete_skill)23
,
72
299
        "delete_spec" => 
Some(handle_delete_spec)23
,
73
276
        "delete_task" => 
Some(handle_delete_task)23
,
74
253
        "create_task" => 
Some(handle_create_task)23
,
75
230
        "import_skill" => 
Some(handle_import_skill)23
,
76
207
        "reimport_skill" => 
Some(handle_reimport_skill)23
,
77
184
        "link_skill_to_component" => 
Some(handle_link_skill_to_component)23
,
78
161
        "unlink_skill_from_component" => 
Some(handle_unlink_skill_from_component)23
,
79
138
        "link_spec" => 
Some(handle_link_spec)23
,
80
115
        "unlink_spec" => 
Some(handle_unlink_spec)23
,
81
92
        "link_task" => 
Some(handle_link_task)23
,
82
69
        "unlink_task" => 
Some(handle_unlink_task)23
,
83
46
        "link_quality_to_component" => 
Some(handle_link_quality_to_component)23
,
84
23
        "unlink_quality_from_component" => Some(handle_unlink_quality_from_component),
85
0
        _ => None,
86
    }
87
736
}
88
89
23
fn all_tool_registry_entries() -> Vec<ToolRegistryEntry<ToolHandler>> {
90
23
    build_all_tool_registry_entries(
91
23
        base_tool_definitions(),
92
23
        crud_tools(),
93
        base_tool_handler_for_name,
94
        tool_auth_scope_for_name,
95
    )
96
23
}
97
98
22
pub(super) fn find_tool_registry_entry(name: &str) -> Option<ToolRegistryEntry<ToolHandler>> {
99
22
    lookup_tool_registry_entry(name, all_tool_registry_entries())
100
22
}
101
102
5
fn all_tool_definitions() -> Vec<ToolDefinition> {
103
5
    build_all_tool_definitions(base_tool_definitions(), crud_tools())
104
5
}
105
106
impl McpHandler {
107
0
    pub(crate) fn handle_list_tools(&self, id: Option<serde_json::Value>) -> JsonRpcResponse {
108
0
        let result = ListToolsResult {
109
0
            tools: all_tool_definitions(),
110
0
        };
111
0
        jsonrpc_success(id, result)
112
0
    }
113
114
22
    pub(super) async fn authorize_tool_call(
115
22
        &mut self,
116
22
        id: Option<serde_json::Value>,
117
22
        scope: super::tools_auth::ToolAuthScope,
118
22
        arguments: Option<&serde_json::Map<String, serde_json::Value>>,
119
22
    ) -> Option<JsonRpcResponse> {
120
22
        let result = match scope {
121
9
            super::tools_auth::ToolAuthScope::Org(action) => {
122
9
                let org_id = self.ensure_org().await;
123
9
                match org_id {
124
9
                    Ok(org_id) => match action {
125
                        crate::authz::Action::AdminOrg => {
126
1
                            self.service
127
1
                                .authz_service
128
1
                                .verify_admin_access(&org_id, &self.user_id)
129
1
                                .await
130
                        }
131
                        crate::authz::Action::ReadOrg | crate::authz::Action::WriteOrg => {
132
8
                            self.service
133
8
                                .authz_service
134
8
                                .verify_org_access(&org_id, &self.user_id)
135
8
                                .await
136
                        }
137
0
                        _ => Ok(()),
138
                    },
139
0
                    Err(err) => Err(err),
140
                }
141
            }
142
13
            super::tools_auth::ToolAuthScope::Project(action) => {
143
13
                let project_name = match arguments
144
13
                    .and_then(|args| args.get("project"))
145
13
                    .and_then(|value| value.as_str())
146
                {
147
13
                    Some(project_name) => project_name,
148
                    None => {
149
0
                        return Some(JsonRpcResponse::error(
150
0
                            id,
151
0
                            INVALID_PARAMS,
152
0
                            "Missing required parameter: project",
153
0
                        ));
154
                    }
155
                };
156
157
13
                match self.resolve_project_by_name(project_name).await {
158
13
                    Ok((org_id, project)) => match action {
159
                        crate::authz::Action::AdminProject => {
160
0
                            self.service
161
0
                                .authz_service
162
0
                                .verify_project_admin_access(&org_id, &project.id, &self.user_id)
163
0
                                .await
164
                        }
165
                        crate::authz::Action::ReadProject | crate::authz::Action::WriteProject => {
166
13
                            self.service
167
13
                                .authz_service
168
13
                                .verify_project_access(&org_id, &project.id, &self.user_id)
169
13
                                .await
170
                        }
171
0
                        _ => Ok(()),
172
                    },
173
0
                    Err(err) => Err(err),
174
                }
175
            }
176
        };
177
178
22
        if let Err(
err0
) = result {
179
0
            return Some(JsonRpcResponse::from_api_error(id, err));
180
22
        }
181
22
        None
182
22
    }
183
184
    // =========================================================================
185
    // Tool Implementations
186
    // =========================================================================
187
188
0
    pub(super) async fn call_get_rules(
189
0
        &mut self,
190
0
        id: Option<serde_json::Value>,
191
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
192
0
    ) -> JsonRpcResponse {
193
0
        let category_filter = arguments
194
0
            .as_ref()
195
0
            .and_then(|args| args.get("category"))
196
0
            .and_then(|v| v.as_str())
197
0
            .map(|s| s.to_string());
198
199
0
        let org_id = match self.ensure_org().await {
200
0
            Ok(id) => id,
201
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
202
        };
203
204
0
        let rules = match self.service.rule_repo.list_enabled_rules(&org_id).await {
205
0
            Ok(r) => r,
206
0
            Err(e) => {
207
0
                error!("Failed to fetch rules: {}", e);
208
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch rules");
209
            }
210
        };
211
212
0
        let filtered_rules: Vec<RuleView> = rules
213
0
            .iter()
214
0
            .filter(|r| {
215
0
                category_filter
216
0
                    .as_ref()
217
0
                    .map(|cat| &r.category == cat || r.category.is_empty())
218
0
                    .unwrap_or(true)
219
0
            })
220
0
            .map(RuleView::from)
221
0
            .collect();
222
223
0
        let mut output = String::new();
224
0
        output.push_str("# Programming Rules\n\n");
225
226
0
        if filtered_rules.is_empty() {
227
0
            output.push_str("No rules found.");
228
0
        } else {
229
0
            let mut current_category = String::new();
230
0
            for rule in &filtered_rules {
231
0
                if rule.category != current_category {
232
0
                    current_category = rule.category.clone();
233
0
                    output.push_str(&format!("\n## {}\n\n", current_category.to_uppercase()));
234
0
                }
235
236
0
                output.push_str(&format!("### {}\n", rule.name));
237
0
                if let Some(ref applies_to) = rule.applies_to {
238
0
                    output.push_str(&format!("*Applies to: {}*\n\n", applies_to));
239
0
                }
240
0
                output.push_str(&format!("{}\n\n", rule.description));
241
            }
242
        }
243
244
0
        let result = CallToolResult {
245
0
            content: vec![ToolContent::Text { text: output }],
246
0
            is_error: None,
247
0
        };
248
249
0
        jsonrpc_success(id, result)
250
0
    }
251
252
5
    pub(super) async fn call_get_project_info(
253
5
        &mut self,
254
5
        id: Option<serde_json::Value>,
255
5
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
256
5
    ) -> JsonRpcResponse {
257
5
        let project_filter = arguments
258
5
            .as_ref()
259
5
            .and_then(|args| args.get("project"))
260
5
            .and_then(|v| v.as_str())
261
5
            .map(|s| s.to_string());
262
263
5
        let path_filter = arguments
264
5
            .as_ref()
265
5
            .and_then(|args| args.get("path"))
266
5
            .and_then(|v| 
v0
.
as_str0
())
267
5
            .and_then(|s| 
{0
268
0
                if s.is_empty() {
269
0
                    None
270
                } else {
271
0
                    Some(s.to_string())
272
                }
273
0
            });
274
275
5
        let org_id = match self.ensure_org().await {
276
5
            Ok(id) => id,
277
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
278
        };
279
280
5
        let filtered_projects = if let Some(ref project_name) = project_filter {
281
5
            match self
282
5
                .service
283
5
                .context_read_service
284
5
                .resolve_project_by_name(&org_id, project_name)
285
5
                .await
286
            {
287
4
                Ok(project) => vec![project],
288
1
                Err(ApiError::NotFound(_)) => Vec::new(),
289
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
290
            }
291
        } else {
292
0
            match self.service.project_repo.list_projects(&org_id).await {
293
0
                Ok(p) => p,
294
0
                Err(e) => {
295
0
                    error!("Failed to fetch projects: {}", e);
296
0
                    return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
297
                }
298
            }
299
        };
300
301
5
        if filtered_projects.is_empty() {
302
1
            let output = if let Some(ref name) = project_filter {
303
1
                format!("No project found with name: {}", name)
304
            } else {
305
0
                "No projects configured. Create projects in tenet to see them here.".to_string()
306
            };
307
308
1
            let result = CallToolResult {
309
1
                content: vec![ToolContent::Text { text: output }],
310
1
                is_error: None,
311
1
            };
312
1
            return jsonrpc_success(id, result);
313
4
        }
314
315
4
        let mut output = String::new();
316
4
        output.push_str("# Project Information\n\n");
317
318
4
        for project in &filtered_projects {
319
4
            let project_view: ProjectView = project.into();
320
321
4
            output.push_str(&format!("## {}\n\n", project_view.name));
322
323
4
            if let Some(
ref desc0
) = project_view.description {
324
0
                output.push_str(&format!("{}\n\n", desc));
325
4
            }
326
327
4
            output.push_str(&format!("- **Type**: {}\n", project_view.project_type));
328
329
4
            if let Some(
ref url0
) = project_view.repository_url {
330
0
                output.push_str(&format!("- **Repository**: {}\n", url));
331
4
            }
332
333
4
            output.push('\n');
334
335
4
            let info_snapshot = match self
336
4
                .service
337
4
                .context_read_service
338
4
                .load_project_info_snapshot(&org_id, &project.id, &self.user_id)
339
4
                .await
340
            {
341
4
                Ok(value) => value,
342
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
343
            };
344
4
            let spec_summary = match self
345
4
                .service
346
4
                .spec_service
347
4
                .project_summary(&org_id, &project.id)
348
4
                .await
349
            {
350
4
                Ok(value) => value,
351
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
352
            };
353
4
            let active_specs = match self
354
4
                .service
355
4
                .spec_service
356
4
                .list_specs(
357
4
                    &org_id,
358
4
                    &project.id,
359
4
                    Some(crate::models::spec::SpecStatus::Active),
360
                )
361
4
                .await
362
            {
363
4
                Ok(value) => value,
364
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
365
            };
366
367
4
            let mut containers = info_snapshot.graph.containers.clone();
368
12
            
containers4
.
sort_by_key4
(|container| container.name.to_ascii_lowercase());
369
4
            let modules_by_parent = info_snapshot.graph.components_by_parent;
370
4
            let connections = info_snapshot.context.connections;
371
4
            let rules = info_snapshot.context.rules;
372
4
            let actors = info_snapshot.actors;
373
4
            let stores = info_snapshot.stores;
374
4
            let external_systems = info_snapshot.external_systems;
375
376
4
            output.push_str("### Specs\n\n");
377
4
            output.push_str(&format!(
378
4
                "- **Total**: {}\n- **Draft**: {}\n- **Active**: {}\n- **Done**: {}\n\n",
379
4
                spec_summary.total, spec_summary.draft, spec_summary.active, spec_summary.done
380
4
            ));
381
4
            if !active_specs.is_empty() {
382
0
                output.push_str("**Active specs:**\n\n");
383
0
                for spec in &active_specs {
384
0
                    output.push_str(&format!(
385
0
                        "- **{}** (`{}`) — {} tasks, {} done\n",
386
0
                        spec.spec.title,
387
0
                        spec.spec.name,
388
0
                        spec.task_summary.total,
389
0
                        spec.task_summary.done
390
0
                    ));
391
0
                }
392
0
                output.push('\n');
393
4
            }
394
395
4
            if let Some(
ref path0
) = path_filter {
396
0
                let mut found_match = false;
397
398
0
                for container in &containers {
399
0
                    if path_matches_glob(path, &container.path) {
400
0
                        found_match = true;
401
0
                        output.push_str(&format!(
402
0
                            "### 📦 Container: {} (matches path)\n\n",
403
0
                            container.name
404
0
                        ));
405
0
                        output.push_str(&format!("- **Path pattern**: `{}`\n", container.path));
406
0
                        if let Some(ref desc) = container.description {
407
0
                            output.push_str(&format!("- **Description**: {}\n", desc));
408
0
                        }
409
0
                        output.push('\n');
410
411
0
                        if !modules_by_parent.is_empty() {
412
0
                            output.push_str("**Components:**\n\n");
413
0
                            format_component_tree(
414
0
                                &mut output,
415
0
                                &modules_by_parent,
416
0
                                &container.id,
417
0
                                0,
418
0
                            );
419
0
                            output.push('\n');
420
0
                        }
421
0
                    }
422
                }
423
424
0
                if !found_match {
425
0
                    output.push_str(&format!("*No container matches path: `{}`*\n\n", path));
426
0
                }
427
            } else {
428
                // No path filter, show all containers
429
4
                if !containers.is_empty() {
430
4
                    output.push_str("### Containers\n\n");
431
9
                    for container in 
&containers4
{
432
9
                        let container_view: ComponentView = container.into();
433
9
                        output.push_str(&format!(
434
9
                            "- **{}** (`{}`)",
435
9
                            container_view.name, container_view.path
436
9
                        ));
437
                        // Add technology/framework info
438
9
                        let tech_info: Vec<&str> = [
439
9
                            container_view.technology.as_deref(),
440
9
                            container_view.framework.as_deref(),
441
9
                        ]
442
9
                        .into_iter()
443
9
                        .flatten()
444
9
                        .collect();
445
9
                        if !tech_info.is_empty() {
446
1
                            output.push_str(&format!(" [{}]", tech_info.join(" / ")));
447
8
                        }
448
9
                        if let Some(
ref desc1
) = container_view.description {
449
1
                            output.push_str(&format!(": {}", desc));
450
8
                        }
451
9
                        output.push('\n');
452
                    }
453
4
                    output.push('\n');
454
0
                }
455
456
4
                let mut module_roots: Vec<(ComponentId, String)> = Vec::new();
457
9
                for container in 
&containers4
{
458
9
                    module_roots.push((
459
9
                        container.id.clone(),
460
9
                        format!("📦 Container: {}", container.name),
461
9
                    ));
462
9
                }
463
464
4
                if !modules_by_parent.is_empty() {
465
4
                    output.push_str("### Components\n\n");
466
9
                    for (root_id, label) in 
module_roots4
{
467
9
                        if modules_by_parent.contains_key(&root_id) {
468
4
                            output.push_str(&format!("#### {}\n\n", label));
469
4
                            format_component_tree(&mut output, &modules_by_parent, &root_id, 0);
470
4
                            output.push('\n');
471
5
                        }
472
                    }
473
0
                }
474
475
4
                if !actors.is_empty() {
476
0
                    output.push_str("### Persons\n\n");
477
0
                    for actor in &actors {
478
0
                        let actor_view: ActorView = actor.into();
479
0
                        let interaction_mode = actor_view
480
0
                            .interaction_mode
481
0
                            .clone()
482
0
                            .unwrap_or_else(|| "n/a".to_string());
483
0
                        output.push_str(&format!(
484
0
                            "- **{}** (`{}`) [{} / {}]\n",
485
0
                            actor_view.name, actor_view.id, actor_view.actor_type, interaction_mode
486
0
                        ));
487
                    }
488
0
                    output.push('\n');
489
4
                }
490
491
4
                if !connections.is_empty() {
492
4
                    output.push_str("### Connections\n\n");
493
494
4
                    let container_names: HashMap<_, _> = containers
495
4
                        .iter()
496
9
                        .
map4
(|container| (container.id.clone(), container.name.clone()))
497
4
                        .collect();
498
4
                    let module_names: HashMap<_, _> = modules_by_parent
499
4
                        .values()
500
4
                        .flatten()
501
4
                        .map(|module| (module.id.clone(), module.name.clone()))
502
4
                        .collect();
503
4
                    let actor_names: HashMap<_, _> = actors
504
4
                        .iter()
505
4
                        .map(|actor| (
actor.id0
.
to_string0
(),
actor.name0
.
clone0
()))
506
4
                        .collect();
507
4
                    let store_names: HashMap<_, _> = stores
508
4
                        .iter()
509
4
                        .map(|store| (
store.id0
.
to_string0
(),
store.name0
.
clone0
()))
510
4
                        .collect();
511
4
                    let external_system_names: HashMap<_, _> = external_systems
512
4
                        .iter()
513
4
                        .map(|system| (
system.id0
.
to_string0
(),
system.name0
.
clone0
()))
514
4
                        .collect();
515
516
4
                    for conn in &connections {
517
4
                        let source_name = match conn.source_type {
518
                            ConnectionSourceKind::Container => {
519
4
                                ComponentId::try_from(conn.source_id.clone())
520
4
                                    .ok()
521
4
                                    .and_then(|id| container_names.get(&id).cloned())
522
4
                                    .unwrap_or_else(|| 
conn.source_id0
.
to_string0
())
523
                            }
524
                            ConnectionSourceKind::Component => {
525
0
                                ComponentId::try_from(conn.source_id.clone())
526
0
                                    .ok()
527
0
                                    .and_then(|id| module_names.get(&id).cloned())
528
0
                                    .unwrap_or_else(|| conn.source_id.to_string())
529
                            }
530
0
                            ConnectionSourceKind::Person => actor_names
531
0
                                .get(&conn.source_id)
532
0
                                .cloned()
533
0
                                .unwrap_or_else(|| conn.source_id.to_string()),
534
0
                            ConnectionSourceKind::Store => store_names
535
0
                                .get(&conn.source_id)
536
0
                                .cloned()
537
0
                                .unwrap_or_else(|| conn.source_id.to_string()),
538
0
                            ConnectionSourceKind::ExternalSystem => external_system_names
539
0
                                .get(&conn.source_id)
540
0
                                .cloned()
541
0
                                .unwrap_or_else(|| conn.source_id.to_string()),
542
                        };
543
4
                        let target_name = match conn.target_type {
544
                            ConnectionTargetKind::Container => {
545
4
                                ComponentId::try_from(conn.target_id.clone())
546
4
                                    .ok()
547
4
                                    .and_then(|id| container_names.get(&id).cloned())
548
4
                                    .unwrap_or_else(|| 
conn.target_id0
.
to_string0
())
549
                            }
550
                            ConnectionTargetKind::Component => {
551
0
                                ComponentId::try_from(conn.target_id.clone())
552
0
                                    .ok()
553
0
                                    .and_then(|id| module_names.get(&id).cloned())
554
0
                                    .unwrap_or_else(|| conn.target_id.to_string())
555
                            }
556
0
                            ConnectionTargetKind::Person => actor_names
557
0
                                .get(&conn.target_id)
558
0
                                .cloned()
559
0
                                .unwrap_or_else(|| conn.target_id.to_string()),
560
0
                            ConnectionTargetKind::Store => store_names
561
0
                                .get(&conn.target_id)
562
0
                                .cloned()
563
0
                                .unwrap_or_else(|| conn.target_id.to_string()),
564
0
                            ConnectionTargetKind::ExternalSystem => external_system_names
565
0
                                .get(&conn.target_id)
566
0
                                .cloned()
567
0
                                .unwrap_or_else(|| conn.target_id.to_string()),
568
                        };
569
570
4
                        output.push_str(&format!(
571
4
                            "- **{}** --[{}]--> **{}**",
572
4
                            source_name, conn.label, target_name
573
4
                        ));
574
4
                        if let Some(ref desc) = conn.description {
575
4
                            output.push_str(&format!(": {}", desc));
576
4
                        
}0
577
4
                        output.push('\n');
578
                    }
579
4
                    output.push('\n');
580
0
                }
581
582
4
                if !stores.is_empty() {
583
0
                    output.push_str("### Stores\n\n");
584
0
                    for store in &stores {
585
0
                        let store_view: StoreView = store.into();
586
0
                        output.push_str(&format!(
587
0
                            "- **{}** (`{}`) [{}] ({})\n",
588
0
                            store_view.name,
589
0
                            store_view.id,
590
0
                            store_view.store_type,
591
0
                            store_view.ownership
592
0
                        ));
593
0
                    }
594
0
                    output.push('\n');
595
4
                }
596
597
4
                if !external_systems.is_empty() {
598
0
                    output.push_str("### External Systems\n\n");
599
0
                    for system in &external_systems {
600
0
                        let system_view: ExternalSystemView = system.into();
601
0
                        output.push_str(&format!(
602
0
                            "- **{}** (`{}`) [{}]\n",
603
0
                            system_view.name, system_view.id, system_view.category
604
0
                        ));
605
0
                    }
606
0
                    output.push('\n');
607
4
                }
608
609
4
                let project_rules = rules
610
4
                    .iter()
611
4
                    .filter(|rule| rule.project_id.as_ref() == Some(&project.id))
612
4
                    .cloned()
613
4
                    .collect::<Vec<_>>();
614
615
4
                if !project_rules.is_empty() {
616
4
                    output.push_str("### Project Rules\n\n");
617
4
                    for rule in &project_rules {
618
4
                        let rule_view: RuleView = rule.into();
619
4
                        output.push_str(&format!("#### {}\n", rule_view.name));
620
4
                        if let Some(ref applies_to) = rule_view.applies_to {
621
4
                            output.push_str(&format!("*Applies to: {}*\n\n", applies_to));
622
4
                        
}0
623
4
                        output.push_str(&format!("{}\n\n", rule_view.description));
624
                    }
625
0
                }
626
627
4
                if containers.is_empty() {
628
0
                    output.push_str("*No containers configured for this project.*\n\n");
629
4
                }
630
            }
631
        }
632
633
4
        let result = CallToolResult {
634
4
            content: vec![ToolContent::Text { text: output }],
635
4
            is_error: None,
636
4
        };
637
638
4
        jsonrpc_success(id, result)
639
5
    }
640
641
1
    pub(super) async fn call_get_working_context(
642
1
        &mut self,
643
1
        id: Option<serde_json::Value>,
644
1
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
645
1
    ) -> JsonRpcResponse {
646
1
        let start = Instant::now();
647
648
1
        let args = match arguments {
649
1
            Some(a) => a,
650
            None => {
651
0
                log_get_working_context_invalid_params(
652
0
                    "missing_arguments",
653
                    false,
654
                    false,
655
                    false,
656
                    false,
657
                );
658
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments");
659
            }
660
        };
661
662
1
        let has_project = args.get("project").is_some();
663
1
        let has_path = args.get("path").is_some();
664
1
        let has_component_name = args.get("component_name").is_some();
665
1
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
666
1
            Some(name) if !name.is_empty() => name,
667
            _ => {
668
0
                log_get_working_context_invalid_params(
669
0
                    "missing_project_argument",
670
                    true,
671
0
                    has_project,
672
0
                    has_path,
673
0
                    has_component_name,
674
                );
675
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
676
            }
677
        };
678
679
1
        let path_value = args.get("path").and_then(|v| v.as_str()).and_then(|value| {
680
1
            if value.trim().is_empty() {
681
0
                None
682
            } else {
683
1
                Some(value)
684
            }
685
1
        });
686
1
        let component_name = args
687
1
            .get("component_name")
688
1
            .and_then(|v| 
v0
.
as_str0
())
689
1
            .and_then(|value| 
{0
690
0
                if value.trim().is_empty() {
691
0
                    None
692
                } else {
693
0
                    Some(value)
694
                }
695
0
            });
696
697
1
        if path_value.is_some() && component_name.is_some() {
698
0
            log_get_working_context_invalid_params(
699
0
                "both_path_and_component_name_provided",
700
                true,
701
0
                has_project,
702
0
                has_path,
703
0
                has_component_name,
704
            );
705
0
            return JsonRpcResponse::error(
706
0
                id,
707
                INVALID_PARAMS,
708
0
                "Provide either 'path' or 'component_name', not both",
709
            );
710
1
        }
711
712
1
        if path_value.is_none() && 
component_name0
.
is_none0
() {
713
0
            log_get_working_context_invalid_params(
714
0
                "neither_path_nor_component_name_provided",
715
                true,
716
0
                has_project,
717
0
                has_path,
718
0
                has_component_name,
719
            );
720
0
            return JsonRpcResponse::error(
721
0
                id,
722
                INVALID_PARAMS,
723
0
                "Provide either 'path' or 'component_name'",
724
            );
725
1
        }
726
727
1
        let org_id = match self.ensure_org().await {
728
1
            Ok(value) => value,
729
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
730
        };
731
732
1
        let project = match self
733
1
            .service
734
1
            .context_read_service
735
1
            .resolve_project_by_name(&org_id, project_name)
736
1
            .await
737
        {
738
1
            Ok(value) => value,
739
            Err(ApiError::NotFound(_)) => {
740
0
                log_get_working_context_invalid_params(
741
0
                    "project_not_found",
742
                    true,
743
0
                    has_project,
744
0
                    has_path,
745
0
                    has_component_name,
746
                );
747
0
                return JsonRpcResponse::error(
748
0
                    id,
749
                    INVALID_PARAMS,
750
0
                    &format!("Project '{}' not found", project_name),
751
                );
752
            }
753
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
754
        };
755
756
1
        let selector = if let Some(path) = path_value {
757
1
            WorkingContextSelector::Path(path)
758
        } else {
759
0
            let Some(component_name) = component_name else {
760
0
                log_get_working_context_invalid_params(
761
0
                    "selector_unavailable_after_normalization",
762
                    true,
763
0
                    has_project,
764
0
                    has_path,
765
0
                    has_component_name,
766
                );
767
0
                return JsonRpcResponse::error(
768
0
                    id,
769
                    INVALID_PARAMS,
770
0
                    "Either component_name or path must be provided",
771
                );
772
            };
773
0
            WorkingContextSelector::ComponentName(component_name)
774
        };
775
776
1
        let 
snapshot0
= match self
777
1
            .service
778
1
            .context_read_service
779
1
            .load_working_context_snapshot(&org_id, &project, selector, &self.user_id)
780
1
            .await
781
        {
782
0
            Ok(value) => value,
783
1
            Err(ApiError::NotFound(_)) if path_value.is_some() => {
784
1
                let snapshot = match self
785
1
                    .service
786
1
                    .context_read_service
787
1
                    .load_snapshot(
788
1
                        &org_id,
789
1
                        &project.id,
790
1
                        &self.user_id,
791
1
                        ContextProjection::Config,
792
                    )
793
1
                    .await
794
                {
795
1
                    Ok(value) => value,
796
0
                    Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
797
                };
798
1
                let view = self
799
1
                    .service
800
1
                    .context_read_service
801
1
                    .build_project_context_view(snapshot, None, path_value);
802
803
1
                info!(
804
                    mcp_tool = "get_working_context",
805
                    project = %project.name,
806
0
                    path = path_value.unwrap_or(""),
807
0
                    component_name = component_name.unwrap_or(""),
808
                    resolution = "project_fallback",
809
0
                    rule_count = view.rules.len(),
810
0
                    skill_count = view.skills.len(),
811
0
                    db_calls = current_db_call_count().unwrap_or_default(),
812
0
                    duration_ms = start.elapsed().as_millis(),
813
                    "mcp_working_context_metrics"
814
                );
815
816
1
                let output = render_project_context_fallback(path_value.unwrap_or_default(), &view);
817
1
                let result = CallToolResult {
818
1
                    content: vec![ToolContent::Text { text: output }],
819
1
                    is_error: None,
820
1
                };
821
822
1
                return jsonrpc_success(id, result);
823
            }
824
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
825
        };
826
0
        let context: WorkingContext = snapshot.context;
827
828
0
        info!(
829
            mcp_tool = "get_working_context",
830
            project = %project.name,
831
            component = %context.component.name,
832
0
            path = path_value.unwrap_or(""),
833
0
            component_name = component_name.unwrap_or(""),
834
            graph_nodes = snapshot.graph_nodes,
835
            graph_parents = snapshot.graph_parent_buckets,
836
0
            parent_chain_count = context.parent_chain.len(),
837
0
            quality_count = context.quality_attributes.len(),
838
0
            rule_count = context.applicable_rules.len(),
839
0
            skill_count = context.skills.len(),
840
0
            db_calls = current_db_call_count().unwrap_or_default(),
841
0
            duration_ms = start.elapsed().as_millis(),
842
            "mcp_working_context_metrics"
843
        );
844
845
0
        let output = render_working_context(&context);
846
0
        let result = CallToolResult {
847
0
            content: vec![ToolContent::Text { text: output }],
848
0
            is_error: None,
849
0
        };
850
851
0
        jsonrpc_success(id, result)
852
1
    }
853
854
0
    pub(super) async fn call_get_graph_layout(
855
0
        &mut self,
856
0
        id: Option<serde_json::Value>,
857
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
858
0
    ) -> JsonRpcResponse {
859
0
        let args = match arguments {
860
0
            Some(a) => a,
861
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
862
        };
863
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
864
0
            Some(name) if !name.trim().is_empty() => name.trim(),
865
0
            _ => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument"),
866
        };
867
0
        let view_level = match args.get("view_level").and_then(|v| v.as_str()) {
868
0
            Some(level) if !level.trim().is_empty() => {
869
0
                match level.trim().parse::<GraphViewLevel>() {
870
0
                    Ok(parsed) => parsed,
871
0
                    Err(e) => {
872
0
                        return JsonRpcResponse::error(
873
0
                            id,
874
                            INVALID_PARAMS,
875
0
                            &format!("Invalid 'view_level' argument: {}", e),
876
                        );
877
                    }
878
                }
879
            }
880
            _ => {
881
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'view_level' argument");
882
            }
883
        };
884
885
0
        let org_id = match self.ensure_org().await {
886
0
            Ok(id) => id,
887
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
888
        };
889
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
890
0
            Ok(list) => list,
891
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
892
        };
893
0
        let project = match projects
894
0
            .into_iter()
895
0
            .find(|project| project.name.eq_ignore_ascii_case(project_name))
896
        {
897
0
            Some(project) => project,
898
            None => {
899
0
                return JsonRpcResponse::error(
900
0
                    id,
901
                    INVALID_PARAMS,
902
0
                    &format!("Project '{}' not found", project_name),
903
                );
904
            }
905
        };
906
907
0
        let Some(graph_layout_service) = self.service.graph_layout_service.clone() else {
908
0
            return JsonRpcResponse::error(
909
0
                id,
910
                INTERNAL_ERROR,
911
0
                "Graph layout service is unavailable",
912
            );
913
        };
914
915
0
        let layout = match graph_layout_service
916
0
            .get_layout(&self.user_id, &org_id, &project.id, view_level)
917
0
            .await
918
        {
919
0
            Ok(layout) => layout,
920
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
921
        };
922
0
        let text = match serde_json::to_string_pretty(&layout) {
923
0
            Ok(value) => value,
924
0
            Err(e) => {
925
0
                return JsonRpcResponse::error(
926
0
                    id,
927
                    INTERNAL_ERROR,
928
0
                    &format!("Failed to serialize response: {}", e),
929
                );
930
            }
931
        };
932
933
0
        let result = CallToolResult {
934
0
            content: vec![ToolContent::Text { text }],
935
0
            is_error: None,
936
0
        };
937
0
        jsonrpc_success(id, result)
938
0
    }
939
940
0
    pub(super) async fn call_update_graph_layout(
941
0
        &mut self,
942
0
        id: Option<serde_json::Value>,
943
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
944
0
    ) -> JsonRpcResponse {
945
0
        let mut args = match arguments {
946
0
            Some(a) => a,
947
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
948
        };
949
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
950
0
            Some(name) if !name.trim().is_empty() => name.trim().to_string(),
951
0
            _ => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument"),
952
        };
953
0
        let view_level = match args.get("view_level").and_then(|v| v.as_str()) {
954
0
            Some(level) if !level.trim().is_empty() => {
955
0
                match level.trim().parse::<GraphViewLevel>() {
956
0
                    Ok(parsed) => parsed,
957
0
                    Err(e) => {
958
0
                        return JsonRpcResponse::error(
959
0
                            id,
960
                            INVALID_PARAMS,
961
0
                            &format!("Invalid 'view_level' argument: {}", e),
962
                        );
963
                    }
964
                }
965
            }
966
            _ => {
967
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'view_level' argument");
968
            }
969
        };
970
0
        let replace = args
971
0
            .get("replace")
972
0
            .and_then(|value| value.as_bool())
973
0
            .unwrap_or(false);
974
0
        args.remove("project");
975
0
        args.remove("view_level");
976
0
        args.remove("replace");
977
978
0
        let org_id = match self.ensure_org().await {
979
0
            Ok(id) => id,
980
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
981
        };
982
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
983
0
            Ok(list) => list,
984
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
985
        };
986
0
        let project = match projects
987
0
            .into_iter()
988
0
            .find(|project| project.name.eq_ignore_ascii_case(&project_name))
989
        {
990
0
            Some(project) => project,
991
            None => {
992
0
                return JsonRpcResponse::error(
993
0
                    id,
994
                    INVALID_PARAMS,
995
0
                    &format!("Project '{}' not found", project_name),
996
                );
997
            }
998
        };
999
1000
0
        let Some(graph_layout_service) = self.service.graph_layout_service.clone() else {
1001
0
            return JsonRpcResponse::error(
1002
0
                id,
1003
                INTERNAL_ERROR,
1004
0
                "Graph layout service is unavailable",
1005
            );
1006
        };
1007
1008
0
        let layout = if replace {
1009
0
            let request = match serde_json::from_value::<PutGraphLayoutRequest>(
1010
0
                serde_json::Value::Object(args),
1011
0
            ) {
1012
0
                Ok(request) => request,
1013
0
                Err(e) => {
1014
0
                    return JsonRpcResponse::error(
1015
0
                        id,
1016
                        INVALID_PARAMS,
1017
0
                        &format!("Invalid graph layout PUT payload: {}", e),
1018
                    );
1019
                }
1020
            };
1021
0
            match graph_layout_service
1022
0
                .put_layout(&self.user_id, &org_id, &project.id, view_level, request)
1023
0
                .await
1024
            {
1025
0
                Ok(layout) => layout,
1026
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
1027
            }
1028
        } else {
1029
0
            let request = match serde_json::from_value::<PatchGraphLayoutRequest>(
1030
0
                serde_json::Value::Object(args),
1031
0
            ) {
1032
0
                Ok(request) => request,
1033
0
                Err(e) => {
1034
0
                    return JsonRpcResponse::error(
1035
0
                        id,
1036
                        INVALID_PARAMS,
1037
0
                        &format!("Invalid graph layout PATCH payload: {}", e),
1038
                    );
1039
                }
1040
            };
1041
0
            match graph_layout_service
1042
0
                .patch_layout(&self.user_id, &org_id, &project.id, view_level, request)
1043
0
                .await
1044
            {
1045
0
                Ok(layout) => layout,
1046
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
1047
            }
1048
        };
1049
1050
0
        let text = match serde_json::to_string_pretty(&layout) {
1051
0
            Ok(value) => value,
1052
0
            Err(e) => {
1053
0
                return JsonRpcResponse::error(
1054
0
                    id,
1055
                    INTERNAL_ERROR,
1056
0
                    &format!("Failed to serialize response: {}", e),
1057
                );
1058
            }
1059
        };
1060
0
        let result = CallToolResult {
1061
0
            content: vec![ToolContent::Text { text }],
1062
0
            is_error: None,
1063
0
        };
1064
0
        jsonrpc_success(id, result)
1065
0
    }
1066
1067
0
    pub(super) async fn call_reset_graph_layout(
1068
0
        &mut self,
1069
0
        id: Option<serde_json::Value>,
1070
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
1071
0
    ) -> JsonRpcResponse {
1072
0
        let args = match arguments {
1073
0
            Some(a) => a,
1074
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
1075
        };
1076
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
1077
0
            Some(name) if !name.trim().is_empty() => name.trim().to_string(),
1078
0
            _ => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument"),
1079
        };
1080
0
        let view_level = match args.get("view_level").and_then(|v| v.as_str()) {
1081
0
            Some(level) if !level.trim().is_empty() => {
1082
0
                match level.trim().parse::<GraphViewLevel>() {
1083
0
                    Ok(parsed) => parsed,
1084
0
                    Err(e) => {
1085
0
                        return JsonRpcResponse::error(
1086
0
                            id,
1087
                            INVALID_PARAMS,
1088
0
                            &format!("Invalid 'view_level' argument: {}", e),
1089
                        );
1090
                    }
1091
                }
1092
            }
1093
            _ => {
1094
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'view_level' argument");
1095
            }
1096
        };
1097
1098
0
        let org_id = match self.ensure_org().await {
1099
0
            Ok(id) => id,
1100
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
1101
        };
1102
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
1103
0
            Ok(list) => list,
1104
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
1105
        };
1106
0
        let project = match projects
1107
0
            .into_iter()
1108
0
            .find(|project| project.name.eq_ignore_ascii_case(&project_name))
1109
        {
1110
0
            Some(project) => project,
1111
            None => {
1112
0
                return JsonRpcResponse::error(
1113
0
                    id,
1114
                    INVALID_PARAMS,
1115
0
                    &format!("Project '{}' not found", project_name),
1116
                );
1117
            }
1118
        };
1119
1120
0
        let Some(graph_layout_service) = self.service.graph_layout_service.clone() else {
1121
0
            return JsonRpcResponse::error(
1122
0
                id,
1123
                INTERNAL_ERROR,
1124
0
                "Graph layout service is unavailable",
1125
            );
1126
        };
1127
0
        if let Err(e) = graph_layout_service
1128
0
            .reset_layout(&self.user_id, &org_id, &project.id, view_level)
1129
0
            .await
1130
        {
1131
0
            return JsonRpcResponse::from_api_error(id, e);
1132
0
        }
1133
1134
0
        let text = serde_json::json!({
1135
0
            "status": "ok",
1136
0
            "project": project.name,
1137
0
            "view_level": view_level.as_str(),
1138
0
            "message": "Graph layout reset",
1139
        })
1140
0
        .to_string();
1141
0
        let result = CallToolResult {
1142
0
            content: vec![ToolContent::Text { text }],
1143
0
            is_error: None,
1144
0
        };
1145
0
        jsonrpc_success(id, result)
1146
0
    }
1147
1148
3
    pub(super) async fn call_create_project_architecture(
1149
3
        &mut self,
1150
3
        id: Option<serde_json::Value>,
1151
3
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
1152
3
    ) -> JsonRpcResponse {
1153
3
        let args = match arguments {
1154
3
            Some(a) => a,
1155
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
1156
        };
1157
1158
3
        let payload: CreateProjectArchitectureArgs =
1159
3
            match serde_json::from_value(serde_json::Value::Object(args)) {
1160
3
                Ok(p) => p,
1161
0
                Err(e) => {
1162
0
                    return JsonRpcResponse::error(
1163
0
                        id,
1164
                        INVALID_PARAMS,
1165
0
                        &format!("Invalid params: {}", e),
1166
                    );
1167
                }
1168
            };
1169
1170
3
        let mut questions: Vec<IntakeQuestion> = Vec::new();
1171
3
        let mut question_ids: HashSet<String> = HashSet::new();
1172
1173
3
        let respond_json = |payload: serde_json::Value| -> JsonRpcResponse {
1174
3
            let text = match serde_json::to_string_pretty(&payload) {
1175
3
                Ok(value) => value,
1176
0
                Err(e) => format!(
1177
                    "{{\"status\":\"error\",\"message\":\"Failed to serialize response: {}\"}}",
1178
                    e
1179
                ),
1180
            };
1181
1182
3
            let result = CallToolResult {
1183
3
                content: vec![ToolContent::Text { text }],
1184
3
                is_error: None,
1185
3
            };
1186
1187
3
            match serde_json::to_value(result) {
1188
3
                Ok(value) => JsonRpcResponse::success(id.clone(), value),
1189
0
                Err(e) => JsonRpcResponse::error(
1190
0
                    id.clone(),
1191
                    INTERNAL_ERROR,
1192
0
                    &format!("Failed to serialize response: {}", e),
1193
                ),
1194
            }
1195
3
        };
1196
1197
3
        let project_name = payload
1198
3
            .project
1199
3
            .as_ref()
1200
3
            .and_then(|project| 
normalize_optional_value2
(
project.name2
.
as_deref2
()));
1201
3
        if project_name.is_none() {
1202
1
            push_question(
1203
1
                &mut questions,
1204
1
                &mut question_ids,
1205
1
                "project.name".to_string(),
1206
1
                "project.name".to_string(),
1207
1
                "Provide the project name.".to_string(),
1208
1
                None,
1209
1
            );
1210
2
        }
1211
1212
3
        let project_description = payload
1213
3
            .project
1214
3
            .as_ref()
1215
3
            .and_then(|project| 
normalize_optional_value2
(
project.description2
.
as_deref2
()));
1216
3
        let repository_url = payload
1217
3
            .project
1218
3
            .as_ref()
1219
3
            .and_then(|project| 
normalize_optional_value2
(
project.repository_url2
.
as_deref2
()));
1220
1221
3
        let project_type_input = payload
1222
3
            .project
1223
3
            .as_ref()
1224
3
            .and_then(|project| 
normalize_optional_value2
(
project.project_type2
.
as_deref2
()));
1225
3
        let mut resolved_project_type = project_type_input
1226
3
            .as_ref()
1227
3
            .and_then(|value| 
ProjectType::from_str2
(
value2
).
map_err2
(|_| ()).
ok2
());
1228
3
        if project_type_input.is_some() && 
resolved_project_type2
.
is_none2
() {
1229
0
            push_question(
1230
0
                &mut questions,
1231
0
                &mut question_ids,
1232
0
                "project.project_type".to_string(),
1233
0
                "project.project_type".to_string(),
1234
0
                "Project type must be one of: monorepo, single, multi.".to_string(),
1235
0
                Some(vec![
1236
0
                    "monorepo".to_string(),
1237
0
                    "single".to_string(),
1238
0
                    "multi".to_string(),
1239
0
                ]),
1240
0
            );
1241
3
        }
1242
1243
3
        let architecture_style = payload
1244
3
            .architecture
1245
3
            .as_ref()
1246
3
            .and_then(|arch| 
normalize_optional_value2
(
arch.style2
.
as_deref2
()));
1247
3
        if architecture_style.is_none() {
1248
1
            push_question(&mut questions, &mut question_ids,
1249
1
                "architecture.style".to_string(),
1250
1
                "architecture.style".to_string(),
1251
1
                "What architecture style should this project follow? (e.g., monolith, microservices, event-driven)".to_string(),
1252
1
                None,
1253
1
            );
1254
2
        }
1255
1256
3
        let deployment_model = payload
1257
3
            .architecture
1258
3
            .as_ref()
1259
3
            .and_then(|arch| 
normalize_optional_value2
(
arch.deployment_model2
.
as_deref2
()));
1260
3
        if deployment_model.is_none() {
1261
1
            push_question(
1262
1
                &mut questions,
1263
1
                &mut question_ids,
1264
1
                "architecture.deployment_model".to_string(),
1265
1
                "architecture.deployment_model".to_string(),
1266
1
                "What is the deployment model? (e.g., serverless, containers, on-prem)".to_string(),
1267
1
                None,
1268
1
            );
1269
2
        }
1270
1271
3
        let constraints = normalize_constraints(
1272
3
            payload
1273
3
                .architecture
1274
3
                .as_ref()
1275
3
                .and_then(|arch| 
arch.constraints2
.
as_ref2
()),
1276
        );
1277
3
        if constraints.is_none() {
1278
1
            push_question(
1279
1
                &mut questions,
1280
1
                &mut question_ids,
1281
1
                "architecture.constraints".to_string(),
1282
1
                "architecture.constraints".to_string(),
1283
1
                "List any constraints (or reply 'none').".to_string(),
1284
1
                None,
1285
1
            );
1286
2
        }
1287
1288
3
        let mut container_names: Vec<String> = Vec::new();
1289
3
        let mut normalized_containers: Vec<crate::models::component::CreateContainerRequest> =
1290
3
            Vec::new();
1291
3
        for (
index2
,
container2
) in payload.containers.iter().enumerate() {
1292
2
            let name = normalize_optional_value(container.name.as_deref());
1293
2
            if name.is_none() {
1294
0
                push_question(
1295
0
                    &mut questions,
1296
0
                    &mut question_ids,
1297
0
                    format!("containers[{}].name", index),
1298
0
                    format!("containers[{}].name", index),
1299
0
                    "Provide the container name.".to_string(),
1300
0
                    None,
1301
0
                );
1302
2
            } else if let Some(ref value) = name {
1303
2
                container_names.push(value.clone());
1304
2
            
}0
1305
1306
2
            let path = normalize_optional_value(container.path.as_deref());
1307
2
            if path.is_none() {
1308
0
                push_question(
1309
0
                    &mut questions,
1310
0
                    &mut question_ids,
1311
0
                    format!("containers[{}].path", index),
1312
0
                    format!("containers[{}].path", index),
1313
0
                    "Provide the container path (glob).".to_string(),
1314
0
                    None,
1315
0
                );
1316
2
            }
1317
1318
2
            let container_kind_input =
1319
2
                normalize_optional_value(container.container_kind.as_deref());
1320
2
            let parsed_container_kind = container_kind_input
1321
2
                .as_ref()
1322
2
                .and_then(|value| crate::models::component::ContainerKind::from_str(value).ok());
1323
2
            if container_kind_input.is_none() {
1324
0
                push_question(
1325
0
                    &mut questions,
1326
0
                    &mut question_ids,
1327
0
                    format!("containers[{}].container_kind", index),
1328
0
                    format!("containers[{}].container_kind", index),
1329
0
                    "Provide the container kind (app, service, library, other).".to_string(),
1330
0
                    Some(vec![
1331
0
                        "app".to_string(),
1332
0
                        "service".to_string(),
1333
0
                        "library".to_string(),
1334
0
                        "other".to_string(),
1335
0
                    ]),
1336
0
                );
1337
2
            } else if parsed_container_kind.is_none() {
1338
0
                push_question(
1339
0
                    &mut questions,
1340
0
                    &mut question_ids,
1341
0
                    format!("containers[{}].container_kind", index),
1342
0
                    format!("containers[{}].container_kind", index),
1343
0
                    "Container kind must be one of: app, service, library, other.".to_string(),
1344
0
                    Some(vec![
1345
0
                        "app".to_string(),
1346
0
                        "service".to_string(),
1347
0
                        "library".to_string(),
1348
0
                        "other".to_string(),
1349
0
                    ]),
1350
0
                );
1351
2
            }
1352
1353
2
            let has_technology = has_non_empty_value(container.technology.as_deref());
1354
2
            if !has_technology {
1355
0
                push_question(
1356
0
                    &mut questions,
1357
0
                    &mut question_ids,
1358
0
                    format!("containers[{}].technology", index),
1359
0
                    format!("containers[{}].technology", index),
1360
0
                    "Provide the container technology/language (or 'none').".to_string(),
1361
0
                    None,
1362
0
                );
1363
2
            }
1364
1365
2
            let has_framework = has_non_empty_value(container.framework.as_deref());
1366
2
            if !has_framework {
1367
0
                push_question(
1368
0
                    &mut questions,
1369
0
                    &mut question_ids,
1370
0
                    format!("containers[{}].framework", index),
1371
0
                    format!("containers[{}].framework", index),
1372
0
                    "Provide the container framework (or 'none').".to_string(),
1373
0
                    None,
1374
0
                );
1375
2
            }
1376
1377
2
            if let (Some(name), Some(path), Some(container_kind)) =
1378
2
                (name.clone(), path.clone(), parsed_container_kind)
1379
2
                && has_technology
1380
2
                && has_framework
1381
2
            {
1382
2
                normalized_containers.push(crate::models::component::CreateContainerRequest {
1383
2
                    name,
1384
2
                    description: normalize_optional_value(container.description.as_deref()),
1385
2
                    path,
1386
2
                    metadata: crate::models::component::ContainerMetadata {
1387
2
                        container_kind,
1388
2
                        technology: normalize_optional_value(container.technology.as_deref()),
1389
2
                        framework: normalize_optional_value(container.framework.as_deref()),
1390
2
                        runtime: None,
1391
2
                        deployment: None,
1392
2
                    },
1393
2
                });
1394
2
            
}0
1395
        }
1396
1397
3
        if container_names.is_empty() {
1398
1
            push_question(
1399
1
                &mut questions,
1400
1
                &mut question_ids,
1401
1
                "containers".to_string(),
1402
1
                "containers".to_string(),
1403
1
                "Provide at least one container to define the architecture.".to_string(),
1404
1
                None,
1405
1
            );
1406
2
        }
1407
1408
3
        let mut component_names: Vec<String> = Vec::new();
1409
3
        for (
index0
,
component0
) in payload.components.iter().enumerate() {
1410
0
            let name = normalize_optional_value(component.name.as_deref());
1411
0
            if name.is_none() {
1412
0
                push_question(
1413
0
                    &mut questions,
1414
0
                    &mut question_ids,
1415
0
                    format!("components[{}].name", index),
1416
0
                    format!("components[{}].name", index),
1417
0
                    "Provide the component name.".to_string(),
1418
0
                    None,
1419
0
                );
1420
0
            } else if let Some(ref value) = name {
1421
0
                component_names.push(value.clone());
1422
0
            }
1423
1424
0
            let path = normalize_optional_value(component.path.as_deref());
1425
0
            if path.is_none() {
1426
0
                push_question(
1427
0
                    &mut questions,
1428
0
                    &mut question_ids,
1429
0
                    format!("components[{}].path", index),
1430
0
                    format!("components[{}].path", index),
1431
0
                    "Provide the component path (glob).".to_string(),
1432
0
                    None,
1433
0
                );
1434
0
            }
1435
1436
0
            let parent_name = normalize_optional_value(component.parent_name.as_deref());
1437
0
            if parent_name.is_none() {
1438
0
                push_question(
1439
0
                    &mut questions,
1440
0
                    &mut question_ids,
1441
0
                    format!("components[{}].parent_name", index),
1442
0
                    format!("components[{}].parent_name", index),
1443
0
                    "Provide the parent component name for this component.".to_string(),
1444
0
                    None,
1445
0
                );
1446
0
            }
1447
        }
1448
1449
3
        let mut store_names: Vec<String> = Vec::new();
1450
3
        let mut normalized_stores: Vec<CreateStoreRequest> = Vec::new();
1451
3
        for (
index0
,
store0
) in payload.stores.iter().enumerate() {
1452
0
            let name = normalize_optional_value(store.name.as_deref());
1453
0
            if name.is_none() {
1454
0
                push_question(
1455
0
                    &mut questions,
1456
0
                    &mut question_ids,
1457
0
                    format!("stores[{}].name", index),
1458
0
                    format!("stores[{}].name", index),
1459
0
                    "Provide the store name.".to_string(),
1460
0
                    None,
1461
0
                );
1462
0
            } else if let Some(ref value) = name {
1463
0
                store_names.push(value.clone());
1464
0
            }
1465
1466
0
            let store_type = normalize_optional_value(store.store_type.as_deref());
1467
0
            if store_type.is_none() {
1468
0
                push_question(
1469
0
                    &mut questions,
1470
0
                    &mut question_ids,
1471
0
                    format!("stores[{}].type", index),
1472
0
                    format!("stores[{}].type", index),
1473
0
                    "Provide the store type (e.g., postgres, redis, s3).".to_string(),
1474
0
                    None,
1475
0
                );
1476
0
            }
1477
1478
0
            let ownership_value = normalize_optional_value(store.ownership.as_deref());
1479
0
            let ownership = ownership_value
1480
0
                .as_ref()
1481
0
                .and_then(|value| StoreOwnership::from_str(value).map_err(|_| ()).ok());
1482
0
            if ownership_value.is_none() {
1483
0
                push_question(
1484
0
                    &mut questions,
1485
0
                    &mut question_ids,
1486
0
                    format!("stores[{}].ownership", index),
1487
0
                    format!("stores[{}].ownership", index),
1488
0
                    "Provide the store ownership (internal, shared, managed).".to_string(),
1489
0
                    Some(vec![
1490
0
                        "internal".to_string(),
1491
0
                        "shared".to_string(),
1492
0
                        "managed".to_string(),
1493
0
                    ]),
1494
0
                );
1495
0
            } else if ownership.is_none() {
1496
0
                push_question(
1497
0
                    &mut questions,
1498
0
                    &mut question_ids,
1499
0
                    format!("stores[{}].ownership", index),
1500
0
                    format!("stores[{}].ownership", index),
1501
0
                    "Store ownership must be one of: internal, shared, managed.".to_string(),
1502
0
                    Some(vec![
1503
0
                        "internal".to_string(),
1504
0
                        "shared".to_string(),
1505
0
                        "managed".to_string(),
1506
0
                    ]),
1507
0
                );
1508
0
            }
1509
1510
0
            if let (Some(name), Some(store_type), Some(ownership)) =
1511
0
                (name.clone(), store_type.clone(), ownership)
1512
0
            {
1513
0
                normalized_stores.push(CreateStoreRequest {
1514
0
                    name,
1515
0
                    description: normalize_optional_value(store.description.as_deref()),
1516
0
                    store_type,
1517
0
                    ownership,
1518
0
                    tags: store.tags.clone(),
1519
0
                    position_x: None,
1520
0
                    position_y: None,
1521
0
                });
1522
0
            }
1523
        }
1524
1525
3
        let mut external_system_names: Vec<String> = Vec::new();
1526
3
        let mut normalized_external_systems: Vec<CreateExternalSystemRequest> = Vec::new();
1527
3
        for (
index0
,
system0
) in payload.external_systems.iter().enumerate() {
1528
0
            let name = normalize_optional_value(system.name.as_deref());
1529
0
            if name.is_none() {
1530
0
                push_question(
1531
0
                    &mut questions,
1532
0
                    &mut question_ids,
1533
0
                    format!("external_systems[{}].name", index),
1534
0
                    format!("external_systems[{}].name", index),
1535
0
                    "Provide the external system name.".to_string(),
1536
0
                    None,
1537
0
                );
1538
0
            } else if let Some(ref value) = name {
1539
0
                external_system_names.push(value.clone());
1540
0
            }
1541
1542
0
            let category = normalize_optional_value(system.category.as_deref());
1543
0
            if category.is_none() {
1544
0
                push_question(
1545
0
                    &mut questions,
1546
0
                    &mut question_ids,
1547
0
                    format!("external_systems[{}].category", index),
1548
0
                    format!("external_systems[{}].category", index),
1549
0
                    "Provide the external system category (e.g., payment, auth, crm).".to_string(),
1550
0
                    None,
1551
0
                );
1552
0
            }
1553
1554
0
            if let (Some(name), Some(category)) = (name.clone(), category.clone()) {
1555
0
                normalized_external_systems.push(CreateExternalSystemRequest {
1556
0
                    name,
1557
0
                    description: normalize_optional_value(system.description.as_deref()),
1558
0
                    category,
1559
0
                    vendor: normalize_optional_value(system.vendor.as_deref()),
1560
0
                    tags: system.tags.clone(),
1561
0
                    position_x: None,
1562
0
                    position_y: None,
1563
0
                });
1564
0
            }
1565
        }
1566
1567
3
        push_duplicate_name_questions(
1568
3
            &container_names,
1569
3
            "container",
1570
3
            "containers",
1571
3
            &mut questions,
1572
3
            &mut question_ids,
1573
        );
1574
3
        push_duplicate_name_questions(
1575
3
            &component_names,
1576
3
            "component",
1577
3
            "components",
1578
3
            &mut questions,
1579
3
            &mut question_ids,
1580
        );
1581
3
        push_duplicate_name_questions(
1582
3
            &store_names,
1583
3
            "store",
1584
3
            "stores",
1585
3
            &mut questions,
1586
3
            &mut question_ids,
1587
        );
1588
3
        push_duplicate_name_questions(
1589
3
            &external_system_names,
1590
3
            "external system",
1591
3
            "external_systems",
1592
3
            &mut questions,
1593
3
            &mut question_ids,
1594
        );
1595
1596
3
        let mut entity_name_kinds: HashMap<String, HashSet<ConnectionTargetKind>> = HashMap::new();
1597
3
        for 
container2
in &normalized_containers {
1598
2
            entity_name_kinds
1599
2
                .entry(normalize_name_key(&container.name))
1600
2
                .or_default()
1601
2
                .insert(ConnectionTargetKind::Container);
1602
2
        }
1603
1604
3
        let mut normalized_components: Vec<NormalizedComponent> = Vec::new();
1605
3
        for (
index0
,
component0
) in payload.components.iter().enumerate() {
1606
0
            let name = normalize_optional_value(component.name.as_deref());
1607
0
            let path = normalize_optional_value(component.path.as_deref());
1608
0
            let parent_name = normalize_optional_value(component.parent_name.as_deref());
1609
1610
0
            if name.is_none() || path.is_none() || parent_name.is_none() {
1611
0
                continue;
1612
0
            }
1613
1614
0
            let parent_name_value = parent_name.clone().unwrap_or_default();
1615
0
            let parent_key = normalize_name_key(&parent_name_value);
1616
1617
0
            let parent_kind_input = normalize_optional_value(component.parent_kind.as_deref());
1618
0
            let parsed_parent_kind = parent_kind_input
1619
0
                .as_ref()
1620
0
                .and_then(|value| parse_architecture_entity_kind(value).ok());
1621
1622
0
            if parent_kind_input.is_some() && parsed_parent_kind.is_none() {
1623
0
                push_question(
1624
0
                    &mut questions,
1625
0
                    &mut question_ids,
1626
0
                    format!("components[{}].parent_kind", index),
1627
0
                    format!("components[{}].parent_kind", index),
1628
0
                    "Parent kind must be one of: container, component.".to_string(),
1629
0
                    Some(vec!["container".to_string(), "component".to_string()]),
1630
                );
1631
0
                continue;
1632
0
            }
1633
1634
0
            let available_kinds = entity_name_kinds.get(&parent_key);
1635
0
            let resolved_parent_kind = if let Some(kind) = parsed_parent_kind {
1636
0
                if available_kinds
1637
0
                    .map(|kinds| kinds.contains(&kind))
1638
0
                    .unwrap_or(false)
1639
                {
1640
0
                    Some(kind)
1641
                } else {
1642
0
                    push_question(
1643
0
                        &mut questions,
1644
0
                        &mut question_ids,
1645
0
                        format!("components[{}].parent_name", index),
1646
0
                        format!("components[{}].parent_name", index),
1647
0
                        format!(
1648
                            "Parent '{}' was not found as a {}.",
1649
                            parent_name_value, kind
1650
                        ),
1651
0
                        None,
1652
                    );
1653
0
                    None
1654
                }
1655
            } else {
1656
0
                match available_kinds {
1657
0
                    Some(kinds) if kinds.len() == 1 => kinds.iter().copied().next(),
1658
0
                    Some(kinds) if kinds.len() > 1 => {
1659
0
                        push_question(
1660
0
                            &mut questions,
1661
0
                            &mut question_ids,
1662
0
                            format!("components[{}].parent_kind", index),
1663
0
                            format!("components[{}].parent_kind", index),
1664
0
                            format!(
1665
                                "Parent '{}' is ambiguous. Specify parent_kind.",
1666
                                parent_name_value
1667
                            ),
1668
0
                            Some(vec!["container".to_string(), "component".to_string()]),
1669
                        );
1670
0
                        None
1671
                    }
1672
                    _ => {
1673
0
                        push_question(
1674
0
                            &mut questions,
1675
0
                            &mut question_ids,
1676
0
                            format!("components[{}].parent_name", index),
1677
0
                            format!("components[{}].parent_name", index),
1678
0
                            format!("Parent '{}' was not found.", parent_name_value),
1679
0
                            None,
1680
                        );
1681
0
                        None
1682
                    }
1683
                }
1684
            };
1685
1686
0
            if let (Some(name), Some(path), Some(parent_kind)) =
1687
0
                (name.clone(), path.clone(), resolved_parent_kind)
1688
            {
1689
0
                if parent_kind == ConnectionTargetKind::Component
1690
0
                    && normalize_name_key(&name) == parent_key
1691
                {
1692
0
                    push_question(
1693
0
                        &mut questions,
1694
0
                        &mut question_ids,
1695
0
                        format!("components[{}].parent_name", index),
1696
0
                        format!("components[{}].parent_name", index),
1697
0
                        "Component parent cannot reference itself. Provide a valid parent."
1698
0
                            .to_string(),
1699
0
                        None,
1700
                    );
1701
0
                    continue;
1702
0
                }
1703
1704
0
                let component_key = normalize_name_key(&name);
1705
0
                normalized_components.push(NormalizedComponent {
1706
0
                    name,
1707
0
                    description: normalize_optional_value(component.description.as_deref()),
1708
0
                    path,
1709
0
                    parent_name: parent_name_value,
1710
0
                    parent_kind,
1711
0
                });
1712
1713
0
                entity_name_kinds
1714
0
                    .entry(component_key)
1715
0
                    .or_default()
1716
0
                    .insert(ConnectionTargetKind::Component);
1717
0
            }
1718
        }
1719
1720
3
        let mut target_name_kinds: HashMap<String, HashSet<ConnectionTargetKind>> = HashMap::new();
1721
3
        for (
name2
,
kinds2
) in &entity_name_kinds {
1722
2
            target_name_kinds.insert(name.clone(), kinds.clone());
1723
2
        }
1724
3
        for 
name0
in &store_names {
1725
0
            target_name_kinds
1726
0
                .entry(normalize_name_key(name))
1727
0
                .or_default()
1728
0
                .insert(ConnectionTargetKind::Store);
1729
0
        }
1730
3
        for 
name0
in &external_system_names {
1731
0
            target_name_kinds
1732
0
                .entry(normalize_name_key(name))
1733
0
                .or_default()
1734
0
                .insert(ConnectionTargetKind::ExternalSystem);
1735
0
        }
1736
1737
3
        let mut normalized_connections: Vec<NormalizedConnection> = Vec::new();
1738
3
        for (
index0
,
connection0
) in payload.connections.iter().enumerate() {
1739
0
            let source_name = normalize_optional_value(connection.source_name.as_deref());
1740
0
            if source_name.is_none() {
1741
0
                push_question(
1742
0
                    &mut questions,
1743
0
                    &mut question_ids,
1744
0
                    format!("connections[{}].source_name", index),
1745
0
                    format!("connections[{}].source_name", index),
1746
0
                    "Provide the connection source name.".to_string(),
1747
0
                    None,
1748
0
                );
1749
0
            }
1750
1751
0
            let target_name = normalize_optional_value(connection.target_name.as_deref());
1752
0
            if target_name.is_none() {
1753
0
                push_question(
1754
0
                    &mut questions,
1755
0
                    &mut question_ids,
1756
0
                    format!("connections[{}].target_name", index),
1757
0
                    format!("connections[{}].target_name", index),
1758
0
                    "Provide the connection target name.".to_string(),
1759
0
                    None,
1760
0
                );
1761
0
            }
1762
1763
0
            let label = normalize_optional_value(connection.label.as_deref());
1764
0
            if label.is_none() {
1765
0
                push_question(
1766
0
                    &mut questions,
1767
0
                    &mut question_ids,
1768
0
                    format!("connections[{}].label", index),
1769
0
                    format!("connections[{}].label", index),
1770
0
                    "Provide the connection label (e.g., uses, REST API).".to_string(),
1771
0
                    None,
1772
0
                );
1773
0
            }
1774
1775
0
            let Some(source_name) = source_name.as_ref() else {
1776
0
                continue;
1777
            };
1778
0
            let Some(target_name) = target_name.as_ref() else {
1779
0
                continue;
1780
            };
1781
0
            let Some(label) = label.as_ref() else {
1782
0
                continue;
1783
            };
1784
1785
0
            let source_key = normalize_name_key(source_name);
1786
0
            let target_key = normalize_name_key(target_name);
1787
1788
0
            let source_kind_input = normalize_optional_value(connection.source_kind.as_deref());
1789
0
            let parsed_source_kind = source_kind_input
1790
0
                .as_ref()
1791
0
                .and_then(|value| parse_architecture_entity_kind(value).ok());
1792
0
            if source_kind_input.is_some() && parsed_source_kind.is_none() {
1793
0
                push_question(
1794
0
                    &mut questions,
1795
0
                    &mut question_ids,
1796
0
                    format!("connections[{}].source_kind", index),
1797
0
                    format!("connections[{}].source_kind", index),
1798
0
                    "Source kind must be one of: container, component.".to_string(),
1799
0
                    Some(vec!["container".to_string(), "component".to_string()]),
1800
                );
1801
0
                continue;
1802
0
            }
1803
1804
0
            let resolved_source_kind = if let Some(kind) = parsed_source_kind {
1805
0
                if entity_name_kinds
1806
0
                    .get(&source_key)
1807
0
                    .map(|kinds| kinds.contains(&kind))
1808
0
                    .unwrap_or(false)
1809
                {
1810
0
                    Some(kind)
1811
                } else {
1812
0
                    push_question(
1813
0
                        &mut questions,
1814
0
                        &mut question_ids,
1815
0
                        format!("connections[{}].source_name", index),
1816
0
                        format!("connections[{}].source_name", index),
1817
0
                        format!("Source '{}' was not found as a {}.", source_name, kind),
1818
0
                        None,
1819
                    );
1820
0
                    None
1821
                }
1822
            } else {
1823
0
                match entity_name_kinds.get(&source_key) {
1824
0
                    Some(kinds) if kinds.len() == 1 => kinds.iter().copied().next(),
1825
0
                    Some(kinds) if kinds.len() > 1 => {
1826
0
                        push_question(
1827
0
                            &mut questions,
1828
0
                            &mut question_ids,
1829
0
                            format!("connections[{}].source_kind", index),
1830
0
                            format!("connections[{}].source_kind", index),
1831
0
                            format!(
1832
                                "Source '{}' is ambiguous. Specify source_kind.",
1833
                                source_name
1834
                            ),
1835
0
                            Some(vec!["container".to_string(), "component".to_string()]),
1836
                        );
1837
0
                        None
1838
                    }
1839
                    _ => {
1840
0
                        push_question(
1841
0
                            &mut questions,
1842
0
                            &mut question_ids,
1843
0
                            format!("connections[{}].source_name", index),
1844
0
                            format!("connections[{}].source_name", index),
1845
0
                            format!("Source '{}' was not found.", source_name),
1846
0
                            None,
1847
                        );
1848
0
                        None
1849
                    }
1850
                }
1851
            };
1852
1853
0
            let target_kind_input = normalize_optional_value(connection.target_kind.as_deref());
1854
0
            let parsed_target_kind = target_kind_input
1855
0
                .as_ref()
1856
0
                .and_then(|value| parse_architecture_connection_target_kind(value).ok());
1857
0
            if target_kind_input.is_some() && parsed_target_kind.is_none() {
1858
0
                push_question(
1859
0
                    &mut questions,
1860
0
                    &mut question_ids,
1861
0
                    format!("connections[{}].target_kind", index),
1862
0
                    format!("connections[{}].target_kind", index),
1863
0
                    "Target kind must be one of: container, component, store, external_system."
1864
0
                        .to_string(),
1865
0
                    Some(vec![
1866
0
                        "container".to_string(),
1867
0
                        "component".to_string(),
1868
0
                        "store".to_string(),
1869
0
                        "external_system".to_string(),
1870
0
                    ]),
1871
                );
1872
0
                continue;
1873
0
            }
1874
1875
0
            let resolved_target_kind = if let Some(kind) = parsed_target_kind {
1876
0
                if target_name_kinds
1877
0
                    .get(&target_key)
1878
0
                    .map(|kinds| kinds.contains(&kind))
1879
0
                    .unwrap_or(false)
1880
                {
1881
0
                    Some(kind)
1882
                } else {
1883
0
                    push_question(
1884
0
                        &mut questions,
1885
0
                        &mut question_ids,
1886
0
                        format!("connections[{}].target_name", index),
1887
0
                        format!("connections[{}].target_name", index),
1888
0
                        format!("Target '{}' was not found as a {}.", target_name, kind),
1889
0
                        None,
1890
                    );
1891
0
                    None
1892
                }
1893
            } else {
1894
0
                match target_name_kinds.get(&target_key) {
1895
0
                    Some(kinds) if kinds.len() == 1 => kinds.iter().copied().next(),
1896
0
                    Some(kinds) if kinds.len() > 1 => {
1897
0
                        push_question(
1898
0
                            &mut questions,
1899
0
                            &mut question_ids,
1900
0
                            format!("connections[{}].target_kind", index),
1901
0
                            format!("connections[{}].target_kind", index),
1902
0
                            format!(
1903
                                "Target '{}' is ambiguous. Specify target_kind.",
1904
                                target_name
1905
                            ),
1906
0
                            Some(vec![
1907
0
                                "container".to_string(),
1908
0
                                "component".to_string(),
1909
0
                                "store".to_string(),
1910
0
                                "external_system".to_string(),
1911
0
                            ]),
1912
                        );
1913
0
                        None
1914
                    }
1915
                    _ => {
1916
0
                        push_question(
1917
0
                            &mut questions,
1918
0
                            &mut question_ids,
1919
0
                            format!("connections[{}].target_name", index),
1920
0
                            format!("connections[{}].target_name", index),
1921
0
                            format!("Target '{}' was not found.", target_name),
1922
0
                            None,
1923
                        );
1924
0
                        None
1925
                    }
1926
                }
1927
            };
1928
1929
0
            let source_side_input = normalize_optional_value(connection.source_side.as_deref());
1930
0
            let parsed_source_side = source_side_input
1931
0
                .as_ref()
1932
0
                .and_then(|value| parse_connection_anchor_side(value).ok());
1933
0
            if source_side_input.is_some() && parsed_source_side.is_none() {
1934
0
                push_question(
1935
0
                    &mut questions,
1936
0
                    &mut question_ids,
1937
0
                    format!("connections[{}].source_side", index),
1938
0
                    format!("connections[{}].source_side", index),
1939
0
                    "Source side must be one of: top, right, bottom, left.".to_string(),
1940
0
                    Some(vec![
1941
0
                        "top".to_string(),
1942
0
                        "right".to_string(),
1943
0
                        "bottom".to_string(),
1944
0
                        "left".to_string(),
1945
0
                    ]),
1946
                );
1947
0
                continue;
1948
0
            }
1949
1950
0
            let target_side_input = normalize_optional_value(connection.target_side.as_deref());
1951
0
            let parsed_target_side = target_side_input
1952
0
                .as_ref()
1953
0
                .and_then(|value| parse_connection_anchor_side(value).ok());
1954
0
            if target_side_input.is_some() && parsed_target_side.is_none() {
1955
0
                push_question(
1956
0
                    &mut questions,
1957
0
                    &mut question_ids,
1958
0
                    format!("connections[{}].target_side", index),
1959
0
                    format!("connections[{}].target_side", index),
1960
0
                    "Target side must be one of: top, right, bottom, left.".to_string(),
1961
0
                    Some(vec![
1962
0
                        "top".to_string(),
1963
0
                        "right".to_string(),
1964
0
                        "bottom".to_string(),
1965
0
                        "left".to_string(),
1966
0
                    ]),
1967
                );
1968
0
                continue;
1969
0
            }
1970
1971
0
            let routing_mode_input = normalize_optional_value(connection.routing_mode.as_deref());
1972
0
            let parsed_routing_mode = routing_mode_input
1973
0
                .as_ref()
1974
0
                .and_then(|value| parse_connection_routing_mode(value).ok());
1975
0
            if routing_mode_input.is_some() && parsed_routing_mode.is_none() {
1976
0
                push_question(
1977
0
                    &mut questions,
1978
0
                    &mut question_ids,
1979
0
                    format!("connections[{}].routing_mode", index),
1980
0
                    format!("connections[{}].routing_mode", index),
1981
0
                    "Routing mode must be one of: auto, manual.".to_string(),
1982
0
                    Some(vec!["auto".to_string(), "manual".to_string()]),
1983
                );
1984
0
                continue;
1985
0
            }
1986
1987
0
            if let (Some(source_kind), Some(target_kind)) =
1988
0
                (resolved_source_kind, resolved_target_kind)
1989
            {
1990
0
                normalized_connections.push(NormalizedConnection {
1991
0
                    source_name: source_name.to_string(),
1992
0
                    source_kind: match source_kind {
1993
0
                        ConnectionTargetKind::Container => ConnectionSourceKind::Container,
1994
0
                        ConnectionTargetKind::Component => ConnectionSourceKind::Component,
1995
                        ConnectionTargetKind::Store
1996
                        | ConnectionTargetKind::Person
1997
0
                        | ConnectionTargetKind::ExternalSystem => ConnectionSourceKind::Container,
1998
                    },
1999
0
                    source_side: parsed_source_side,
2000
0
                    target_name: target_name.to_string(),
2001
0
                    target_kind,
2002
0
                    target_side: parsed_target_side,
2003
0
                    routing_mode: parsed_routing_mode,
2004
0
                    control_points: connection.control_points.clone(),
2005
0
                    label: label.to_string(),
2006
0
                    description: normalize_optional_value(connection.description.as_deref()),
2007
                });
2008
0
            }
2009
        }
2010
2011
3
        let mut connection_keys: HashSet<String> = HashSet::new();
2012
3
        for 
connection0
in &normalized_connections {
2013
0
            let key = format!(
2014
                "{}|{}|{}|{}",
2015
                connection.source_kind,
2016
0
                normalize_name_key(&connection.source_name),
2017
                connection.target_kind,
2018
0
                normalize_name_key(&connection.target_name)
2019
            );
2020
0
            if !connection_keys.insert(key) {
2021
0
                push_question(
2022
0
                    &mut questions,
2023
0
                    &mut question_ids,
2024
0
                    "connections.duplicate".to_string(),
2025
0
                    "connections".to_string(),
2026
0
                    "Duplicate connections detected. Ensure each source/target pair is unique."
2027
0
                        .to_string(),
2028
0
                    None,
2029
                );
2030
0
                break;
2031
0
            }
2032
        }
2033
2034
3
        if !questions.is_empty() {
2035
1
            return respond_json(serde_json::json!({
2036
1
                "status": "needs_input",
2037
1
                "questions": questions
2038
1
            }));
2039
2
        }
2040
2041
2
        let project_type = resolved_project_type
2042
2
            .take()
2043
2
            .unwrap_or_else(|| 
infer_project_type0
(
&normalized_containers0
));
2044
2045
2
        let normalized_project = NormalizedProject {
2046
2
            name: project_name.unwrap_or_else(|| 
"New Project"0
.
to_string0
()),
2047
2
            description: project_description,
2048
2
            project_type,
2049
2
            repository_url,
2050
        };
2051
2052
2
        let normalized_architecture = NormalizedArchitecture {
2053
2
            style: architecture_style.unwrap_or_else(|| 
"unspecified"0
.
to_string0
()),
2054
2
            deployment_model: deployment_model.unwrap_or_else(|| 
"unspecified"0
.
to_string0
()),
2055
2
            constraints: constraints.unwrap_or_default(),
2056
        };
2057
2058
2
        let summary = serde_json::json!({
2059
2
            "project": normalized_project,
2060
2
            "architecture": normalized_architecture,
2061
2
            "containers": normalized_containers,
2062
2
            "components": normalized_components,
2063
2
            "stores": normalized_stores,
2064
2
            "external_systems": normalized_external_systems,
2065
2
            "connections": normalized_connections
2066
        });
2067
2068
2
        if !payload.confirm {
2069
1
            return respond_json(serde_json::json!({
2070
1
                "status": "needs_confirmation",
2071
1
                "summary": summary,
2072
1
                "next": "Set confirm=true to create the project and architecture."
2073
1
            }));
2074
1
        }
2075
2076
1
        let org_id = match self.ensure_org().await {
2077
1
            Ok(value) => value,
2078
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
2079
        };
2080
2081
1
        let existing_projects = match self.service.project_repo.list_projects(&org_id).await {
2082
1
            Ok(list) => list,
2083
0
            Err(e) => {
2084
0
                error!("Failed to fetch projects: {}", e);
2085
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
2086
            }
2087
        };
2088
2089
1
        if existing_projects
2090
1
            .iter()
2091
1
            .any(|project| project.name.eq_ignore_ascii_case(&normalized_project.name))
2092
        {
2093
0
            return JsonRpcResponse::error(
2094
0
                id,
2095
                INVALID_PARAMS,
2096
0
                "Project name already exists in this organisation",
2097
            );
2098
1
        }
2099
2100
1
        let create_project_request = CreateProjectRequest {
2101
1
            name: normalized_project.name.clone(),
2102
1
            description: normalized_project.description.clone(),
2103
1
            project_type: normalized_project.project_type,
2104
1
            repository_url: normalized_project.repository_url.clone(),
2105
1
        };
2106
2107
1
        let project = match self
2108
1
            .service
2109
1
            .project_service
2110
1
            .create_project(&org_id, &create_project_request)
2111
1
            .await
2112
        {
2113
1
            Ok(value) => value,
2114
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
2115
        };
2116
2117
1
        let mut container_id_by_name: HashMap<String, ComponentId> = HashMap::new();
2118
1
        let mut component_id_by_name: HashMap<String, ComponentId> = HashMap::new();
2119
2120
1
        let mut created_containers: Vec<ComponentView> = Vec::new();
2121
1
        for container in &normalized_containers {
2122
1
            let created = match self
2123
1
                .service
2124
1
                .component_service
2125
1
                .create_container(&org_id, &project.id, &self.user_id, container)
2126
1
                .await
2127
            {
2128
1
                Ok(value) => value,
2129
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
2130
            };
2131
1
            container_id_by_name.insert(normalize_name_key(&created.name), created.id.clone());
2132
1
            created_containers.push((&created).into());
2133
        }
2134
2135
1
        let mut created_components: Vec<ComponentView> = Vec::new();
2136
1
        let mut pending_components = normalized_components.clone();
2137
1
        let mut progressed = true;
2138
1
        let mut iterations = 0;
2139
1
        while progressed && !pending_components.is_empty() {
2140
0
            progressed = false;
2141
0
            iterations += 1;
2142
0
            let mut remaining: Vec<NormalizedComponent> = Vec::new();
2143
2144
0
            for component in pending_components {
2145
0
                let parent_id = match component.parent_kind {
2146
                    ConnectionTargetKind::Container => {
2147
0
                        container_id_by_name.get(&normalize_name_key(&component.parent_name))
2148
                    }
2149
                    ConnectionTargetKind::Component => {
2150
0
                        component_id_by_name.get(&normalize_name_key(&component.parent_name))
2151
                    }
2152
                    ConnectionTargetKind::Person
2153
                    | ConnectionTargetKind::Store
2154
0
                    | ConnectionTargetKind::ExternalSystem => None,
2155
                };
2156
2157
0
                if let Some(parent_id) = parent_id {
2158
0
                    let request = crate::models::component::CreateArchitectureComponentRequest {
2159
0
                        name: component.name.clone(),
2160
0
                        description: component.description.clone(),
2161
0
                        path: component.path.clone(),
2162
0
                        metadata: crate::models::component::ArchitectureComponentMetadata {
2163
0
                            component_kind:
2164
0
                                crate::models::component::ArchitectureComponentKind::Module,
2165
0
                        },
2166
0
                    };
2167
0
                    let created = match self
2168
0
                        .service
2169
0
                        .component_service
2170
0
                        .create_component(&org_id, &project.id, &self.user_id, parent_id, &request)
2171
0
                        .await
2172
                    {
2173
0
                        Ok(value) => value,
2174
0
                        Err(e) => return JsonRpcResponse::from_api_error(id, e),
2175
                    };
2176
0
                    component_id_by_name
2177
0
                        .insert(normalize_name_key(&created.name), created.id.clone());
2178
0
                    created_components.push((&created).into());
2179
0
                    progressed = true;
2180
0
                } else {
2181
0
                    remaining.push(component);
2182
0
                }
2183
            }
2184
2185
0
            pending_components = remaining;
2186
2187
0
            if iterations > normalized_components.len() + 1 {
2188
0
                break;
2189
0
            }
2190
        }
2191
2192
1
        if !pending_components.is_empty() {
2193
0
            return JsonRpcResponse::error(
2194
0
                id,
2195
                INTERNAL_ERROR,
2196
0
                "Failed to resolve component parents. Check component parent references.",
2197
            );
2198
1
        }
2199
2200
1
        let mut created_stores: Vec<StoreView> = Vec::new();
2201
1
        let mut store_id_by_name: HashMap<String, StoreId> = HashMap::new();
2202
1
        for 
store0
in &normalized_stores {
2203
0
            let created = match self
2204
0
                .service
2205
0
                .store_service
2206
0
                .create_store(&org_id, &project.id, &self.user_id, store)
2207
0
                .await
2208
            {
2209
0
                Ok(value) => value,
2210
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
2211
            };
2212
0
            store_id_by_name.insert(normalize_name_key(&created.name), created.id.clone());
2213
0
            created_stores.push((&created).into());
2214
        }
2215
2216
1
        let mut created_external_systems: Vec<ExternalSystemView> = Vec::new();
2217
1
        let mut external_id_by_name: HashMap<String, ExternalSystemId> = HashMap::new();
2218
1
        for 
system0
in &normalized_external_systems {
2219
0
            let created = match self
2220
0
                .service
2221
0
                .external_system_service
2222
0
                .create_external_system(&org_id, &project.id, &self.user_id, system)
2223
0
                .await
2224
            {
2225
0
                Ok(value) => value,
2226
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
2227
            };
2228
0
            external_id_by_name.insert(normalize_name_key(&created.name), created.id.clone());
2229
0
            created_external_systems.push((&created).into());
2230
        }
2231
2232
1
        let mut created_connections: Vec<ConnectionView> = Vec::new();
2233
1
        for 
normalized_connection0
in &normalized_connections {
2234
0
            let source_id = match normalized_connection.source_kind {
2235
                ConnectionSourceKind::Container => {
2236
0
                    let key = normalize_name_key(&normalized_connection.source_name);
2237
0
                    container_id_by_name.get(&key)
2238
                }
2239
0
                ConnectionSourceKind::Component => component_id_by_name
2240
0
                    .get(&normalize_name_key(&normalized_connection.source_name)),
2241
                ConnectionSourceKind::Person
2242
                | ConnectionSourceKind::Store
2243
0
                | ConnectionSourceKind::ExternalSystem => None,
2244
            };
2245
2246
0
            let source_id = match source_id {
2247
0
                Some(value) => value.clone(),
2248
                None => {
2249
0
                    return JsonRpcResponse::error(
2250
0
                        id,
2251
                        INTERNAL_ERROR,
2252
0
                        "Failed to resolve connection source ID",
2253
                    );
2254
                }
2255
            };
2256
2257
0
            let (target_id, target_kind) = match normalized_connection.target_kind {
2258
                ConnectionTargetKind::Container => (
2259
                    {
2260
0
                        let key = normalize_name_key(&normalized_connection.target_name);
2261
0
                        container_id_by_name.get(&key)
2262
                    }
2263
0
                    .map(|id| id.to_string()),
2264
0
                    ConnectionTargetKind::Container,
2265
                ),
2266
                ConnectionTargetKind::Component => (
2267
0
                    component_id_by_name
2268
0
                        .get(&normalize_name_key(&normalized_connection.target_name))
2269
0
                        .map(|id| id.to_string()),
2270
0
                    ConnectionTargetKind::Component,
2271
                ),
2272
0
                ConnectionTargetKind::Person => (None, ConnectionTargetKind::Person),
2273
                ConnectionTargetKind::Store => (
2274
0
                    store_id_by_name
2275
0
                        .get(&normalize_name_key(&normalized_connection.target_name))
2276
0
                        .map(|id| id.to_string()),
2277
0
                    ConnectionTargetKind::Store,
2278
                ),
2279
                ConnectionTargetKind::ExternalSystem => (
2280
0
                    external_id_by_name
2281
0
                        .get(&normalize_name_key(&normalized_connection.target_name))
2282
0
                        .map(|id| id.to_string()),
2283
0
                    ConnectionTargetKind::ExternalSystem,
2284
                ),
2285
            };
2286
2287
0
            let target_id = match target_id {
2288
0
                Some(value) => value,
2289
                None => {
2290
0
                    return JsonRpcResponse::error(
2291
0
                        id,
2292
                        INTERNAL_ERROR,
2293
0
                        "Failed to resolve connection target ID",
2294
                    );
2295
                }
2296
            };
2297
2298
0
            let mut connection = Connection::new(
2299
0
                project.id.clone(),
2300
0
                org_id.clone(),
2301
0
                source_id.to_string(),
2302
0
                normalized_connection.source_kind,
2303
0
                target_id,
2304
0
                target_kind,
2305
0
                normalized_connection.label.clone(),
2306
0
                normalized_connection.description.clone(),
2307
            );
2308
0
            connection.source_side = normalized_connection.source_side;
2309
0
            connection.target_side = normalized_connection.target_side;
2310
0
            connection.routing_mode = normalized_connection.routing_mode;
2311
0
            connection.control_points = normalized_connection.control_points.clone();
2312
0
            connection.normalize_routing_fields();
2313
0
            if let Err(e) = connection.validate() {
2314
0
                return JsonRpcResponse::from_api_error(id, e);
2315
0
            }
2316
2317
0
            if let Err(e) = self.service.connection_repo.create(&connection).await {
2318
0
                return JsonRpcResponse::error(
2319
0
                    id,
2320
                    INTERNAL_ERROR,
2321
0
                    &format!("Failed to create connection: {}", e),
2322
                );
2323
0
            }
2324
2325
0
            created_connections.push((&connection).into());
2326
        }
2327
2328
1
        respond_json(serde_json::json!({
2329
1
            "status": "created",
2330
1
            "project": ProjectView::from(&project),
2331
1
            "containers": created_containers,
2332
1
            "components": created_components,
2333
1
            "stores": created_stores,
2334
1
            "external_systems": created_external_systems,
2335
1
            "connections": created_connections
2336
1
        }))
2337
3
    }
2338
2339
0
    pub(super) async fn call_create_container(
2340
0
        &mut self,
2341
0
        id: Option<serde_json::Value>,
2342
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
2343
0
    ) -> JsonRpcResponse {
2344
0
        let args = match arguments {
2345
0
            Some(a) => a,
2346
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
2347
        };
2348
2349
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
2350
0
            Some(p) => p,
2351
            None => {
2352
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
2353
            }
2354
        };
2355
2356
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
2357
0
            Some(n) => n,
2358
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
2359
        };
2360
2361
0
        let path = match args.get("path").and_then(|v| v.as_str()) {
2362
0
            Some(p) => p,
2363
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'path' argument"),
2364
        };
2365
0
        let container_kind = match args.get("container_kind").and_then(|v| v.as_str()) {
2366
0
            Some(value) => match crate::models::component::ContainerKind::from_str(value) {
2367
0
                Ok(kind) => kind,
2368
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
2369
            },
2370
            None => {
2371
0
                return JsonRpcResponse::error(
2372
0
                    id,
2373
                    INVALID_PARAMS,
2374
0
                    "Missing 'container_kind' argument",
2375
                );
2376
            }
2377
        };
2378
2379
0
        let description = args
2380
0
            .get("description")
2381
0
            .and_then(|v| v.as_str())
2382
0
            .map(|s| s.to_string());
2383
2384
0
        let technology = args
2385
0
            .get("technology")
2386
0
            .and_then(|v| v.as_str())
2387
0
            .map(|s| s.to_string());
2388
2389
0
        let framework = args
2390
0
            .get("framework")
2391
0
            .and_then(|v| v.as_str())
2392
0
            .map(|s| s.to_string());
2393
0
        let runtime = args
2394
0
            .get("runtime")
2395
0
            .and_then(|v| v.as_str())
2396
0
            .map(|s| s.to_string());
2397
0
        let deployment = args
2398
0
            .get("deployment")
2399
0
            .and_then(|v| v.as_str())
2400
0
            .map(|s| s.to_string());
2401
2402
0
        let org_id = match self.ensure_org().await {
2403
0
            Ok(id) => id,
2404
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
2405
        };
2406
2407
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
2408
0
            Ok(p) => p,
2409
0
            Err(e) => {
2410
0
                error!("Failed to fetch projects: {}", e);
2411
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
2412
            }
2413
        };
2414
2415
0
        let project = match projects
2416
0
            .iter()
2417
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
2418
        {
2419
0
            Some(p) => p,
2420
            None => {
2421
0
                return JsonRpcResponse::error(
2422
0
                    id,
2423
                    INVALID_PARAMS,
2424
0
                    &format!("Project '{}' not found", project_name),
2425
                );
2426
            }
2427
        };
2428
2429
0
        let container = match self
2430
0
            .service
2431
0
            .component_service
2432
0
            .create_container(
2433
0
                &org_id,
2434
0
                &project.id,
2435
0
                &self.user_id,
2436
0
                &crate::models::component::CreateContainerRequest {
2437
0
                    name: name.to_string(),
2438
0
                    description,
2439
0
                    path: path.to_string(),
2440
0
                    metadata: crate::models::component::ContainerMetadata {
2441
0
                        container_kind,
2442
0
                        technology,
2443
0
                        framework,
2444
0
                        runtime,
2445
0
                        deployment,
2446
0
                    },
2447
0
                },
2448
            )
2449
0
            .await
2450
        {
2451
0
            Ok(value) => value,
2452
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
2453
        };
2454
2455
0
        let container_view: ComponentView = (&container).into();
2456
0
        let mut output = format!(
2457
            "✅ Created container **{}** in project **{}**\n\n\
2458
             - **Path**: `{}`",
2459
            container_view.name, project.name, container_view.path
2460
        );
2461
0
        if let Some(ref tech) = container_view.technology {
2462
0
            output.push_str(&format!("\n- **Technology**: {}", tech));
2463
0
        }
2464
0
        if let Some(ref fw) = container_view.framework {
2465
0
            output.push_str(&format!("\n- **Framework**: {}", fw));
2466
0
        }
2467
0
        output.push_str(&format!("\n- **Kind**: {}", container.kind.as_str()));
2468
0
        output.push_str(&format!("\n- **ID**: `{}`", container_view.id));
2469
2470
0
        let result = CallToolResult {
2471
0
            content: vec![ToolContent::Text { text: output }],
2472
0
            is_error: None,
2473
0
        };
2474
2475
0
        jsonrpc_success(id, result)
2476
0
    }
2477
2478
0
    pub(super) async fn call_get_container(
2479
0
        &mut self,
2480
0
        id: Option<serde_json::Value>,
2481
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
2482
0
    ) -> JsonRpcResponse {
2483
0
        let request_id = id
2484
0
            .as_ref()
2485
0
            .map(std::string::ToString::to_string)
2486
0
            .unwrap_or_else(|| "null".to_string());
2487
0
        info!(
2488
            mcp_tool = "get_container",
2489
            mcp_request_id = %request_id,
2490
0
            has_arguments = arguments.is_some(),
2491
            "mcp_container_tool_start"
2492
        );
2493
2494
0
        let args = match arguments {
2495
0
            Some(a) => a,
2496
            None => {
2497
0
                error!(
2498
                    mcp_tool = "get_container",
2499
                    mcp_request_id = %request_id,
2500
                    "mcp_container_tool_invalid_params: missing arguments"
2501
                );
2502
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments");
2503
            }
2504
        };
2505
2506
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
2507
0
            Some(p) => p,
2508
            None => {
2509
0
                error!(
2510
                    mcp_tool = "get_container",
2511
                    mcp_request_id = %request_id,
2512
0
                    args_keys = ?args.keys().collect::<Vec<_>>(),
2513
                    "mcp_container_tool_invalid_params: missing project"
2514
                );
2515
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
2516
            }
2517
        };
2518
2519
0
        let org_id = match self.ensure_org().await {
2520
0
            Ok(id) => id,
2521
0
            Err(e) => {
2522
0
                error!(
2523
                    mcp_tool = "get_container",
2524
                    mcp_request_id = %request_id,
2525
                    project = project_name,
2526
                    user_id = %self.user_id,
2527
                    error = %e,
2528
                    "mcp_container_tool_failed: ensure_org"
2529
                );
2530
0
                return JsonRpcResponse::from_api_error(id, e);
2531
            }
2532
        };
2533
2534
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
2535
0
            Ok(p) => p,
2536
0
            Err(e) => {
2537
0
                error!(
2538
                    mcp_tool = "get_container",
2539
                    mcp_request_id = %request_id,
2540
                    org_id = %org_id,
2541
                    project = project_name,
2542
                    user_id = %self.user_id,
2543
                    error = %e,
2544
                    "mcp_container_tool_failed: list_projects"
2545
                );
2546
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
2547
            }
2548
        };
2549
2550
0
        let project = match projects
2551
0
            .iter()
2552
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
2553
        {
2554
0
            Some(p) => p,
2555
            None => {
2556
0
                error!(
2557
                    mcp_tool = "get_container",
2558
                    mcp_request_id = %request_id,
2559
                    org_id = %org_id,
2560
                    project = project_name,
2561
                    "mcp_container_tool_invalid_params: project not found"
2562
                );
2563
0
                return JsonRpcResponse::error(
2564
0
                    id,
2565
                    INVALID_PARAMS,
2566
0
                    &format!("Project '{}' not found", project_name),
2567
                );
2568
            }
2569
        };
2570
2571
0
        let container_id =
2572
0
            if let Some(container_id) = args.get("container_id").and_then(|v| v.as_str()) {
2573
0
                match ComponentId::try_from(container_id.to_string()) {
2574
0
                    Ok(value) => value,
2575
0
                    Err(e) => {
2576
0
                        error!(
2577
                            mcp_tool = "get_container",
2578
                            mcp_request_id = %request_id,
2579
                            org_id = %org_id,
2580
                            project_id = %project.id,
2581
                            project = project.name,
2582
                            container_id = container_id,
2583
                            error = %e,
2584
                            "mcp_container_tool_invalid_params: invalid container_id"
2585
                        );
2586
0
                        return JsonRpcResponse::from_api_error(id, e);
2587
                    }
2588
                }
2589
0
            } else if let Some(name) = args.get("name").and_then(|v| v.as_str()) {
2590
0
                let containers = match self
2591
0
                    .service
2592
0
                    .component_repo
2593
0
                    .list_containers(&project.id)
2594
0
                    .await
2595
                {
2596
0
                    Ok(c) => c,
2597
0
                    Err(e) => {
2598
0
                        error!(
2599
                            mcp_tool = "get_container",
2600
                            mcp_request_id = %request_id,
2601
                            org_id = %org_id,
2602
                            project_id = %project.id,
2603
                            project = project.name,
2604
                            user_id = %self.user_id,
2605
                            error = %e,
2606
                            "mcp_container_tool_failed: list_containers_by_name_lookup"
2607
                        );
2608
0
                        return JsonRpcResponse::error(
2609
0
                            id,
2610
                            INTERNAL_ERROR,
2611
0
                            "Failed to fetch containers",
2612
                        );
2613
                    }
2614
                };
2615
0
                match containers.into_iter().find(|component| {
2616
0
                    component.is_container() && component.name.eq_ignore_ascii_case(name)
2617
0
                }) {
2618
0
                    Some(c) => c.id,
2619
                    None => {
2620
0
                        error!(
2621
                            mcp_tool = "get_container",
2622
                            mcp_request_id = %request_id,
2623
                            org_id = %org_id,
2624
                            project_id = %project.id,
2625
                            project = project.name,
2626
                            container_name = name,
2627
                            "mcp_container_tool_invalid_params: container name not found"
2628
                        );
2629
0
                        return JsonRpcResponse::error(
2630
0
                            id,
2631
                            INVALID_PARAMS,
2632
0
                            &format!(
2633
0
                                "Container '{}' not found in project '{}'",
2634
0
                                name, project_name
2635
0
                            ),
2636
                        );
2637
                    }
2638
                }
2639
            } else {
2640
0
                error!(
2641
                    mcp_tool = "get_container",
2642
                    mcp_request_id = %request_id,
2643
                    org_id = %org_id,
2644
                    project_id = %project.id,
2645
                    project = project.name,
2646
0
                    args_keys = ?args.keys().collect::<Vec<_>>(),
2647
                    "mcp_container_tool_invalid_params: neither container_id nor name provided"
2648
                );
2649
0
                return JsonRpcResponse::error(
2650
0
                    id,
2651
                    INVALID_PARAMS,
2652
0
                    "Provide either 'container_id' or 'name'",
2653
                );
2654
            };
2655
2656
0
        let container = match self
2657
0
            .service
2658
0
            .component_service
2659
0
            .get_container(&org_id, &project.id, &container_id)
2660
0
            .await
2661
        {
2662
0
            Ok(c) => c,
2663
0
            Err(e) => {
2664
0
                error!(
2665
                    mcp_tool = "get_container",
2666
                    mcp_request_id = %request_id,
2667
                    org_id = %org_id,
2668
                    project_id = %project.id,
2669
                    project = project.name,
2670
                    container_id = %container_id,
2671
                    user_id = %self.user_id,
2672
                    error = %e,
2673
                    "mcp_container_tool_failed: service_get_container"
2674
                );
2675
0
                return JsonRpcResponse::from_api_error(id, e);
2676
            }
2677
        };
2678
0
        let container_view: ComponentView = (&container).into();
2679
0
        info!(
2680
            mcp_tool = "get_container",
2681
            mcp_request_id = %request_id,
2682
            org_id = %org_id,
2683
            project_id = %project.id,
2684
            project = project.name,
2685
            container_id = %container_view.id,
2686
            container_name = container_view.name,
2687
            "mcp_container_tool_success"
2688
        );
2689
2690
0
        let mut output = format!(
2691
            "📦 Container **{}**\n\n- **ID**: `{}`\n- **Path**: `{}`\n- **Kind**: {}",
2692
            container_view.name,
2693
            container_view.id,
2694
            container_view.path,
2695
0
            container.kind.as_str()
2696
        );
2697
0
        if let Some(ref desc) = container_view.description {
2698
0
            output.push_str(&format!("\n- **Description**: {}", desc));
2699
0
        }
2700
0
        if let Some(ref tech) = container_view.technology {
2701
0
            output.push_str(&format!("\n- **Technology**: {}", tech));
2702
0
        }
2703
0
        if let Some(ref fw) = container_view.framework {
2704
0
            output.push_str(&format!("\n- **Framework**: {}", fw));
2705
0
        }
2706
2707
0
        let result = CallToolResult {
2708
0
            content: vec![ToolContent::Text { text: output }],
2709
0
            is_error: None,
2710
0
        };
2711
2712
0
        jsonrpc_success(id, result)
2713
0
    }
2714
2715
0
    pub(super) async fn call_list_containers(
2716
0
        &mut self,
2717
0
        id: Option<serde_json::Value>,
2718
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
2719
0
    ) -> JsonRpcResponse {
2720
0
        let request_id = id
2721
0
            .as_ref()
2722
0
            .map(std::string::ToString::to_string)
2723
0
            .unwrap_or_else(|| "null".to_string());
2724
0
        info!(
2725
            mcp_tool = "list_containers",
2726
            mcp_request_id = %request_id,
2727
0
            has_arguments = arguments.is_some(),
2728
            "mcp_container_tool_start"
2729
        );
2730
2731
0
        let args = match arguments {
2732
0
            Some(a) => a,
2733
            None => {
2734
0
                error!(
2735
                    mcp_tool = "list_containers",
2736
                    mcp_request_id = %request_id,
2737
                    "mcp_container_tool_invalid_params: missing arguments"
2738
                );
2739
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments");
2740
            }
2741
        };
2742
2743
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
2744
0
            Some(p) => p,
2745
            None => {
2746
0
                error!(
2747
                    mcp_tool = "list_containers",
2748
                    mcp_request_id = %request_id,
2749
0
                    args_keys = ?args.keys().collect::<Vec<_>>(),
2750
                    "mcp_container_tool_invalid_params: missing project"
2751
                );
2752
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
2753
            }
2754
        };
2755
2756
0
        let org_id = match self.ensure_org().await {
2757
0
            Ok(id) => id,
2758
0
            Err(e) => {
2759
0
                error!(
2760
                    mcp_tool = "list_containers",
2761
                    mcp_request_id = %request_id,
2762
                    project = project_name,
2763
                    user_id = %self.user_id,
2764
                    error = %e,
2765
                    "mcp_container_tool_failed: ensure_org"
2766
                );
2767
0
                return JsonRpcResponse::from_api_error(id, e);
2768
            }
2769
        };
2770
2771
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
2772
0
            Ok(p) => p,
2773
0
            Err(e) => {
2774
0
                error!(
2775
                    mcp_tool = "list_containers",
2776
                    mcp_request_id = %request_id,
2777
                    org_id = %org_id,
2778
                    project = project_name,
2779
                    user_id = %self.user_id,
2780
                    error = %e,
2781
                    "mcp_container_tool_failed: list_projects"
2782
                );
2783
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
2784
            }
2785
        };
2786
2787
0
        let project = match projects
2788
0
            .iter()
2789
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
2790
        {
2791
0
            Some(p) => p,
2792
            None => {
2793
0
                error!(
2794
                    mcp_tool = "list_containers",
2795
                    mcp_request_id = %request_id,
2796
                    org_id = %org_id,
2797
                    project = project_name,
2798
                    "mcp_container_tool_invalid_params: project not found"
2799
                );
2800
0
                return JsonRpcResponse::error(
2801
0
                    id,
2802
                    INVALID_PARAMS,
2803
0
                    &format!("Project '{}' not found", project_name),
2804
                );
2805
            }
2806
        };
2807
2808
0
        let containers = match self
2809
0
            .service
2810
0
            .component_service
2811
0
            .list_containers(&org_id, &project.id)
2812
0
            .await
2813
        {
2814
0
            Ok(c) => c,
2815
0
            Err(e) => {
2816
0
                error!(
2817
                    mcp_tool = "list_containers",
2818
                    mcp_request_id = %request_id,
2819
                    org_id = %org_id,
2820
                    project_id = %project.id,
2821
                    project = project.name,
2822
                    user_id = %self.user_id,
2823
                    error = %e,
2824
                    "mcp_container_tool_failed: service_list_containers"
2825
                );
2826
0
                return JsonRpcResponse::from_api_error(id, e);
2827
            }
2828
        };
2829
2830
0
        let container_views: Vec<ComponentView> = containers
2831
0
            .into_iter()
2832
0
            .filter(|component| component.is_container())
2833
0
            .map(|component| (&component).into())
2834
0
            .collect();
2835
0
        info!(
2836
            mcp_tool = "list_containers",
2837
            mcp_request_id = %request_id,
2838
            org_id = %org_id,
2839
            project_id = %project.id,
2840
            project = project.name,
2841
0
            container_count = container_views.len(),
2842
            "mcp_container_tool_success"
2843
        );
2844
2845
0
        let mut output = format!("📦 Containers in project **{}**\n\n", project.name);
2846
0
        if container_views.is_empty() {
2847
0
            output.push_str("No containers found.");
2848
0
        } else {
2849
0
            for container in &container_views {
2850
0
                output.push_str(&format!("- **{}** (`{}`)", container.name, container.id));
2851
0
                if let Some(ref tech) = container.technology {
2852
0
                    output.push_str(&format!(" [{}]", tech));
2853
0
                }
2854
0
                output.push('\n');
2855
            }
2856
        }
2857
2858
0
        let result = CallToolResult {
2859
0
            content: vec![ToolContent::Text { text: output }],
2860
0
            is_error: None,
2861
0
        };
2862
2863
0
        jsonrpc_success(id, result)
2864
0
    }
2865
2866
7
    pub(super) async fn call_create_connection(
2867
7
        &mut self,
2868
7
        id: Option<serde_json::Value>,
2869
7
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
2870
7
    ) -> JsonRpcResponse {
2871
7
        let args = match arguments {
2872
7
            Some(a) => a,
2873
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
2874
        };
2875
2876
7
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
2877
7
            Some(p) => p,
2878
            None => {
2879
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
2880
            }
2881
        };
2882
2883
7
        let source_name = match args.get("source_name").and_then(|v| v.as_str()) {
2884
7
            Some(n) => n,
2885
            None => {
2886
0
                return JsonRpcResponse::error(
2887
0
                    id,
2888
                    INVALID_PARAMS,
2889
0
                    "Missing 'source_name' argument",
2890
                );
2891
            }
2892
        };
2893
7
        let source_kind = match args.get("source_kind").and_then(|v| v.as_str()) {
2894
7
            Some(value) => match parse_connection_source_kind(value) {
2895
7
                Ok(kind) => kind,
2896
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
2897
            },
2898
            None => {
2899
0
                return JsonRpcResponse::error(
2900
0
                    id,
2901
                    INVALID_PARAMS,
2902
0
                    "Missing 'source_kind' argument",
2903
                );
2904
            }
2905
        };
2906
2907
7
        let target_name = match args.get("target_name").and_then(|v| v.as_str()) {
2908
7
            Some(n) => n,
2909
            None => {
2910
0
                return JsonRpcResponse::error(
2911
0
                    id,
2912
                    INVALID_PARAMS,
2913
0
                    "Missing 'target_name' argument",
2914
                );
2915
            }
2916
        };
2917
7
        let target_kind = match args.get("target_kind").and_then(|v| v.as_str()) {
2918
7
            Some(value) => match parse_connection_target_kind(value) {
2919
7
                Ok(kind) => kind,
2920
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
2921
            },
2922
            None => {
2923
0
                return JsonRpcResponse::error(
2924
0
                    id,
2925
                    INVALID_PARAMS,
2926
0
                    "Missing 'target_kind' argument",
2927
                );
2928
            }
2929
        };
2930
2931
7
        let label = match args.get("label").and_then(|v| v.as_str()) {
2932
7
            Some(l) => l,
2933
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'label' argument"),
2934
        };
2935
2936
7
        let description = args
2937
7
            .get("description")
2938
7
            .and_then(|v| 
v0
.
as_str0
())
2939
7
            .map(|s| 
s0
.
to_string0
());
2940
7
        let source_side = match args.get("source_side").and_then(|v| 
v1
.
as_str1
()) {
2941
1
            Some(value) => match parse_connection_anchor_side(value) {
2942
1
                Ok(side) => Some(side),
2943
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
2944
            },
2945
6
            None => None,
2946
        };
2947
7
        let target_side = match args.get("target_side").and_then(|v| 
v1
.
as_str1
()) {
2948
1
            Some(value) => match parse_connection_anchor_side(value) {
2949
1
                Ok(side) => Some(side),
2950
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
2951
            },
2952
6
            None => None,
2953
        };
2954
7
        let routing_mode = match args.get("routing_mode").and_then(|v| 
v1
.
as_str1
()) {
2955
1
            Some(value) => match parse_connection_routing_mode(value) {
2956
1
                Ok(mode) => Some(mode),
2957
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
2958
            },
2959
6
            None => None,
2960
        };
2961
7
        let control_points = match args
2962
7
            .get("control_points")
2963
7
            .cloned()
2964
7
            .map(serde_json::from_value)
2965
        {
2966
1
            Some(Ok(points)) => Some(points),
2967
0
            Some(Err(e)) => {
2968
0
                return JsonRpcResponse::error(
2969
0
                    id,
2970
                    INVALID_PARAMS,
2971
0
                    &format!("Invalid 'control_points' argument: {}", e),
2972
                );
2973
            }
2974
6
            None => None,
2975
        };
2976
2977
7
        let org_id = match self.ensure_org().await {
2978
7
            Ok(id) => id,
2979
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
2980
        };
2981
2982
7
        let projects = match self.service.project_repo.list_projects(&org_id).await {
2983
7
            Ok(p) => p,
2984
0
            Err(e) => {
2985
0
                error!("Failed to fetch projects: {}", e);
2986
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
2987
            }
2988
        };
2989
2990
7
        let project = match projects
2991
7
            .iter()
2992
7
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
2993
        {
2994
7
            Some(p) => p,
2995
            None => {
2996
0
                return JsonRpcResponse::error(
2997
0
                    id,
2998
                    INVALID_PARAMS,
2999
0
                    &format!("Project '{}' not found", project_name),
3000
                );
3001
            }
3002
        };
3003
3004
7
        let containers = self
3005
7
            .service
3006
7
            .component_repo
3007
7
            .list_containers(&project.id)
3008
7
            .await
3009
7
            .unwrap_or_default();
3010
7
        let mut containers = containers;
3011
18
        
containers7
.
sort_by_key7
(|container| container.name.to_ascii_lowercase());
3012
7
        let actors = self
3013
7
            .service
3014
7
            .actor_service
3015
7
            .list_actors(&org_id, &project.id, &self.user_id)
3016
7
            .await
3017
7
            .unwrap_or_default();
3018
7
        let (
source_id6
,
source_type6
,
source_display6
) = match source_kind {
3019
            ConnectionSourceKind::Container => {
3020
1
                let (component_id, _) = match resolve_component_ref_by_entity_kind(
3021
1
                    self.service.component_repo.as_ref(),
3022
1
                    &project.id,
3023
1
                    source_name,
3024
1
                    ConnectionTargetKind::Container,
3025
                )
3026
1
                .await
3027
                {
3028
1
                    Ok(value) => value,
3029
0
                    Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
3030
                };
3031
1
                (
3032
1
                    component_id.to_string(),
3033
1
                    ConnectionSourceKind::Container,
3034
1
                    format!("container:{}", source_name),
3035
1
                )
3036
            }
3037
            ConnectionSourceKind::Component => {
3038
4
                let (
component_id3
, _) = match resolve_component_ref_by_entity_kind(
3039
4
                    self.service.component_repo.as_ref(),
3040
4
                    &project.id,
3041
4
                    source_name,
3042
4
                    ConnectionTargetKind::Component,
3043
                )
3044
4
                .await
3045
                {
3046
3
                    Ok(value) => value,
3047
1
                    Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
3048
                };
3049
3
                (
3050
3
                    component_id.to_string(),
3051
3
                    ConnectionSourceKind::Component,
3052
3
                    format!("component:{}", source_name),
3053
3
                )
3054
            }
3055
            ConnectionSourceKind::Person => {
3056
0
                match actors
3057
0
                    .iter()
3058
0
                    .find(|a| a.name.eq_ignore_ascii_case(source_name))
3059
                {
3060
0
                    Some(actor) => (
3061
0
                        actor.id.to_string(),
3062
0
                        ConnectionSourceKind::Person,
3063
0
                        format!("person:{}", actor.name),
3064
0
                    ),
3065
                    None => {
3066
0
                        return JsonRpcResponse::error(
3067
0
                            id,
3068
                            INVALID_PARAMS,
3069
0
                            &format!("Unknown person source: {}", source_name),
3070
                        );
3071
                    }
3072
                }
3073
            }
3074
            ConnectionSourceKind::Store => {
3075
2
                let store_id = match resolve_store_id_from_value(
3076
2
                    &self.service.store_service,
3077
2
                    &org_id,
3078
2
                    &project.id,
3079
2
                    &self.user_id,
3080
2
                    source_name,
3081
                )
3082
2
                .await
3083
                {
3084
2
                    Ok(value) => value,
3085
                    Err(_) => {
3086
0
                        return JsonRpcResponse::error(
3087
0
                            id,
3088
                            INVALID_PARAMS,
3089
0
                            &format!("Unknown store source: {}", source_name),
3090
                        );
3091
                    }
3092
                };
3093
2
                (
3094
2
                    store_id.to_string(),
3095
2
                    ConnectionSourceKind::Store,
3096
2
                    format!("store:{}", source_name),
3097
2
                )
3098
            }
3099
            ConnectionSourceKind::ExternalSystem => {
3100
0
                let system_id = match resolve_external_system_id_from_value(
3101
0
                    &self.service.external_system_service,
3102
0
                    &org_id,
3103
0
                    &project.id,
3104
0
                    &self.user_id,
3105
0
                    source_name,
3106
                )
3107
0
                .await
3108
                {
3109
0
                    Ok(value) => value,
3110
                    Err(_) => {
3111
0
                        return JsonRpcResponse::error(
3112
0
                            id,
3113
                            INVALID_PARAMS,
3114
0
                            &format!("Unknown external system source: {}", source_name),
3115
                        );
3116
                    }
3117
                };
3118
0
                (
3119
0
                    system_id.to_string(),
3120
0
                    ConnectionSourceKind::ExternalSystem,
3121
0
                    format!("external-system:{}", source_name),
3122
0
                )
3123
            }
3124
        };
3125
3126
6
        let (target_id, target_type, target_display) = match target_kind {
3127
            ConnectionTargetKind::Container => {
3128
1
                let (component_id, _) = match resolve_component_ref_by_entity_kind(
3129
1
                    self.service.component_repo.as_ref(),
3130
1
                    &project.id,
3131
1
                    target_name,
3132
1
                    ConnectionTargetKind::Container,
3133
                )
3134
1
                .await
3135
                {
3136
1
                    Ok(value) => value,
3137
0
                    Err(e) => return JsonRpcResponse::from_api_error(id, e),
3138
                };
3139
1
                (
3140
1
                    component_id.to_string(),
3141
1
                    ConnectionTargetKind::Container,
3142
1
                    format!("container:{}", target_name),
3143
1
                )
3144
            }
3145
            ConnectionTargetKind::Component => {
3146
1
                let (component_id, _) = match resolve_component_ref_by_entity_kind(
3147
1
                    self.service.component_repo.as_ref(),
3148
1
                    &project.id,
3149
1
                    target_name,
3150
1
                    ConnectionTargetKind::Component,
3151
                )
3152
1
                .await
3153
                {
3154
1
                    Ok(value) => value,
3155
0
                    Err(e) => return JsonRpcResponse::from_api_error(id, e),
3156
                };
3157
1
                (
3158
1
                    component_id.to_string(),
3159
1
                    ConnectionTargetKind::Component,
3160
1
                    format!("component:{}", target_name),
3161
1
                )
3162
            }
3163
            ConnectionTargetKind::Person => {
3164
0
                match actors
3165
0
                    .iter()
3166
0
                    .find(|a| a.name.eq_ignore_ascii_case(target_name))
3167
                {
3168
0
                    Some(actor) => (
3169
0
                        actor.id.to_string(),
3170
0
                        ConnectionTargetKind::Person,
3171
0
                        format!("person:{}", actor.name),
3172
0
                    ),
3173
                    None => {
3174
0
                        return JsonRpcResponse::error(
3175
0
                            id,
3176
                            INVALID_PARAMS,
3177
0
                            &format!("Unknown person target: {}", target_name),
3178
                        );
3179
                    }
3180
                }
3181
            }
3182
            ConnectionTargetKind::Store => {
3183
2
                let store_id = match resolve_store_id_from_value(
3184
2
                    &self.service.store_service,
3185
2
                    &org_id,
3186
2
                    &project.id,
3187
2
                    &self.user_id,
3188
2
                    target_name,
3189
                )
3190
2
                .await
3191
                {
3192
2
                    Ok(value) => value,
3193
                    Err(_) => {
3194
0
                        return JsonRpcResponse::error(
3195
0
                            id,
3196
                            INVALID_PARAMS,
3197
0
                            &format!("Unknown store target: {}", target_name),
3198
                        );
3199
                    }
3200
                };
3201
2
                (
3202
2
                    store_id.to_string(),
3203
2
                    ConnectionTargetKind::Store,
3204
2
                    format!("store:{}", target_name),
3205
2
                )
3206
            }
3207
            ConnectionTargetKind::ExternalSystem => {
3208
2
                let system_id = match resolve_external_system_id_from_value(
3209
2
                    &self.service.external_system_service,
3210
2
                    &org_id,
3211
2
                    &project.id,
3212
2
                    &self.user_id,
3213
2
                    target_name,
3214
                )
3215
2
                .await
3216
                {
3217
2
                    Ok(value) => value,
3218
                    Err(_) => {
3219
0
                        return JsonRpcResponse::error(
3220
0
                            id,
3221
                            INVALID_PARAMS,
3222
0
                            &format!("Unknown external system target: {}", target_name),
3223
                        );
3224
                    }
3225
                };
3226
2
                (
3227
2
                    system_id.to_string(),
3228
2
                    ConnectionTargetKind::ExternalSystem,
3229
2
                    format!("external-system:{}", target_name),
3230
2
                )
3231
            }
3232
        };
3233
3234
        // Check if connection already exists
3235
6
        if let Ok(true) = self
3236
6
            .service
3237
6
            .connection_repo
3238
6
            .connection_exists(source_id.as_str(), target_id.as_str(), &project.id)
3239
6
            .await
3240
        {
3241
1
            return JsonRpcResponse::error(
3242
1
                id,
3243
                INVALID_PARAMS,
3244
1
                &format!(
3245
1
                    "Connection from '{}' to '{}' already exists",
3246
1
                    source_name, target_name
3247
1
                ),
3248
            );
3249
5
        }
3250
3251
5
        let connection = match self
3252
5
            .service
3253
5
            .connection_service
3254
5
            .create_connection(
3255
5
                &self.user_id,
3256
5
                &project.id,
3257
5
                &org_id,
3258
5
                source_id,
3259
5
                source_type,
3260
5
                source_side,
3261
5
                target_id,
3262
5
                target_type,
3263
5
                target_side,
3264
5
                label.to_string(),
3265
5
                description,
3266
5
                routing_mode,
3267
5
                control_points,
3268
            )
3269
5
            .await
3270
        {
3271
5
            Ok(connection) => connection,
3272
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
3273
        };
3274
3275
5
        let conn_view: ConnectionView = (&connection).into();
3276
5
        let output = format!(
3277
            "✅ Created connection in project **{}**\n\n\
3278
             **{}** --[{}]--> **{}**\n\n\
3279
             - **ID**: `{}`",
3280
            project.name, source_display, conn_view.label, target_display, conn_view.id
3281
        );
3282
3283
5
        let result = CallToolResult {
3284
5
            content: vec![ToolContent::Text { text: output }],
3285
5
            is_error: None,
3286
5
        };
3287
3288
5
        jsonrpc_success(id, result)
3289
7
    }
3290
3291
1
    pub(super) async fn call_create_component(
3292
1
        &mut self,
3293
1
        id: Option<serde_json::Value>,
3294
1
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
3295
1
    ) -> JsonRpcResponse {
3296
1
        let args = match arguments {
3297
1
            Some(a) => a,
3298
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
3299
        };
3300
3301
1
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
3302
1
            Some(p) => p,
3303
            None => {
3304
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
3305
            }
3306
        };
3307
3308
1
        let parent_name = match args.get("parent_name").and_then(|v| v.as_str()) {
3309
1
            Some(p) => p,
3310
            None => {
3311
0
                return JsonRpcResponse::error(
3312
0
                    id,
3313
                    INVALID_PARAMS,
3314
0
                    "Missing 'parent_name' argument",
3315
                );
3316
            }
3317
        };
3318
3319
1
        let name = match args.get("name").and_then(|v| v.as_str()) {
3320
1
            Some(n) => n,
3321
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
3322
        };
3323
3324
1
        let path = match args.get("path").and_then(|v| v.as_str()) {
3325
1
            Some(p) => p,
3326
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'path' argument"),
3327
        };
3328
3329
1
        let description = args
3330
1
            .get("description")
3331
1
            .and_then(|v| 
v0
.
as_str0
())
3332
1
            .map(|s| 
s0
.
to_string0
());
3333
3334
1
        let org_id = match self.ensure_org().await {
3335
1
            Ok(id) => id,
3336
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
3337
        };
3338
3339
1
        let projects = match self.service.project_repo.list_projects(&org_id).await {
3340
1
            Ok(p) => p,
3341
0
            Err(e) => {
3342
0
                error!("Failed to fetch projects: {}", e);
3343
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
3344
            }
3345
        };
3346
3347
1
        let project = match projects
3348
1
            .iter()
3349
1
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
3350
        {
3351
1
            Some(p) => p,
3352
            None => {
3353
0
                return JsonRpcResponse::error(
3354
0
                    id,
3355
                    INVALID_PARAMS,
3356
0
                    &format!("Project '{}' not found", project_name),
3357
                );
3358
            }
3359
        };
3360
3361
1
        let parent_kind = match args.get("parent_kind").and_then(|v| v.as_str()) {
3362
1
            Some("container") => 
None0
,
3363
1
            Some("component") => Some(ComponentKind::Module),
3364
0
            Some(other) => match ComponentKind::from_str(other) {
3365
0
                Ok(kind) => Some(kind),
3366
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
3367
            },
3368
0
            None => None,
3369
        };
3370
3371
1
        let 
parent_id0
= match resolve_component_id_from_value(
3372
1
            self.service.component_repo.as_ref(),
3373
1
            &project.id,
3374
1
            parent_name,
3375
            true,
3376
1
            parent_kind,
3377
        )
3378
1
        .await
3379
        {
3380
0
            Ok(id) => id,
3381
1
            Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
3382
        };
3383
3384
0
        let component_kind = match args.get("component_kind").and_then(|v| v.as_str()) {
3385
0
            Some(value) => {
3386
0
                match crate::models::component::ArchitectureComponentKind::from_str(value) {
3387
0
                    Ok(kind) => kind,
3388
0
                    Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
3389
                }
3390
            }
3391
0
            None => crate::models::component::ArchitectureComponentKind::Module,
3392
        };
3393
3394
0
        let request = crate::models::component::CreateArchitectureComponentRequest {
3395
0
            name: name.to_string(),
3396
0
            description,
3397
0
            path: path.to_string(),
3398
0
            metadata: crate::models::component::ArchitectureComponentMetadata { component_kind },
3399
0
        };
3400
3401
0
        let component = match self
3402
0
            .service
3403
0
            .component_service
3404
0
            .create_component(&org_id, &project.id, &self.user_id, &parent_id, &request)
3405
0
            .await
3406
        {
3407
0
            Ok(m) => m,
3408
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
3409
        };
3410
3411
0
        let component_view: ComponentView = (&component).into();
3412
0
        let mut output = format!(
3413
            "✅ Created component **{}** in project **{}**\n\n\
3414
             - **Path**: `{}`\n\
3415
             - **Parent**: `{}`",
3416
            component_view.name, project.name, component_view.path, parent_name
3417
        );
3418
0
        if let Some(ref desc) = component_view.description {
3419
0
            output.push_str(&format!("\n- **Description**: {}", desc));
3420
0
        }
3421
3422
0
        let result = CallToolResult {
3423
0
            content: vec![ToolContent::Text { text: output }],
3424
0
            is_error: None,
3425
0
        };
3426
3427
0
        jsonrpc_success(id, result)
3428
1
    }
3429
3430
0
    pub(super) async fn call_get_component(
3431
0
        &mut self,
3432
0
        id: Option<serde_json::Value>,
3433
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
3434
0
    ) -> JsonRpcResponse {
3435
0
        let args = match arguments {
3436
0
            Some(a) => a,
3437
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
3438
        };
3439
3440
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
3441
0
            Some(p) => p,
3442
            None => {
3443
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
3444
            }
3445
        };
3446
0
        let parent_name = match args.get("parent_name").and_then(|v| v.as_str()) {
3447
0
            Some(p) => p,
3448
            None => {
3449
0
                return JsonRpcResponse::error(
3450
0
                    id,
3451
                    INVALID_PARAMS,
3452
0
                    "Missing 'parent_name' argument",
3453
                );
3454
            }
3455
        };
3456
0
        let parent_kind = match args.get("parent_kind").and_then(|v| v.as_str()) {
3457
0
            Some("container") => None,
3458
0
            Some("component") => Some(ComponentKind::Module),
3459
0
            Some(other) => match ComponentKind::from_str(other) {
3460
0
                Ok(kind) => Some(kind),
3461
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
3462
            },
3463
0
            None => None,
3464
        };
3465
3466
0
        let org_id = match self.ensure_org().await {
3467
0
            Ok(id) => id,
3468
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
3469
        };
3470
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
3471
0
            Ok(p) => p,
3472
0
            Err(e) => {
3473
0
                error!("Failed to fetch projects: {}", e);
3474
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
3475
            }
3476
        };
3477
0
        let project = match projects
3478
0
            .iter()
3479
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
3480
        {
3481
0
            Some(p) => p,
3482
            None => {
3483
0
                return JsonRpcResponse::error(
3484
0
                    id,
3485
                    INVALID_PARAMS,
3486
0
                    &format!("Project '{}' not found", project_name),
3487
                );
3488
            }
3489
        };
3490
3491
0
        let parent_id = match resolve_component_id_from_value(
3492
0
            self.service.component_repo.as_ref(),
3493
0
            &project.id,
3494
0
            parent_name,
3495
            true,
3496
0
            parent_kind,
3497
        )
3498
0
        .await
3499
        {
3500
0
            Ok(value) => value,
3501
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
3502
        };
3503
3504
0
        let component_id =
3505
0
            if let Some(component_id) = args.get("component_id").and_then(|v| v.as_str()) {
3506
0
                match ComponentId::try_from(component_id.to_string()) {
3507
0
                    Ok(value) => value,
3508
0
                    Err(e) => return JsonRpcResponse::from_api_error(id, e),
3509
                }
3510
0
            } else if let Some(component_name) = args.get("name").and_then(|v| v.as_str()) {
3511
0
                match resolve_component_id_from_parent_value(
3512
0
                    self.service.component_repo.as_ref(),
3513
0
                    &parent_id,
3514
0
                    component_name,
3515
                )
3516
0
                .await
3517
                {
3518
0
                    Ok(value) => value,
3519
0
                    Err(e) => return JsonRpcResponse::from_api_error(id, e),
3520
                }
3521
            } else {
3522
0
                return JsonRpcResponse::error(
3523
0
                    id,
3524
                    INVALID_PARAMS,
3525
0
                    "Provide either 'component_id' or 'name'",
3526
                );
3527
            };
3528
3529
0
        let component = match self
3530
0
            .service
3531
0
            .component_service
3532
0
            .get_component(&org_id, &project.id, &parent_id, &component_id)
3533
0
            .await
3534
        {
3535
0
            Ok(value) => value,
3536
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
3537
        };
3538
3539
0
        let component_view: ComponentView = (&component).into();
3540
0
        let mut output = format!(
3541
            "📦 Component **{}**\n\n- **ID**: `{}`\n- **Path**: `{}`\n- **Parent**: `{}`",
3542
            component_view.name, component_view.id, component_view.path, parent_name
3543
        );
3544
0
        if let Some(ref desc) = component_view.description {
3545
0
            output.push_str(&format!("\n- **Description**: {}", desc));
3546
0
        }
3547
0
        if let Some(ref tech) = component_view.technology {
3548
0
            output.push_str(&format!("\n- **Technology**: {}", tech));
3549
0
        }
3550
0
        if let Some(ref fw) = component_view.framework {
3551
0
            output.push_str(&format!("\n- **Framework**: {}", fw));
3552
0
        }
3553
3554
0
        let result = CallToolResult {
3555
0
            content: vec![ToolContent::Text { text: output }],
3556
0
            is_error: None,
3557
0
        };
3558
3559
0
        jsonrpc_success(id, result)
3560
0
    }
3561
3562
0
    pub(super) async fn call_list_components(
3563
0
        &mut self,
3564
0
        id: Option<serde_json::Value>,
3565
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
3566
0
    ) -> JsonRpcResponse {
3567
0
        let args = match arguments {
3568
0
            Some(a) => a,
3569
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
3570
        };
3571
3572
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
3573
0
            Some(p) => p,
3574
            None => {
3575
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
3576
            }
3577
        };
3578
3579
0
        let parent_name = match args.get("parent_name").and_then(|v| v.as_str()) {
3580
0
            Some(p) => p,
3581
            None => {
3582
0
                return JsonRpcResponse::error(
3583
0
                    id,
3584
                    INVALID_PARAMS,
3585
0
                    "Missing 'parent_name' argument",
3586
                );
3587
            }
3588
        };
3589
3590
0
        let org_id = match self.ensure_org().await {
3591
0
            Ok(id) => id,
3592
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
3593
        };
3594
3595
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
3596
0
            Ok(p) => p,
3597
0
            Err(e) => {
3598
0
                error!("Failed to fetch projects: {}", e);
3599
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
3600
            }
3601
        };
3602
3603
0
        let project = match projects
3604
0
            .iter()
3605
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
3606
        {
3607
0
            Some(p) => p,
3608
            None => {
3609
0
                return JsonRpcResponse::error(
3610
0
                    id,
3611
                    INVALID_PARAMS,
3612
0
                    &format!("Project '{}' not found", project_name),
3613
                );
3614
            }
3615
        };
3616
3617
0
        let parent_kind = match args.get("parent_kind").and_then(|v| v.as_str()) {
3618
0
            Some("container") => None,
3619
0
            Some("component") => Some(ComponentKind::Module),
3620
0
            Some(other) => match ComponentKind::from_str(other) {
3621
0
                Ok(kind) => Some(kind),
3622
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
3623
            },
3624
0
            None => None,
3625
        };
3626
3627
0
        let parent_id = match resolve_component_id_from_value(
3628
0
            self.service.component_repo.as_ref(),
3629
0
            &project.id,
3630
0
            parent_name,
3631
            true,
3632
0
            parent_kind,
3633
        )
3634
0
        .await
3635
        {
3636
0
            Ok(id) => id,
3637
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
3638
        };
3639
3640
0
        let components = match self
3641
0
            .service
3642
0
            .component_service
3643
0
            .list_components(&org_id, &project.id, &parent_id)
3644
0
            .await
3645
        {
3646
0
            Ok(list) => list,
3647
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
3648
        };
3649
0
        let components: Vec<_> = components
3650
0
            .into_iter()
3651
0
            .filter(|component| component.is_architecture_component())
3652
0
            .collect();
3653
3654
0
        let mut output = format!(
3655
            "📦 Components under parent `{}` in project **{}**\n\n",
3656
            parent_name, project.name
3657
        );
3658
3659
0
        if components.is_empty() {
3660
0
            output.push_str("No components found.");
3661
0
        } else {
3662
0
            for component in &components {
3663
0
                let component_view: ComponentView = component.into();
3664
0
                output.push_str(&format!(
3665
0
                    "- **{}** (`{}`)",
3666
0
                    component_view.name, component_view.path
3667
0
                ));
3668
0
                if let Some(ref desc) = component_view.description {
3669
0
                    output.push_str(&format!(": {}", desc));
3670
0
                }
3671
0
                output.push('\n');
3672
            }
3673
        }
3674
3675
0
        let result = CallToolResult {
3676
0
            content: vec![ToolContent::Text { text: output }],
3677
0
            is_error: None,
3678
0
        };
3679
3680
0
        jsonrpc_success(id, result)
3681
0
    }
3682
3683
0
    pub(super) async fn call_update_component(
3684
0
        &mut self,
3685
0
        id: Option<serde_json::Value>,
3686
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
3687
0
    ) -> JsonRpcResponse {
3688
0
        let args = match arguments {
3689
0
            Some(a) => a,
3690
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
3691
        };
3692
3693
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
3694
0
            Some(p) => p,
3695
            None => {
3696
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
3697
            }
3698
        };
3699
3700
0
        let parent_name = match args.get("parent_name").and_then(|v| v.as_str()) {
3701
0
            Some(p) => p,
3702
            None => {
3703
0
                return JsonRpcResponse::error(
3704
0
                    id,
3705
                    INVALID_PARAMS,
3706
0
                    "Missing 'parent_name' argument",
3707
                );
3708
            }
3709
        };
3710
3711
0
        let component_id = match args.get("component_id").and_then(|v| v.as_str()) {
3712
0
            Some(value) => match ComponentId::try_from(value.to_string()) {
3713
0
                Ok(id) => Some(id),
3714
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
3715
            },
3716
0
            None => None,
3717
        };
3718
0
        let component_name = args.get("name").and_then(|v| v.as_str());
3719
3720
0
        let new_name = args
3721
0
            .get("new_name")
3722
0
            .and_then(|v| v.as_str())
3723
0
            .map(|s| s.to_string());
3724
0
        let new_path = args
3725
0
            .get("path")
3726
0
            .and_then(|v| v.as_str())
3727
0
            .map(|s| s.to_string());
3728
0
        let new_description = args
3729
0
            .get("description")
3730
0
            .and_then(|v| v.as_str())
3731
0
            .map(|s| s.to_string());
3732
3733
0
        let org_id = match self.ensure_org().await {
3734
0
            Ok(id) => id,
3735
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
3736
        };
3737
3738
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
3739
0
            Ok(p) => p,
3740
0
            Err(e) => {
3741
0
                error!("Failed to fetch projects: {}", e);
3742
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
3743
            }
3744
        };
3745
3746
0
        let project = match projects
3747
0
            .iter()
3748
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
3749
        {
3750
0
            Some(p) => p,
3751
            None => {
3752
0
                return JsonRpcResponse::error(
3753
0
                    id,
3754
                    INVALID_PARAMS,
3755
0
                    &format!("Project '{}' not found", project_name),
3756
                );
3757
            }
3758
        };
3759
3760
0
        let parent_kind = match args.get("parent_kind").and_then(|v| v.as_str()) {
3761
0
            Some("container") => None,
3762
0
            Some("component") => Some(ComponentKind::Module),
3763
0
            Some(other) => match ComponentKind::from_str(other) {
3764
0
                Ok(kind) => Some(kind),
3765
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
3766
            },
3767
0
            None => None,
3768
        };
3769
3770
0
        let parent_id = match resolve_component_id_from_value(
3771
0
            self.service.component_repo.as_ref(),
3772
0
            &project.id,
3773
0
            parent_name,
3774
            true,
3775
0
            parent_kind,
3776
        )
3777
0
        .await
3778
        {
3779
0
            Ok(id) => id,
3780
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
3781
        };
3782
3783
0
        let component_id = match (component_id, component_name) {
3784
0
            (Some(id), _) => id,
3785
0
            (None, Some(name)) => match resolve_component_id_from_parent_value(
3786
0
                self.service.component_repo.as_ref(),
3787
0
                &parent_id,
3788
0
                name,
3789
            )
3790
0
            .await
3791
            {
3792
0
                Ok(id) => id,
3793
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
3794
            },
3795
            (None, None) => {
3796
0
                return JsonRpcResponse::error(
3797
0
                    id,
3798
                    INVALID_PARAMS,
3799
0
                    "Provide either 'component_id' or 'name'",
3800
                );
3801
            }
3802
        };
3803
3804
0
        let component_kind = args
3805
0
            .get("component_kind")
3806
0
            .and_then(|v| v.as_str())
3807
0
            .map(crate::models::component::ArchitectureComponentKind::from_str)
3808
0
            .transpose();
3809
0
        let component_kind = match component_kind {
3810
0
            Ok(value) => value,
3811
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
3812
        };
3813
3814
0
        let request = crate::models::component::UpdateArchitectureComponentRequest {
3815
0
            name: new_name,
3816
0
            description: new_description,
3817
0
            path: new_path,
3818
0
            metadata: component_kind.map(|kind| {
3819
0
                crate::models::component::ArchitectureComponentMetadata {
3820
0
                    component_kind: kind,
3821
0
                }
3822
0
            }),
3823
0
            position_x: None,
3824
0
            position_y: None,
3825
        };
3826
3827
0
        let component = match self
3828
0
            .service
3829
0
            .component_service
3830
0
            .update_component(
3831
0
                &org_id,
3832
0
                &project.id,
3833
0
                &self.user_id,
3834
0
                &parent_id,
3835
0
                &component_id,
3836
0
                request,
3837
            )
3838
0
            .await
3839
        {
3840
0
            Ok(m) => m,
3841
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
3842
        };
3843
3844
0
        let component_view: ComponentView = (&component).into();
3845
0
        let mut output = format!(
3846
            "✅ Updated component **{}** in project **{}**\n\n\
3847
             - **Path**: `{}`\n\
3848
             - **Parent**: `{}`",
3849
            component_view.name, project.name, component_view.path, parent_name
3850
        );
3851
0
        if let Some(ref desc) = component_view.description {
3852
0
            output.push_str(&format!("\n- **Description**: {}", desc));
3853
0
        }
3854
3855
0
        let result = CallToolResult {
3856
0
            content: vec![ToolContent::Text { text: output }],
3857
0
            is_error: None,
3858
0
        };
3859
3860
0
        jsonrpc_success(id, result)
3861
0
    }
3862
3863
0
    pub(super) async fn call_delete_component(
3864
0
        &mut self,
3865
0
        id: Option<serde_json::Value>,
3866
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
3867
0
    ) -> JsonRpcResponse {
3868
0
        let args = match arguments {
3869
0
            Some(a) => a,
3870
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
3871
        };
3872
3873
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
3874
0
            Some(p) => p,
3875
            None => {
3876
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
3877
            }
3878
        };
3879
3880
0
        let parent_name = match args.get("parent_name").and_then(|v| v.as_str()) {
3881
0
            Some(p) => p,
3882
            None => {
3883
0
                return JsonRpcResponse::error(
3884
0
                    id,
3885
                    INVALID_PARAMS,
3886
0
                    "Missing 'parent_name' argument",
3887
                );
3888
            }
3889
        };
3890
3891
0
        let component_id = match args.get("component_id").and_then(|v| v.as_str()) {
3892
0
            Some(value) => match ComponentId::try_from(value.to_string()) {
3893
0
                Ok(id) => Some(id),
3894
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
3895
            },
3896
0
            None => None,
3897
        };
3898
0
        let component_name = args.get("name").and_then(|v| v.as_str());
3899
3900
0
        let org_id = match self.ensure_org().await {
3901
0
            Ok(id) => id,
3902
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
3903
        };
3904
3905
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
3906
0
            Ok(p) => p,
3907
0
            Err(e) => {
3908
0
                error!("Failed to fetch projects: {}", e);
3909
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
3910
            }
3911
        };
3912
3913
0
        let project = match projects
3914
0
            .iter()
3915
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
3916
        {
3917
0
            Some(p) => p,
3918
            None => {
3919
0
                return JsonRpcResponse::error(
3920
0
                    id,
3921
                    INVALID_PARAMS,
3922
0
                    &format!("Project '{}' not found", project_name),
3923
                );
3924
            }
3925
        };
3926
3927
0
        let parent_kind = match args.get("parent_kind").and_then(|v| v.as_str()) {
3928
0
            Some("container") => None,
3929
0
            Some("component") => Some(ComponentKind::Module),
3930
0
            Some(other) => match ComponentKind::from_str(other) {
3931
0
                Ok(kind) => Some(kind),
3932
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
3933
            },
3934
0
            None => None,
3935
        };
3936
3937
0
        let parent_id = match resolve_component_id_from_value(
3938
0
            self.service.component_repo.as_ref(),
3939
0
            &project.id,
3940
0
            parent_name,
3941
            true,
3942
0
            parent_kind,
3943
        )
3944
0
        .await
3945
        {
3946
0
            Ok(id) => id,
3947
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
3948
        };
3949
3950
0
        let component_id = match (component_id, component_name) {
3951
0
            (Some(id), _) => id,
3952
0
            (None, Some(name)) => match resolve_component_id_from_parent_value(
3953
0
                self.service.component_repo.as_ref(),
3954
0
                &parent_id,
3955
0
                name,
3956
            )
3957
0
            .await
3958
            {
3959
0
                Ok(id) => id,
3960
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
3961
            },
3962
            (None, None) => {
3963
0
                return JsonRpcResponse::error(
3964
0
                    id,
3965
                    INVALID_PARAMS,
3966
0
                    "Provide either 'component_id' or 'name'",
3967
                );
3968
            }
3969
        };
3970
3971
0
        match self
3972
0
            .service
3973
0
            .component_service
3974
0
            .delete_component(
3975
0
                &org_id,
3976
0
                &project.id,
3977
0
                &self.user_id,
3978
0
                &parent_id,
3979
0
                &component_id,
3980
            )
3981
0
            .await
3982
        {
3983
0
            Ok(()) => {}
3984
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
3985
        }
3986
3987
0
        let output = format!(
3988
            "🗑️ Deleted component from parent `{}` in project **{}**",
3989
            parent_name, project.name
3990
        );
3991
3992
0
        let result = CallToolResult {
3993
0
            content: vec![ToolContent::Text { text: output }],
3994
0
            is_error: None,
3995
0
        };
3996
3997
0
        jsonrpc_success(id, result)
3998
0
    }
3999
4000
0
    pub(super) async fn call_delete_container(
4001
0
        &mut self,
4002
0
        id: Option<serde_json::Value>,
4003
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
4004
0
    ) -> JsonRpcResponse {
4005
0
        let args = match arguments {
4006
0
            Some(a) => a,
4007
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
4008
        };
4009
4010
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
4011
0
            Some(p) => p,
4012
            None => {
4013
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
4014
            }
4015
        };
4016
4017
0
        let name = args.get("name").and_then(|v| v.as_str());
4018
0
        let container_id = match args.get("container_id").and_then(|v| v.as_str()) {
4019
0
            Some(value) => match ComponentId::try_from(value.to_string()) {
4020
0
                Ok(id) => Some(id),
4021
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
4022
            },
4023
0
            None => None,
4024
        };
4025
4026
0
        let org_id = match self.ensure_org().await {
4027
0
            Ok(id) => id,
4028
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4029
        };
4030
4031
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
4032
0
            Ok(p) => p,
4033
0
            Err(e) => {
4034
0
                error!("Failed to fetch projects: {}", e);
4035
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
4036
            }
4037
        };
4038
4039
0
        let project = match projects
4040
0
            .iter()
4041
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
4042
        {
4043
0
            Some(p) => p,
4044
            None => {
4045
0
                return JsonRpcResponse::error(
4046
0
                    id,
4047
                    INVALID_PARAMS,
4048
0
                    &format!("Project '{}' not found", project_name),
4049
                );
4050
            }
4051
        };
4052
4053
0
        let containers = match self
4054
0
            .service
4055
0
            .component_repo
4056
0
            .list_containers(&project.id)
4057
0
            .await
4058
        {
4059
0
            Ok(c) => c,
4060
0
            Err(e) => {
4061
0
                error!("Failed to fetch containers: {}", e);
4062
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch containers");
4063
            }
4064
        };
4065
0
        let target_container = match (container_id, name) {
4066
0
            (Some(target_id), _) => match containers.into_iter().find(|c| c.id == target_id) {
4067
0
                Some(container) => container,
4068
                None => {
4069
0
                    return JsonRpcResponse::error(
4070
0
                        id,
4071
                        INVALID_PARAMS,
4072
0
                        &format!(
4073
0
                            "Container '{}' not found in project '{}'",
4074
0
                            target_id, project_name
4075
0
                        ),
4076
                    );
4077
                }
4078
            },
4079
0
            (None, Some(target_name)) => match containers.into_iter().find(|component| {
4080
0
                component.is_container() && component.name.eq_ignore_ascii_case(target_name)
4081
0
            }) {
4082
0
                Some(container) => container,
4083
                None => {
4084
0
                    return JsonRpcResponse::error(
4085
0
                        id,
4086
                        INVALID_PARAMS,
4087
0
                        &format!(
4088
0
                            "Container '{}' not found in project '{}'",
4089
0
                            target_name, project_name
4090
0
                        ),
4091
                    );
4092
                }
4093
            },
4094
            (None, None) => {
4095
0
                return JsonRpcResponse::error(
4096
0
                    id,
4097
                    INVALID_PARAMS,
4098
0
                    "Provide either 'container_id' or 'name'",
4099
                );
4100
            }
4101
        };
4102
4103
0
        match self
4104
0
            .service
4105
0
            .component_service
4106
0
            .delete_container(&org_id, &project.id, &self.user_id, &target_container.id)
4107
0
            .await
4108
        {
4109
0
            Ok(()) => {}
4110
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4111
        }
4112
4113
0
        let output = format!(
4114
            "🗑️ Deleted container **{}** from project **{}**",
4115
            target_container.name, project.name
4116
        );
4117
4118
0
        let result = CallToolResult {
4119
0
            content: vec![ToolContent::Text { text: output }],
4120
0
            is_error: None,
4121
0
        };
4122
4123
0
        jsonrpc_success(id, result)
4124
0
    }
4125
4126
0
    pub(super) async fn call_update_container(
4127
0
        &mut self,
4128
0
        id: Option<serde_json::Value>,
4129
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
4130
0
    ) -> JsonRpcResponse {
4131
0
        let args = match arguments {
4132
0
            Some(a) => a,
4133
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
4134
        };
4135
4136
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
4137
0
            Some(p) => p,
4138
            None => {
4139
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
4140
            }
4141
        };
4142
4143
0
        let name = args.get("name").and_then(|v| v.as_str());
4144
0
        let container_id = match args.get("container_id").and_then(|v| v.as_str()) {
4145
0
            Some(value) => match ComponentId::try_from(value.to_string()) {
4146
0
                Ok(id) => Some(id),
4147
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
4148
            },
4149
0
            None => None,
4150
        };
4151
4152
0
        let new_name = args
4153
0
            .get("new_name")
4154
0
            .and_then(|v| v.as_str())
4155
0
            .map(|s| s.to_string());
4156
0
        let new_path = args
4157
0
            .get("path")
4158
0
            .and_then(|v| v.as_str())
4159
0
            .map(|s| s.to_string());
4160
0
        let new_description = args
4161
0
            .get("description")
4162
0
            .and_then(|v| v.as_str())
4163
0
            .map(|s| s.to_string());
4164
4165
0
        let new_technology = args
4166
0
            .get("technology")
4167
0
            .and_then(|v| v.as_str())
4168
0
            .map(|s| s.to_string());
4169
4170
0
        let new_framework = args
4171
0
            .get("framework")
4172
0
            .and_then(|v| v.as_str())
4173
0
            .map(|s| s.to_string());
4174
0
        let new_runtime = args
4175
0
            .get("runtime")
4176
0
            .and_then(|v| v.as_str())
4177
0
            .map(|s| s.to_string());
4178
0
        let new_deployment = args
4179
0
            .get("deployment")
4180
0
            .and_then(|v| v.as_str())
4181
0
            .map(|s| s.to_string());
4182
0
        let new_container_kind = args
4183
0
            .get("container_kind")
4184
0
            .and_then(|v| v.as_str())
4185
0
            .map(crate::models::component::ContainerKind::from_str)
4186
0
            .transpose();
4187
0
        let new_container_kind = match new_container_kind {
4188
0
            Ok(value) => value,
4189
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
4190
        };
4191
4192
0
        let org_id = match self.ensure_org().await {
4193
0
            Ok(id) => id,
4194
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4195
        };
4196
4197
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
4198
0
            Ok(p) => p,
4199
0
            Err(e) => {
4200
0
                error!("Failed to fetch projects: {}", e);
4201
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
4202
            }
4203
        };
4204
4205
0
        let project = match projects
4206
0
            .iter()
4207
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
4208
        {
4209
0
            Some(p) => p,
4210
            None => {
4211
0
                return JsonRpcResponse::error(
4212
0
                    id,
4213
                    INVALID_PARAMS,
4214
0
                    &format!("Project '{}' not found", project_name),
4215
                );
4216
            }
4217
        };
4218
4219
0
        let containers = match self
4220
0
            .service
4221
0
            .component_repo
4222
0
            .list_containers(&project.id)
4223
0
            .await
4224
        {
4225
0
            Ok(c) => c,
4226
0
            Err(e) => {
4227
0
                error!("Failed to fetch containers: {}", e);
4228
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch containers");
4229
            }
4230
        };
4231
0
        let target_container = match (container_id, name) {
4232
0
            (Some(target_id), _) => match containers.into_iter().find(|c| c.id == target_id) {
4233
0
                Some(container) => container,
4234
                None => {
4235
0
                    return JsonRpcResponse::error(
4236
0
                        id,
4237
                        INVALID_PARAMS,
4238
0
                        &format!(
4239
0
                            "Container '{}' not found in project '{}'",
4240
0
                            target_id, project_name
4241
0
                        ),
4242
                    );
4243
                }
4244
            },
4245
0
            (None, Some(target_name)) => match containers.into_iter().find(|component| {
4246
0
                component.is_container() && component.name.eq_ignore_ascii_case(target_name)
4247
0
            }) {
4248
0
                Some(container) => container,
4249
                None => {
4250
0
                    return JsonRpcResponse::error(
4251
0
                        id,
4252
                        INVALID_PARAMS,
4253
0
                        &format!(
4254
0
                            "Container '{}' not found in project '{}'",
4255
0
                            target_name, project_name
4256
0
                        ),
4257
                    );
4258
                }
4259
            },
4260
            (None, None) => {
4261
0
                return JsonRpcResponse::error(
4262
0
                    id,
4263
                    INVALID_PARAMS,
4264
0
                    "Provide either 'container_id' or 'name'",
4265
                );
4266
            }
4267
        };
4268
4269
0
        let container_kind = new_container_kind.unwrap_or(match target_container.kind {
4270
0
            ComponentKind::App => crate::models::component::ContainerKind::App,
4271
0
            ComponentKind::Service => crate::models::component::ContainerKind::Service,
4272
0
            ComponentKind::Library => crate::models::component::ContainerKind::Library,
4273
0
            _ => crate::models::component::ContainerKind::Other,
4274
        });
4275
4276
0
        let metadata_changed = new_container_kind.is_some()
4277
0
            || new_technology.is_some()
4278
0
            || new_framework.is_some()
4279
0
            || new_runtime.is_some()
4280
0
            || new_deployment.is_some();
4281
4282
0
        let update_request = crate::models::component::UpdateContainerRequest {
4283
0
            name: new_name.clone(),
4284
0
            path: new_path.clone(),
4285
0
            description: new_description.clone(),
4286
0
            metadata: metadata_changed.then_some(crate::models::component::ContainerMetadata {
4287
0
                container_kind,
4288
0
                technology: new_technology,
4289
0
                framework: new_framework,
4290
0
                runtime: new_runtime,
4291
0
                deployment: new_deployment,
4292
0
            }),
4293
0
            position_x: None,
4294
0
            position_y: None,
4295
0
        };
4296
4297
0
        match self
4298
0
            .service
4299
0
            .component_service
4300
0
            .update_container(
4301
0
                &org_id,
4302
0
                &project.id,
4303
0
                &self.user_id,
4304
0
                &target_container.id,
4305
0
                update_request,
4306
            )
4307
0
            .await
4308
        {
4309
0
            Ok(updated_container) => {
4310
0
                let container_view: ComponentView = (&updated_container).into();
4311
0
                let output = format!(
4312
                    "✅ Updated container **{}** in project **{}**\n\n\
4313
                     - **Path**: `{}`\n\
4314
                     - **Description**: {}",
4315
                    container_view.name,
4316
                    project.name,
4317
                    container_view.path,
4318
0
                    container_view.description.as_deref().unwrap_or("(none)")
4319
                );
4320
4321
0
                let result = CallToolResult {
4322
0
                    content: vec![ToolContent::Text { text: output }],
4323
0
                    is_error: None,
4324
0
                };
4325
4326
0
                jsonrpc_success(id, result)
4327
            }
4328
0
            Err(e) => {
4329
0
                error!("Failed to update container: {}", e);
4330
0
                JsonRpcResponse::error(
4331
0
                    id,
4332
                    INTERNAL_ERROR,
4333
0
                    &format!("Failed to update container: {}", e),
4334
                )
4335
            }
4336
        }
4337
0
    }
4338
4339
1
    pub(super) async fn call_create_store(
4340
1
        &mut self,
4341
1
        id: Option<serde_json::Value>,
4342
1
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
4343
1
    ) -> JsonRpcResponse {
4344
1
        let args = match arguments {
4345
1
            Some(a) => a,
4346
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
4347
        };
4348
4349
1
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
4350
1
            Some(p) => p,
4351
            None => {
4352
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
4353
            }
4354
        };
4355
4356
1
        let name = match args.get("name").and_then(|v| v.as_str()) {
4357
1
            Some(n) => n,
4358
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
4359
        };
4360
4361
1
        let store_type = match args.get("store_type").and_then(|v| v.as_str()) {
4362
1
            Some(t) => t,
4363
            None => {
4364
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'store_type' argument");
4365
            }
4366
        };
4367
4368
1
        let ownership_str = match args.get("ownership").and_then(|v| v.as_str()) {
4369
1
            Some(o) => o,
4370
            None => {
4371
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'ownership' argument");
4372
            }
4373
        };
4374
4375
1
        let ownership = match StoreOwnership::from_str(ownership_str) {
4376
1
            Ok(o) => o,
4377
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
4378
        };
4379
4380
1
        let description = args
4381
1
            .get("description")
4382
1
            .and_then(|v| v.as_str())
4383
1
            .map(|s| s.to_string());
4384
4385
1
        let tags = match parse_optional_string_list(&args, "tags") {
4386
1
            Ok(tags) => tags,
4387
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4388
        };
4389
4390
1
        let position_x = args.get("position_x").and_then(|v| 
v0
.
as_f640
());
4391
1
        let position_y = args.get("position_y").and_then(|v| 
v0
.
as_f640
());
4392
4393
1
        let org_id = match self.ensure_org().await {
4394
1
            Ok(id) => id,
4395
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4396
        };
4397
4398
1
        let projects = match self.service.project_repo.list_projects(&org_id).await {
4399
1
            Ok(p) => p,
4400
0
            Err(e) => {
4401
0
                error!("Failed to fetch projects: {}", e);
4402
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
4403
            }
4404
        };
4405
4406
1
        let project = match projects
4407
1
            .iter()
4408
1
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
4409
        {
4410
1
            Some(p) => p,
4411
            None => {
4412
0
                return JsonRpcResponse::error(
4413
0
                    id,
4414
                    INVALID_PARAMS,
4415
0
                    &format!("Project '{}' not found", project_name),
4416
                );
4417
            }
4418
        };
4419
4420
1
        let request = CreateStoreRequest {
4421
1
            name: name.to_string(),
4422
1
            description,
4423
1
            store_type: store_type.to_string(),
4424
1
            ownership,
4425
1
            tags,
4426
1
            position_x,
4427
1
            position_y,
4428
1
        };
4429
4430
1
        let store = match self
4431
1
            .service
4432
1
            .store_service
4433
1
            .create_store(&org_id, &project.id, &self.user_id, &request)
4434
1
            .await
4435
        {
4436
1
            Ok(s) => s,
4437
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4438
        };
4439
4440
1
        let store_view: StoreView = (&store).into();
4441
1
        let mut output = format!(
4442
            "✅ Created store **{}** in project **{}**\n\n- **Type**: `{}`\n- **Ownership**: {}",
4443
            store_view.name, project.name, store_view.store_type, store_view.ownership
4444
        );
4445
4446
1
        if let Some(ref desc) = store_view.description {
4447
1
            output.push_str(&format!("\n- **Description**: {}", desc));
4448
1
        
}0
4449
1
        if let Some(
ref tags0
) = store_view.tags {
4450
0
            output.push_str(&format!("\n- **Tags**: {}", tags.join(", ")));
4451
1
        }
4452
4453
1
        output.push_str(&format!("\n- **ID**: `{}`", store_view.id));
4454
4455
1
        let result = CallToolResult {
4456
1
            content: vec![ToolContent::Text { text: output }],
4457
1
            is_error: None,
4458
1
        };
4459
4460
1
        jsonrpc_success(id, result)
4461
1
    }
4462
4463
0
    pub(super) async fn call_get_store(
4464
0
        &mut self,
4465
0
        id: Option<serde_json::Value>,
4466
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
4467
0
    ) -> JsonRpcResponse {
4468
0
        let args = match arguments {
4469
0
            Some(a) => a,
4470
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
4471
        };
4472
4473
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
4474
0
            Some(p) => p,
4475
            None => {
4476
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
4477
            }
4478
        };
4479
4480
0
        let store_value = args
4481
0
            .get("store_id")
4482
0
            .and_then(|v| v.as_str())
4483
0
            .or_else(|| args.get("name").and_then(|v| v.as_str()));
4484
4485
0
        let store_value = match store_value {
4486
0
            Some(v) => v,
4487
            None => {
4488
0
                return JsonRpcResponse::error(
4489
0
                    id,
4490
                    INVALID_PARAMS,
4491
0
                    "Missing 'store_id' or 'name' argument",
4492
                );
4493
            }
4494
        };
4495
4496
0
        let org_id = match self.ensure_org().await {
4497
0
            Ok(id) => id,
4498
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4499
        };
4500
4501
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
4502
0
            Ok(p) => p,
4503
0
            Err(e) => {
4504
0
                error!("Failed to fetch projects: {}", e);
4505
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
4506
            }
4507
        };
4508
4509
0
        let project = match projects
4510
0
            .iter()
4511
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
4512
        {
4513
0
            Some(p) => p,
4514
            None => {
4515
0
                return JsonRpcResponse::error(
4516
0
                    id,
4517
                    INVALID_PARAMS,
4518
0
                    &format!("Project '{}' not found", project_name),
4519
                );
4520
            }
4521
        };
4522
4523
0
        let store_id = match resolve_store_id_from_value(
4524
0
            &self.service.store_service,
4525
0
            &org_id,
4526
0
            &project.id,
4527
0
            &self.user_id,
4528
0
            store_value,
4529
        )
4530
0
        .await
4531
        {
4532
0
            Ok(id) => id,
4533
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4534
        };
4535
4536
0
        let store = match self
4537
0
            .service
4538
0
            .store_service
4539
0
            .get_store(&org_id, &project.id, &store_id, &self.user_id)
4540
0
            .await
4541
        {
4542
0
            Ok(s) => s,
4543
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4544
        };
4545
4546
0
        let store_view: StoreView = (&store).into();
4547
0
        let mut output = format!(
4548
            "🗄️ Store **{}**\n\n- **Type**: `{}`\n- **Ownership**: {}",
4549
            store_view.name, store_view.store_type, store_view.ownership
4550
        );
4551
4552
0
        if let Some(ref desc) = store_view.description {
4553
0
            output.push_str(&format!("\n- **Description**: {}", desc));
4554
0
        }
4555
0
        if let Some(ref tags) = store_view.tags {
4556
0
            output.push_str(&format!("\n- **Tags**: {}", tags.join(", ")));
4557
0
        }
4558
4559
0
        output.push_str(&format!("\n- **ID**: `{}`", store_view.id));
4560
4561
0
        let result = CallToolResult {
4562
0
            content: vec![ToolContent::Text { text: output }],
4563
0
            is_error: None,
4564
0
        };
4565
4566
0
        jsonrpc_success(id, result)
4567
0
    }
4568
4569
0
    pub(super) async fn call_list_stores(
4570
0
        &mut self,
4571
0
        id: Option<serde_json::Value>,
4572
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
4573
0
    ) -> JsonRpcResponse {
4574
0
        let args = match arguments {
4575
0
            Some(a) => a,
4576
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
4577
        };
4578
4579
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
4580
0
            Some(p) => p,
4581
            None => {
4582
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
4583
            }
4584
        };
4585
4586
0
        let org_id = match self.ensure_org().await {
4587
0
            Ok(id) => id,
4588
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4589
        };
4590
4591
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
4592
0
            Ok(p) => p,
4593
0
            Err(e) => {
4594
0
                error!("Failed to fetch projects: {}", e);
4595
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
4596
            }
4597
        };
4598
4599
0
        let project = match projects
4600
0
            .iter()
4601
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
4602
        {
4603
0
            Some(p) => p,
4604
            None => {
4605
0
                return JsonRpcResponse::error(
4606
0
                    id,
4607
                    INVALID_PARAMS,
4608
0
                    &format!("Project '{}' not found", project_name),
4609
                );
4610
            }
4611
        };
4612
4613
0
        let stores = match self
4614
0
            .service
4615
0
            .store_service
4616
0
            .list_stores(&org_id, &project.id, &self.user_id)
4617
0
            .await
4618
        {
4619
0
            Ok(list) => list,
4620
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4621
        };
4622
4623
0
        let mut output = format!("🗄️ Stores in project **{}**\n\n", project.name);
4624
4625
0
        if stores.is_empty() {
4626
0
            output.push_str("No stores found.");
4627
0
        } else {
4628
0
            for store in &stores {
4629
0
                let store_view: StoreView = store.into();
4630
0
                output.push_str(&format!(
4631
0
                    "- **{}** (`{}`) [{}]\n",
4632
0
                    store_view.name, store_view.id, store_view.store_type
4633
0
                ));
4634
0
            }
4635
        }
4636
4637
0
        let result = CallToolResult {
4638
0
            content: vec![ToolContent::Text { text: output }],
4639
0
            is_error: None,
4640
0
        };
4641
4642
0
        jsonrpc_success(id, result)
4643
0
    }
4644
4645
0
    pub(super) async fn call_update_store(
4646
0
        &mut self,
4647
0
        id: Option<serde_json::Value>,
4648
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
4649
0
    ) -> JsonRpcResponse {
4650
0
        let args = match arguments {
4651
0
            Some(a) => a,
4652
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
4653
        };
4654
4655
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
4656
0
            Some(p) => p,
4657
            None => {
4658
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
4659
            }
4660
        };
4661
4662
0
        let store_value = args
4663
0
            .get("store_id")
4664
0
            .and_then(|v| v.as_str())
4665
0
            .or_else(|| args.get("name").and_then(|v| v.as_str()));
4666
4667
0
        let store_value = match store_value {
4668
0
            Some(v) => v,
4669
            None => {
4670
0
                return JsonRpcResponse::error(
4671
0
                    id,
4672
                    INVALID_PARAMS,
4673
0
                    "Missing 'store_id' or 'name' argument",
4674
                );
4675
            }
4676
        };
4677
4678
0
        let new_name = args
4679
0
            .get("new_name")
4680
0
            .and_then(|v| v.as_str())
4681
0
            .map(|s| s.to_string());
4682
4683
0
        let new_description = args
4684
0
            .get("description")
4685
0
            .and_then(|v| v.as_str())
4686
0
            .map(|s| s.to_string());
4687
4688
0
        let new_type = args
4689
0
            .get("store_type")
4690
0
            .and_then(|v| v.as_str())
4691
0
            .map(|s| s.to_string());
4692
4693
0
        let new_ownership = match args.get("ownership").and_then(|v| v.as_str()) {
4694
0
            Some(value) => Some(match StoreOwnership::from_str(value) {
4695
0
                Ok(o) => o,
4696
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e.into()),
4697
            }),
4698
0
            None => None,
4699
        };
4700
4701
0
        let new_tags = match parse_optional_string_list(&args, "tags") {
4702
0
            Ok(tags) => tags,
4703
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4704
        };
4705
4706
0
        let position_x = args.get("position_x").and_then(|v| v.as_f64());
4707
0
        let position_y = args.get("position_y").and_then(|v| v.as_f64());
4708
4709
0
        let org_id = match self.ensure_org().await {
4710
0
            Ok(id) => id,
4711
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4712
        };
4713
4714
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
4715
0
            Ok(p) => p,
4716
0
            Err(e) => {
4717
0
                error!("Failed to fetch projects: {}", e);
4718
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
4719
            }
4720
        };
4721
4722
0
        let project = match projects
4723
0
            .iter()
4724
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
4725
        {
4726
0
            Some(p) => p,
4727
            None => {
4728
0
                return JsonRpcResponse::error(
4729
0
                    id,
4730
                    INVALID_PARAMS,
4731
0
                    &format!("Project '{}' not found", project_name),
4732
                );
4733
            }
4734
        };
4735
4736
0
        let store_id = match resolve_store_id_from_value(
4737
0
            &self.service.store_service,
4738
0
            &org_id,
4739
0
            &project.id,
4740
0
            &self.user_id,
4741
0
            store_value,
4742
        )
4743
0
        .await
4744
        {
4745
0
            Ok(id) => id,
4746
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4747
        };
4748
4749
0
        let request = UpdateStoreRequest {
4750
0
            name: new_name,
4751
0
            description: new_description,
4752
0
            store_type: new_type,
4753
0
            ownership: new_ownership,
4754
0
            tags: new_tags,
4755
0
            position_x,
4756
0
            position_y,
4757
0
        };
4758
4759
0
        let store = match self
4760
0
            .service
4761
0
            .store_service
4762
0
            .update_store(&org_id, &project.id, &store_id, &self.user_id, request)
4763
0
            .await
4764
        {
4765
0
            Ok(s) => s,
4766
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4767
        };
4768
4769
0
        let store_view: StoreView = (&store).into();
4770
0
        let mut output = format!(
4771
            "✅ Updated store **{}** in project **{}**\n\n- **Type**: `{}`\n- **Ownership**: {}",
4772
            store_view.name, project.name, store_view.store_type, store_view.ownership
4773
        );
4774
4775
0
        if let Some(ref desc) = store_view.description {
4776
0
            output.push_str(&format!("\n- **Description**: {}", desc));
4777
0
        }
4778
0
        if let Some(ref tags) = store_view.tags {
4779
0
            output.push_str(&format!("\n- **Tags**: {}", tags.join(", ")));
4780
0
        }
4781
4782
0
        output.push_str(&format!("\n- **ID**: `{}`", store_view.id));
4783
4784
0
        let result = CallToolResult {
4785
0
            content: vec![ToolContent::Text { text: output }],
4786
0
            is_error: None,
4787
0
        };
4788
4789
0
        jsonrpc_success(id, result)
4790
0
    }
4791
4792
0
    pub(super) async fn call_delete_store(
4793
0
        &mut self,
4794
0
        id: Option<serde_json::Value>,
4795
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
4796
0
    ) -> JsonRpcResponse {
4797
0
        let args = match arguments {
4798
0
            Some(a) => a,
4799
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
4800
        };
4801
4802
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
4803
0
            Some(p) => p,
4804
            None => {
4805
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
4806
            }
4807
        };
4808
4809
0
        let store_value = args
4810
0
            .get("store_id")
4811
0
            .and_then(|v| v.as_str())
4812
0
            .or_else(|| args.get("name").and_then(|v| v.as_str()));
4813
4814
0
        let store_value = match store_value {
4815
0
            Some(v) => v,
4816
            None => {
4817
0
                return JsonRpcResponse::error(
4818
0
                    id,
4819
                    INVALID_PARAMS,
4820
0
                    "Missing 'store_id' or 'name' argument",
4821
                );
4822
            }
4823
        };
4824
4825
0
        let org_id = match self.ensure_org().await {
4826
0
            Ok(id) => id,
4827
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4828
        };
4829
4830
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
4831
0
            Ok(p) => p,
4832
0
            Err(e) => {
4833
0
                error!("Failed to fetch projects: {}", e);
4834
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
4835
            }
4836
        };
4837
4838
0
        let project = match projects
4839
0
            .iter()
4840
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
4841
        {
4842
0
            Some(p) => p,
4843
            None => {
4844
0
                return JsonRpcResponse::error(
4845
0
                    id,
4846
                    INVALID_PARAMS,
4847
0
                    &format!("Project '{}' not found", project_name),
4848
                );
4849
            }
4850
        };
4851
4852
0
        let store_id = match resolve_store_id_from_value(
4853
0
            &self.service.store_service,
4854
0
            &org_id,
4855
0
            &project.id,
4856
0
            &self.user_id,
4857
0
            store_value,
4858
        )
4859
0
        .await
4860
        {
4861
0
            Ok(id) => id,
4862
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4863
        };
4864
4865
0
        match self
4866
0
            .service
4867
0
            .store_service
4868
0
            .delete_store(&org_id, &project.id, &store_id, &self.user_id)
4869
0
            .await
4870
        {
4871
0
            Ok(()) => {}
4872
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4873
        }
4874
4875
0
        let output = format!(
4876
            "🗑️ Deleted store `{}` from project **{}**",
4877
            store_id, project.name
4878
        );
4879
4880
0
        let result = CallToolResult {
4881
0
            content: vec![ToolContent::Text { text: output }],
4882
0
            is_error: None,
4883
0
        };
4884
4885
0
        jsonrpc_success(id, result)
4886
0
    }
4887
4888
0
    pub(super) async fn call_create_actor(
4889
0
        &mut self,
4890
0
        id: Option<serde_json::Value>,
4891
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
4892
0
    ) -> JsonRpcResponse {
4893
0
        let args = match arguments {
4894
0
            Some(a) => a,
4895
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
4896
        };
4897
4898
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
4899
0
            Some(p) => p,
4900
            None => {
4901
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
4902
            }
4903
        };
4904
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
4905
0
            Some(v) => v.to_string(),
4906
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
4907
        };
4908
0
        let actor_type = match args.get("actor_type").and_then(|v| v.as_str()) {
4909
0
            Some(v) => v.to_string(),
4910
            None => {
4911
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'actor_type' argument");
4912
            }
4913
        };
4914
4915
0
        let description = args
4916
0
            .get("description")
4917
0
            .and_then(|v| v.as_str())
4918
0
            .map(str::to_string);
4919
0
        let interaction_mode = args
4920
0
            .get("interaction_mode")
4921
0
            .and_then(|v| v.as_str())
4922
0
            .map(str::to_string);
4923
0
        let responsibilities = match parse_optional_string_list(&args, "responsibilities") {
4924
0
            Ok(value) => value,
4925
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4926
        };
4927
0
        let position_x = args.get("position_x").and_then(|v| v.as_f64());
4928
0
        let position_y = args.get("position_y").and_then(|v| v.as_f64());
4929
4930
0
        let org_id = match self.ensure_org().await {
4931
0
            Ok(value) => value,
4932
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4933
        };
4934
0
        let project = match self
4935
0
            .service
4936
0
            .project_repo
4937
0
            .list_projects(&org_id)
4938
0
            .await
4939
0
            .ok()
4940
0
            .and_then(|projects| {
4941
0
                projects
4942
0
                    .into_iter()
4943
0
                    .find(|p| p.name.eq_ignore_ascii_case(project_name))
4944
0
            }) {
4945
0
            Some(value) => value,
4946
            None => {
4947
0
                return JsonRpcResponse::error(
4948
0
                    id,
4949
                    INVALID_PARAMS,
4950
0
                    &format!("Project '{}' not found", project_name),
4951
                );
4952
            }
4953
        };
4954
4955
0
        let request = CreateActorRequest {
4956
0
            name,
4957
0
            description,
4958
0
            actor_type,
4959
0
            interaction_mode,
4960
0
            responsibilities,
4961
0
            position_x,
4962
0
            position_y,
4963
0
        };
4964
4965
0
        let actor = match self
4966
0
            .service
4967
0
            .actor_service
4968
0
            .create_actor(&org_id, &project.id, &self.user_id, &request)
4969
0
            .await
4970
        {
4971
0
            Ok(value) => value,
4972
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
4973
        };
4974
4975
0
        let view: ActorView = (&actor).into();
4976
0
        let result = CallToolResult {
4977
0
            content: vec![ToolContent::Text {
4978
0
                text: format!(
4979
0
                    "✅ Created actor **{}** in project **{}**\n\n- **Type**: `{}`\n- **ID**: `{}`",
4980
0
                    view.name, project.name, view.actor_type, view.id
4981
0
                ),
4982
0
            }],
4983
0
            is_error: None,
4984
0
        };
4985
0
        jsonrpc_success(id, result)
4986
0
    }
4987
4988
0
    pub(super) async fn call_get_actor(
4989
0
        &mut self,
4990
0
        id: Option<serde_json::Value>,
4991
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
4992
0
    ) -> JsonRpcResponse {
4993
0
        let args = match arguments {
4994
0
            Some(a) => a,
4995
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
4996
        };
4997
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
4998
0
            Some(v) => v,
4999
            None => {
5000
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
5001
            }
5002
        };
5003
0
        let actor_value = args
5004
0
            .get("actor_id")
5005
0
            .and_then(|v| v.as_str())
5006
0
            .or_else(|| args.get("name").and_then(|v| v.as_str()));
5007
0
        let actor_value = match actor_value {
5008
0
            Some(v) => v,
5009
            None => {
5010
0
                return JsonRpcResponse::error(
5011
0
                    id,
5012
                    INVALID_PARAMS,
5013
0
                    "Missing 'actor_id' or 'name' argument",
5014
                );
5015
            }
5016
        };
5017
0
        let org_id = match self.ensure_org().await {
5018
0
            Ok(value) => value,
5019
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5020
        };
5021
0
        let project = match self
5022
0
            .service
5023
0
            .project_repo
5024
0
            .list_projects(&org_id)
5025
0
            .await
5026
0
            .ok()
5027
0
            .and_then(|projects| {
5028
0
                projects
5029
0
                    .into_iter()
5030
0
                    .find(|p| p.name.eq_ignore_ascii_case(project_name))
5031
0
            }) {
5032
0
            Some(value) => value,
5033
            None => {
5034
0
                return JsonRpcResponse::error(
5035
0
                    id,
5036
                    INVALID_PARAMS,
5037
0
                    &format!("Project '{}' not found", project_name),
5038
                );
5039
            }
5040
        };
5041
5042
0
        let actor_id = match resolve_actor_id_from_value(
5043
0
            &self.service.actor_service,
5044
0
            &org_id,
5045
0
            &project.id,
5046
0
            &self.user_id,
5047
0
            actor_value,
5048
        )
5049
0
        .await
5050
        {
5051
0
            Ok(value) => value,
5052
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5053
        };
5054
5055
0
        let actor = match self
5056
0
            .service
5057
0
            .actor_service
5058
0
            .get_actor(&org_id, &project.id, &actor_id, &self.user_id)
5059
0
            .await
5060
        {
5061
0
            Ok(value) => value,
5062
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5063
        };
5064
0
        let view: ActorView = (&actor).into();
5065
0
        let mut text = format!(
5066
            "🧑 Actor **{}**\n\n- **Type**: `{}`\n- **ID**: `{}`",
5067
            view.name, view.actor_type, view.id
5068
        );
5069
0
        if let Some(description) = view.description {
5070
0
            text.push_str(&format!("\n- **Description**: {}", description));
5071
0
        }
5072
0
        if let Some(interaction_mode) = view.interaction_mode {
5073
0
            text.push_str(&format!("\n- **Interaction Mode**: `{}`", interaction_mode));
5074
0
        }
5075
0
        if let Some(responsibilities) = view.responsibilities
5076
0
            && !responsibilities.is_empty()
5077
0
        {
5078
0
            text.push_str(&format!(
5079
0
                "\n- **Responsibilities**: {}",
5080
0
                responsibilities.join(", ")
5081
0
            ));
5082
0
        }
5083
0
        if let (Some(x), Some(y)) = (view.position_x, view.position_y) {
5084
0
            text.push_str(&format!("\n- **Position**: ({x:.2}, {y:.2})"));
5085
0
        }
5086
0
        let result = CallToolResult {
5087
0
            content: vec![ToolContent::Text { text }],
5088
0
            is_error: None,
5089
0
        };
5090
0
        jsonrpc_success(id, result)
5091
0
    }
5092
5093
0
    pub(super) async fn call_list_actors(
5094
0
        &mut self,
5095
0
        id: Option<serde_json::Value>,
5096
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
5097
0
    ) -> JsonRpcResponse {
5098
0
        let args = match arguments {
5099
0
            Some(a) => a,
5100
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
5101
        };
5102
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
5103
0
            Some(v) => v,
5104
            None => {
5105
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
5106
            }
5107
        };
5108
0
        let org_id = match self.ensure_org().await {
5109
0
            Ok(value) => value,
5110
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5111
        };
5112
0
        let project = match self
5113
0
            .service
5114
0
            .project_repo
5115
0
            .list_projects(&org_id)
5116
0
            .await
5117
0
            .ok()
5118
0
            .and_then(|projects| {
5119
0
                projects
5120
0
                    .into_iter()
5121
0
                    .find(|p| p.name.eq_ignore_ascii_case(project_name))
5122
0
            }) {
5123
0
            Some(value) => value,
5124
            None => {
5125
0
                return JsonRpcResponse::error(
5126
0
                    id,
5127
                    INVALID_PARAMS,
5128
0
                    &format!("Project '{}' not found", project_name),
5129
                );
5130
            }
5131
        };
5132
5133
0
        let actors = match self
5134
0
            .service
5135
0
            .actor_service
5136
0
            .list_actors(&org_id, &project.id, &self.user_id)
5137
0
            .await
5138
        {
5139
0
            Ok(value) => value,
5140
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5141
        };
5142
5143
0
        let mut text = format!("🧑 Persons in project **{}**\n\n", project.name);
5144
0
        if actors.is_empty() {
5145
0
            text.push_str("No actors found.");
5146
0
        } else {
5147
0
            for actor in &actors {
5148
0
                let view: ActorView = actor.into();
5149
0
                text.push_str(&format!(
5150
0
                    "- **{}** (`{}`) [{}]",
5151
0
                    view.name, view.id, view.actor_type
5152
0
                ));
5153
0
                if let Some(ref interaction_mode) = view.interaction_mode {
5154
0
                    text.push_str(&format!(" mode=`{}`", interaction_mode));
5155
0
                }
5156
0
                if let Some(ref responsibilities) = view.responsibilities
5157
0
                    && !responsibilities.is_empty()
5158
0
                {
5159
0
                    text.push_str(&format!(
5160
0
                        " responsibilities={}",
5161
0
                        responsibilities.join(", ")
5162
0
                    ));
5163
0
                }
5164
0
                if let Some(ref description) = view.description {
5165
0
                    text.push_str(&format!(" - {}", description));
5166
0
                }
5167
0
                text.push('\n');
5168
            }
5169
        }
5170
5171
0
        jsonrpc_success(
5172
0
            id,
5173
0
            CallToolResult {
5174
0
                content: vec![ToolContent::Text { text }],
5175
0
                is_error: None,
5176
0
            },
5177
        )
5178
0
    }
5179
5180
0
    pub(super) async fn call_update_actor(
5181
0
        &mut self,
5182
0
        id: Option<serde_json::Value>,
5183
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
5184
0
    ) -> JsonRpcResponse {
5185
0
        let args = match arguments {
5186
0
            Some(a) => a,
5187
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
5188
        };
5189
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
5190
0
            Some(v) => v,
5191
            None => {
5192
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
5193
            }
5194
        };
5195
0
        let actor_value = args
5196
0
            .get("actor_id")
5197
0
            .and_then(|v| v.as_str())
5198
0
            .or_else(|| args.get("name").and_then(|v| v.as_str()));
5199
0
        let actor_value = match actor_value {
5200
0
            Some(v) => v,
5201
            None => {
5202
0
                return JsonRpcResponse::error(
5203
0
                    id,
5204
                    INVALID_PARAMS,
5205
0
                    "Missing 'actor_id' or 'name' argument",
5206
                );
5207
            }
5208
        };
5209
0
        let org_id = match self.ensure_org().await {
5210
0
            Ok(value) => value,
5211
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5212
        };
5213
0
        let project = match self
5214
0
            .service
5215
0
            .project_repo
5216
0
            .list_projects(&org_id)
5217
0
            .await
5218
0
            .ok()
5219
0
            .and_then(|projects| {
5220
0
                projects
5221
0
                    .into_iter()
5222
0
                    .find(|p| p.name.eq_ignore_ascii_case(project_name))
5223
0
            }) {
5224
0
            Some(value) => value,
5225
            None => {
5226
0
                return JsonRpcResponse::error(
5227
0
                    id,
5228
                    INVALID_PARAMS,
5229
0
                    &format!("Project '{}' not found", project_name),
5230
                );
5231
            }
5232
        };
5233
5234
0
        let actor_id = match resolve_actor_id_from_value(
5235
0
            &self.service.actor_service,
5236
0
            &org_id,
5237
0
            &project.id,
5238
0
            &self.user_id,
5239
0
            actor_value,
5240
        )
5241
0
        .await
5242
        {
5243
0
            Ok(value) => value,
5244
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5245
        };
5246
5247
0
        let request = UpdateActorRequest {
5248
0
            name: args
5249
0
                .get("new_name")
5250
0
                .and_then(|v| v.as_str())
5251
0
                .map(str::to_string),
5252
0
            description: args
5253
0
                .get("description")
5254
0
                .and_then(|v| v.as_str())
5255
0
                .map(str::to_string),
5256
0
            actor_type: args
5257
0
                .get("actor_type")
5258
0
                .and_then(|v| v.as_str())
5259
0
                .map(str::to_string),
5260
0
            interaction_mode: args
5261
0
                .get("interaction_mode")
5262
0
                .and_then(|v| v.as_str())
5263
0
                .map(str::to_string),
5264
0
            responsibilities: match parse_optional_string_list(&args, "responsibilities") {
5265
0
                Ok(value) => value,
5266
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
5267
            },
5268
0
            position_x: args.get("position_x").and_then(|v| v.as_f64()),
5269
0
            position_y: args.get("position_y").and_then(|v| v.as_f64()),
5270
        };
5271
5272
0
        let actor = match self
5273
0
            .service
5274
0
            .actor_service
5275
0
            .update_actor(&org_id, &project.id, &actor_id, &self.user_id, request)
5276
0
            .await
5277
        {
5278
0
            Ok(value) => value,
5279
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5280
        };
5281
0
        let view: ActorView = (&actor).into();
5282
0
        jsonrpc_success(
5283
0
            id,
5284
0
            CallToolResult {
5285
0
                content: vec![ToolContent::Text {
5286
0
                    text: format!(
5287
0
                        "✅ Updated actor **{}** in project **{}**\n\n- **Type**: `{}`\n- **ID**: `{}`",
5288
0
                        view.name, project.name, view.actor_type, view.id
5289
0
                    ),
5290
0
                }],
5291
0
                is_error: None,
5292
0
            },
5293
        )
5294
0
    }
5295
5296
0
    pub(super) async fn call_delete_actor(
5297
0
        &mut self,
5298
0
        id: Option<serde_json::Value>,
5299
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
5300
0
    ) -> JsonRpcResponse {
5301
0
        let args = match arguments {
5302
0
            Some(a) => a,
5303
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
5304
        };
5305
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
5306
0
            Some(v) => v,
5307
            None => {
5308
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
5309
            }
5310
        };
5311
0
        let actor_value = args
5312
0
            .get("actor_id")
5313
0
            .and_then(|v| v.as_str())
5314
0
            .or_else(|| args.get("name").and_then(|v| v.as_str()));
5315
0
        let actor_value = match actor_value {
5316
0
            Some(v) => v,
5317
            None => {
5318
0
                return JsonRpcResponse::error(
5319
0
                    id,
5320
                    INVALID_PARAMS,
5321
0
                    "Missing 'actor_id' or 'name' argument",
5322
                );
5323
            }
5324
        };
5325
0
        let org_id = match self.ensure_org().await {
5326
0
            Ok(value) => value,
5327
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5328
        };
5329
0
        let project = match self
5330
0
            .service
5331
0
            .project_repo
5332
0
            .list_projects(&org_id)
5333
0
            .await
5334
0
            .ok()
5335
0
            .and_then(|projects| {
5336
0
                projects
5337
0
                    .into_iter()
5338
0
                    .find(|p| p.name.eq_ignore_ascii_case(project_name))
5339
0
            }) {
5340
0
            Some(value) => value,
5341
            None => {
5342
0
                return JsonRpcResponse::error(
5343
0
                    id,
5344
                    INVALID_PARAMS,
5345
0
                    &format!("Project '{}' not found", project_name),
5346
                );
5347
            }
5348
        };
5349
0
        let actor_id = match resolve_actor_id_from_value(
5350
0
            &self.service.actor_service,
5351
0
            &org_id,
5352
0
            &project.id,
5353
0
            &self.user_id,
5354
0
            actor_value,
5355
        )
5356
0
        .await
5357
        {
5358
0
            Ok(value) => value,
5359
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5360
        };
5361
5362
0
        match self
5363
0
            .service
5364
0
            .actor_service
5365
0
            .delete_actor(&org_id, &project.id, &actor_id, &self.user_id)
5366
0
            .await
5367
        {
5368
0
            Ok(()) => {}
5369
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5370
        }
5371
5372
0
        jsonrpc_success(
5373
0
            id,
5374
0
            CallToolResult {
5375
0
                content: vec![ToolContent::Text {
5376
0
                    text: format!(
5377
0
                        "🗑️ Deleted actor `{}` from project **{}**",
5378
0
                        actor_id, project.name
5379
0
                    ),
5380
0
                }],
5381
0
                is_error: None,
5382
0
            },
5383
        )
5384
0
    }
5385
5386
1
    pub(super) async fn call_create_external_system(
5387
1
        &mut self,
5388
1
        id: Option<serde_json::Value>,
5389
1
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
5390
1
    ) -> JsonRpcResponse {
5391
1
        let args = match arguments {
5392
1
            Some(a) => a,
5393
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
5394
        };
5395
5396
1
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
5397
1
            Some(p) => p,
5398
            None => {
5399
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
5400
            }
5401
        };
5402
5403
1
        let name = match args.get("name").and_then(|v| v.as_str()) {
5404
1
            Some(n) => n,
5405
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
5406
        };
5407
5408
1
        let category = match args.get("category").and_then(|v| v.as_str()) {
5409
1
            Some(c) => c,
5410
            None => {
5411
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'category' argument");
5412
            }
5413
        };
5414
5415
1
        let vendor = args
5416
1
            .get("vendor")
5417
1
            .and_then(|v| v.as_str())
5418
1
            .map(|s| s.to_string());
5419
5420
1
        let description = args
5421
1
            .get("description")
5422
1
            .and_then(|v| 
v0
.
as_str0
())
5423
1
            .map(|s| 
s0
.
to_string0
());
5424
5425
1
        let tags = match parse_optional_string_list(&args, "tags") {
5426
1
            Ok(tags) => tags,
5427
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5428
        };
5429
5430
1
        let position_x = args.get("position_x").and_then(|v| 
v0
.
as_f640
());
5431
1
        let position_y = args.get("position_y").and_then(|v| 
v0
.
as_f640
());
5432
5433
1
        let org_id = match self.ensure_org().await {
5434
1
            Ok(id) => id,
5435
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5436
        };
5437
5438
1
        let projects = match self.service.project_repo.list_projects(&org_id).await {
5439
1
            Ok(p) => p,
5440
0
            Err(e) => {
5441
0
                error!("Failed to fetch projects: {}", e);
5442
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
5443
            }
5444
        };
5445
5446
1
        let project = match projects
5447
1
            .iter()
5448
1
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
5449
        {
5450
1
            Some(p) => p,
5451
            None => {
5452
0
                return JsonRpcResponse::error(
5453
0
                    id,
5454
                    INVALID_PARAMS,
5455
0
                    &format!("Project '{}' not found", project_name),
5456
                );
5457
            }
5458
        };
5459
5460
1
        let request = CreateExternalSystemRequest {
5461
1
            name: name.to_string(),
5462
1
            description,
5463
1
            category: category.to_string(),
5464
1
            vendor,
5465
1
            tags,
5466
1
            position_x,
5467
1
            position_y,
5468
1
        };
5469
5470
1
        let system = match self
5471
1
            .service
5472
1
            .external_system_service
5473
1
            .create_external_system(&org_id, &project.id, &self.user_id, &request)
5474
1
            .await
5475
        {
5476
1
            Ok(s) => s,
5477
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5478
        };
5479
5480
1
        let system_view: ExternalSystemView = (&system).into();
5481
1
        let mut output = format!(
5482
            "✅ Created external system **{}** in project **{}**\n\n- **Category**: `{}`",
5483
            system_view.name, project.name, system_view.category
5484
        );
5485
5486
1
        if let Some(ref vendor) = system_view.vendor {
5487
1
            output.push_str(&format!("\n- **Vendor**: {}", vendor));
5488
1
        
}0
5489
1
        if let Some(
ref desc0
) = system_view.description {
5490
0
            output.push_str(&format!("\n- **Description**: {}", desc));
5491
1
        }
5492
1
        if let Some(
ref tags0
) = system_view.tags {
5493
0
            output.push_str(&format!("\n- **Tags**: {}", tags.join(", ")));
5494
1
        }
5495
5496
1
        output.push_str(&format!("\n- **ID**: `{}`", system_view.id));
5497
5498
1
        let result = CallToolResult {
5499
1
            content: vec![ToolContent::Text { text: output }],
5500
1
            is_error: None,
5501
1
        };
5502
5503
1
        jsonrpc_success(id, result)
5504
1
    }
5505
5506
0
    pub(super) async fn call_get_external_system(
5507
0
        &mut self,
5508
0
        id: Option<serde_json::Value>,
5509
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
5510
0
    ) -> JsonRpcResponse {
5511
0
        let args = match arguments {
5512
0
            Some(a) => a,
5513
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
5514
        };
5515
5516
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
5517
0
            Some(p) => p,
5518
            None => {
5519
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
5520
            }
5521
        };
5522
5523
0
        let system_value = args
5524
0
            .get("external_system_id")
5525
0
            .and_then(|v| v.as_str())
5526
0
            .or_else(|| args.get("name").and_then(|v| v.as_str()));
5527
5528
0
        let system_value = match system_value {
5529
0
            Some(v) => v,
5530
            None => {
5531
0
                return JsonRpcResponse::error(
5532
0
                    id,
5533
                    INVALID_PARAMS,
5534
0
                    "Missing 'external_system_id' or 'name' argument",
5535
                );
5536
            }
5537
        };
5538
5539
0
        let org_id = match self.ensure_org().await {
5540
0
            Ok(id) => id,
5541
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5542
        };
5543
5544
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
5545
0
            Ok(p) => p,
5546
0
            Err(e) => {
5547
0
                error!("Failed to fetch projects: {}", e);
5548
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
5549
            }
5550
        };
5551
5552
0
        let project = match projects
5553
0
            .iter()
5554
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
5555
        {
5556
0
            Some(p) => p,
5557
            None => {
5558
0
                return JsonRpcResponse::error(
5559
0
                    id,
5560
                    INVALID_PARAMS,
5561
0
                    &format!("Project '{}' not found", project_name),
5562
                );
5563
            }
5564
        };
5565
5566
0
        let system_id = match resolve_external_system_id_from_value(
5567
0
            &self.service.external_system_service,
5568
0
            &org_id,
5569
0
            &project.id,
5570
0
            &self.user_id,
5571
0
            system_value,
5572
        )
5573
0
        .await
5574
        {
5575
0
            Ok(id) => id,
5576
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5577
        };
5578
5579
0
        let system = match self
5580
0
            .service
5581
0
            .external_system_service
5582
0
            .get_external_system(&org_id, &project.id, &system_id, &self.user_id)
5583
0
            .await
5584
        {
5585
0
            Ok(s) => s,
5586
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5587
        };
5588
5589
0
        let system_view: ExternalSystemView = (&system).into();
5590
0
        let mut output = format!(
5591
            "🔌 External system **{}**\n\n- **Category**: `{}`",
5592
            system_view.name, system_view.category
5593
        );
5594
5595
0
        if let Some(ref vendor) = system_view.vendor {
5596
0
            output.push_str(&format!("\n- **Vendor**: {}", vendor));
5597
0
        }
5598
0
        if let Some(ref desc) = system_view.description {
5599
0
            output.push_str(&format!("\n- **Description**: {}", desc));
5600
0
        }
5601
0
        if let Some(ref tags) = system_view.tags {
5602
0
            output.push_str(&format!("\n- **Tags**: {}", tags.join(", ")));
5603
0
        }
5604
5605
0
        output.push_str(&format!("\n- **ID**: `{}`", system_view.id));
5606
5607
0
        let result = CallToolResult {
5608
0
            content: vec![ToolContent::Text { text: output }],
5609
0
            is_error: None,
5610
0
        };
5611
5612
0
        jsonrpc_success(id, result)
5613
0
    }
5614
5615
0
    pub(super) async fn call_list_external_systems(
5616
0
        &mut self,
5617
0
        id: Option<serde_json::Value>,
5618
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
5619
0
    ) -> JsonRpcResponse {
5620
0
        let args = match arguments {
5621
0
            Some(a) => a,
5622
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
5623
        };
5624
5625
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
5626
0
            Some(p) => p,
5627
            None => {
5628
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
5629
            }
5630
        };
5631
5632
0
        let org_id = match self.ensure_org().await {
5633
0
            Ok(id) => id,
5634
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5635
        };
5636
5637
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
5638
0
            Ok(p) => p,
5639
0
            Err(e) => {
5640
0
                error!("Failed to fetch projects: {}", e);
5641
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
5642
            }
5643
        };
5644
5645
0
        let project = match projects
5646
0
            .iter()
5647
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
5648
        {
5649
0
            Some(p) => p,
5650
            None => {
5651
0
                return JsonRpcResponse::error(
5652
0
                    id,
5653
                    INVALID_PARAMS,
5654
0
                    &format!("Project '{}' not found", project_name),
5655
                );
5656
            }
5657
        };
5658
5659
0
        let systems = match self
5660
0
            .service
5661
0
            .external_system_service
5662
0
            .list_external_systems(&org_id, &project.id, &self.user_id)
5663
0
            .await
5664
        {
5665
0
            Ok(list) => list,
5666
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5667
        };
5668
5669
0
        let mut output = format!("🔌 External systems in project **{}**\n\n", project.name);
5670
5671
0
        if systems.is_empty() {
5672
0
            output.push_str("No external systems found.");
5673
0
        } else {
5674
0
            for system in &systems {
5675
0
                let system_view: ExternalSystemView = system.into();
5676
0
                output.push_str(&format!(
5677
0
                    "- **{}** (`{}`) [{}]\n",
5678
0
                    system_view.name, system_view.id, system_view.category
5679
0
                ));
5680
0
            }
5681
        }
5682
5683
0
        let result = CallToolResult {
5684
0
            content: vec![ToolContent::Text { text: output }],
5685
0
            is_error: None,
5686
0
        };
5687
5688
0
        jsonrpc_success(id, result)
5689
0
    }
5690
5691
0
    pub(super) async fn call_update_external_system(
5692
0
        &mut self,
5693
0
        id: Option<serde_json::Value>,
5694
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
5695
0
    ) -> JsonRpcResponse {
5696
0
        let args = match arguments {
5697
0
            Some(a) => a,
5698
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
5699
        };
5700
5701
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
5702
0
            Some(p) => p,
5703
            None => {
5704
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
5705
            }
5706
        };
5707
5708
0
        let system_value = args
5709
0
            .get("external_system_id")
5710
0
            .and_then(|v| v.as_str())
5711
0
            .or_else(|| args.get("name").and_then(|v| v.as_str()));
5712
5713
0
        let system_value = match system_value {
5714
0
            Some(v) => v,
5715
            None => {
5716
0
                return JsonRpcResponse::error(
5717
0
                    id,
5718
                    INVALID_PARAMS,
5719
0
                    "Missing 'external_system_id' or 'name' argument",
5720
                );
5721
            }
5722
        };
5723
5724
0
        let new_name = args
5725
0
            .get("new_name")
5726
0
            .and_then(|v| v.as_str())
5727
0
            .map(|s| s.to_string());
5728
5729
0
        let new_description = args
5730
0
            .get("description")
5731
0
            .and_then(|v| v.as_str())
5732
0
            .map(|s| s.to_string());
5733
5734
0
        let new_category = args
5735
0
            .get("category")
5736
0
            .and_then(|v| v.as_str())
5737
0
            .map(|s| s.to_string());
5738
5739
0
        let new_vendor = args
5740
0
            .get("vendor")
5741
0
            .and_then(|v| v.as_str())
5742
0
            .map(|s| s.to_string());
5743
5744
0
        let new_tags = match parse_optional_string_list(&args, "tags") {
5745
0
            Ok(tags) => tags,
5746
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5747
        };
5748
5749
0
        let position_x = args.get("position_x").and_then(|v| v.as_f64());
5750
0
        let position_y = args.get("position_y").and_then(|v| v.as_f64());
5751
5752
0
        let org_id = match self.ensure_org().await {
5753
0
            Ok(id) => id,
5754
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5755
        };
5756
5757
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
5758
0
            Ok(p) => p,
5759
0
            Err(e) => {
5760
0
                error!("Failed to fetch projects: {}", e);
5761
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
5762
            }
5763
        };
5764
5765
0
        let project = match projects
5766
0
            .iter()
5767
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
5768
        {
5769
0
            Some(p) => p,
5770
            None => {
5771
0
                return JsonRpcResponse::error(
5772
0
                    id,
5773
                    INVALID_PARAMS,
5774
0
                    &format!("Project '{}' not found", project_name),
5775
                );
5776
            }
5777
        };
5778
5779
0
        let system_id = match resolve_external_system_id_from_value(
5780
0
            &self.service.external_system_service,
5781
0
            &org_id,
5782
0
            &project.id,
5783
0
            &self.user_id,
5784
0
            system_value,
5785
        )
5786
0
        .await
5787
        {
5788
0
            Ok(id) => id,
5789
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5790
        };
5791
5792
0
        let request = UpdateExternalSystemRequest {
5793
0
            name: new_name,
5794
0
            description: new_description,
5795
0
            category: new_category,
5796
0
            vendor: new_vendor,
5797
0
            tags: new_tags,
5798
0
            position_x,
5799
0
            position_y,
5800
0
        };
5801
5802
0
        let system = match self
5803
0
            .service
5804
0
            .external_system_service
5805
0
            .update_external_system(&org_id, &project.id, &system_id, &self.user_id, request)
5806
0
            .await
5807
        {
5808
0
            Ok(s) => s,
5809
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5810
        };
5811
5812
0
        let system_view: ExternalSystemView = (&system).into();
5813
0
        let mut output = format!(
5814
            "✅ Updated external system **{}** in project **{}**\n\n- **Category**: `{}`",
5815
            system_view.name, project.name, system_view.category
5816
        );
5817
5818
0
        if let Some(ref vendor) = system_view.vendor {
5819
0
            output.push_str(&format!("\n- **Vendor**: {}", vendor));
5820
0
        }
5821
0
        if let Some(ref desc) = system_view.description {
5822
0
            output.push_str(&format!("\n- **Description**: {}", desc));
5823
0
        }
5824
0
        if let Some(ref tags) = system_view.tags {
5825
0
            output.push_str(&format!("\n- **Tags**: {}", tags.join(", ")));
5826
0
        }
5827
5828
0
        output.push_str(&format!("\n- **ID**: `{}`", system_view.id));
5829
5830
0
        let result = CallToolResult {
5831
0
            content: vec![ToolContent::Text { text: output }],
5832
0
            is_error: None,
5833
0
        };
5834
5835
0
        jsonrpc_success(id, result)
5836
0
    }
5837
5838
0
    pub(super) async fn call_delete_external_system(
5839
0
        &mut self,
5840
0
        id: Option<serde_json::Value>,
5841
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
5842
0
    ) -> JsonRpcResponse {
5843
0
        let args = match arguments {
5844
0
            Some(a) => a,
5845
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
5846
        };
5847
5848
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
5849
0
            Some(p) => p,
5850
            None => {
5851
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
5852
            }
5853
        };
5854
5855
0
        let system_value = args
5856
0
            .get("external_system_id")
5857
0
            .and_then(|v| v.as_str())
5858
0
            .or_else(|| args.get("name").and_then(|v| v.as_str()));
5859
5860
0
        let system_value = match system_value {
5861
0
            Some(v) => v,
5862
            None => {
5863
0
                return JsonRpcResponse::error(
5864
0
                    id,
5865
                    INVALID_PARAMS,
5866
0
                    "Missing 'external_system_id' or 'name' argument",
5867
                );
5868
            }
5869
        };
5870
5871
0
        let org_id = match self.ensure_org().await {
5872
0
            Ok(id) => id,
5873
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5874
        };
5875
5876
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
5877
0
            Ok(p) => p,
5878
0
            Err(e) => {
5879
0
                error!("Failed to fetch projects: {}", e);
5880
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
5881
            }
5882
        };
5883
5884
0
        let project = match projects
5885
0
            .iter()
5886
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
5887
        {
5888
0
            Some(p) => p,
5889
            None => {
5890
0
                return JsonRpcResponse::error(
5891
0
                    id,
5892
                    INVALID_PARAMS,
5893
0
                    &format!("Project '{}' not found", project_name),
5894
                );
5895
            }
5896
        };
5897
5898
0
        let system_id = match resolve_external_system_id_from_value(
5899
0
            &self.service.external_system_service,
5900
0
            &org_id,
5901
0
            &project.id,
5902
0
            &self.user_id,
5903
0
            system_value,
5904
        )
5905
0
        .await
5906
        {
5907
0
            Ok(id) => id,
5908
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5909
        };
5910
5911
0
        match self
5912
0
            .service
5913
0
            .external_system_service
5914
0
            .delete_external_system(&org_id, &project.id, &system_id, &self.user_id)
5915
0
            .await
5916
        {
5917
0
            Ok(()) => {}
5918
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5919
        }
5920
5921
0
        let output = format!(
5922
            "🗑️ Deleted external system `{}` from project **{}**",
5923
            system_id, project.name
5924
        );
5925
5926
0
        let result = CallToolResult {
5927
0
            content: vec![ToolContent::Text { text: output }],
5928
0
            is_error: None,
5929
0
        };
5930
5931
0
        jsonrpc_success(id, result)
5932
0
    }
5933
5934
0
    pub(super) async fn call_create_quality_attribute(
5935
0
        &mut self,
5936
0
        id: Option<serde_json::Value>,
5937
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
5938
0
    ) -> JsonRpcResponse {
5939
0
        let args = match arguments {
5940
0
            Some(a) => a,
5941
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
5942
        };
5943
5944
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
5945
0
            Some(p) => p,
5946
            None => {
5947
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
5948
            }
5949
        };
5950
5951
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
5952
0
            Some(n) => n,
5953
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
5954
        };
5955
5956
0
        let priority = match args.get("priority").and_then(|v| v.as_i64()) {
5957
0
            Some(p) => p as i32,
5958
            None => {
5959
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'priority' argument");
5960
            }
5961
        };
5962
5963
0
        let description = args
5964
0
            .get("description")
5965
0
            .and_then(|v| v.as_str())
5966
0
            .map(|s| s.to_string());
5967
5968
0
        let constraints = match parse_optional_string_list(&args, "constraints") {
5969
0
            Ok(values) => values,
5970
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5971
        };
5972
5973
0
        let org_id = match self.ensure_org().await {
5974
0
            Ok(id) => id,
5975
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
5976
        };
5977
5978
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
5979
0
            Ok(p) => p,
5980
0
            Err(e) => {
5981
0
                error!("Failed to fetch projects: {}", e);
5982
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
5983
            }
5984
        };
5985
5986
0
        let project = match projects
5987
0
            .iter()
5988
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
5989
        {
5990
0
            Some(p) => p,
5991
            None => {
5992
0
                return JsonRpcResponse::error(
5993
0
                    id,
5994
                    INVALID_PARAMS,
5995
0
                    &format!("Project '{}' not found", project_name),
5996
                );
5997
            }
5998
        };
5999
6000
0
        let request = CreateQualityAttributeRequest {
6001
0
            name: name.to_string(),
6002
0
            priority,
6003
0
            description,
6004
0
            constraints,
6005
0
        };
6006
6007
0
        let attribute = match self
6008
0
            .service
6009
0
            .quality_attribute_service
6010
0
            .create_quality_attribute(&org_id, &project.id, &self.user_id, &request)
6011
0
            .await
6012
        {
6013
0
            Ok(value) => value,
6014
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6015
        };
6016
6017
0
        let view: QualityAttributeView = (&attribute).into();
6018
0
        let mut output = format!(
6019
            "✅ Created quality attribute **{}** (priority {}) in project **{}**",
6020
            view.name, view.priority, project.name
6021
        );
6022
6023
0
        if let Some(ref desc) = view.description {
6024
0
            output.push_str(&format!("\n- **Description**: {}", desc));
6025
0
        }
6026
0
        if let Some(ref constraints) = view.constraints
6027
0
            && !constraints.is_empty()
6028
0
        {
6029
0
            output.push_str(&format!("\n- **Constraints**: {}", constraints.join("; ")));
6030
0
        }
6031
6032
0
        output.push_str(&format!("\n- **ID**: `{}`", view.id));
6033
6034
0
        let result = CallToolResult {
6035
0
            content: vec![ToolContent::Text { text: output }],
6036
0
            is_error: None,
6037
0
        };
6038
6039
0
        jsonrpc_success(id, result)
6040
0
    }
6041
6042
0
    pub(super) async fn call_list_quality_attributes(
6043
0
        &mut self,
6044
0
        id: Option<serde_json::Value>,
6045
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
6046
0
    ) -> JsonRpcResponse {
6047
0
        let args = match arguments {
6048
0
            Some(a) => a,
6049
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
6050
        };
6051
6052
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
6053
0
            Some(p) => p,
6054
            None => {
6055
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
6056
            }
6057
        };
6058
6059
0
        let org_id = match self.ensure_org().await {
6060
0
            Ok(id) => id,
6061
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6062
        };
6063
6064
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
6065
0
            Ok(p) => p,
6066
0
            Err(e) => {
6067
0
                error!("Failed to fetch projects: {}", e);
6068
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
6069
            }
6070
        };
6071
6072
0
        let project = match projects
6073
0
            .iter()
6074
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
6075
        {
6076
0
            Some(p) => p,
6077
            None => {
6078
0
                return JsonRpcResponse::error(
6079
0
                    id,
6080
                    INVALID_PARAMS,
6081
0
                    &format!("Project '{}' not found", project_name),
6082
                );
6083
            }
6084
        };
6085
6086
0
        let attributes = match self
6087
0
            .service
6088
0
            .quality_attribute_service
6089
0
            .list_quality_attributes(&org_id, &project.id, &self.user_id)
6090
0
            .await
6091
        {
6092
0
            Ok(list) => list,
6093
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6094
        };
6095
6096
0
        let mut output = format!("⭐ Quality attributes in project **{}**\n\n", project.name);
6097
0
        if attributes.is_empty() {
6098
0
            output.push_str("No quality attributes found.");
6099
0
        } else {
6100
0
            for attribute in &attributes {
6101
0
                output.push_str(&format!(
6102
0
                    "- **{}** (priority {}) `{}`\n",
6103
0
                    attribute.name, attribute.priority, attribute.id
6104
0
                ));
6105
0
            }
6106
        }
6107
6108
0
        let result = CallToolResult {
6109
0
            content: vec![ToolContent::Text { text: output }],
6110
0
            is_error: None,
6111
0
        };
6112
6113
0
        jsonrpc_success(id, result)
6114
0
    }
6115
6116
0
    pub(super) async fn call_get_quality_attribute(
6117
0
        &mut self,
6118
0
        id: Option<serde_json::Value>,
6119
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
6120
0
    ) -> JsonRpcResponse {
6121
0
        let args = match arguments {
6122
0
            Some(a) => a,
6123
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
6124
        };
6125
6126
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
6127
0
            Some(p) => p,
6128
            None => {
6129
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
6130
            }
6131
        };
6132
6133
0
        let attribute_value = args
6134
0
            .get("quality_attribute_id")
6135
0
            .and_then(|v| v.as_str())
6136
0
            .or_else(|| args.get("name").and_then(|v| v.as_str()));
6137
6138
0
        let attribute_value = match attribute_value {
6139
0
            Some(v) => v,
6140
            None => {
6141
0
                return JsonRpcResponse::error(
6142
0
                    id,
6143
                    INVALID_PARAMS,
6144
0
                    "Missing 'quality_attribute_id' or 'name' argument",
6145
                );
6146
            }
6147
        };
6148
6149
0
        let org_id = match self.ensure_org().await {
6150
0
            Ok(id) => id,
6151
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6152
        };
6153
6154
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
6155
0
            Ok(p) => p,
6156
0
            Err(e) => {
6157
0
                error!("Failed to fetch projects: {}", e);
6158
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
6159
            }
6160
        };
6161
6162
0
        let project = match projects
6163
0
            .iter()
6164
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
6165
        {
6166
0
            Some(p) => p,
6167
            None => {
6168
0
                return JsonRpcResponse::error(
6169
0
                    id,
6170
                    INVALID_PARAMS,
6171
0
                    &format!("Project '{}' not found", project_name),
6172
                );
6173
            }
6174
        };
6175
6176
0
        let attributes = match self
6177
0
            .service
6178
0
            .quality_attribute_service
6179
0
            .list_quality_attributes(&org_id, &project.id, &self.user_id)
6180
0
            .await
6181
        {
6182
0
            Ok(list) => list,
6183
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6184
        };
6185
6186
0
        let attribute = if let Ok(attr_id) =
6187
0
            QualityAttributeId::try_from(attribute_value.to_string())
6188
        {
6189
0
            attributes.into_iter().find(|attr| attr.id == attr_id)
6190
        } else {
6191
0
            let matches: Vec<QualityAttribute> = attributes
6192
0
                .into_iter()
6193
0
                .filter(|attr| attr.name.eq_ignore_ascii_case(attribute_value))
6194
0
                .collect();
6195
0
            match matches.len() {
6196
0
                0 => None,
6197
0
                1 => Some(matches[0].clone()),
6198
                _ => {
6199
0
                    return JsonRpcResponse::error(
6200
0
                        id,
6201
                        INVALID_PARAMS,
6202
0
                        &format!(
6203
0
                            "Ambiguous quality attribute name '{}'. Use quality_attribute_id instead.",
6204
0
                            attribute_value
6205
0
                        ),
6206
                    );
6207
                }
6208
            }
6209
        };
6210
6211
0
        let attribute = match attribute {
6212
0
            Some(attr) => attr,
6213
            None => {
6214
0
                return JsonRpcResponse::error(
6215
0
                    id,
6216
                    INVALID_PARAMS,
6217
0
                    &format!("Quality attribute '{}' not found", attribute_value),
6218
                );
6219
            }
6220
        };
6221
6222
0
        let view: QualityAttributeView = (&attribute).into();
6223
0
        let mut output = format!(
6224
            "⭐ Quality attribute **{}** (priority {})\n\n- **ID**: `{}`",
6225
            view.name, view.priority, view.id
6226
        );
6227
0
        if let Some(ref desc) = view.description {
6228
0
            output.push_str(&format!("\n- **Description**: {}", desc));
6229
0
        }
6230
0
        if let Some(ref constraints) = view.constraints
6231
0
            && !constraints.is_empty()
6232
0
        {
6233
0
            output.push_str(&format!("\n- **Constraints**: {}", constraints.join("; ")));
6234
0
        }
6235
6236
0
        let links = match self
6237
0
            .service
6238
0
            .quality_component_link_service
6239
0
            .list_links_for_quality(&org_id, &project.id, &attribute.id)
6240
0
            .await
6241
        {
6242
0
            Ok(links) => links,
6243
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6244
        };
6245
6246
0
        if links.is_empty() {
6247
0
            output.push_str("\n- **Linked Components**: none");
6248
0
        } else {
6249
0
            output.push_str("\n- **Linked Components**:");
6250
0
            for link in links {
6251
0
                let component_name = if link.component_type.requires_parent() {
6252
0
                    match self
6253
0
                        .service
6254
0
                        .component_service
6255
0
                        .get_component_by_id(&org_id, &project.id, &link.component_id)
6256
0
                        .await
6257
                    {
6258
0
                        Ok(component) => component.name,
6259
0
                        Err(e) => return JsonRpcResponse::from_api_error(id, e),
6260
                    }
6261
                } else {
6262
0
                    match self
6263
0
                        .service
6264
0
                        .component_service
6265
0
                        .get_container(&org_id, &project.id, &link.component_id)
6266
0
                        .await
6267
                    {
6268
0
                        Ok(component) => component.name,
6269
0
                        Err(e) => return JsonRpcResponse::from_api_error(id, e),
6270
                    }
6271
                };
6272
6273
0
                output.push_str(&format!(
6274
0
                    "\n  - **{}** ({}) `{}`",
6275
0
                    component_name, link.component_type, link.component_id
6276
0
                ));
6277
0
                if let Some(notes) = link.notes.as_ref().filter(|n| !n.is_empty()) {
6278
0
                    output.push_str(&format!("\n    - Notes: {}", notes));
6279
0
                }
6280
0
                if let Some(overrides) = link.override_constraints.as_ref()
6281
0
                    && !overrides.is_empty()
6282
0
                {
6283
0
                    output.push_str(&format!("\n    - Overrides: {}", overrides.join("; ")));
6284
0
                }
6285
            }
6286
        }
6287
6288
0
        let result = CallToolResult {
6289
0
            content: vec![ToolContent::Text { text: output }],
6290
0
            is_error: None,
6291
0
        };
6292
6293
0
        jsonrpc_success(id, result)
6294
0
    }
6295
6296
0
    pub(super) async fn call_update_quality_attribute(
6297
0
        &mut self,
6298
0
        id: Option<serde_json::Value>,
6299
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
6300
0
    ) -> JsonRpcResponse {
6301
0
        let args = match arguments {
6302
0
            Some(a) => a,
6303
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
6304
        };
6305
6306
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
6307
0
            Some(p) => p,
6308
            None => {
6309
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
6310
            }
6311
        };
6312
6313
0
        let attribute_value = args
6314
0
            .get("quality_attribute_id")
6315
0
            .and_then(|v| v.as_str())
6316
0
            .or_else(|| args.get("name").and_then(|v| v.as_str()));
6317
6318
0
        let attribute_value = match attribute_value {
6319
0
            Some(v) => v,
6320
            None => {
6321
0
                return JsonRpcResponse::error(
6322
0
                    id,
6323
                    INVALID_PARAMS,
6324
0
                    "Missing 'quality_attribute_id' or 'name' argument",
6325
                );
6326
            }
6327
        };
6328
6329
0
        let new_name = args
6330
0
            .get("name")
6331
0
            .and_then(|v| v.as_str())
6332
0
            .map(|s| s.to_string());
6333
6334
0
        let new_priority = args
6335
0
            .get("priority")
6336
0
            .and_then(|v| v.as_i64())
6337
0
            .map(|p| p as i32);
6338
6339
0
        let new_description = args
6340
0
            .get("description")
6341
0
            .and_then(|v| v.as_str())
6342
0
            .map(|s| s.to_string());
6343
6344
0
        let new_constraints = match parse_optional_string_list(&args, "constraints") {
6345
0
            Ok(values) => values,
6346
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6347
        };
6348
6349
0
        let org_id = match self.ensure_org().await {
6350
0
            Ok(id) => id,
6351
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6352
        };
6353
6354
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
6355
0
            Ok(p) => p,
6356
0
            Err(e) => {
6357
0
                error!("Failed to fetch projects: {}", e);
6358
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
6359
            }
6360
        };
6361
6362
0
        let project = match projects
6363
0
            .iter()
6364
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
6365
        {
6366
0
            Some(p) => p,
6367
            None => {
6368
0
                return JsonRpcResponse::error(
6369
0
                    id,
6370
                    INVALID_PARAMS,
6371
0
                    &format!("Project '{}' not found", project_name),
6372
                );
6373
            }
6374
        };
6375
6376
0
        let attributes = match self
6377
0
            .service
6378
0
            .quality_attribute_service
6379
0
            .list_quality_attributes(&org_id, &project.id, &self.user_id)
6380
0
            .await
6381
        {
6382
0
            Ok(list) => list,
6383
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6384
        };
6385
6386
0
        let attribute = if let Ok(attr_id) =
6387
0
            QualityAttributeId::try_from(attribute_value.to_string())
6388
        {
6389
0
            attributes.into_iter().find(|attr| attr.id == attr_id)
6390
        } else {
6391
0
            let matches: Vec<QualityAttribute> = attributes
6392
0
                .into_iter()
6393
0
                .filter(|attr| attr.name.eq_ignore_ascii_case(attribute_value))
6394
0
                .collect();
6395
0
            match matches.len() {
6396
0
                0 => None,
6397
0
                1 => Some(matches[0].clone()),
6398
                _ => {
6399
0
                    return JsonRpcResponse::error(
6400
0
                        id,
6401
                        INVALID_PARAMS,
6402
0
                        &format!(
6403
0
                            "Ambiguous quality attribute name '{}'. Use quality_attribute_id instead.",
6404
0
                            attribute_value
6405
0
                        ),
6406
                    );
6407
                }
6408
            }
6409
        };
6410
6411
0
        let attribute = match attribute {
6412
0
            Some(attr) => attr,
6413
            None => {
6414
0
                return JsonRpcResponse::error(
6415
0
                    id,
6416
                    INVALID_PARAMS,
6417
0
                    &format!("Quality attribute '{}' not found", attribute_value),
6418
                );
6419
            }
6420
        };
6421
6422
0
        let request = UpdateQualityAttributeRequest {
6423
0
            name: new_name,
6424
0
            priority: new_priority,
6425
0
            description: new_description,
6426
0
            constraints: new_constraints,
6427
0
        };
6428
6429
0
        let updated = match self
6430
0
            .service
6431
0
            .quality_attribute_service
6432
0
            .update_quality_attribute(&org_id, &project.id, &attribute.id, &self.user_id, request)
6433
0
            .await
6434
        {
6435
0
            Ok(value) => value,
6436
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6437
        };
6438
6439
0
        let view: QualityAttributeView = (&updated).into();
6440
0
        let mut output = format!(
6441
            "✅ Updated quality attribute **{}** (priority {})",
6442
            view.name, view.priority
6443
        );
6444
0
        if let Some(ref desc) = view.description {
6445
0
            output.push_str(&format!("\n- **Description**: {}", desc));
6446
0
        }
6447
0
        if let Some(ref constraints) = view.constraints
6448
0
            && !constraints.is_empty()
6449
0
        {
6450
0
            output.push_str(&format!("\n- **Constraints**: {}", constraints.join("; ")));
6451
0
        }
6452
0
        output.push_str(&format!("\n- **ID**: `{}`", view.id));
6453
6454
0
        let result = CallToolResult {
6455
0
            content: vec![ToolContent::Text { text: output }],
6456
0
            is_error: None,
6457
0
        };
6458
6459
0
        jsonrpc_success(id, result)
6460
0
    }
6461
6462
0
    pub(super) async fn call_delete_quality_attribute(
6463
0
        &mut self,
6464
0
        id: Option<serde_json::Value>,
6465
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
6466
0
    ) -> JsonRpcResponse {
6467
0
        let args = match arguments {
6468
0
            Some(a) => a,
6469
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
6470
        };
6471
6472
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
6473
0
            Some(p) => p,
6474
            None => {
6475
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
6476
            }
6477
        };
6478
6479
0
        let attribute_value = args
6480
0
            .get("quality_attribute_id")
6481
0
            .and_then(|v| v.as_str())
6482
0
            .or_else(|| args.get("name").and_then(|v| v.as_str()));
6483
6484
0
        let attribute_value = match attribute_value {
6485
0
            Some(v) => v,
6486
            None => {
6487
0
                return JsonRpcResponse::error(
6488
0
                    id,
6489
                    INVALID_PARAMS,
6490
0
                    "Missing 'quality_attribute_id' or 'name' argument",
6491
                );
6492
            }
6493
        };
6494
6495
0
        let org_id = match self.ensure_org().await {
6496
0
            Ok(id) => id,
6497
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6498
        };
6499
6500
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
6501
0
            Ok(p) => p,
6502
0
            Err(e) => {
6503
0
                error!("Failed to fetch projects: {}", e);
6504
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
6505
            }
6506
        };
6507
6508
0
        let project = match projects
6509
0
            .iter()
6510
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
6511
        {
6512
0
            Some(p) => p,
6513
            None => {
6514
0
                return JsonRpcResponse::error(
6515
0
                    id,
6516
                    INVALID_PARAMS,
6517
0
                    &format!("Project '{}' not found", project_name),
6518
                );
6519
            }
6520
        };
6521
6522
0
        let attributes = match self
6523
0
            .service
6524
0
            .quality_attribute_service
6525
0
            .list_quality_attributes(&org_id, &project.id, &self.user_id)
6526
0
            .await
6527
        {
6528
0
            Ok(list) => list,
6529
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6530
        };
6531
6532
0
        let attribute = if let Ok(attr_id) =
6533
0
            QualityAttributeId::try_from(attribute_value.to_string())
6534
        {
6535
0
            attributes.into_iter().find(|attr| attr.id == attr_id)
6536
        } else {
6537
0
            let matches: Vec<QualityAttribute> = attributes
6538
0
                .into_iter()
6539
0
                .filter(|attr| attr.name.eq_ignore_ascii_case(attribute_value))
6540
0
                .collect();
6541
0
            match matches.len() {
6542
0
                0 => None,
6543
0
                1 => Some(matches[0].clone()),
6544
                _ => {
6545
0
                    return JsonRpcResponse::error(
6546
0
                        id,
6547
                        INVALID_PARAMS,
6548
0
                        &format!(
6549
0
                            "Ambiguous quality attribute name '{}'. Use quality_attribute_id instead.",
6550
0
                            attribute_value
6551
0
                        ),
6552
                    );
6553
                }
6554
            }
6555
        };
6556
6557
0
        let attribute = match attribute {
6558
0
            Some(attr) => attr,
6559
            None => {
6560
0
                return JsonRpcResponse::error(
6561
0
                    id,
6562
                    INVALID_PARAMS,
6563
0
                    &format!("Quality attribute '{}' not found", attribute_value),
6564
                );
6565
            }
6566
        };
6567
6568
0
        match self
6569
0
            .service
6570
0
            .quality_attribute_service
6571
0
            .delete_quality_attribute(&org_id, &project.id, &attribute.id, &self.user_id)
6572
0
            .await
6573
        {
6574
0
            Ok(()) => {}
6575
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
6576
        }
6577
6578
0
        let output = format!(
6579
            "🗑️ Deleted quality attribute `{}` from project **{}**",
6580
            attribute.id, project.name
6581
        );
6582
6583
0
        let result = CallToolResult {
6584
0
            content: vec![ToolContent::Text { text: output }],
6585
0
            is_error: None,
6586
0
        };
6587
6588
0
        jsonrpc_success(id, result)
6589
0
    }
6590
6591
14
    pub(super) async fn resolve_project_by_name(
6592
14
        &mut self,
6593
14
        project_name: &str,
6594
14
    ) -> Result<(OrganisationId, crate::models::project::Project), ApiError> {
6595
14
        let project_name = project_name.trim();
6596
14
        let org_id = self.ensure_org().await
?0
;
6597
14
        let projects = self.service.project_repo.list_projects(&org_id).await
?0
;
6598
6599
1
        if let Ok(project_id) =
6600
14
            crate::models::project::ProjectId::try_from(project_name.to_string())
6601
1
            && let Some(project) = projects.iter().find(|p| p.id == project_id)
6602
        {
6603
1
            return Ok((org_id, project.clone()));
6604
13
        }
6605
6606
13
        let project = projects
6607
13
            .into_iter()
6608
13
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
6609
13
            .ok_or_else(|| 
{0
6610
0
                ApiError::InvalidRequest(format!("Project '{}' not found", project_name))
6611
0
            })?;
6612
13
        Ok((org_id, project))
6613
14
    }
6614
6615
0
    async fn resolve_skill_id_by_name(
6616
0
        &mut self,
6617
0
        org_id: &OrganisationId,
6618
0
        project_id: &ProjectId,
6619
0
        project_name: &str,
6620
0
        skill_name: &str,
6621
0
    ) -> Result<crate::models::skill::SkillId, ApiError> {
6622
0
        if let Ok(id) = crate::models::skill::SkillId::try_from(skill_name.to_string()) {
6623
0
            return Ok(id);
6624
0
        }
6625
6626
0
        if skill_name.contains('/') {
6627
0
            return self
6628
0
                .resolve_skill_id_by_path(org_id, project_id, project_name, skill_name)
6629
0
                .await;
6630
0
        }
6631
6632
0
        let roots = self
6633
0
            .service
6634
0
            .skill_service
6635
0
            .list_skills(org_id, project_id, None)
6636
0
            .await?;
6637
6638
0
        for root in roots {
6639
0
            if root.name.eq_ignore_ascii_case(skill_name) {
6640
0
                return Ok(root.id);
6641
0
            }
6642
0
            let children = self
6643
0
                .service
6644
0
                .skill_service
6645
0
                .list_skills(org_id, project_id, Some(&root.id))
6646
0
                .await?;
6647
0
            if let Some(child) = children.into_iter().find(|skill| {
6648
0
                skill.name.eq_ignore_ascii_case(skill_name)
6649
0
                    || skill
6650
0
                        .name
6651
0
                        .rsplit('/')
6652
0
                        .next()
6653
0
                        .is_some_and(|leaf| leaf.eq_ignore_ascii_case(skill_name))
6654
0
            }) {
6655
0
                return Ok(child.id);
6656
0
            }
6657
        }
6658
6659
0
        Err(ApiError::InvalidRequest(format!(
6660
0
            "Skill '{}' not found in project '{}'",
6661
0
            skill_name, project_name
6662
0
        )))
6663
0
    }
6664
6665
0
    async fn resolve_skill_id_by_path(
6666
0
        &mut self,
6667
0
        org_id: &OrganisationId,
6668
0
        project_id: &ProjectId,
6669
0
        project_name: &str,
6670
0
        skill_path: &str,
6671
0
    ) -> Result<crate::models::skill::SkillId, ApiError> {
6672
0
        let segments = skill_path
6673
0
            .split('/')
6674
0
            .map(str::trim)
6675
0
            .filter(|segment| !segment.is_empty())
6676
0
            .collect::<Vec<_>>();
6677
0
        if segments.is_empty() {
6678
0
            return Err(ApiError::InvalidRequest(format!(
6679
0
                "Skill path '{}' is empty",
6680
0
                skill_path
6681
0
            )));
6682
0
        }
6683
6684
0
        let roots = self
6685
0
            .service
6686
0
            .skill_service
6687
0
            .list_skills(org_id, project_id, None)
6688
0
            .await?;
6689
6690
0
        let mut expected_name = segments[0].to_string();
6691
0
        let mut current = roots
6692
0
            .into_iter()
6693
0
            .find(|skill| skill.name.eq_ignore_ascii_case(&expected_name))
6694
0
            .ok_or_else(|| {
6695
0
                ApiError::InvalidRequest(format!(
6696
0
                    "Skill '{}' not found in project '{}'",
6697
0
                    skill_path, project_name
6698
0
                ))
6699
0
            })?;
6700
6701
0
        for segment in segments.iter().skip(1) {
6702
0
            expected_name = format!("{expected_name}/{segment}");
6703
0
            let children = self
6704
0
                .service
6705
0
                .skill_service
6706
0
                .list_skills(org_id, project_id, Some(&current.id))
6707
0
                .await?;
6708
0
            current = children
6709
0
                .into_iter()
6710
0
                .find(|skill| skill.name.eq_ignore_ascii_case(&expected_name))
6711
0
                .ok_or_else(|| {
6712
0
                    ApiError::InvalidRequest(format!(
6713
0
                        "Skill '{}' not found in project '{}'",
6714
0
                        skill_path, project_name
6715
0
                    ))
6716
0
                })?;
6717
        }
6718
6719
0
        Ok(current.id)
6720
0
    }
6721
6722
0
    async fn resolve_spec_id_by_name(
6723
0
        &mut self,
6724
0
        org_id: &OrganisationId,
6725
0
        project_id: &ProjectId,
6726
0
        project_name: &str,
6727
0
        spec_name: &str,
6728
0
    ) -> Result<crate::models::spec::SpecId, ApiError> {
6729
0
        if let Ok(id) = crate::models::spec::SpecId::try_from(spec_name.to_string()) {
6730
0
            return Ok(id);
6731
0
        }
6732
6733
0
        self.service
6734
0
            .spec_service
6735
0
            .list_specs(org_id, project_id, None)
6736
0
            .await?
6737
0
            .into_iter()
6738
0
            .find(|spec| spec.spec.name.eq_ignore_ascii_case(spec_name))
6739
0
            .map(|spec| spec.spec.id)
6740
0
            .ok_or_else(|| {
6741
0
                ApiError::InvalidRequest(format!(
6742
0
                    "Spec '{}' not found in project '{}'",
6743
0
                    spec_name, project_name
6744
0
                ))
6745
0
            })
6746
0
    }
6747
6748
0
    async fn resolve_task_id_by_name(
6749
0
        &mut self,
6750
0
        org_id: &OrganisationId,
6751
0
        project_id: &ProjectId,
6752
0
        spec_id: &crate::models::spec::SpecId,
6753
0
        spec_name: &str,
6754
0
        task_name: &str,
6755
0
    ) -> Result<crate::models::spec::TaskId, ApiError> {
6756
0
        if let Ok(id) = crate::models::spec::TaskId::try_from(task_name.to_string()) {
6757
0
            return Ok(id);
6758
0
        }
6759
6760
0
        self.service
6761
0
            .spec_service
6762
0
            .list_tasks(org_id, project_id, spec_id)
6763
0
            .await?
6764
0
            .into_iter()
6765
0
            .find(|task| task.name.eq_ignore_ascii_case(task_name))
6766
0
            .map(|task| task.id)
6767
0
            .ok_or_else(|| {
6768
0
                ApiError::InvalidRequest(format!(
6769
0
                    "Task '{}' not found in spec '{}'",
6770
0
                    task_name, spec_name
6771
0
                ))
6772
0
            })
6773
0
    }
6774
6775
0
    pub(super) async fn call_create_skill(
6776
0
        &mut self,
6777
0
        id: Option<serde_json::Value>,
6778
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
6779
0
    ) -> JsonRpcResponse {
6780
0
        let args = match arguments {
6781
0
            Some(a) => a,
6782
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
6783
        };
6784
6785
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
6786
0
            Some(value) if !value.trim().is_empty() => value,
6787
0
            _ => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument"),
6788
        };
6789
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
6790
0
            Some(value) if !value.trim().is_empty() => value,
6791
0
            _ => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
6792
        };
6793
0
        let display_name = match args.get("display_name").and_then(|v| v.as_str()) {
6794
0
            Some(value) if !value.trim().is_empty() => value,
6795
            _ => {
6796
0
                return JsonRpcResponse::error(
6797
0
                    id,
6798
                    INVALID_PARAMS,
6799
0
                    "Missing 'display_name' argument",
6800
                );
6801
            }
6802
        };
6803
0
        let description = match args.get("description").and_then(|v| v.as_str()) {
6804
0
            Some(value) => value,
6805
            None => {
6806
0
                return JsonRpcResponse::error(
6807
0
                    id,
6808
                    INVALID_PARAMS,
6809
0
                    "Missing 'description' argument",
6810
                );
6811
            }
6812
        };
6813
0
        let content = match args.get("content").and_then(|v| v.as_str()) {
6814
0
            Some(value) => value,
6815
            None => {
6816
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'content' argument");
6817
            }
6818
        };
6819
6820
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
6821
0
            Ok(value) => value,
6822
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
6823
        };
6824
6825
0
        let parent_skill_id = match args.get("parent_skill_name").and_then(|v| v.as_str()) {
6826
0
            Some(parent_name) if !parent_name.trim().is_empty() => {
6827
0
                match self
6828
0
                    .resolve_skill_id_by_name(&org_id, &project.id, project_name, parent_name)
6829
0
                    .await
6830
                {
6831
0
                    Ok(skill_id) => Some(skill_id),
6832
0
                    Err(error) => return JsonRpcResponse::from_api_error(id, error),
6833
                }
6834
            }
6835
0
            _ => None,
6836
        };
6837
6838
0
        let request = crate::models::skill::CreateSkillRequest {
6839
0
            name: name.trim().to_string(),
6840
0
            display_name: display_name.trim().to_string(),
6841
0
            description: description.to_string(),
6842
0
            content: content.to_string(),
6843
0
            parent_skill_id,
6844
0
            sort_order: args
6845
0
                .get("sort_order")
6846
0
                .and_then(|value| value.as_i64())
6847
0
                .map(|value| value as i32),
6848
        };
6849
6850
0
        match self
6851
0
            .service
6852
0
            .skill_service
6853
0
            .create_skill(&org_id, &project.id, &request)
6854
0
            .await
6855
        {
6856
0
            Ok(skill) => jsonrpc_success(
6857
0
                id,
6858
0
                CallToolResult {
6859
0
                    content: vec![ToolContent::Text {
6860
0
                        text: format!(
6861
0
                            "✅ Created skill **{}** (`{}`)",
6862
0
                            skill.display_name, skill.id
6863
0
                        ),
6864
0
                    }],
6865
0
                    is_error: None,
6866
0
                },
6867
            ),
6868
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
6869
        }
6870
0
    }
6871
6872
0
    pub(super) async fn call_get_skill(
6873
0
        &mut self,
6874
0
        id: Option<serde_json::Value>,
6875
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
6876
0
    ) -> JsonRpcResponse {
6877
0
        let args = match arguments {
6878
0
            Some(a) => a,
6879
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
6880
        };
6881
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
6882
0
            Some(value) => value,
6883
            None => {
6884
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
6885
            }
6886
        };
6887
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
6888
0
            Some(value) => value,
6889
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
6890
        };
6891
6892
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
6893
0
            Ok(value) => value,
6894
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
6895
        };
6896
0
        let skill_id = match self
6897
0
            .resolve_skill_id_by_name(&org_id, &project.id, project_name, name)
6898
0
            .await
6899
        {
6900
0
            Ok(value) => value,
6901
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
6902
        };
6903
6904
0
        match self
6905
0
            .service
6906
0
            .skill_service
6907
0
            .get_skill(&org_id, &project.id, &skill_id)
6908
0
            .await
6909
        {
6910
0
            Ok(detail) => {
6911
0
                let text = match serde_json::to_string_pretty(&detail) {
6912
0
                    Ok(json) => json,
6913
0
                    Err(error) => {
6914
0
                        return JsonRpcResponse::error(
6915
0
                            id,
6916
                            INTERNAL_ERROR,
6917
0
                            &format!("Failed to serialize skill detail: {}", error),
6918
                        );
6919
                    }
6920
                };
6921
0
                jsonrpc_success(
6922
0
                    id,
6923
0
                    CallToolResult {
6924
0
                        content: vec![ToolContent::Text { text }],
6925
0
                        is_error: None,
6926
0
                    },
6927
                )
6928
            }
6929
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
6930
        }
6931
0
    }
6932
6933
0
    pub(super) async fn call_list_skills(
6934
0
        &mut self,
6935
0
        id: Option<serde_json::Value>,
6936
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
6937
0
    ) -> JsonRpcResponse {
6938
0
        let args = match arguments {
6939
0
            Some(a) => a,
6940
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
6941
        };
6942
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
6943
0
            Some(value) => value,
6944
            None => {
6945
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
6946
            }
6947
        };
6948
6949
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
6950
0
            Ok(value) => value,
6951
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
6952
        };
6953
6954
0
        let parent_skill_id = match args.get("parent_skill_name").and_then(|v| v.as_str()) {
6955
0
            Some(parent_name) if !parent_name.trim().is_empty() => {
6956
0
                match self
6957
0
                    .resolve_skill_id_by_name(&org_id, &project.id, project_name, parent_name)
6958
0
                    .await
6959
                {
6960
0
                    Ok(value) => Some(value),
6961
0
                    Err(error) => return JsonRpcResponse::from_api_error(id, error),
6962
                }
6963
            }
6964
0
            _ => None,
6965
        };
6966
6967
0
        match self
6968
0
            .service
6969
0
            .skill_service
6970
0
            .list_skills(&org_id, &project.id, parent_skill_id.as_ref())
6971
0
            .await
6972
        {
6973
0
            Ok(list) => {
6974
0
                let text = match serde_json::to_string_pretty(&list) {
6975
0
                    Ok(json) => json,
6976
0
                    Err(error) => {
6977
0
                        return JsonRpcResponse::error(
6978
0
                            id,
6979
                            INTERNAL_ERROR,
6980
0
                            &format!("Failed to serialize skills list: {}", error),
6981
                        );
6982
                    }
6983
                };
6984
0
                jsonrpc_success(
6985
0
                    id,
6986
0
                    CallToolResult {
6987
0
                        content: vec![ToolContent::Text { text }],
6988
0
                        is_error: None,
6989
0
                    },
6990
                )
6991
            }
6992
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
6993
        }
6994
0
    }
6995
6996
0
    pub(super) async fn call_update_skill(
6997
0
        &mut self,
6998
0
        id: Option<serde_json::Value>,
6999
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7000
0
    ) -> JsonRpcResponse {
7001
0
        let args = match arguments {
7002
0
            Some(a) => a,
7003
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7004
        };
7005
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7006
0
            Some(value) => value,
7007
            None => {
7008
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
7009
            }
7010
        };
7011
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
7012
0
            Some(value) => value,
7013
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
7014
        };
7015
7016
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7017
0
            Ok(value) => value,
7018
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7019
        };
7020
0
        let skill_id = match self
7021
0
            .resolve_skill_id_by_name(&org_id, &project.id, project_name, name)
7022
0
            .await
7023
        {
7024
0
            Ok(value) => value,
7025
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7026
        };
7027
7028
0
        let request = crate::models::skill::UpdateSkillRequest {
7029
0
            new_name: args
7030
0
                .get("new_name")
7031
0
                .and_then(|value| value.as_str())
7032
0
                .map(str::to_string),
7033
0
            display_name: args
7034
0
                .get("display_name")
7035
0
                .and_then(|value| value.as_str())
7036
0
                .map(str::to_string),
7037
0
            description: args
7038
0
                .get("description")
7039
0
                .and_then(|value| value.as_str())
7040
0
                .map(str::to_string),
7041
0
            content: args
7042
0
                .get("content")
7043
0
                .and_then(|value| value.as_str())
7044
0
                .map(str::to_string),
7045
0
            source_url: None,
7046
0
            source_hash: None,
7047
0
            sort_order: args
7048
0
                .get("sort_order")
7049
0
                .and_then(|value| value.as_i64())
7050
0
                .map(|value| value as i32),
7051
        };
7052
7053
0
        match self
7054
0
            .service
7055
0
            .skill_service
7056
0
            .update_skill(&org_id, &project.id, &skill_id, request)
7057
0
            .await
7058
        {
7059
0
            Ok(skill) => jsonrpc_success(
7060
0
                id,
7061
0
                CallToolResult {
7062
0
                    content: vec![ToolContent::Text {
7063
0
                        text: format!(
7064
0
                            "✏️ Updated skill **{}** (`{}`)",
7065
0
                            skill.display_name, skill.id
7066
0
                        ),
7067
0
                    }],
7068
0
                    is_error: None,
7069
0
                },
7070
            ),
7071
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
7072
        }
7073
0
    }
7074
7075
0
    pub(super) async fn call_delete_skill(
7076
0
        &mut self,
7077
0
        id: Option<serde_json::Value>,
7078
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7079
0
    ) -> JsonRpcResponse {
7080
0
        let args = match arguments {
7081
0
            Some(a) => a,
7082
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7083
        };
7084
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7085
0
            Some(value) => value,
7086
            None => {
7087
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
7088
            }
7089
        };
7090
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
7091
0
            Some(value) => value,
7092
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
7093
        };
7094
7095
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7096
0
            Ok(value) => value,
7097
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7098
        };
7099
0
        let skill_id = match self
7100
0
            .resolve_skill_id_by_name(&org_id, &project.id, project_name, name)
7101
0
            .await
7102
        {
7103
0
            Ok(value) => value,
7104
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7105
        };
7106
7107
0
        match self
7108
0
            .service
7109
0
            .skill_service
7110
0
            .delete_skill(&org_id, &project.id, &skill_id)
7111
0
            .await
7112
        {
7113
0
            Ok(summary) => jsonrpc_success(
7114
0
                id,
7115
0
                CallToolResult {
7116
0
                    content: vec![ToolContent::Text {
7117
0
                        text: format!(
7118
0
                            "🗑️ Deleted skill '{}', removed {} skill nodes and {} links",
7119
0
                            name, summary.deleted_count, summary.edge_deleted_count
7120
0
                        ),
7121
0
                    }],
7122
0
                    is_error: None,
7123
0
                },
7124
            ),
7125
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
7126
        }
7127
0
    }
7128
7129
0
    pub(super) async fn call_create_spec(
7130
0
        &mut self,
7131
0
        id: Option<serde_json::Value>,
7132
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7133
0
    ) -> JsonRpcResponse {
7134
0
        let args = match arguments {
7135
0
            Some(a) => a,
7136
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7137
        };
7138
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7139
0
            Some(value) if !value.trim().is_empty() => value,
7140
0
            _ => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument"),
7141
        };
7142
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
7143
0
            Some(value) if !value.trim().is_empty() => value,
7144
0
            _ => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
7145
        };
7146
0
        let title = match args.get("title").and_then(|v| v.as_str()) {
7147
0
            Some(value) if !value.trim().is_empty() => value,
7148
0
            _ => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'title' argument"),
7149
        };
7150
0
        let spec_type = match args.get("spec_type").and_then(|v| v.as_str()) {
7151
0
            Some("feature") => crate::models::spec::SpecType::Feature,
7152
0
            Some("bugfix") => crate::models::spec::SpecType::Bugfix,
7153
0
            Some("architecture") => crate::models::spec::SpecType::Architecture,
7154
            Some(_) => {
7155
0
                return JsonRpcResponse::error(
7156
0
                    id,
7157
                    INVALID_PARAMS,
7158
0
                    &format!("spec_type must be {}", SpecType::HELP),
7159
                );
7160
            }
7161
            None => {
7162
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'spec_type' argument");
7163
            }
7164
        };
7165
0
        let owner = match args.get("owner").and_then(|v| v.as_str()) {
7166
0
            Some(value) if !value.trim().is_empty() => value,
7167
0
            _ => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'owner' argument"),
7168
        };
7169
0
        let description = match args.get("description").and_then(|v| v.as_str()) {
7170
0
            Some(value) => value,
7171
            None => {
7172
0
                return JsonRpcResponse::error(
7173
0
                    id,
7174
                    INVALID_PARAMS,
7175
0
                    "Missing 'description' argument",
7176
                );
7177
            }
7178
        };
7179
0
        let content = match args.get("content").and_then(|v| v.as_str()) {
7180
0
            Some(value) => value,
7181
            None => {
7182
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'content' argument");
7183
            }
7184
        };
7185
0
        let status = match args.get("status").and_then(|v| v.as_str()) {
7186
0
            Some("draft") => Some(crate::models::spec::SpecStatus::Draft),
7187
0
            Some("active") => Some(crate::models::spec::SpecStatus::Active),
7188
0
            Some("done") => Some(crate::models::spec::SpecStatus::Done),
7189
            Some(_) => {
7190
0
                return JsonRpcResponse::error(
7191
0
                    id,
7192
                    INVALID_PARAMS,
7193
0
                    &format!("status must be {}", SpecStatus::HELP),
7194
                );
7195
            }
7196
0
            None => None,
7197
        };
7198
7199
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7200
0
            Ok(value) => value,
7201
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7202
        };
7203
0
        let request = crate::models::spec::CreateSpecRequest {
7204
0
            name: name.trim().to_string(),
7205
0
            title: title.trim().to_string(),
7206
0
            spec_type,
7207
0
            owner: owner.trim().to_string(),
7208
0
            description: description.to_string(),
7209
0
            content: content.to_string(),
7210
0
            status,
7211
0
        };
7212
0
        match self
7213
0
            .service
7214
0
            .spec_service
7215
0
            .create_spec(&org_id, &project.id, &request)
7216
0
            .await
7217
        {
7218
0
            Ok(spec) => jsonrpc_success(
7219
0
                id,
7220
0
                CallToolResult {
7221
0
                    content: vec![ToolContent::Text {
7222
0
                        text: format!("Created spec '{}' (`{}`)", spec.title, spec.id),
7223
0
                    }],
7224
0
                    is_error: None,
7225
0
                },
7226
            ),
7227
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
7228
        }
7229
0
    }
7230
7231
0
    pub(super) async fn call_get_spec(
7232
0
        &mut self,
7233
0
        id: Option<serde_json::Value>,
7234
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7235
0
    ) -> JsonRpcResponse {
7236
0
        let args = match arguments {
7237
0
            Some(a) => a,
7238
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7239
        };
7240
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7241
0
            Some(value) => value,
7242
            None => {
7243
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
7244
            }
7245
        };
7246
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
7247
0
            Some(value) => value,
7248
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
7249
        };
7250
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7251
0
            Ok(value) => value,
7252
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7253
        };
7254
0
        let spec_id = match self
7255
0
            .resolve_spec_id_by_name(&org_id, &project.id, project_name, name)
7256
0
            .await
7257
        {
7258
0
            Ok(value) => value,
7259
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7260
        };
7261
0
        match self
7262
0
            .service
7263
0
            .spec_service
7264
0
            .get_spec(&org_id, &project.id, &spec_id)
7265
0
            .await
7266
        {
7267
0
            Ok(detail) => match serde_json::to_string_pretty(&detail) {
7268
0
                Ok(text) => jsonrpc_success(
7269
0
                    id,
7270
0
                    CallToolResult {
7271
0
                        content: vec![ToolContent::Text { text }],
7272
0
                        is_error: None,
7273
0
                    },
7274
                ),
7275
0
                Err(error) => JsonRpcResponse::error(
7276
0
                    id,
7277
                    INTERNAL_ERROR,
7278
0
                    &format!("Failed to serialize spec detail: {}", error),
7279
                ),
7280
            },
7281
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
7282
        }
7283
0
    }
7284
7285
0
    pub(super) async fn call_list_specs(
7286
0
        &mut self,
7287
0
        id: Option<serde_json::Value>,
7288
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7289
0
    ) -> JsonRpcResponse {
7290
0
        let args = match arguments {
7291
0
            Some(a) => a,
7292
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7293
        };
7294
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7295
0
            Some(value) => value,
7296
            None => {
7297
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
7298
            }
7299
        };
7300
0
        let status = match args.get("status").and_then(|v| v.as_str()) {
7301
0
            Some("draft") => Some(crate::models::spec::SpecStatus::Draft),
7302
0
            Some("active") => Some(crate::models::spec::SpecStatus::Active),
7303
0
            Some("done") => Some(crate::models::spec::SpecStatus::Done),
7304
            Some(_) => {
7305
0
                return JsonRpcResponse::error(
7306
0
                    id,
7307
                    INVALID_PARAMS,
7308
0
                    &format!("status must be {}", SpecStatus::HELP),
7309
                );
7310
            }
7311
0
            None => None,
7312
        };
7313
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7314
0
            Ok(value) => value,
7315
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7316
        };
7317
0
        match self
7318
0
            .service
7319
0
            .spec_service
7320
0
            .list_specs(&org_id, &project.id, status)
7321
0
            .await
7322
        {
7323
0
            Ok(list) => match serde_json::to_string_pretty(&list) {
7324
0
                Ok(text) => jsonrpc_success(
7325
0
                    id,
7326
0
                    CallToolResult {
7327
0
                        content: vec![ToolContent::Text { text }],
7328
0
                        is_error: None,
7329
0
                    },
7330
                ),
7331
0
                Err(error) => JsonRpcResponse::error(
7332
0
                    id,
7333
                    INTERNAL_ERROR,
7334
0
                    &format!("Failed to serialize specs list: {}", error),
7335
                ),
7336
            },
7337
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
7338
        }
7339
0
    }
7340
7341
0
    pub(super) async fn call_update_spec(
7342
0
        &mut self,
7343
0
        id: Option<serde_json::Value>,
7344
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7345
0
    ) -> JsonRpcResponse {
7346
0
        let args = match arguments {
7347
0
            Some(a) => a,
7348
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7349
        };
7350
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7351
0
            Some(value) => value,
7352
            None => {
7353
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
7354
            }
7355
        };
7356
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
7357
0
            Some(value) => value,
7358
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
7359
        };
7360
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7361
0
            Ok(value) => value,
7362
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7363
        };
7364
0
        let spec_id = match self
7365
0
            .resolve_spec_id_by_name(&org_id, &project.id, project_name, name)
7366
0
            .await
7367
        {
7368
0
            Ok(value) => value,
7369
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7370
        };
7371
0
        let spec_type = match args.get("spec_type").and_then(|v| v.as_str()) {
7372
0
            Some("feature") => Some(crate::models::spec::SpecType::Feature),
7373
0
            Some("bugfix") => Some(crate::models::spec::SpecType::Bugfix),
7374
0
            Some("architecture") => Some(crate::models::spec::SpecType::Architecture),
7375
            Some(_) => {
7376
0
                return JsonRpcResponse::error(
7377
0
                    id,
7378
                    INVALID_PARAMS,
7379
0
                    &format!("spec_type must be {}", SpecType::HELP),
7380
                );
7381
            }
7382
0
            None => None,
7383
        };
7384
0
        let status = match args.get("status").and_then(|v| v.as_str()) {
7385
0
            Some("draft") => Some(crate::models::spec::SpecStatus::Draft),
7386
0
            Some("active") => Some(crate::models::spec::SpecStatus::Active),
7387
0
            Some("done") => Some(crate::models::spec::SpecStatus::Done),
7388
            Some(_) => {
7389
0
                return JsonRpcResponse::error(
7390
0
                    id,
7391
                    INVALID_PARAMS,
7392
0
                    &format!("status must be {}", SpecStatus::HELP),
7393
                );
7394
            }
7395
0
            None => None,
7396
        };
7397
0
        let request = crate::models::spec::UpdateSpecRequest {
7398
0
            new_name: args
7399
0
                .get("new_name")
7400
0
                .and_then(|value| value.as_str())
7401
0
                .map(str::to_string),
7402
0
            title: args
7403
0
                .get("title")
7404
0
                .and_then(|value| value.as_str())
7405
0
                .map(str::to_string),
7406
0
            spec_type,
7407
0
            status,
7408
0
            owner: args
7409
0
                .get("owner")
7410
0
                .and_then(|value| value.as_str())
7411
0
                .map(str::to_string),
7412
0
            description: args
7413
0
                .get("description")
7414
0
                .and_then(|value| value.as_str())
7415
0
                .map(str::to_string),
7416
0
            content: args
7417
0
                .get("content")
7418
0
                .and_then(|value| value.as_str())
7419
0
                .map(str::to_string),
7420
        };
7421
0
        match self
7422
0
            .service
7423
0
            .spec_service
7424
0
            .update_spec(&org_id, &project.id, &spec_id, request)
7425
0
            .await
7426
        {
7427
0
            Ok(spec) => jsonrpc_success(
7428
0
                id,
7429
0
                CallToolResult {
7430
0
                    content: vec![ToolContent::Text {
7431
0
                        text: format!("Updated spec '{}' (`{}`)", spec.title, spec.id),
7432
0
                    }],
7433
0
                    is_error: None,
7434
0
                },
7435
            ),
7436
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
7437
        }
7438
0
    }
7439
7440
0
    pub(super) async fn call_delete_spec(
7441
0
        &mut self,
7442
0
        id: Option<serde_json::Value>,
7443
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7444
0
    ) -> JsonRpcResponse {
7445
0
        let args = match arguments {
7446
0
            Some(a) => a,
7447
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7448
        };
7449
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7450
0
            Some(value) => value,
7451
            None => {
7452
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
7453
            }
7454
        };
7455
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
7456
0
            Some(value) => value,
7457
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
7458
        };
7459
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7460
0
            Ok(value) => value,
7461
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7462
        };
7463
0
        let spec_id = match self
7464
0
            .resolve_spec_id_by_name(&org_id, &project.id, project_name, name)
7465
0
            .await
7466
        {
7467
0
            Ok(value) => value,
7468
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7469
        };
7470
0
        match self
7471
0
            .service
7472
0
            .spec_service
7473
0
            .delete_spec(&org_id, &project.id, &spec_id)
7474
0
            .await
7475
        {
7476
0
            Ok(()) => jsonrpc_success(
7477
0
                id,
7478
0
                CallToolResult {
7479
0
                    content: vec![ToolContent::Text {
7480
0
                        text: format!("Deleted spec '{}'", name),
7481
0
                    }],
7482
0
                    is_error: None,
7483
0
                },
7484
            ),
7485
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
7486
        }
7487
0
    }
7488
7489
0
    pub(super) async fn call_create_task(
7490
0
        &mut self,
7491
0
        id: Option<serde_json::Value>,
7492
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7493
0
    ) -> JsonRpcResponse {
7494
0
        let args = match arguments {
7495
0
            Some(a) => a,
7496
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7497
        };
7498
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7499
0
            Some(value) => value,
7500
            None => {
7501
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
7502
            }
7503
        };
7504
0
        let spec_name = match args.get("spec_name").and_then(|v| v.as_str()) {
7505
0
            Some(value) => value,
7506
            None => {
7507
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'spec_name' argument");
7508
            }
7509
        };
7510
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
7511
0
            Some(value) => value,
7512
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
7513
        };
7514
0
        let title = match args.get("title").and_then(|v| v.as_str()) {
7515
0
            Some(value) => value,
7516
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'title' argument"),
7517
        };
7518
0
        let description = match args.get("description").and_then(|v| v.as_str()) {
7519
0
            Some(value) => value,
7520
            None => {
7521
0
                return JsonRpcResponse::error(
7522
0
                    id,
7523
                    INVALID_PARAMS,
7524
0
                    "Missing 'description' argument",
7525
                );
7526
            }
7527
        };
7528
0
        let executor_type = match args.get("executor_type").and_then(|v| v.as_str()) {
7529
0
            Some("human") => crate::models::spec::ExecutorType::Human,
7530
0
            Some("agent") => crate::models::spec::ExecutorType::Agent,
7531
0
            Some("both") => crate::models::spec::ExecutorType::Both,
7532
            Some(_) => {
7533
0
                return JsonRpcResponse::error(
7534
0
                    id,
7535
                    INVALID_PARAMS,
7536
0
                    &format!("executor_type must be {}", ExecutorType::HELP),
7537
                );
7538
            }
7539
            None => {
7540
0
                return JsonRpcResponse::error(
7541
0
                    id,
7542
                    INVALID_PARAMS,
7543
0
                    "Missing 'executor_type' argument",
7544
                );
7545
            }
7546
        };
7547
0
        let status = match args.get("status").and_then(|v| v.as_str()) {
7548
0
            Some("todo") => Some(crate::models::spec::TaskStatus::Todo),
7549
0
            Some("in_progress") => Some(crate::models::spec::TaskStatus::InProgress),
7550
0
            Some("done") => Some(crate::models::spec::TaskStatus::Done),
7551
0
            Some("skipped") => Some(crate::models::spec::TaskStatus::Skipped),
7552
            Some(_) => {
7553
0
                return JsonRpcResponse::error(
7554
0
                    id,
7555
                    INVALID_PARAMS,
7556
0
                    &format!("status must be {}", TaskStatus::HELP),
7557
                );
7558
            }
7559
0
            None => None,
7560
        };
7561
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7562
0
            Ok(value) => value,
7563
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7564
        };
7565
0
        let spec_id = match self
7566
0
            .resolve_spec_id_by_name(&org_id, &project.id, project_name, spec_name)
7567
0
            .await
7568
        {
7569
0
            Ok(value) => value,
7570
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7571
        };
7572
0
        let request = crate::models::spec::CreateTaskRequest {
7573
0
            name: name.to_string(),
7574
0
            title: title.to_string(),
7575
0
            description: description.to_string(),
7576
0
            executor_type,
7577
0
            status,
7578
0
            sort_order: args
7579
0
                .get("sort_order")
7580
0
                .and_then(|value| value.as_i64())
7581
0
                .map(|value| value as i32),
7582
0
            acceptance_criteria: args
7583
0
                .get("acceptance_criteria")
7584
0
                .and_then(|value| value.as_str())
7585
0
                .map(str::to_string),
7586
0
            effort_estimate: args
7587
0
                .get("effort_estimate")
7588
0
                .and_then(|value| value.as_str())
7589
0
                .map(str::to_string),
7590
        };
7591
0
        match self
7592
0
            .service
7593
0
            .spec_service
7594
0
            .create_task(&org_id, &project.id, &spec_id, &request)
7595
0
            .await
7596
        {
7597
0
            Ok(task) => jsonrpc_success(
7598
0
                id,
7599
0
                CallToolResult {
7600
0
                    content: vec![ToolContent::Text {
7601
0
                        text: format!("Created task '{}' (`{}`)", task.title, task.id),
7602
0
                    }],
7603
0
                    is_error: None,
7604
0
                },
7605
            ),
7606
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
7607
        }
7608
0
    }
7609
7610
0
    pub(super) async fn call_get_task(
7611
0
        &mut self,
7612
0
        id: Option<serde_json::Value>,
7613
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7614
0
    ) -> JsonRpcResponse {
7615
0
        let args = match arguments {
7616
0
            Some(a) => a,
7617
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7618
        };
7619
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7620
0
            Some(value) => value,
7621
            None => {
7622
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
7623
            }
7624
        };
7625
0
        let spec_name = match args.get("spec_name").and_then(|v| v.as_str()) {
7626
0
            Some(value) => value,
7627
            None => {
7628
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'spec_name' argument");
7629
            }
7630
        };
7631
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
7632
0
            Some(value) => value,
7633
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
7634
        };
7635
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7636
0
            Ok(value) => value,
7637
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7638
        };
7639
0
        let spec_id = match self
7640
0
            .resolve_spec_id_by_name(&org_id, &project.id, project_name, spec_name)
7641
0
            .await
7642
        {
7643
0
            Ok(value) => value,
7644
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7645
        };
7646
0
        let task_id = match self
7647
0
            .resolve_task_id_by_name(&org_id, &project.id, &spec_id, spec_name, name)
7648
0
            .await
7649
        {
7650
0
            Ok(value) => value,
7651
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7652
        };
7653
0
        match self
7654
0
            .service
7655
0
            .spec_service
7656
0
            .get_task(&org_id, &project.id, &spec_id, &task_id)
7657
0
            .await
7658
        {
7659
0
            Ok(detail) => match serde_json::to_string_pretty(&detail) {
7660
0
                Ok(text) => jsonrpc_success(
7661
0
                    id,
7662
0
                    CallToolResult {
7663
0
                        content: vec![ToolContent::Text { text }],
7664
0
                        is_error: None,
7665
0
                    },
7666
                ),
7667
0
                Err(error) => JsonRpcResponse::error(
7668
0
                    id,
7669
                    INTERNAL_ERROR,
7670
0
                    &format!("Failed to serialize task detail: {}", error),
7671
                ),
7672
            },
7673
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
7674
        }
7675
0
    }
7676
7677
0
    pub(super) async fn call_list_tasks(
7678
0
        &mut self,
7679
0
        id: Option<serde_json::Value>,
7680
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7681
0
    ) -> JsonRpcResponse {
7682
0
        let args = match arguments {
7683
0
            Some(a) => a,
7684
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7685
        };
7686
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7687
0
            Some(value) => value,
7688
            None => {
7689
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
7690
            }
7691
        };
7692
0
        let spec_name = match args.get("spec_name").and_then(|v| v.as_str()) {
7693
0
            Some(value) => value,
7694
            None => {
7695
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'spec_name' argument");
7696
            }
7697
        };
7698
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7699
0
            Ok(value) => value,
7700
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7701
        };
7702
0
        let spec_id = match self
7703
0
            .resolve_spec_id_by_name(&org_id, &project.id, project_name, spec_name)
7704
0
            .await
7705
        {
7706
0
            Ok(value) => value,
7707
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7708
        };
7709
0
        match self
7710
0
            .service
7711
0
            .spec_service
7712
0
            .list_tasks(&org_id, &project.id, &spec_id)
7713
0
            .await
7714
        {
7715
0
            Ok(list) => match serde_json::to_string_pretty(&list) {
7716
0
                Ok(text) => jsonrpc_success(
7717
0
                    id,
7718
0
                    CallToolResult {
7719
0
                        content: vec![ToolContent::Text { text }],
7720
0
                        is_error: None,
7721
0
                    },
7722
                ),
7723
0
                Err(error) => JsonRpcResponse::error(
7724
0
                    id,
7725
                    INTERNAL_ERROR,
7726
0
                    &format!("Failed to serialize tasks list: {}", error),
7727
                ),
7728
            },
7729
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
7730
        }
7731
0
    }
7732
7733
0
    pub(super) async fn call_update_task(
7734
0
        &mut self,
7735
0
        id: Option<serde_json::Value>,
7736
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7737
0
    ) -> JsonRpcResponse {
7738
0
        let args = match arguments {
7739
0
            Some(a) => a,
7740
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7741
        };
7742
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7743
0
            Some(value) => value,
7744
            None => {
7745
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
7746
            }
7747
        };
7748
0
        let spec_name = match args.get("spec_name").and_then(|v| v.as_str()) {
7749
0
            Some(value) => value,
7750
            None => {
7751
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'spec_name' argument");
7752
            }
7753
        };
7754
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
7755
0
            Some(value) => value,
7756
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
7757
        };
7758
0
        let status = match args.get("status").and_then(|v| v.as_str()) {
7759
0
            Some("todo") => Some(crate::models::spec::TaskStatus::Todo),
7760
0
            Some("in_progress") => Some(crate::models::spec::TaskStatus::InProgress),
7761
0
            Some("done") => Some(crate::models::spec::TaskStatus::Done),
7762
0
            Some("skipped") => Some(crate::models::spec::TaskStatus::Skipped),
7763
            Some(_) => {
7764
0
                return JsonRpcResponse::error(
7765
0
                    id,
7766
                    INVALID_PARAMS,
7767
0
                    &format!("status must be {}", TaskStatus::HELP),
7768
                );
7769
            }
7770
0
            None => None,
7771
        };
7772
0
        let executor_type = match args.get("executor_type").and_then(|v| v.as_str()) {
7773
0
            Some("human") => Some(crate::models::spec::ExecutorType::Human),
7774
0
            Some("agent") => Some(crate::models::spec::ExecutorType::Agent),
7775
0
            Some("both") => Some(crate::models::spec::ExecutorType::Both),
7776
            Some(_) => {
7777
0
                return JsonRpcResponse::error(
7778
0
                    id,
7779
                    INVALID_PARAMS,
7780
0
                    &format!("executor_type must be {}", ExecutorType::HELP),
7781
                );
7782
            }
7783
0
            None => None,
7784
        };
7785
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7786
0
            Ok(value) => value,
7787
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7788
        };
7789
0
        let spec_id = match self
7790
0
            .resolve_spec_id_by_name(&org_id, &project.id, project_name, spec_name)
7791
0
            .await
7792
        {
7793
0
            Ok(value) => value,
7794
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7795
        };
7796
0
        let task_id = match self
7797
0
            .resolve_task_id_by_name(&org_id, &project.id, &spec_id, spec_name, name)
7798
0
            .await
7799
        {
7800
0
            Ok(value) => value,
7801
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7802
        };
7803
0
        let request = crate::models::spec::UpdateTaskRequest {
7804
0
            new_name: args
7805
0
                .get("new_name")
7806
0
                .and_then(|value| value.as_str())
7807
0
                .map(str::to_string),
7808
0
            title: args
7809
0
                .get("title")
7810
0
                .and_then(|value| value.as_str())
7811
0
                .map(str::to_string),
7812
0
            description: args
7813
0
                .get("description")
7814
0
                .and_then(|value| value.as_str())
7815
0
                .map(str::to_string),
7816
0
            executor_type,
7817
0
            status,
7818
0
            sort_order: args
7819
0
                .get("sort_order")
7820
0
                .and_then(|value| value.as_i64())
7821
0
                .map(|value| value as i32),
7822
0
            acceptance_criteria: args
7823
0
                .get("acceptance_criteria")
7824
0
                .and_then(|value| value.as_str())
7825
0
                .map(str::to_string),
7826
0
            effort_estimate: args
7827
0
                .get("effort_estimate")
7828
0
                .and_then(|value| value.as_str())
7829
0
                .map(str::to_string),
7830
        };
7831
0
        match self
7832
0
            .service
7833
0
            .spec_service
7834
0
            .update_task(&org_id, &project.id, &spec_id, &task_id, request)
7835
0
            .await
7836
        {
7837
0
            Ok(task) => jsonrpc_success(
7838
0
                id,
7839
0
                CallToolResult {
7840
0
                    content: vec![ToolContent::Text {
7841
0
                        text: format!("Updated task '{}' (`{}`)", task.title, task.id),
7842
0
                    }],
7843
0
                    is_error: None,
7844
0
                },
7845
            ),
7846
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
7847
        }
7848
0
    }
7849
7850
0
    pub(super) async fn call_delete_task(
7851
0
        &mut self,
7852
0
        id: Option<serde_json::Value>,
7853
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7854
0
    ) -> JsonRpcResponse {
7855
0
        let args = match arguments {
7856
0
            Some(a) => a,
7857
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7858
        };
7859
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7860
0
            Some(value) => value,
7861
            None => {
7862
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
7863
            }
7864
        };
7865
0
        let spec_name = match args.get("spec_name").and_then(|v| v.as_str()) {
7866
0
            Some(value) => value,
7867
            None => {
7868
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'spec_name' argument");
7869
            }
7870
        };
7871
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
7872
0
            Some(value) => value,
7873
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
7874
        };
7875
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7876
0
            Ok(value) => value,
7877
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7878
        };
7879
0
        let spec_id = match self
7880
0
            .resolve_spec_id_by_name(&org_id, &project.id, project_name, spec_name)
7881
0
            .await
7882
        {
7883
0
            Ok(value) => value,
7884
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7885
        };
7886
0
        let task_id = match self
7887
0
            .resolve_task_id_by_name(&org_id, &project.id, &spec_id, spec_name, name)
7888
0
            .await
7889
        {
7890
0
            Ok(value) => value,
7891
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7892
        };
7893
0
        match self
7894
0
            .service
7895
0
            .spec_service
7896
0
            .delete_task(&org_id, &project.id, &spec_id, &task_id)
7897
0
            .await
7898
        {
7899
0
            Ok(()) => jsonrpc_success(
7900
0
                id,
7901
0
                CallToolResult {
7902
0
                    content: vec![ToolContent::Text {
7903
0
                        text: format!("Deleted task '{}'", name),
7904
0
                    }],
7905
0
                    is_error: None,
7906
0
                },
7907
            ),
7908
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
7909
        }
7910
0
    }
7911
7912
0
    pub(super) async fn call_import_skill(
7913
0
        &mut self,
7914
0
        id: Option<serde_json::Value>,
7915
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
7916
0
    ) -> JsonRpcResponse {
7917
0
        let args = match arguments {
7918
0
            Some(a) => a,
7919
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
7920
        };
7921
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
7922
0
            Some(value) => value,
7923
            None => {
7924
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
7925
            }
7926
        };
7927
0
        let source_url = match args.get("source_url").and_then(|v| v.as_str()) {
7928
0
            Some(value) => value,
7929
            None => {
7930
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'source_url' argument");
7931
            }
7932
        };
7933
7934
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
7935
0
            Ok(value) => value,
7936
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
7937
        };
7938
7939
0
        let parent_skill_id = match args.get("parent_skill_name").and_then(|v| v.as_str()) {
7940
0
            Some(parent_name) if !parent_name.trim().is_empty() => {
7941
0
                match self
7942
0
                    .resolve_skill_id_by_name(&org_id, &project.id, project_name, parent_name)
7943
0
                    .await
7944
                {
7945
0
                    Ok(value) => Some(value),
7946
0
                    Err(error) => return JsonRpcResponse::from_api_error(id, error),
7947
                }
7948
            }
7949
0
            _ => None,
7950
        };
7951
7952
0
        let request = crate::models::skill::ImportSkillRequest {
7953
0
            source_url: source_url.to_string(),
7954
0
            name: args
7955
0
                .get("name")
7956
0
                .and_then(|value| value.as_str())
7957
0
                .map(str::to_string),
7958
0
            display_name: args
7959
0
                .get("display_name")
7960
0
                .and_then(|value| value.as_str())
7961
0
                .map(str::to_string),
7962
0
            description: args
7963
0
                .get("description")
7964
0
                .and_then(|value| value.as_str())
7965
0
                .map(str::to_string),
7966
0
            content: args
7967
0
                .get("content")
7968
0
                .and_then(|value| value.as_str())
7969
0
                .map(str::to_string),
7970
0
            parent_skill_id,
7971
0
            sort_order: args
7972
0
                .get("sort_order")
7973
0
                .and_then(|value| value.as_i64())
7974
0
                .map(|value| value as i32),
7975
        };
7976
7977
0
        match self
7978
0
            .service
7979
0
            .skill_service
7980
0
            .import_skill(&org_id, &project.id, &request)
7981
0
            .await
7982
        {
7983
0
            Ok(result_value) => jsonrpc_success(
7984
0
                id,
7985
0
                CallToolResult {
7986
0
                    content: vec![ToolContent::Text {
7987
0
                        text: format!(
7988
0
                            "📥 Imported skill **{}** (`{}`)\n{}\nSub-skills: created {}, updated {}, failed {}, skipped {}, removed {}",
7989
0
                            result_value.skill.display_name,
7990
0
                            result_value.skill.id,
7991
0
                            result_value.summary,
7992
0
                            result_value.subskills.created_count,
7993
0
                            result_value.subskills.updated_count,
7994
0
                            result_value.subskills.failed_count,
7995
0
                            result_value.subskills.skipped_count,
7996
0
                            result_value.subskills.removed_count,
7997
0
                        ),
7998
0
                    }],
7999
0
                    is_error: None,
8000
0
                },
8001
            ),
8002
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
8003
        }
8004
0
    }
8005
8006
0
    pub(super) async fn call_reimport_skill(
8007
0
        &mut self,
8008
0
        id: Option<serde_json::Value>,
8009
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
8010
0
    ) -> JsonRpcResponse {
8011
0
        let args = match arguments {
8012
0
            Some(a) => a,
8013
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
8014
        };
8015
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
8016
0
            Some(value) => value,
8017
            None => {
8018
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
8019
            }
8020
        };
8021
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
8022
0
            Some(value) => value,
8023
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
8024
        };
8025
8026
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
8027
0
            Ok(value) => value,
8028
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8029
        };
8030
0
        let skill_id = match self
8031
0
            .resolve_skill_id_by_name(&org_id, &project.id, project_name, name)
8032
0
            .await
8033
        {
8034
0
            Ok(value) => value,
8035
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8036
        };
8037
8038
0
        match self
8039
0
            .service
8040
0
            .skill_service
8041
0
            .reimport_skill(&org_id, &project.id, &skill_id)
8042
0
            .await
8043
        {
8044
0
            Ok(result_value) => jsonrpc_success(
8045
0
                id,
8046
                CallToolResult {
8047
0
                    content: vec![ToolContent::Text {
8048
0
                        text: format!(
8049
                            "🔁 Reimport {} for **{}** (`{}`): {}\nSub-skills: created {}, updated {}, failed {}, skipped {}, removed {}",
8050
0
                            if result_value.changed {
8051
0
                                "changed"
8052
                            } else {
8053
0
                                "no-change"
8054
                            },
8055
                            result_value.skill.display_name,
8056
                            result_value.skill.id,
8057
                            result_value.summary,
8058
                            result_value.subskills.created_count,
8059
                            result_value.subskills.updated_count,
8060
                            result_value.subskills.failed_count,
8061
                            result_value.subskills.skipped_count,
8062
                            result_value.subskills.removed_count,
8063
                        ),
8064
                    }],
8065
0
                    is_error: None,
8066
                },
8067
            ),
8068
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
8069
        }
8070
0
    }
8071
8072
0
    pub(super) async fn call_link_skill_to_component(
8073
0
        &mut self,
8074
0
        id: Option<serde_json::Value>,
8075
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
8076
0
    ) -> JsonRpcResponse {
8077
0
        let args = match arguments {
8078
0
            Some(a) => a,
8079
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
8080
        };
8081
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
8082
0
            Some(value) => value,
8083
            None => {
8084
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
8085
            }
8086
        };
8087
0
        let skill_name = match args.get("skill_name").and_then(|v| v.as_str()) {
8088
0
            Some(value) => value,
8089
            None => {
8090
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'skill_name' argument");
8091
            }
8092
        };
8093
0
        let target_name = match args.get("target_name").and_then(|v| v.as_str()) {
8094
0
            Some(value) => value,
8095
            None => {
8096
0
                return JsonRpcResponse::error(
8097
0
                    id,
8098
                    INVALID_PARAMS,
8099
0
                    "Missing 'target_name' argument",
8100
                );
8101
            }
8102
        };
8103
0
        let target_kind = match args.get("target_kind").and_then(|v| v.as_str()) {
8104
0
            Some(value) => {
8105
0
                match crate::models::skill::ArchitectureLinkTargetKind::from_str(value) {
8106
0
                    Ok(kind) => kind,
8107
0
                    Err(error) => return JsonRpcResponse::from_api_error(id, error.into()),
8108
                }
8109
            }
8110
            None => {
8111
0
                return JsonRpcResponse::error(
8112
0
                    id,
8113
                    INVALID_PARAMS,
8114
0
                    "Missing 'target_kind' argument",
8115
                );
8116
            }
8117
        };
8118
8119
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
8120
0
            Ok(value) => value,
8121
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8122
        };
8123
0
        let skill_id = match self
8124
0
            .resolve_skill_id_by_name(&org_id, &project.id, project_name, skill_name)
8125
0
            .await
8126
        {
8127
0
            Ok(value) => value,
8128
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8129
        };
8130
8131
0
        let target_id = match target_kind {
8132
            crate::models::skill::ArchitectureLinkTargetKind::Container => {
8133
0
                match resolve_component_ref_by_entity_kind(
8134
0
                    self.service.component_repo.as_ref(),
8135
0
                    &project.id,
8136
0
                    target_name,
8137
0
                    ConnectionTargetKind::Container,
8138
                )
8139
0
                .await
8140
                {
8141
0
                    Ok((component_id, _)) => component_id.to_string(),
8142
0
                    Err(error) => return JsonRpcResponse::from_api_error(id, error),
8143
                }
8144
            }
8145
            crate::models::skill::ArchitectureLinkTargetKind::Component => {
8146
0
                match resolve_component_ref_by_entity_kind(
8147
0
                    self.service.component_repo.as_ref(),
8148
0
                    &project.id,
8149
0
                    target_name,
8150
0
                    ConnectionTargetKind::Component,
8151
                )
8152
0
                .await
8153
                {
8154
0
                    Ok((component_id, _)) => component_id.to_string(),
8155
0
                    Err(error) => return JsonRpcResponse::from_api_error(id, error),
8156
                }
8157
            }
8158
            crate::models::skill::ArchitectureLinkTargetKind::Store => {
8159
0
                match resolve_store_id_from_value(
8160
0
                    &self.service.store_service,
8161
0
                    &org_id,
8162
0
                    &project.id,
8163
0
                    &self.user_id,
8164
0
                    target_name,
8165
                )
8166
0
                .await
8167
                {
8168
0
                    Ok(store_id) => store_id.to_string(),
8169
0
                    Err(error) => return JsonRpcResponse::from_api_error(id, error),
8170
                }
8171
            }
8172
            crate::models::skill::ArchitectureLinkTargetKind::ExternalSystem => {
8173
0
                match resolve_external_system_id_from_value(
8174
0
                    &self.service.external_system_service,
8175
0
                    &org_id,
8176
0
                    &project.id,
8177
0
                    &self.user_id,
8178
0
                    target_name,
8179
                )
8180
0
                .await
8181
                {
8182
0
                    Ok(system_id) => system_id.to_string(),
8183
0
                    Err(error) => return JsonRpcResponse::from_api_error(id, error),
8184
                }
8185
            }
8186
        };
8187
8188
0
        match self
8189
0
            .service
8190
0
            .skill_service
8191
0
            .link_skill(
8192
0
                &org_id,
8193
0
                &project.id,
8194
0
                &skill_id,
8195
0
                &target_id,
8196
0
                target_kind,
8197
0
                &self.user_id,
8198
            )
8199
0
            .await
8200
        {
8201
0
            Ok(link) => jsonrpc_success(
8202
0
                id,
8203
0
                CallToolResult {
8204
0
                    content: vec![ToolContent::Text {
8205
0
                        text: format!(
8206
0
                            "🔗 Linked skill '{}' (link_id=`{}`)",
8207
0
                            skill_name, link.edge_id
8208
0
                        ),
8209
0
                    }],
8210
0
                    is_error: None,
8211
0
                },
8212
            ),
8213
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
8214
        }
8215
0
    }
8216
8217
0
    pub(super) async fn call_unlink_skill_from_component(
8218
0
        &mut self,
8219
0
        id: Option<serde_json::Value>,
8220
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
8221
0
    ) -> JsonRpcResponse {
8222
0
        let args = match arguments {
8223
0
            Some(a) => a,
8224
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
8225
        };
8226
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
8227
0
            Some(value) => value,
8228
            None => {
8229
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
8230
            }
8231
        };
8232
0
        let skill_name = match args.get("skill_name").and_then(|v| v.as_str()) {
8233
0
            Some(value) => value,
8234
            None => {
8235
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'skill_name' argument");
8236
            }
8237
        };
8238
0
        let link_id = match args.get("link_id").and_then(|v| v.as_str()) {
8239
0
            Some(value) => value,
8240
            None => {
8241
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'link_id' argument");
8242
            }
8243
        };
8244
8245
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
8246
0
            Ok(value) => value,
8247
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8248
        };
8249
0
        let skill_id = match self
8250
0
            .resolve_skill_id_by_name(&org_id, &project.id, project_name, skill_name)
8251
0
            .await
8252
        {
8253
0
            Ok(value) => value,
8254
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8255
        };
8256
8257
0
        match self
8258
0
            .service
8259
0
            .skill_service
8260
0
            .unlink_skill(&org_id, &project.id, &skill_id, link_id, &self.user_id)
8261
0
            .await
8262
        {
8263
0
            Ok(()) => jsonrpc_success(
8264
0
                id,
8265
0
                CallToolResult {
8266
0
                    content: vec![ToolContent::Text {
8267
0
                        text: format!("🔗 Unlinked skill '{}' (link_id=`{}`)", skill_name, link_id),
8268
0
                    }],
8269
0
                    is_error: None,
8270
0
                },
8271
            ),
8272
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
8273
        }
8274
0
    }
8275
8276
0
    pub(super) async fn call_link_spec(
8277
0
        &mut self,
8278
0
        id: Option<serde_json::Value>,
8279
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
8280
0
    ) -> JsonRpcResponse {
8281
0
        let args = match arguments {
8282
0
            Some(a) => a,
8283
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
8284
        };
8285
0
        let project_name = match required_arg(&args, "project", id.clone()) {
8286
0
            Ok(value) => value,
8287
0
            Err(response) => return response,
8288
        };
8289
0
        let spec_name = match required_arg(&args, "spec_name", id.clone()) {
8290
0
            Ok(value) => value,
8291
0
            Err(response) => return response,
8292
        };
8293
0
        let target_name = match required_arg(&args, "target_name", id.clone()) {
8294
0
            Ok(value) => value,
8295
0
            Err(response) => return response,
8296
        };
8297
0
        let target_kind = match required_arg(&args, "target_kind", id.clone()) {
8298
0
            Ok(value) => match value.parse::<crate::models::spec::SpecTaskTargetKind>() {
8299
0
                Ok(kind) => kind,
8300
0
                Err(error) => return JsonRpcResponse::from_api_error(id, error.into()),
8301
            },
8302
0
            Err(response) => return response,
8303
        };
8304
0
        let relation_value = match required_arg(&args, "relation", id.clone()) {
8305
0
            Ok(value) => value,
8306
0
            Err(response) => return response,
8307
        };
8308
0
        let relation = match relation_value.parse::<crate::models::spec::SpecLinkRelation>() {
8309
0
            Ok(value) => value,
8310
            Err(_) => {
8311
0
                return JsonRpcResponse::error(
8312
0
                    id,
8313
                    INVALID_PARAMS,
8314
0
                    &format!("relation must be {}", SpecLinkRelation::HELP),
8315
                );
8316
            }
8317
        };
8318
8319
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
8320
0
            Ok(value) => value,
8321
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8322
        };
8323
0
        let spec_id = match self
8324
0
            .resolve_spec_id_by_name(&org_id, &project.id, project_name, spec_name)
8325
0
            .await
8326
        {
8327
0
            Ok(value) => value,
8328
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8329
        };
8330
8331
0
        let (target_id, target_kind_value) = match relation {
8332
0
            crate::models::spec::SpecLinkRelation::Blocks => match self
8333
0
                .resolve_spec_id_by_name(&org_id, &project.id, project_name, target_name)
8334
0
                .await
8335
            {
8336
0
                Ok(value) => (value.to_string(), "spec".to_string()),
8337
0
                Err(error) => return JsonRpcResponse::from_api_error(id, error),
8338
            },
8339
            crate::models::spec::SpecLinkRelation::ConstrainedBy => {
8340
0
                let qualities = match self
8341
0
                    .service
8342
0
                    .quality_attribute_service
8343
0
                    .list_quality_attributes(&org_id, &project.id, &self.user_id)
8344
0
                    .await
8345
                {
8346
0
                    Ok(list) => list,
8347
0
                    Err(error) => return JsonRpcResponse::from_api_error(id, error),
8348
                };
8349
0
                let matches = qualities
8350
0
                    .into_iter()
8351
0
                    .filter(|attr| attr.name.eq_ignore_ascii_case(target_name))
8352
0
                    .collect::<Vec<_>>();
8353
0
                if matches.len() != 1 {
8354
0
                    return JsonRpcResponse::error(
8355
0
                        id,
8356
                        INVALID_PARAMS,
8357
0
                        "target_name must resolve to exactly one quality attribute",
8358
                    );
8359
0
                }
8360
0
                (matches[0].id.to_string(), "quality_attribute".to_string())
8361
            }
8362
0
            _ => match target_kind {
8363
                crate::models::spec::SpecTaskTargetKind::Container => {
8364
0
                    match resolve_component_ref_by_entity_kind(
8365
0
                        self.service.component_repo.as_ref(),
8366
0
                        &project.id,
8367
0
                        target_name,
8368
0
                        ConnectionTargetKind::Container,
8369
                    )
8370
0
                    .await
8371
                    {
8372
0
                        Ok((component_id, _)) => {
8373
0
                            (component_id.to_string(), target_kind.to_string())
8374
                        }
8375
0
                        Err(error) => return JsonRpcResponse::from_api_error(id, error),
8376
                    }
8377
                }
8378
                crate::models::spec::SpecTaskTargetKind::Component => {
8379
0
                    match resolve_component_ref_by_entity_kind(
8380
0
                        self.service.component_repo.as_ref(),
8381
0
                        &project.id,
8382
0
                        target_name,
8383
0
                        ConnectionTargetKind::Component,
8384
                    )
8385
0
                    .await
8386
                    {
8387
0
                        Ok((component_id, _)) => {
8388
0
                            (component_id.to_string(), target_kind.to_string())
8389
                        }
8390
0
                        Err(error) => return JsonRpcResponse::from_api_error(id, error),
8391
                    }
8392
                }
8393
                crate::models::spec::SpecTaskTargetKind::Store => {
8394
0
                    match resolve_store_id_from_value(
8395
0
                        &self.service.store_service,
8396
0
                        &org_id,
8397
0
                        &project.id,
8398
0
                        &self.user_id,
8399
0
                        target_name,
8400
                    )
8401
0
                    .await
8402
                    {
8403
0
                        Ok(store_id) => (store_id.to_string(), target_kind.to_string()),
8404
0
                        Err(error) => return JsonRpcResponse::from_api_error(id, error),
8405
                    }
8406
                }
8407
                crate::models::spec::SpecTaskTargetKind::ExternalSystem => {
8408
0
                    match resolve_external_system_id_from_value(
8409
0
                        &self.service.external_system_service,
8410
0
                        &org_id,
8411
0
                        &project.id,
8412
0
                        &self.user_id,
8413
0
                        target_name,
8414
                    )
8415
0
                    .await
8416
                    {
8417
0
                        Ok(system_id) => (system_id.to_string(), target_kind.to_string()),
8418
0
                        Err(error) => return JsonRpcResponse::from_api_error(id, error),
8419
                    }
8420
                }
8421
                crate::models::spec::SpecTaskTargetKind::Actor => {
8422
0
                    match resolve_actor_id_from_value(
8423
0
                        &self.service.actor_service,
8424
0
                        &org_id,
8425
0
                        &project.id,
8426
0
                        &self.user_id,
8427
0
                        target_name,
8428
                    )
8429
0
                    .await
8430
                    {
8431
0
                        Ok(actor_id) => (actor_id.to_string(), target_kind.to_string()),
8432
0
                        Err(error) => return JsonRpcResponse::from_api_error(id, error),
8433
                    }
8434
                }
8435
                _ => {
8436
0
                    return JsonRpcResponse::error(
8437
0
                        id,
8438
                        INVALID_PARAMS,
8439
0
                        &format!(
8440
0
                            "target_kind must be {}",
8441
0
                            crate::models::spec::SpecTaskTargetKind::HELP
8442
0
                        ),
8443
                    );
8444
                }
8445
            },
8446
        };
8447
8448
0
        let request = crate::models::spec::CreateSpecLinkRequest {
8449
0
            target_id,
8450
0
            target_kind: target_kind_value.parse().expect("resolved target kind"),
8451
0
            relation,
8452
0
        };
8453
8454
0
        match self
8455
0
            .service
8456
0
            .spec_service
8457
0
            .link_spec(&org_id, &project.id, &spec_id, &request)
8458
0
            .await
8459
        {
8460
0
            Ok(link) => jsonrpc_success(
8461
0
                id,
8462
0
                CallToolResult {
8463
0
                    content: vec![ToolContent::Text {
8464
0
                        text: format!(
8465
0
                            "🔗 Linked spec '{}' (link_id=`{}`)",
8466
0
                            spec_name, link.edge_id
8467
0
                        ),
8468
0
                    }],
8469
0
                    is_error: None,
8470
0
                },
8471
            ),
8472
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
8473
        }
8474
0
    }
8475
8476
0
    pub(super) async fn call_unlink_spec(
8477
0
        &mut self,
8478
0
        id: Option<serde_json::Value>,
8479
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
8480
0
    ) -> JsonRpcResponse {
8481
0
        let args = match arguments {
8482
0
            Some(a) => a,
8483
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
8484
        };
8485
0
        let project_name = match required_arg(&args, "project", id.clone()) {
8486
0
            Ok(value) => value,
8487
0
            Err(response) => return response,
8488
        };
8489
0
        let spec_name = match required_arg(&args, "spec_name", id.clone()) {
8490
0
            Ok(value) => value,
8491
0
            Err(response) => return response,
8492
        };
8493
0
        let link_id = match required_arg(&args, "link_id", id.clone()) {
8494
0
            Ok(value) => value,
8495
0
            Err(response) => return response,
8496
        };
8497
8498
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
8499
0
            Ok(value) => value,
8500
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8501
        };
8502
0
        let spec_id = match self
8503
0
            .resolve_spec_id_by_name(&org_id, &project.id, project_name, spec_name)
8504
0
            .await
8505
        {
8506
0
            Ok(value) => value,
8507
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8508
        };
8509
8510
0
        match self
8511
0
            .service
8512
0
            .spec_service
8513
0
            .unlink_spec(&org_id, &project.id, &spec_id, link_id)
8514
0
            .await
8515
        {
8516
0
            Ok(()) => jsonrpc_success(
8517
0
                id,
8518
0
                CallToolResult {
8519
0
                    content: vec![ToolContent::Text {
8520
0
                        text: format!("🔗 Unlinked spec '{}' (link_id=`{}`)", spec_name, link_id),
8521
0
                    }],
8522
0
                    is_error: None,
8523
0
                },
8524
            ),
8525
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
8526
        }
8527
0
    }
8528
8529
0
    pub(super) async fn call_link_task(
8530
0
        &mut self,
8531
0
        id: Option<serde_json::Value>,
8532
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
8533
0
    ) -> JsonRpcResponse {
8534
0
        let args = match arguments {
8535
0
            Some(a) => a,
8536
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
8537
        };
8538
0
        let project_name = match required_arg(&args, "project", id.clone()) {
8539
0
            Ok(value) => value,
8540
0
            Err(response) => return response,
8541
        };
8542
0
        let spec_name = match required_arg(&args, "spec_name", id.clone()) {
8543
0
            Ok(value) => value,
8544
0
            Err(response) => return response,
8545
        };
8546
0
        let task_name = match required_arg(&args, "task_name", id.clone()) {
8547
0
            Ok(value) => value,
8548
0
            Err(response) => return response,
8549
        };
8550
0
        let target_name = match required_arg(&args, "target_name", id.clone()) {
8551
0
            Ok(value) => value,
8552
0
            Err(response) => return response,
8553
        };
8554
0
        let target_kind = match required_arg(&args, "target_kind", id.clone()) {
8555
0
            Ok(value) => match value.parse::<crate::models::spec::SpecTaskTargetKind>() {
8556
0
                Ok(kind) => kind,
8557
0
                Err(error) => return JsonRpcResponse::from_api_error(id, error.into()),
8558
            },
8559
0
            Err(response) => return response,
8560
        };
8561
0
        let relation_value = match required_arg(&args, "relation", id.clone()) {
8562
0
            Ok(value) => value,
8563
0
            Err(response) => return response,
8564
        };
8565
0
        let relation = match relation_value.parse::<crate::models::spec::TaskLinkRelation>() {
8566
0
            Ok(value) => value,
8567
            Err(_) => {
8568
0
                return JsonRpcResponse::error(
8569
0
                    id,
8570
                    INVALID_PARAMS,
8571
0
                    &format!("relation must be {}", TaskLinkRelation::HELP),
8572
                );
8573
            }
8574
        };
8575
8576
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
8577
0
            Ok(value) => value,
8578
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8579
        };
8580
0
        let spec_id = match self
8581
0
            .resolve_spec_id_by_name(&org_id, &project.id, project_name, spec_name)
8582
0
            .await
8583
        {
8584
0
            Ok(value) => value,
8585
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8586
        };
8587
0
        let task_id = match self
8588
0
            .resolve_task_id_by_name(&org_id, &project.id, &spec_id, spec_name, task_name)
8589
0
            .await
8590
        {
8591
0
            Ok(value) => value,
8592
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8593
        };
8594
8595
0
        let (target_id, target_kind_value) = match relation {
8596
0
            crate::models::spec::TaskLinkRelation::DependsOn => match self
8597
0
                .resolve_task_id_by_name(&org_id, &project.id, &spec_id, spec_name, target_name)
8598
0
                .await
8599
            {
8600
0
                Ok(value) => (value.to_string(), "task".to_string()),
8601
0
                Err(error) => return JsonRpcResponse::from_api_error(id, error),
8602
            },
8603
0
            crate::models::spec::TaskLinkRelation::Modifies => match target_kind {
8604
                crate::models::spec::SpecTaskTargetKind::Container => {
8605
0
                    match resolve_component_ref_by_entity_kind(
8606
0
                        self.service.component_repo.as_ref(),
8607
0
                        &project.id,
8608
0
                        target_name,
8609
0
                        ConnectionTargetKind::Container,
8610
                    )
8611
0
                    .await
8612
                    {
8613
0
                        Ok((component_id, _)) => {
8614
0
                            (component_id.to_string(), target_kind.to_string())
8615
                        }
8616
0
                        Err(error) => return JsonRpcResponse::from_api_error(id, error),
8617
                    }
8618
                }
8619
                crate::models::spec::SpecTaskTargetKind::Component => {
8620
0
                    match resolve_component_ref_by_entity_kind(
8621
0
                        self.service.component_repo.as_ref(),
8622
0
                        &project.id,
8623
0
                        target_name,
8624
0
                        ConnectionTargetKind::Component,
8625
                    )
8626
0
                    .await
8627
                    {
8628
0
                        Ok((component_id, _)) => {
8629
0
                            (component_id.to_string(), target_kind.to_string())
8630
                        }
8631
0
                        Err(error) => return JsonRpcResponse::from_api_error(id, error),
8632
                    }
8633
                }
8634
                crate::models::spec::SpecTaskTargetKind::Store => {
8635
0
                    match resolve_store_id_from_value(
8636
0
                        &self.service.store_service,
8637
0
                        &org_id,
8638
0
                        &project.id,
8639
0
                        &self.user_id,
8640
0
                        target_name,
8641
                    )
8642
0
                    .await
8643
                    {
8644
0
                        Ok(store_id) => (store_id.to_string(), target_kind.to_string()),
8645
0
                        Err(error) => return JsonRpcResponse::from_api_error(id, error),
8646
                    }
8647
                }
8648
                crate::models::spec::SpecTaskTargetKind::ExternalSystem => {
8649
0
                    match resolve_external_system_id_from_value(
8650
0
                        &self.service.external_system_service,
8651
0
                        &org_id,
8652
0
                        &project.id,
8653
0
                        &self.user_id,
8654
0
                        target_name,
8655
                    )
8656
0
                    .await
8657
                    {
8658
0
                        Ok(system_id) => (system_id.to_string(), target_kind.to_string()),
8659
0
                        Err(error) => return JsonRpcResponse::from_api_error(id, error),
8660
                    }
8661
                }
8662
                crate::models::spec::SpecTaskTargetKind::Actor => {
8663
0
                    match resolve_actor_id_from_value(
8664
0
                        &self.service.actor_service,
8665
0
                        &org_id,
8666
0
                        &project.id,
8667
0
                        &self.user_id,
8668
0
                        target_name,
8669
                    )
8670
0
                    .await
8671
                    {
8672
0
                        Ok(actor_id) => (actor_id.to_string(), target_kind.to_string()),
8673
0
                        Err(error) => return JsonRpcResponse::from_api_error(id, error),
8674
                    }
8675
                }
8676
                _ => {
8677
0
                    return JsonRpcResponse::error(
8678
0
                        id,
8679
                        INVALID_PARAMS,
8680
0
                        &format!(
8681
0
                            "target_kind must be {}",
8682
0
                            crate::models::spec::SpecTaskTargetKind::HELP
8683
0
                        ),
8684
                    );
8685
                }
8686
            },
8687
        };
8688
8689
0
        let request = crate::models::spec::CreateTaskLinkRequest {
8690
0
            target_id,
8691
0
            target_kind: target_kind_value.parse().expect("resolved target kind"),
8692
0
            relation,
8693
0
        };
8694
8695
0
        match self
8696
0
            .service
8697
0
            .spec_service
8698
0
            .link_task(&org_id, &project.id, &task_id, &request)
8699
0
            .await
8700
        {
8701
0
            Ok(link) => jsonrpc_success(
8702
0
                id,
8703
0
                CallToolResult {
8704
0
                    content: vec![ToolContent::Text {
8705
0
                        text: format!(
8706
0
                            "🔗 Linked task '{}' (link_id=`{}`)",
8707
0
                            task_name, link.edge_id
8708
0
                        ),
8709
0
                    }],
8710
0
                    is_error: None,
8711
0
                },
8712
            ),
8713
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
8714
        }
8715
0
    }
8716
8717
0
    pub(super) async fn call_unlink_task(
8718
0
        &mut self,
8719
0
        id: Option<serde_json::Value>,
8720
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
8721
0
    ) -> JsonRpcResponse {
8722
0
        let args = match arguments {
8723
0
            Some(a) => a,
8724
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
8725
        };
8726
0
        let project_name = match required_arg(&args, "project", id.clone()) {
8727
0
            Ok(value) => value,
8728
0
            Err(response) => return response,
8729
        };
8730
0
        let spec_name = match required_arg(&args, "spec_name", id.clone()) {
8731
0
            Ok(value) => value,
8732
0
            Err(response) => return response,
8733
        };
8734
0
        let task_name = match required_arg(&args, "task_name", id.clone()) {
8735
0
            Ok(value) => value,
8736
0
            Err(response) => return response,
8737
        };
8738
0
        let link_id = match required_arg(&args, "link_id", id.clone()) {
8739
0
            Ok(value) => value,
8740
0
            Err(response) => return response,
8741
        };
8742
8743
0
        let (org_id, project) = match self.resolve_project_by_name(project_name).await {
8744
0
            Ok(value) => value,
8745
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8746
        };
8747
0
        let spec_id = match self
8748
0
            .resolve_spec_id_by_name(&org_id, &project.id, project_name, spec_name)
8749
0
            .await
8750
        {
8751
0
            Ok(value) => value,
8752
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8753
        };
8754
0
        let task_id = match self
8755
0
            .resolve_task_id_by_name(&org_id, &project.id, &spec_id, spec_name, task_name)
8756
0
            .await
8757
        {
8758
0
            Ok(value) => value,
8759
0
            Err(error) => return JsonRpcResponse::from_api_error(id, error),
8760
        };
8761
8762
0
        match self
8763
0
            .service
8764
0
            .spec_service
8765
0
            .unlink_task(&org_id, &project.id, &task_id, link_id)
8766
0
            .await
8767
        {
8768
0
            Ok(()) => jsonrpc_success(
8769
0
                id,
8770
0
                CallToolResult {
8771
0
                    content: vec![ToolContent::Text {
8772
0
                        text: format!("🔗 Unlinked task '{}' (link_id=`{}`)", task_name, link_id),
8773
0
                    }],
8774
0
                    is_error: None,
8775
0
                },
8776
            ),
8777
0
            Err(error) => JsonRpcResponse::from_api_error(id, error),
8778
        }
8779
0
    }
8780
8781
2
    pub(super) async fn call_link_quality_to_component(
8782
2
        &mut self,
8783
2
        id: Option<serde_json::Value>,
8784
2
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
8785
2
    ) -> JsonRpcResponse {
8786
2
        let args = match arguments {
8787
2
            Some(a) => a,
8788
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
8789
        };
8790
8791
2
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
8792
2
            Some(p) => p,
8793
            None => {
8794
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
8795
            }
8796
        };
8797
8798
2
        let quality_name = match args.get("quality_name").and_then(|v| v.as_str()) {
8799
2
            Some(q) => q,
8800
            None => {
8801
0
                return JsonRpcResponse::error(
8802
0
                    id,
8803
                    INVALID_PARAMS,
8804
0
                    "Missing 'quality_name' argument",
8805
                );
8806
            }
8807
        };
8808
8809
2
        let component_name = match args.get("component_name").and_then(|v| v.as_str()) {
8810
2
            Some(c) => c,
8811
            None => {
8812
0
                return JsonRpcResponse::error(
8813
0
                    id,
8814
                    INVALID_PARAMS,
8815
0
                    "Missing 'component_name' argument",
8816
                );
8817
            }
8818
        };
8819
8820
2
        let 
component_type1
= match args.get("component_type").and_then(|v| v.as_str()) {
8821
2
            Some(value) => match parse_architecture_entity_kind(value) {
8822
1
                Ok(kind) => kind,
8823
1
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
8824
            },
8825
            None => {
8826
0
                return JsonRpcResponse::error(
8827
0
                    id,
8828
                    INVALID_PARAMS,
8829
0
                    "Missing 'component_type' argument",
8830
                );
8831
            }
8832
        };
8833
8834
1
        let override_constraints = match parse_optional_string_list(&args, "override_constraints") {
8835
1
            Ok(values) => values,
8836
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
8837
        };
8838
8839
1
        let notes = args
8840
1
            .get("notes")
8841
1
            .and_then(|v| 
v0
.
as_str0
())
8842
1
            .map(|s| 
s0
.
to_string0
());
8843
8844
1
        let org_id = match self.ensure_org().await {
8845
1
            Ok(id) => id,
8846
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
8847
        };
8848
8849
1
        let projects = match self.service.project_repo.list_projects(&org_id).await {
8850
1
            Ok(p) => p,
8851
0
            Err(e) => {
8852
0
                error!("Failed to fetch projects: {}", e);
8853
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
8854
            }
8855
        };
8856
8857
1
        let project = match projects
8858
1
            .iter()
8859
1
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
8860
        {
8861
1
            Some(p) => p,
8862
            None => {
8863
0
                return JsonRpcResponse::error(
8864
0
                    id,
8865
                    INVALID_PARAMS,
8866
0
                    &format!("Project '{}' not found", project_name),
8867
                );
8868
            }
8869
        };
8870
8871
1
        let qualities = match self
8872
1
            .service
8873
1
            .quality_attribute_service
8874
1
            .list_quality_attributes(&org_id, &project.id, &self.user_id)
8875
1
            .await
8876
        {
8877
1
            Ok(list) => list,
8878
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
8879
        };
8880
8881
1
        let matches: Vec<&QualityAttribute> = qualities
8882
1
            .iter()
8883
1
            .filter(|attr| attr.name.eq_ignore_ascii_case(quality_name))
8884
1
            .collect();
8885
8886
1
        if matches.is_empty() {
8887
0
            return JsonRpcResponse::error(
8888
0
                id,
8889
                INVALID_PARAMS,
8890
0
                &format!("Quality attribute '{}' not found", quality_name),
8891
            );
8892
1
        }
8893
1
        if matches.len() > 1 {
8894
0
            return JsonRpcResponse::error(
8895
0
                id,
8896
                INVALID_PARAMS,
8897
0
                &format!(
8898
0
                    "Ambiguous quality attribute name '{}'. Use quality_attribute_id instead.",
8899
0
                    quality_name
8900
0
                ),
8901
            );
8902
1
        }
8903
8904
1
        let quality_id = matches[0].id.clone();
8905
8906
1
        let (component_id, _) = match resolve_component_ref_by_entity_kind(
8907
1
            self.service.component_repo.as_ref(),
8908
1
            &project.id,
8909
1
            component_name,
8910
1
            component_type,
8911
        )
8912
1
        .await
8913
        {
8914
1
            Ok(value) => value,
8915
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
8916
        };
8917
8918
1
        let request = CreateQualityComponentLinkRequest {
8919
1
            component_id: component_id.clone(),
8920
1
            component_type: match component_type {
8921
                ConnectionTargetKind::Container => {
8922
0
                    crate::models::quality_component_link::QualityLinkComponentType::Container
8923
                }
8924
                ConnectionTargetKind::Component => {
8925
1
                    crate::models::quality_component_link::QualityLinkComponentType::Component
8926
                }
8927
                _ => {
8928
0
                    return JsonRpcResponse::error(
8929
0
                        id,
8930
                        INVALID_PARAMS,
8931
0
                        "component_type must be either 'container' or 'component'",
8932
                    );
8933
                }
8934
            },
8935
1
            override_constraints,
8936
1
            notes,
8937
        };
8938
8939
1
        let link = match self
8940
1
            .service
8941
1
            .quality_component_link_service
8942
1
            .link_quality_to_component(&org_id, &project.id, &quality_id, &request, &self.user_id)
8943
1
            .await
8944
        {
8945
1
            Ok(value) => value,
8946
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
8947
        };
8948
8949
1
        let output = format!(
8950
            "🔗 Linked quality **{}** to {} **{}**\n- **Link ID**: `{}`",
8951
            quality_name,
8952
1
            component_type.as_str(),
8953
            component_name,
8954
            link.id
8955
        );
8956
8957
1
        let result = CallToolResult {
8958
1
            content: vec![ToolContent::Text { text: output }],
8959
1
            is_error: None,
8960
1
        };
8961
8962
1
        jsonrpc_success(id, result)
8963
2
    }
8964
8965
0
    pub(super) async fn call_unlink_quality_from_component(
8966
0
        &mut self,
8967
0
        id: Option<serde_json::Value>,
8968
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
8969
0
    ) -> JsonRpcResponse {
8970
0
        let args = match arguments {
8971
0
            Some(a) => a,
8972
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
8973
        };
8974
8975
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
8976
0
            Some(p) => p,
8977
            None => {
8978
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
8979
            }
8980
        };
8981
8982
0
        let quality_name = match args.get("quality_name").and_then(|v| v.as_str()) {
8983
0
            Some(q) => q,
8984
            None => {
8985
0
                return JsonRpcResponse::error(
8986
0
                    id,
8987
                    INVALID_PARAMS,
8988
0
                    "Missing 'quality_name' argument",
8989
                );
8990
            }
8991
        };
8992
8993
0
        let component_name = match args.get("component_name").and_then(|v| v.as_str()) {
8994
0
            Some(c) => c,
8995
            None => {
8996
0
                return JsonRpcResponse::error(
8997
0
                    id,
8998
                    INVALID_PARAMS,
8999
0
                    "Missing 'component_name' argument",
9000
                );
9001
            }
9002
        };
9003
9004
0
        let component_type = match args.get("component_type").and_then(|v| v.as_str()) {
9005
0
            Some(value) => match parse_architecture_entity_kind(value) {
9006
0
                Ok(kind) => kind,
9007
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
9008
            },
9009
            None => {
9010
0
                return JsonRpcResponse::error(
9011
0
                    id,
9012
                    INVALID_PARAMS,
9013
0
                    "Missing 'component_type' argument",
9014
                );
9015
            }
9016
        };
9017
9018
0
        let org_id = match self.ensure_org().await {
9019
0
            Ok(id) => id,
9020
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9021
        };
9022
9023
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
9024
0
            Ok(p) => p,
9025
0
            Err(e) => {
9026
0
                error!("Failed to fetch projects: {}", e);
9027
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
9028
            }
9029
        };
9030
9031
0
        let project = match projects
9032
0
            .iter()
9033
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
9034
        {
9035
0
            Some(p) => p,
9036
            None => {
9037
0
                return JsonRpcResponse::error(
9038
0
                    id,
9039
                    INVALID_PARAMS,
9040
0
                    &format!("Project '{}' not found", project_name),
9041
                );
9042
            }
9043
        };
9044
9045
0
        let qualities = match self
9046
0
            .service
9047
0
            .quality_attribute_service
9048
0
            .list_quality_attributes(&org_id, &project.id, &self.user_id)
9049
0
            .await
9050
        {
9051
0
            Ok(list) => list,
9052
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9053
        };
9054
9055
0
        let matches: Vec<&QualityAttribute> = qualities
9056
0
            .iter()
9057
0
            .filter(|attr| attr.name.eq_ignore_ascii_case(quality_name))
9058
0
            .collect();
9059
9060
0
        if matches.is_empty() {
9061
0
            return JsonRpcResponse::error(
9062
0
                id,
9063
                INVALID_PARAMS,
9064
0
                &format!("Quality attribute '{}' not found", quality_name),
9065
            );
9066
0
        }
9067
0
        if matches.len() > 1 {
9068
0
            return JsonRpcResponse::error(
9069
0
                id,
9070
                INVALID_PARAMS,
9071
0
                &format!(
9072
0
                    "Ambiguous quality attribute name '{}'. Use quality_attribute_id instead.",
9073
0
                    quality_name
9074
0
                ),
9075
            );
9076
0
        }
9077
9078
0
        let quality_id = matches[0].id.clone();
9079
9080
0
        let (component_id, _) = match resolve_component_ref_by_entity_kind(
9081
0
            self.service.component_repo.as_ref(),
9082
0
            &project.id,
9083
0
            component_name,
9084
0
            component_type,
9085
        )
9086
0
        .await
9087
        {
9088
0
            Ok(value) => value,
9089
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9090
        };
9091
9092
0
        match self
9093
0
            .service
9094
0
            .quality_component_link_service
9095
0
            .unlink_quality_from_component(
9096
0
                &org_id,
9097
0
                &project.id,
9098
0
                &quality_id,
9099
0
                &component_id,
9100
0
                match component_type {
9101
                    ConnectionTargetKind::Container => {
9102
0
                        crate::models::quality_component_link::QualityLinkComponentType::Container
9103
                    }
9104
                    ConnectionTargetKind::Component => {
9105
0
                        crate::models::quality_component_link::QualityLinkComponentType::Component
9106
                    }
9107
                    _ => {
9108
0
                        return JsonRpcResponse::error(
9109
0
                            id,
9110
                            INVALID_PARAMS,
9111
0
                            "component_type must be either 'container' or 'component'",
9112
                        );
9113
                    }
9114
                },
9115
0
                &self.user_id,
9116
            )
9117
0
            .await
9118
        {
9119
0
            Ok(()) => {}
9120
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9121
        }
9122
9123
0
        let output = format!(
9124
            "🔗 Removed quality **{}** from {} **{}**",
9125
            quality_name,
9126
0
            component_type.as_str(),
9127
            component_name
9128
        );
9129
9130
0
        let result = CallToolResult {
9131
0
            content: vec![ToolContent::Text { text: output }],
9132
0
            is_error: None,
9133
0
        };
9134
9135
0
        jsonrpc_success(id, result)
9136
0
    }
9137
9138
0
    pub(super) async fn call_link_actor_to_component(
9139
0
        &mut self,
9140
0
        id: Option<serde_json::Value>,
9141
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
9142
0
    ) -> JsonRpcResponse {
9143
0
        let args = match arguments {
9144
0
            Some(a) => a,
9145
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
9146
        };
9147
9148
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
9149
0
            Some(p) => p,
9150
            None => {
9151
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
9152
            }
9153
        };
9154
0
        let actor_name = match args.get("actor_name").and_then(|v| v.as_str()) {
9155
0
            Some(v) => v,
9156
            None => {
9157
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'actor_name' argument");
9158
            }
9159
        };
9160
0
        let component_name = match args.get("component_name").and_then(|v| v.as_str()) {
9161
0
            Some(v) => v,
9162
            None => {
9163
0
                return JsonRpcResponse::error(
9164
0
                    id,
9165
                    INVALID_PARAMS,
9166
0
                    "Missing 'component_name' argument",
9167
                );
9168
            }
9169
        };
9170
0
        let component_type = match args.get("component_type").and_then(|v| v.as_str()) {
9171
0
            Some(value) => match parse_architecture_entity_kind(value) {
9172
0
                Ok(kind) => kind,
9173
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
9174
            },
9175
            None => {
9176
0
                return JsonRpcResponse::error(
9177
0
                    id,
9178
                    INVALID_PARAMS,
9179
0
                    "Missing 'component_type' argument",
9180
                );
9181
            }
9182
        };
9183
0
        let label = args
9184
0
            .get("label")
9185
0
            .and_then(|v| v.as_str())
9186
0
            .unwrap_or("uses")
9187
0
            .to_string();
9188
0
        let description = args
9189
0
            .get("description")
9190
0
            .and_then(|v| v.as_str())
9191
0
            .map(|s| s.to_string());
9192
9193
0
        let org_id = match self.ensure_org().await {
9194
0
            Ok(id) => id,
9195
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9196
        };
9197
9198
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
9199
0
            Ok(p) => p,
9200
0
            Err(e) => {
9201
0
                error!("Failed to fetch projects: {}", e);
9202
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
9203
            }
9204
        };
9205
0
        let project = match projects
9206
0
            .iter()
9207
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
9208
        {
9209
0
            Some(p) => p,
9210
            None => {
9211
0
                return JsonRpcResponse::error(
9212
0
                    id,
9213
                    INVALID_PARAMS,
9214
0
                    &format!("Project '{}' not found", project_name),
9215
                );
9216
            }
9217
        };
9218
9219
0
        let actor_id = match resolve_actor_id_from_value(
9220
0
            &self.service.actor_service,
9221
0
            &org_id,
9222
0
            &project.id,
9223
0
            &self.user_id,
9224
0
            actor_name,
9225
        )
9226
0
        .await
9227
        {
9228
0
            Ok(value) => value,
9229
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9230
        };
9231
9232
0
        let (component_id, _component_kind) = match resolve_component_ref_by_entity_kind(
9233
0
            self.service.component_repo.as_ref(),
9234
0
            &project.id,
9235
0
            component_name,
9236
0
            component_type,
9237
        )
9238
0
        .await
9239
        {
9240
0
            Ok(value) => value,
9241
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9242
        };
9243
9244
0
        let connection = match self
9245
0
            .service
9246
0
            .connection_service
9247
0
            .create_connection(
9248
0
                &self.user_id,
9249
0
                &project.id,
9250
0
                &org_id,
9251
0
                actor_id.to_string(),
9252
0
                ConnectionSourceKind::Person,
9253
0
                None,
9254
0
                component_id.to_string(),
9255
0
                component_type,
9256
0
                None,
9257
0
                label.clone(),
9258
0
                description.clone(),
9259
0
                None,
9260
0
                None,
9261
            )
9262
0
            .await
9263
        {
9264
0
            Ok(connection) => connection,
9265
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9266
        };
9267
9268
0
        let result = CallToolResult {
9269
0
            content: vec![ToolContent::Text {
9270
0
                text: format!(
9271
0
                    "🔗 Linked actor **{}** to {} **{}**\n- **Label**: `{}`\n- **Connection ID**: `{}`",
9272
0
                    actor_name,
9273
0
                    component_type.as_str(),
9274
0
                    component_name,
9275
0
                    label,
9276
0
                    connection.id
9277
0
                ),
9278
0
            }],
9279
0
            is_error: None,
9280
0
        };
9281
0
        jsonrpc_success(id, result)
9282
0
    }
9283
9284
0
    pub(super) async fn call_unlink_actor_from_component(
9285
0
        &mut self,
9286
0
        id: Option<serde_json::Value>,
9287
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
9288
0
    ) -> JsonRpcResponse {
9289
0
        let args = match arguments {
9290
0
            Some(a) => a,
9291
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
9292
        };
9293
9294
0
        let project_name = match args.get("project").and_then(|v| v.as_str()) {
9295
0
            Some(p) => p,
9296
            None => {
9297
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'project' argument");
9298
            }
9299
        };
9300
0
        let actor_name = match args.get("actor_name").and_then(|v| v.as_str()) {
9301
0
            Some(v) => v,
9302
            None => {
9303
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'actor_name' argument");
9304
            }
9305
        };
9306
0
        let component_name = match args.get("component_name").and_then(|v| v.as_str()) {
9307
0
            Some(v) => v,
9308
            None => {
9309
0
                return JsonRpcResponse::error(
9310
0
                    id,
9311
                    INVALID_PARAMS,
9312
0
                    "Missing 'component_name' argument",
9313
                );
9314
            }
9315
        };
9316
0
        let component_type = match args.get("component_type").and_then(|v| v.as_str()) {
9317
0
            Some(value) => match parse_architecture_entity_kind(value) {
9318
0
                Ok(kind) => kind,
9319
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
9320
            },
9321
            None => {
9322
0
                return JsonRpcResponse::error(
9323
0
                    id,
9324
                    INVALID_PARAMS,
9325
0
                    "Missing 'component_type' argument",
9326
                );
9327
            }
9328
        };
9329
0
        let label_filter = args
9330
0
            .get("label")
9331
0
            .and_then(|v| v.as_str())
9332
0
            .map(|s| s.to_string());
9333
9334
0
        let org_id = match self.ensure_org().await {
9335
0
            Ok(id) => id,
9336
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9337
        };
9338
9339
0
        let projects = match self.service.project_repo.list_projects(&org_id).await {
9340
0
            Ok(p) => p,
9341
0
            Err(e) => {
9342
0
                error!("Failed to fetch projects: {}", e);
9343
0
                return JsonRpcResponse::error(id, INTERNAL_ERROR, "Failed to fetch projects");
9344
            }
9345
        };
9346
0
        let project = match projects
9347
0
            .iter()
9348
0
            .find(|p| p.name.eq_ignore_ascii_case(project_name))
9349
        {
9350
0
            Some(p) => p,
9351
            None => {
9352
0
                return JsonRpcResponse::error(
9353
0
                    id,
9354
                    INVALID_PARAMS,
9355
0
                    &format!("Project '{}' not found", project_name),
9356
                );
9357
            }
9358
        };
9359
9360
0
        let actor_id = match resolve_actor_id_from_value(
9361
0
            &self.service.actor_service,
9362
0
            &org_id,
9363
0
            &project.id,
9364
0
            &self.user_id,
9365
0
            actor_name,
9366
        )
9367
0
        .await
9368
        {
9369
0
            Ok(value) => value,
9370
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9371
        };
9372
9373
0
        let (component_id, _component_kind) = match resolve_component_ref_by_entity_kind(
9374
0
            self.service.component_repo.as_ref(),
9375
0
            &project.id,
9376
0
            component_name,
9377
0
            component_type,
9378
        )
9379
0
        .await
9380
        {
9381
0
            Ok(value) => value,
9382
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9383
        };
9384
9385
0
        let connections = match self
9386
0
            .service
9387
0
            .connection_service
9388
0
            .list_project_connections(&self.user_id, &org_id, &project.id)
9389
0
            .await
9390
        {
9391
0
            Ok(value) => value,
9392
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9393
        };
9394
0
        let matches: Vec<Connection> = connections
9395
0
            .into_iter()
9396
0
            .filter(|connection| {
9397
0
                connection.source_type == ConnectionSourceKind::Person
9398
0
                    && connection.source_id == actor_id.as_ref()
9399
0
                    && connection.target_id == component_id.as_ref()
9400
0
                    && connection.target_type == component_type
9401
0
                    && label_filter
9402
0
                        .as_ref()
9403
0
                        .map(|label| connection.label.eq_ignore_ascii_case(label))
9404
0
                        .unwrap_or(true)
9405
0
            })
9406
0
            .collect();
9407
9408
0
        if matches.is_empty() {
9409
0
            return JsonRpcResponse::error(
9410
0
                id,
9411
                INVALID_PARAMS,
9412
0
                &format!(
9413
0
                    "No actor link found between '{}' and {} '{}'",
9414
0
                    actor_name,
9415
0
                    component_type.as_str(),
9416
0
                    component_name
9417
0
                ),
9418
            );
9419
0
        }
9420
9421
0
        for connection in &matches {
9422
0
            if let Err(e) = self
9423
0
                .service
9424
0
                .connection_service
9425
0
                .delete_connection(&self.user_id, &connection.id)
9426
0
                .await
9427
            {
9428
0
                return JsonRpcResponse::from_api_error(id, e);
9429
0
            }
9430
        }
9431
9432
0
        let result = CallToolResult {
9433
0
            content: vec![ToolContent::Text {
9434
0
                text: format!(
9435
0
                    "🔓 Unlinked actor **{}** from {} **{}** (removed {} link(s))",
9436
0
                    actor_name,
9437
0
                    component_type.as_str(),
9438
0
                    component_name,
9439
0
                    matches.len()
9440
0
                ),
9441
0
            }],
9442
0
            is_error: None,
9443
0
        };
9444
0
        jsonrpc_success(id, result)
9445
0
    }
9446
9447
0
    pub(super) async fn call_create_rule(
9448
0
        &mut self,
9449
0
        id: Option<serde_json::Value>,
9450
0
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
9451
0
    ) -> JsonRpcResponse {
9452
0
        let args = match arguments {
9453
0
            Some(a) => a,
9454
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
9455
        };
9456
9457
0
        let name = match args.get("name").and_then(|v| v.as_str()) {
9458
0
            Some(n) => n,
9459
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'name' argument"),
9460
        };
9461
9462
0
        let description = match args.get("description").and_then(|v| v.as_str()) {
9463
0
            Some(d) => d,
9464
            None => {
9465
0
                return JsonRpcResponse::error(
9466
0
                    id,
9467
                    INVALID_PARAMS,
9468
0
                    "Missing 'description' argument",
9469
                );
9470
            }
9471
        };
9472
9473
0
        let category = match args.get("category").and_then(|v| v.as_str()) {
9474
0
            Some(c) => c,
9475
            None => {
9476
0
                return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'category' argument");
9477
            }
9478
        };
9479
9480
0
        let applies_to = args
9481
0
            .get("applies_to")
9482
0
            .and_then(|v| v.as_str())
9483
0
            .map(|s| s.to_string());
9484
9485
0
        let priority = args
9486
0
            .get("priority")
9487
0
            .and_then(|v| v.as_i64())
9488
0
            .map(|p| p as i32)
9489
0
            .unwrap_or(100);
9490
9491
0
        let org_id = match self.ensure_org().await {
9492
0
            Ok(id) => id,
9493
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9494
        };
9495
9496
0
        let rule = Rule::new_with_options(
9497
0
            org_id.clone(),
9498
0
            None, // org-level rule, no project_id
9499
0
            name.to_string(),
9500
0
            description.to_string(),
9501
0
            applies_to.clone(),
9502
0
            category.to_string(),
9503
            true, // enabled by default
9504
0
            priority,
9505
        );
9506
9507
0
        if let Err(e) = self.service.rule_repo.put_rule(&rule).await {
9508
0
            error!("Failed to create rule: {}", e);
9509
0
            return JsonRpcResponse::error(
9510
0
                id,
9511
                INTERNAL_ERROR,
9512
0
                &format!("Failed to create rule: {}", e),
9513
            );
9514
0
        }
9515
9516
0
        let rule_view: RuleView = (&rule).into();
9517
0
        let mut output = format!(
9518
            "✅ Created rule **{}** in category **{}**\n\n\
9519
             - **Description**: {}\n\
9520
             - **Priority**: {}",
9521
            rule_view.name, rule_view.category, rule_view.description, rule_view.priority
9522
        );
9523
9524
0
        if let Some(ref applies) = rule_view.applies_to {
9525
0
            output.push_str(&format!("\n- **Applies to**: {}", applies));
9526
0
        }
9527
9528
0
        let result = CallToolResult {
9529
0
            content: vec![ToolContent::Text { text: output }],
9530
0
            is_error: None,
9531
0
        };
9532
9533
0
        jsonrpc_success(id, result)
9534
0
    }
9535
9536
1
    pub(super) async fn call_delete_rule(
9537
1
        &mut self,
9538
1
        id: Option<serde_json::Value>,
9539
1
        arguments: Option<serde_json::Map<String, serde_json::Value>>,
9540
1
    ) -> JsonRpcResponse {
9541
1
        let args = match arguments {
9542
1
            Some(a) => a,
9543
0
            None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing arguments"),
9544
        };
9545
9546
1
        let org_id = match self.ensure_org().await {
9547
1
            Ok(id) => id,
9548
0
            Err(e) => return JsonRpcResponse::from_api_error(id, e),
9549
        };
9550
9551
1
        let rule_id_arg = args.get("rule_id").and_then(|v| 
v0
.
as_str0
());
9552
1
        let rule_name_arg = args.get("name").and_then(|v| v.as_str());
9553
9554
1
        if rule_id_arg.is_none() && rule_name_arg.is_none() {
9555
0
            return JsonRpcResponse::error(
9556
0
                id,
9557
                INVALID_PARAMS,
9558
0
                "Missing 'rule_id' or 'name' argument",
9559
            );
9560
1
        }
9561
9562
1
        let project_name = args.get("project").and_then(|v| 
v0
.
as_str0
());
9563
9564
1
        if let Some(
project_name0
) = project_name {
9565
0
            let project = match self
9566
0
                .service
9567
0
                .project_repo
9568
0
                .list_projects(&org_id)
9569
0
                .await
9570
0
                .ok()
9571
0
                .and_then(|projects| {
9572
0
                    projects
9573
0
                        .into_iter()
9574
0
                        .find(|p| p.name.eq_ignore_ascii_case(project_name))
9575
0
                }) {
9576
0
                Some(value) => value,
9577
                None => {
9578
0
                    return JsonRpcResponse::error(
9579
0
                        id,
9580
                        INVALID_PARAMS,
9581
0
                        &format!("Project '{}' not found", project_name),
9582
                    );
9583
                }
9584
            };
9585
9586
0
            let resolved_rule_id = if let Some(rule_id_raw) = rule_id_arg {
9587
0
                match crate::models::rule::RuleId::try_from(rule_id_raw.to_string()) {
9588
0
                    Ok(value) => value,
9589
0
                    Err(e) => return JsonRpcResponse::from_api_error(id, e),
9590
                }
9591
            } else {
9592
0
                let rules = match self.service.rule_repo.list_project_rules(&project.id).await {
9593
0
                    Ok(value) => value,
9594
0
                    Err(e) => return JsonRpcResponse::from_api_error(id, e),
9595
                };
9596
0
                let Some(name) = rule_name_arg else {
9597
0
                    return JsonRpcResponse::error(
9598
0
                        id,
9599
                        INVALID_PARAMS,
9600
0
                        "Either rule_id or rule_name must be provided",
9601
                    );
9602
                };
9603
0
                let matches: Vec<_> = rules
9604
0
                    .into_iter()
9605
0
                    .filter(|rule| rule.name.eq_ignore_ascii_case(name))
9606
0
                    .collect();
9607
0
                if matches.is_empty() {
9608
0
                    return JsonRpcResponse::error(
9609
0
                        id,
9610
                        INVALID_PARAMS,
9611
0
                        &format!("Rule '{}' not found in project '{}'", name, project.name),
9612
                    );
9613
0
                }
9614
0
                if matches.len() > 1 {
9615
0
                    return JsonRpcResponse::error(
9616
0
                        id,
9617
                        INVALID_PARAMS,
9618
0
                        &format!(
9619
0
                            "Ambiguous project rule name '{}'. Use rule_id instead.",
9620
0
                            name
9621
0
                        ),
9622
                    );
9623
0
                }
9624
0
                matches[0].id.clone()
9625
            };
9626
9627
0
            if let Err(e) = self
9628
0
                .service
9629
0
                .rule_repo
9630
0
                .delete_project_rule(&org_id, &project.id, &resolved_rule_id)
9631
0
                .await
9632
            {
9633
0
                return JsonRpcResponse::from_api_error(id, e);
9634
0
            }
9635
9636
0
            let result = CallToolResult {
9637
0
                content: vec![ToolContent::Text {
9638
0
                    text: format!(
9639
0
                        "🗑️ Deleted project rule `{}` from **{}**",
9640
0
                        resolved_rule_id, project.name
9641
0
                    ),
9642
0
                }],
9643
0
                is_error: None,
9644
0
            };
9645
0
            return jsonrpc_success(id, result);
9646
1
        }
9647
9648
1
        let resolved_rule_id = if let Some(
rule_id_raw0
) = rule_id_arg {
9649
0
            match crate::models::rule::RuleId::try_from(rule_id_raw.to_string()) {
9650
0
                Ok(value) => value,
9651
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
9652
            }
9653
        } else {
9654
1
            let rules = match self.service.rule_repo.list_rules(&org_id).await {
9655
1
                Ok(value) => value,
9656
0
                Err(e) => return JsonRpcResponse::from_api_error(id, e),
9657
            };
9658
1
            let Some(name) = rule_name_arg else {
9659
0
                return JsonRpcResponse::error(
9660
0
                    id,
9661
                    INVALID_PARAMS,
9662
0
                    "Either rule_id or rule_name must be provided",
9663
                );
9664
            };
9665
1
            let matches: Vec<_> = rules
9666
1
                .into_iter()
9667
1
                .filter(|rule| rule.name.eq_ignore_ascii_case(name))
9668
1
                .collect();
9669
1
            if matches.is_empty() {
9670
0
                return JsonRpcResponse::error(
9671
0
                    id,
9672
                    INVALID_PARAMS,
9673
0
                    &format!("Organisation rule '{}' not found", name),
9674
                );
9675
1
            }
9676
1
            if matches.len() > 1 {
9677
0
                return JsonRpcResponse::error(
9678
0
                    id,
9679
                    INVALID_PARAMS,
9680
0
                    &format!(
9681
0
                        "Ambiguous organisation rule name '{}'. Use rule_id instead.",
9682
0
                        name
9683
0
                    ),
9684
                );
9685
1
            }
9686
1
            matches[0].id.clone()
9687
        };
9688
9689
1
        if let Err(
e0
) = self
9690
1
            .service
9691
1
            .rule_repo
9692
1
            .delete_rule(&org_id, &resolved_rule_id)
9693
1
            .await
9694
        {
9695
0
            return JsonRpcResponse::from_api_error(id, e);
9696
1
        }
9697
9698
1
        let result = CallToolResult {
9699
1
            content: vec![ToolContent::Text {
9700
1
                text: format!("🗑️ Deleted organisation rule `{}`", resolved_rule_id),
9701
1
            }],
9702
1
            is_error: None,
9703
1
        };
9704
1
        jsonrpc_success(id, result)
9705
1
    }
9706
}
9707
9708
0
fn required_arg<'a>(
9709
0
    args: &'a serde_json::Map<String, serde_json::Value>,
9710
0
    key: &str,
9711
0
    id: Option<serde_json::Value>,
9712
0
) -> Result<&'a str, JsonRpcResponse> {
9713
0
    args.get(key)
9714
0
        .and_then(|value| value.as_str())
9715
0
        .ok_or_else(|| {
9716
0
            JsonRpcResponse::error(id, INVALID_PARAMS, &format!("Missing '{}' argument", key))
9717
0
        })
9718
0
}
9719
9720
8
async fn resolve_component_ref_by_entity_kind(
9721
8
    component_repo: &dyn crate::db::traits::ComponentRepositoryTrait,
9722
8
    project_id: &ProjectId,
9723
8
    value: &str,
9724
8
    entity_kind: ConnectionTargetKind,
9725
8
) -> Result<(ComponentId, ComponentKind), ApiError> {
9726
8
    let containers = component_repo.list_containers(project_id).await
?0
;
9727
8
    let root_ids: Vec<ComponentId> = containers
9728
8
        .iter()
9729
18
        .
map8
(|container| container.id.clone())
9730
8
        .collect();
9731
8
    let components_by_parent = collect_components_by_parent(component_repo, root_ids).await;
9732
9733
8
    let mut all_components: Vec<Component> = containers;
9734
8
    all_components.extend(components_by_parent.values().flatten().cloned());
9735
9736
9
    let 
is_match8
= |component: &Component| match entity_kind {
9737
2
        ConnectionTargetKind::Container => component.is_container(),
9738
7
        ConnectionTargetKind::Component => component.is_architecture_component(),
9739
        ConnectionTargetKind::Person
9740
        | ConnectionTargetKind::Store
9741
0
        | ConnectionTargetKind::ExternalSystem => false,
9742
9
    };
9743
9744
8
    let matches: Vec<&Component> = if let Ok(
component_id0
) =
9745
8
        ComponentId::try_from(value.to_string())
9746
    {
9747
0
        all_components
9748
0
            .iter()
9749
0
            .filter(|component| component.id == component_id && is_match(component))
9750
0
            .collect()
9751
    } else {
9752
8
        all_components
9753
8
            .iter()
9754
35
            .
filter8
(|component| component.name.eq_ignore_ascii_case(value) &&
is_match(component)9
)
9755
8
            .collect()
9756
    };
9757
9758
8
    if matches.len() == 1 {
9759
7
        let component = matches[0];
9760
7
        return Ok((component.id.clone(), component.kind));
9761
1
    }
9762
9763
1
    let expected = match entity_kind {
9764
0
        ConnectionTargetKind::Container => "container",
9765
1
        ConnectionTargetKind::Component => "component",
9766
0
        ConnectionTargetKind::Store => "store",
9767
0
        ConnectionTargetKind::Person => "person",
9768
0
        ConnectionTargetKind::ExternalSystem => "external_system",
9769
    };
9770
9771
1
    if matches.is_empty() {
9772
0
        return Err(ApiError::InvalidRequest(format!(
9773
0
            "Unknown {} name: {}",
9774
0
            expected, value
9775
0
        )));
9776
1
    }
9777
9778
1
    Err(ApiError::InvalidRequest(format!(
9779
1
        "Ambiguous {} name '{}'. Use a component ID instead.",
9780
1
        expected, value
9781
1
    )))
9782
8
}
9783
9784
#[cfg(test)]
9785
#[path = "tools_tests.rs"]
9786
mod tests;