Skip to main content
Glama
list91
by list91

mcp_write

Write data to the CopyQ clipboard manager by adding, updating, deleting, or moving items across organized tabs like info, notes, and workspace projects.

Instructions

Write to CopyQ clipboard manager.

IMPORTANT: All data stored under "mcp/" prefix. When you use tab="info", actual CopyQ path is "mcp/info".

Available tabs:

  • info - general storage (use tab="info")

  • заметки - notes (use tab="заметки")

  • workspace - projects, supports subtabs (use tab="workspace" or "workspace/myproject")

Modes:

  • add: Add item to tab. Params: tab, text, tags (optional), note (optional)

  • update: Update item. Params: tab, index, field, text/tags/note

  • delete: Delete item. Params: tab, index

  • move: Move item. Params: tab, index, to_tab

  • tab_create: Create subtab in workspace only. Params: path (e.g. "workspace/newproject")

  • tab_delete: Delete subtab. Params: path

Response shows full_path (e.g. "mcp/info") confirming where data was written.

Errors: TAB_NOT_FOUND, INDEX_OUT_OF_BOUNDS, PERMISSION_DENIED, MISSING_PARAM

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
modeYesOperation mode
tabNoTab path
indexNoItem index
textNoText content
tagsNoTags list
noteNoNote content
fieldNoField to update
edit_modeNoreplace
matchNoString to match (for substitute)
to_tabNoDestination tab (for move)
pathNoTab path (for tab_create/tab_delete)
intentNoexecute

Implementation Reference

  • Main handler function for the mcp_write tool. Validates input mode and dispatches to specialized helper functions based on the operation mode (add, update, delete, move, tab_create, tab_delete). Supports preview mode.
    async def mcp_write(
        mode: str,
        tab: str | None = None,
        index: int | None = None,
        text: str | None = None,
        tags: list[str] | None = None,
        note: str | None = None,
        field: str | None = None,
        edit_mode: str = "replace",
        match: str | None = None,
        to_tab: str | None = None,
        path: str | None = None,
        intent: str = "execute",
    ) -> str:
        """
        Universal write operation for CopyQ MCP.
    
        Modes:
        - add: Add new item to tab
        - update: Update existing item
        - delete: Delete item
        - move: Move item to another tab
        - tab_create: Create new subtab (workspace only)
        - tab_delete: Delete subtab (workspace only)
    
        intent:
        - execute: Perform the operation
        - preview: Show what would happen without executing
    
        Returns compact pipe-separated format.
        """
        try:
            # Validate mode
            valid_modes = [m.value for m in WriteMode]
            if mode not in valid_modes:
                raise InvalidModeError(mode, valid_modes)
    
            is_preview = intent == "preview"
    
            if mode == "add":
                return await _write_add(tab, text, tags, note, is_preview)
    
            elif mode == "update":
                return await _write_update(
                    tab, index, field, edit_mode, text, tags, note, match, is_preview
                )
    
            elif mode == "delete":
                return await _write_delete(tab, index, is_preview)
    
            elif mode == "move":
                return await _write_move(tab, index, to_tab, is_preview)
    
            elif mode == "tab_create":
                return await _write_tab_create(path, is_preview)
    
            elif mode == "tab_delete":
                return await _write_tab_delete(path, is_preview)
    
            return "error|INVALID_MODE|Unknown mode"
    
        except MCPCopyQError as e:
            return e.to_response()
        except Exception as e:
            return f"error|INTERNAL|{str(e)}"
  • MCP tool registration for mcp_write, including name, detailed description, and JSON inputSchema defining parameters, enums, and validation rules.
            Tool(
                name="mcp_write",
                description="""Write to CopyQ clipboard manager.
    
    IMPORTANT: All data stored under "mcp/" prefix. When you use tab="info", actual CopyQ path is "mcp/info".
    
    Available tabs:
    - info - general storage (use tab="info")
    - заметки - notes (use tab="заметки")
    - workspace - projects, supports subtabs (use tab="workspace" or "workspace/myproject")
    
    Modes:
    - add: Add item to tab. Params: tab, text, tags (optional), note (optional)
    - update: Update item. Params: tab, index, field, text/tags/note
    - delete: Delete item. Params: tab, index
    - move: Move item. Params: tab, index, to_tab
    - tab_create: Create subtab in workspace only. Params: path (e.g. "workspace/newproject")
    - tab_delete: Delete subtab. Params: path
    
    Response shows full_path (e.g. "mcp/info") confirming where data was written.
    
    Errors: TAB_NOT_FOUND, INDEX_OUT_OF_BOUNDS, PERMISSION_DENIED, MISSING_PARAM""",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "mode": {
                            "type": "string",
                            "enum": ["add", "update", "delete", "move", "tab_create", "tab_delete"],
                            "description": "Operation mode"
                        },
                        "tab": {
                            "type": "string",
                            "description": "Tab path"
                        },
                        "index": {
                            "type": "integer",
                            "description": "Item index"
                        },
                        "text": {
                            "type": "string",
                            "description": "Text content"
                        },
                        "tags": {
                            "type": "array",
                            "items": {"type": "string"},
                            "description": "Tags list"
                        },
                        "note": {
                            "type": "string",
                            "description": "Note content"
                        },
                        "field": {
                            "type": "string",
                            "enum": ["text", "note", "tags"],
                            "description": "Field to update"
                        },
                        "edit_mode": {
                            "type": "string",
                            "enum": ["replace", "append", "prepend", "substitute", "remove"],
                            "default": "replace"
                        },
                        "match": {
                            "type": "string",
                            "description": "String to match (for substitute)"
                        },
                        "to_tab": {
                            "type": "string",
                            "description": "Destination tab (for move)"
                        },
                        "path": {
                            "type": "string",
                            "description": "Tab path (for tab_create/tab_delete)"
                        },
                        "intent": {
                            "type": "string",
                            "enum": ["execute", "preview"],
                            "default": "execute"
                        }
                    },
                    "required": ["mode"]
                }
            ),
  • Pydantic model and enums (WriteMode, EditMode, FieldType, Intent) defining structured input validation for mcp_write parameters.
    class WriteRequest(BaseModel):
        """Parameters for mcp_write tool."""
    
        mode: WriteMode = Field(..., description="Operation mode")
        tab: str | None = Field(default=None, description="Tab path")
        index: int | None = Field(default=None, description="Item index")
        text: str | None = Field(default=None, description="Text content")
        tags: list[str] | None = Field(default=None, description="Tags list")
        note: str | None = Field(default=None, description="Note content")
        field: FieldType | None = Field(default=None, description="Field to update")
        edit_mode: EditMode = Field(default=EditMode.REPLACE, description="How to edit")
        match: str | None = Field(default=None, description="String to match (for substitute)")
        to_tab: str | None = Field(default=None, description="Destination tab (for move)")
        path: str | None = Field(default=None, description="Tab path (for tab_create/tab_delete)")
        intent: Intent = Field(default=Intent.EXECUTE, description="Execute or preview")
  • Helper function for validating mcp_write parameters without execution, used by the mcp_validate tool. Checks modes, required params, tab/index existence, permissions.
    async def _validate_write(params: dict) -> tuple[list[str], list[str]]:
        """Validate mcp_write parameters."""
        errors: list[str] = []
        warnings: list[str] = []
    
        mode = params.get("mode")
        tab = params.get("tab")
        index = params.get("index")
        text = params.get("text")
        tags = params.get("tags")
        note = params.get("note")
        field = params.get("field")
        edit_mode = params.get("edit_mode", "replace")
        match = params.get("match")
        to_tab = params.get("to_tab")
        path = params.get("path")
        intent = params.get("intent", "execute")
    
        # Validate mode
        valid_modes = [m.value for m in WriteMode]
        if not mode:
            errors.append("MISSING_PARAM: mode is required")
        elif mode not in valid_modes:
            errors.append(f"INVALID_MODE: '{mode}' not in {valid_modes}")
            return errors, warnings
    
        # Validate intent
        if intent not in [i.value for i in Intent]:
            errors.append(f"INVALID_PARAM: intent '{intent}' invalid")
    
        # Mode-specific validation
        if mode == "add":
            if not tab:
                errors.append("MISSING_PARAM: tab required for mode=add")
            if not text:
                errors.append("MISSING_PARAM: text required for mode=add")
            if tab:
                root = tab.split("/")[0]
                if root not in ALLOWED_ROOT_TABS:
                    errors.append(f"TAB_NOT_FOUND: root '{root}' not allowed")
    
        elif mode == "update":
            if not tab:
                errors.append("MISSING_PARAM: tab required for mode=update")
            if index is None:
                errors.append("MISSING_PARAM: index required for mode=update")
            if not field:
                errors.append("MISSING_PARAM: field required for mode=update")
            elif field not in [f.value for f in FieldType]:
                errors.append(f"INVALID_PARAM: field '{field}' invalid")
    
            if edit_mode not in [e.value for e in EditMode]:
                errors.append(f"INVALID_PARAM: edit_mode '{edit_mode}' invalid")
    
            if edit_mode == "substitute" and not match:
                errors.append("MISSING_PARAM: match required for edit_mode=substitute")
    
            # Check field/value match
            if field == "text" and text is None:
                errors.append("MISSING_PARAM: text required for field=text")
            if field == "tags" and tags is None:
                errors.append("MISSING_PARAM: tags required for field=tags")
            if field == "note" and note is None:
                errors.append("MISSING_PARAM: note required for field=note")
    
            # Check tab and index exist
            if tab and index is not None:
                if not await client.tab_exists(tab):
                    errors.append(f"TAB_NOT_FOUND: {tab}")
                else:
                    count = await client.get_count(tab)
                    if index < 0 or index >= count:
                        errors.append(f"INDEX_OUT_OF_BOUNDS: {index} (count: {count})")
    
        elif mode == "delete":
            if not tab:
                errors.append("MISSING_PARAM: tab required for mode=delete")
            if index is None:
                errors.append("MISSING_PARAM: index required for mode=delete")
            if tab and index is not None:
                if not await client.tab_exists(tab):
                    errors.append(f"TAB_NOT_FOUND: {tab}")
                else:
                    count = await client.get_count(tab)
                    if index < 0 or index >= count:
                        errors.append(f"INDEX_OUT_OF_BOUNDS: {index} (count: {count})")
    
        elif mode == "move":
            if not tab:
                errors.append("MISSING_PARAM: tab required for mode=move")
            if index is None:
                errors.append("MISSING_PARAM: index required for mode=move")
            if not to_tab:
                errors.append("MISSING_PARAM: to_tab required for mode=move")
            if tab and not await client.tab_exists(tab):
                errors.append(f"TAB_NOT_FOUND: {tab}")
    
        elif mode == "tab_create":
            if not path:
                errors.append("MISSING_PARAM: path required for mode=tab_create")
            elif not path.startswith("workspace"):
                errors.append(f"PERMISSION_DENIED: tab_create only allowed in workspace/")
            if path and await client.tab_exists(path):
                warnings.append(f"Tab already exists: {path}")
    
        elif mode == "tab_delete":
            if not path:
                errors.append("MISSING_PARAM: path required for mode=tab_delete")
            elif not path.startswith("workspace"):
                errors.append(f"PERMISSION_DENIED: tab_delete only allowed in workspace/")
            if path and not await client.tab_exists(path):
                errors.append(f"TAB_NOT_FOUND: {path}")
    
        return errors, warnings
  • Example helper function for 'add' mode: validates params, generates preview if requested, adds item via CopyQ client, returns formatted response.
    async def _write_add(
        tab: str | None,
        text: str | None,
        tags: list[str] | None,
        note: str | None,
        is_preview: bool,
    ) -> str:
        """Add new item to tab."""
        if not tab:
            raise MissingParamError("tab", "add")
        if not text:
            raise MissingParamError("text", "add")
    
        # Check tab exists (will be created if not)
        if not await client.tab_exists(tab):
            # Auto-create only for existing root tabs
            root = tab.split("/")[0]
            if root not in ALLOWED_ROOT_TABS:
                raise TabNotFoundError(tab)
    
        if is_preview:
            text_preview = text[:PREVIEW_LENGTH] + "..." if len(text) > PREVIEW_LENGTH else text
            tags_str = f"[{','.join(tags)}]" if tags else "[]"
            note_preview = (note[:50] + "...") if note and len(note) > 50 else (note or "")
            return (
                f"preview|mode:add\n"
                f"tab:{tab}\n"
                f"will_add:\n"
                f"  text:\"{text_preview}\"\n"
                f"  tags:{tags_str}\n"
                f"  note:\"{note_preview}\""
            )
    
        new_index = await client.add_item(tab, text, tags, note)
    
        return (
            f"ok|mode:add|tab:{tab}|full_path:{MCP_ROOT}/{tab}|index:{new_index}|"
            f"text_len:{len(text)}|tags:{len(tags) if tags else 0}|note:{bool(note)}"
        )
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden and does well by disclosing key behavioral traits: data storage location ('mcp/' prefix), available tabs with usage examples, operation modes with parameter requirements, response format ('full_path'), and specific error conditions. It provides comprehensive operational context beyond basic functionality.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear sections (IMPORTANT note, Available tabs, Modes, Response, Errors) and front-loads critical information. Most sentences earn their place by providing essential operational details, though some redundancy exists (e.g., repeating 'use tab=' in tab descriptions).

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex tool with 12 parameters, no annotations, and no output schema, the description provides substantial context: operation modes, tab system, parameter requirements, response format, and error conditions. It covers most aspects needed for effective use, though additional details about authentication or rate limits could further enhance completeness.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 83% schema description coverage, the baseline is 3, but the description adds significant value by explaining parameter usage in context: it maps modes to their required parameters (e.g., 'add: Params: tab, text, tags (optional), note (optional)'), clarifies tab usage with examples, and explains path parameter usage for workspace operations. This provides practical guidance beyond schema definitions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description starts with a clear verb ('Write to') and resource ('CopyQ clipboard manager'), specifying it's for writing operations. It distinguishes from sibling tools mcp_read (reading) and mcp_validate (validation) by focusing on write operations, making the purpose specific and differentiated.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context for when to use this tool (writing to CopyQ with data under 'mcp/' prefix) and explains tab usage scenarios. However, it doesn't explicitly state when NOT to use it or directly compare to alternatives like mcp_read, though the focus on writing implies usage for write operations rather than reading.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/list91/mcp-copyq'

If you have feedback or need assistance with the MCP directory API, please join our Discord server