bctx-nexus 0.1.13

bctx-nexus — MCP/Nexus gateway with permission enforcement and tool registry
Documentation
use std::time::Instant;

use anyhow::{bail, Result};
use serde_json::Value;

use atlas::invocation::{SkillContext, SkillInput};
use atlas::registry::SkillRegistry;

use crate::permission::{PermissionDecision, PermissionEngine};
use crate::registry::ToolRegistry;

pub struct ToolGateway {
    pub manifests: ToolRegistry,
    pub skills: SkillRegistry,
    pub permission: PermissionEngine,
}

impl ToolGateway {
    pub fn new(
        manifests: ToolRegistry,
        skills: SkillRegistry,
        permission: PermissionEngine,
    ) -> Self {
        Self {
            manifests,
            skills,
            permission,
        }
    }

    /// Dispatch a tool call: permission check → skill lookup → execute → wrap result.
    pub fn dispatch(&self, tool_name: &str, input: Value, caller: &str) -> Result<Value> {
        // 1. Resolve manifest (for scope + description)
        let manifest = self
            .manifests
            .get(tool_name)
            .ok_or_else(|| anyhow::anyhow!("unknown tool: {tool_name}"))?;

        // 2. Permission gate
        match self.permission.check(manifest.scope, caller) {
            PermissionDecision::Allow => {}
            PermissionDecision::Deny(reason) => {
                bail!("permission denied for '{tool_name}': {reason}")
            }
        }

        // 3. Resolve skill
        let skill = self.skills.get(tool_name).ok_or_else(|| {
            anyhow::anyhow!("tool '{tool_name}' has no registered skill implementation")
        })?;

        // 4. Build context from caller + cwd
        let cwd = std::env::current_dir()
            .map(|p| p.to_string_lossy().to_string())
            .unwrap_or_else(|_| ".".into());
        let ctx = SkillContext::new(caller, cwd);
        let skill_input = SkillInput::new(input);

        // 5. Execute with timing
        let t = Instant::now();
        let outcome = skill
            .execute(skill_input, &ctx)
            .map_err(|e| anyhow::anyhow!("skill '{tool_name}' failed: {e}"))?;
        let elapsed_ms = t.elapsed().as_millis() as u64;

        // 6. Return structured MCP-compatible response
        Ok(serde_json::json!({
            "tool": tool_name,
            "status": "ok",
            "duration_ms": elapsed_ms,
            "tokens_used": outcome.tokens_used,
            "result": outcome.result,
        }))
    }

    /// List all registered tools with their descriptions (for MCP tool discovery).
    pub fn list_tools(&self) -> Value {
        let tools: Vec<Value> = self
            .manifests
            .all()
            .iter()
            .map(|m| {
                serde_json::json!({
                    "name": m.name,
                    "description": m.description,
                    "inputSchema": m.input_schema,
                })
            })
            .collect();
        serde_json::json!({ "tools": tools })
    }
}