op-mcp 0.1.0

MCP server providing LLM access to 1Password CLI
Documentation
//! Document management tools for 1Password

use rust_mcp_schema::schema_utils::CallToolError;
use rust_mcp_schema::CallToolResult;
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
use serde::{Deserialize, Serialize};

use crate::op::OpClient;
use crate::tools::{json_result, op_error_to_tool_error, text_result};

// ============================================================================
// document_list Tool
// ============================================================================

/// List documents in 1Password.
#[mcp_tool(
    name = "document_list",
    description = "List all documents stored in 1Password. Can filter by vault. Returns document names, IDs, and metadata."
)]
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DocumentListTool {
    /// Optional vault name or ID to filter documents.
    #[serde(default)]
    pub vault: Option<String>,
}

impl DocumentListTool {
    pub async fn call(&self, client: &OpClient) -> Result<CallToolResult, CallToolError> {
        let result = client
            .document_list(self.vault.as_deref())
            .await
            .map_err(op_error_to_tool_error)?;
        json_result(&result)
    }
}

// ============================================================================
// document_get Tool
// ============================================================================

/// Get/download a document from 1Password.
#[mcp_tool(
    name = "document_get",
    description = "Download a document from 1Password. Returns the document content as base64-encoded data or saves to a file path if specified."
)]
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DocumentGetTool {
    /// The document name or ID to retrieve.
    pub document: String,

    /// Optional vault name or ID.
    #[serde(default)]
    pub vault: Option<String>,

    /// Optional file path to save the document to. If not specified, returns base64-encoded content.
    #[serde(default)]
    pub output: Option<String>,
}

impl DocumentGetTool {
    pub async fn call(&self, client: &OpClient) -> Result<CallToolResult, CallToolError> {
        let result = client
            .document_get(&self.document, self.vault.as_deref(), self.output.as_deref())
            .await
            .map_err(op_error_to_tool_error)?;
        text_result(result)
    }
}

// ============================================================================
// document_create Tool
// ============================================================================

/// Upload a new document to 1Password.
#[mcp_tool(
    name = "document_create",
    description = "Upload a new document to 1Password from a local file."
)]
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DocumentCreateTool {
    /// The local file path to upload.
    pub file_path: String,

    /// The vault name or ID to store the document in.
    #[serde(default)]
    pub vault: Option<String>,

    /// Optional title for the document. Defaults to the filename.
    #[serde(default)]
    pub title: Option<String>,

    /// Optional tags to apply to the document.
    #[serde(default)]
    pub tags: Option<Vec<String>>,
}

impl DocumentCreateTool {
    pub async fn call(&self, client: &OpClient) -> Result<CallToolResult, CallToolError> {
        let tags_refs: Option<Vec<&str>> = self
            .tags
            .as_ref()
            .map(|t| t.iter().map(|s| s.as_str()).collect());

        let result = client
            .document_create(
                &self.file_path,
                self.vault.as_deref(),
                self.title.as_deref(),
                tags_refs.as_deref(),
            )
            .await
            .map_err(op_error_to_tool_error)?;
        json_result(&result)
    }
}

// ============================================================================
// document_edit Tool
// ============================================================================

/// Replace a document's content.
#[mcp_tool(
    name = "document_edit",
    description = "Replace an existing document's content with a new file. The document title and metadata are preserved unless explicitly changed."
)]
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DocumentEditTool {
    /// The document name or ID to edit.
    pub document: String,

    /// The local file path with new content.
    pub file_path: String,

    /// Optional vault name or ID.
    #[serde(default)]
    pub vault: Option<String>,

    /// New title for the document.
    #[serde(default)]
    pub title: Option<String>,
}

impl DocumentEditTool {
    pub async fn call(&self, client: &OpClient) -> Result<CallToolResult, CallToolError> {
        let result = client
            .document_edit(
                &self.document,
                &self.file_path,
                self.vault.as_deref(),
                self.title.as_deref(),
            )
            .await
            .map_err(op_error_to_tool_error)?;
        json_result(&result)
    }
}

// ============================================================================
// document_delete Tool
// ============================================================================

/// Delete a document from 1Password.
#[mcp_tool(
    name = "document_delete",
    description = "Delete a document from 1Password. By default, moves to archive (can be recovered). Use archive=false for permanent deletion."
)]
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DocumentDeleteTool {
    /// The document name or ID to delete.
    pub document: String,

    /// Optional vault name or ID.
    #[serde(default)]
    pub vault: Option<String>,

    /// If true (default), archive instead of permanently deleting.
    #[serde(default = "default_true")]
    pub archive: bool,
}

fn default_true() -> bool {
    true
}

impl DocumentDeleteTool {
    pub async fn call(&self, client: &OpClient) -> Result<CallToolResult, CallToolError> {
        let result = client
            .document_delete(&self.document, self.vault.as_deref(), self.archive)
            .await
            .map_err(op_error_to_tool_error)?;
        text_result(result)
    }
}