laburnum 1.17.1

An LSP framework for building language servers and compilers, powered by an incremental query tree with content-addressed storage, task-based dataflow, and parallel queries.
Documentation
// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

//! `workspace/executeCommand` extension hatch.
//!
//! The implementing crate registers each available command via
//! [`CommandDescriptor`]. Each descriptor becomes its own typed MCP tool
//! (named `execute_<command>`), so agents see documented per-command tools
//! rather than one opaque `execute_command(name, args[])`.
//!
//! If the implementing crate registers no commands, no `execute_*` tool
//! appears in the MCP catalog.
//!
//! Unlike the built-in tools, these are constructed at runtime from
//! implementer-supplied data, so they can't be ZST markers implementing
//! the typed `Tool` trait. They implement `DynTool` directly and the
//! framework registers them via the `register_dyn` hatch.

use {
  crate::{
    connect::lsp::{
      LspClient,
      request::ExecuteCommand,
    },
    connect::mcp::tool::{
      DynTool,
      ToolRegistry,
    },
    protocol::{
      lsp::ExecuteCommandParams,
      mcp::CallToolResult,
    },
  },
  std::sync::Arc,
};

/// One implementer-supplied LSP command, surfaced as a single MCP tool.
///
/// The implementing crate owns the contract: name, description, and JSON
/// Schema for the command's arguments. The framework has no way to validate
/// that any given command is safe to expose to agents — that judgement is
/// the implementer's.
#[derive(Clone)]
pub struct CommandDescriptor {
  /// LSP command name (the `command` field of
  /// [`crate::protocol::lsp::ExecuteCommandParams`]).
  pub command: String,

  /// Human-readable description shown to the agent in `tools/list`.
  pub description: String,

  /// JSON Schema for the tool's `arguments`.
  pub input_schema: serde_json::Value,
}

/// Register one MCP tool per `CommandDescriptor`. Each is named
/// `execute_<command>` (with `/`, `.`, `-`, and spaces in the LSP command
/// name replaced by `_` for MCP-name friendliness).
pub fn register_commands(
  registry: &mut ToolRegistry,
  commands: impl IntoIterator<Item = CommandDescriptor>,
) {
  for descriptor in commands {
    registry.register_dyn(Arc::new(ExecuteCommandTool::new(descriptor)));
  }
}

struct ExecuteCommandTool {
  tool_name:  String,
  descriptor: CommandDescriptor,
}

impl ExecuteCommandTool {
  fn new(descriptor: CommandDescriptor) -> Self {
    let sanitized = descriptor.command.replace(['/', '.', '-', ' '], "_");
    let tool_name = format!("execute_{sanitized}");
    Self {
      tool_name,
      descriptor,
    }
  }
}

impl DynTool for ExecuteCommandTool {
  fn name(&self) -> &str {
    &self.tool_name
  }

  fn description(&self) -> &str {
    &self.descriptor.description
  }

  fn input_schema(&self) -> serde_json::Value {
    self.descriptor.input_schema.clone()
  }

  fn call<'a>(
    &'a self,
    client: &'a LspClient,
    arguments: serde_json::Value,
  ) -> std::pin::Pin<
    Box<dyn std::future::Future<Output = CallToolResult> + Send + 'a>,
  > {
    Box::pin(async move {
      // The implementer's input schema describes the *agent-facing* shape
      // of the arguments. LSP `workspace/executeCommand` takes a positional
      // `arguments: Vec<Value>` array. We pass the entire MCP arguments
      // value as the first (and only) element — implementers should design
      // their command handlers to accept a single JSON object.
      let lsp_arguments = if arguments.is_null() {
        Vec::new()
      } else {
        vec![arguments]
      };
      let params = ExecuteCommandParams {
        command:                   self.descriptor.command.clone(),
        arguments:                 lsp_arguments,
        work_done_progress_params: Default::default(),
      };
      match client.send_request::<ExecuteCommand>(params).await {
        | Ok(value) => {
          CallToolResult::ok_json(&value.unwrap_or(serde_json::Value::Null))
        },
        | Err(e) => {
          CallToolResult::error_text(format!("executeCommand failed: {e}"))
        },
      }
    })
  }
}