frigg 0.3.1

Local-first MCP server for code understanding.
Documentation
use super::*;
use rmcp::model::ProgressNotificationParam;

use crate::domain::{NormalizedWorkloadMetadata, WorkloadPrecisionMode};

impl ReadOnlyToolExecutionContext {
    pub(super) fn normalized_workload(
        &self,
        repository_ids: &[String],
        precision_mode: WorkloadPrecisionMode,
    ) -> NormalizedWorkloadMetadata {
        NormalizedWorkloadMetadata::from_repository_ids(
            self.tool_name,
            repository_ids,
            precision_mode,
        )
    }
}

impl ScopedReadOnlyToolExecutionContext {
    #[cfg(test)]
    pub(super) fn normalized_workload(
        &self,
        precision_mode: WorkloadPrecisionMode,
    ) -> NormalizedWorkloadMetadata {
        self.base
            .normalized_workload(&self.scoped_repository_ids, precision_mode)
    }
}

#[derive(Debug, Clone)]
pub(super) struct ToolExecutionFinalization {
    pub(super) source_refs: Value,
    pub(super) normalized_workload: Option<NormalizedWorkloadMetadata>,
}

impl ToolExecutionFinalization {
    pub(super) fn new(
        source_refs: Value,
        normalized_workload: Option<NormalizedWorkloadMetadata>,
    ) -> Self {
        Self {
            source_refs,
            normalized_workload,
        }
    }
}

impl FriggMcpServer {
    pub(super) async fn notify_progress(
        meta: &Meta,
        client: &Peer<RoleServer>,
        progress: f64,
        total: f64,
        message: impl Into<String>,
    ) {
        let Some(progress_token) = meta.get_progress_token() else {
            return;
        };
        let _ = client
            .notify_progress(ProgressNotificationParam {
                progress_token,
                progress,
                total: Some(total),
                message: Some(message.into()),
            })
            .await;
    }

    pub(super) fn read_only_tool_execution_context(
        &self,
        tool_name: &'static str,
        repository_hint: Option<String>,
    ) -> ReadOnlyToolExecutionContext {
        ReadOnlyToolExecutionContext {
            tool_name,
            repository_hint,
        }
    }

    pub(super) fn scoped_read_only_tool_execution_context(
        &self,
        tool_name: &'static str,
        repository_hint: Option<String>,
        freshness_mode: RepositoryResponseCacheFreshnessMode,
    ) -> Result<ScopedReadOnlyToolExecutionContext, ErrorData> {
        let base = self.read_only_tool_execution_context(tool_name, repository_hint);
        let scoped_workspaces =
            self.attached_workspaces_for_repository(base.repository_hint.as_deref())?;
        let scoped_repository_ids = scoped_workspaces
            .iter()
            .map(|workspace| workspace.repository_id.clone())
            .collect::<Vec<_>>();
        let cache_freshness =
            self.repository_response_cache_freshness(&scoped_workspaces, freshness_mode)?;

        Ok(ScopedReadOnlyToolExecutionContext {
            #[cfg(test)]
            base,
            scoped_workspaces,
            scoped_repository_ids,
            cache_freshness,
        })
    }

    pub(super) async fn run_read_only_tool_blocking<T, F>(
        &self,
        context: &ReadOnlyToolExecutionContext,
        task_fn: F,
    ) -> Result<T, ErrorData>
    where
        T: Send + 'static,
        F: FnOnce() -> T + Send + 'static,
    {
        Self::run_blocking_task(context.tool_name, task_fn).await
    }

    pub(super) fn finalize_read_only_tool<T>(
        &self,
        context: &ReadOnlyToolExecutionContext,
        result: Result<T, ErrorData>,
        provenance_result: Result<(), ErrorData>,
    ) -> Result<T, ErrorData> {
        self.finalize_with_provenance(context.tool_name, result, provenance_result)
    }

    pub(super) async fn run_blocking_task<T, F>(
        operation: &'static str,
        task_fn: F,
    ) -> Result<T, ErrorData>
    where
        T: Send + 'static,
        F: FnOnce() -> T + Send + 'static,
    {
        task::spawn_blocking(task_fn).await.map_err(|err| {
            Self::internal(
                format!("blocking task join failure in {operation}: {err}"),
                Some(json!({
                    "operation": operation,
                    "join_error": Self::bounded_text(&err.to_string()),
                })),
            )
        })
    }

    pub(super) fn tool_execution_finalization(
        &self,
        source_refs: Value,
        normalized_workload: Option<NormalizedWorkloadMetadata>,
    ) -> ToolExecutionFinalization {
        ToolExecutionFinalization::new(source_refs, normalized_workload)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::settings::FriggConfig;
    use std::path::PathBuf;

    fn fixture_server() -> (FriggMcpServer, AttachedWorkspace) {
        let workspace_root = std::env::current_dir()
            .expect("current working directory should exist for MCP execution tests");
        let config = FriggConfig::from_optional_workspace_roots(Vec::<PathBuf>::new())
            .expect("fixture config should build");
        let server = FriggMcpServer::new_with_runtime_options(config, false, false);
        let _ = server
            .attach_workspace_internal(&workspace_root, true, WorkspaceResolveMode::GitRoot)
            .expect("fixture workspace should attach");
        let workspace = server
            .attached_workspaces()
            .into_iter()
            .next()
            .expect("fixture server should attach one workspace");
        (server, workspace)
    }

    #[test]
    fn tool_execution_context_scopes_to_explicit_repository() {
        let (server, workspace) = fixture_server();

        let context = server
            .scoped_read_only_tool_execution_context(
                "search_text",
                Some(workspace.repository_id.clone()),
                RepositoryResponseCacheFreshnessMode::ManifestOnly,
            )
            .expect("explicit repository scope should resolve");

        assert_eq!(context.base.tool_name, "search_text");
        assert_eq!(
            context.base.repository_hint.as_deref(),
            Some(workspace.repository_id.as_str())
        );
        assert_eq!(context.scoped_workspaces.len(), 1);
        assert_eq!(context.scoped_repository_ids, vec![workspace.repository_id]);
    }

    #[test]
    fn tool_execution_context_uses_session_default_repository() {
        let (server, workspace) = fixture_server();
        server.set_current_repository_id(Some(workspace.repository_id.clone()));

        let context = server
            .scoped_read_only_tool_execution_context(
                "workspace_current",
                None,
                RepositoryResponseCacheFreshnessMode::ManifestOnly,
            )
            .expect("session repository scope should resolve");

        assert_eq!(context.base.repository_hint, None);
        assert_eq!(context.scoped_workspaces.len(), 1);
        assert_eq!(context.scoped_repository_ids, vec![workspace.repository_id]);
    }

    #[test]
    fn tool_execution_finalization_preserves_typed_workload_metadata() {
        let (server, workspace) = fixture_server();
        let context = server
            .scoped_read_only_tool_execution_context(
                "search_text",
                Some(workspace.repository_id.clone()),
                RepositoryResponseCacheFreshnessMode::ManifestOnly,
            )
            .expect("explicit repository scope should resolve");
        let normalized_workload = context.normalized_workload(WorkloadPrecisionMode::Exact);
        let finalization = server.tool_execution_finalization(
            json!({ "scoped_repository_ids": context.scoped_repository_ids }),
            Some(normalized_workload.clone()),
        );

        assert_eq!(
            finalization.source_refs["scoped_repository_ids"],
            json!(context.scoped_repository_ids)
        );
        assert_eq!(
            finalization
                .normalized_workload
                .as_ref()
                .map(NormalizedWorkloadMetadata::repository_scope_label),
            Some("single")
        );
        assert_eq!(
            finalization
                .normalized_workload
                .as_ref()
                .map(|metadata| metadata.precision_mode),
            Some(WorkloadPrecisionMode::Exact)
        );
    }
}