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
| Name | Required | Description | Default |
|---|---|---|---|
| mode | Yes | Operation mode | |
| tab | No | Tab path | |
| index | No | Item index | |
| text | No | Text content | |
| tags | No | Tags list | |
| note | No | Note content | |
| field | No | Field to update | |
| edit_mode | No | replace | |
| match | No | String to match (for substitute) | |
| to_tab | No | Destination tab (for move) | |
| path | No | Tab path (for tab_create/tab_delete) | |
| intent | No | execute |
Implementation Reference
- src/mcp_copyq/tools/write.py:23-88 (handler)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)}"
- src/mcp_copyq/server.py:118-199 (registration)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"] } ),
- src/mcp_copyq/models.py:78-93 (schema)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
- src/mcp_copyq/tools/write.py:98-137 (helper)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)}" )