mcp_read
Read clipboard content from CopyQ tabs, including MCP-managed storage and external clipboard data, with options to browse tab structures, list items, retrieve single items, or search across tabs using regex patterns.
Instructions
Read from CopyQ clipboard manager.
MCP tabs (full read/write access):
mcp/info - general information storage
mcp/заметки - notes storage
mcp/workspace - projects (supports subtabs like workspace/myproject)
External tabs (READ-ONLY access with scope="all" or scope="external"):
All other CopyQ tabs like "&clipboard", personal tabs, etc.
Use scope="all" to see all tabs, scope="external" for non-mcp only
Modes:
tree: Get tab structure with previews. Use FIRST to see available tabs.
list: Get items from tab with pagination
item: Get single item with full content (text, tags, note)
search: Search by regex across tabs
Parameters:
mode (required): "tree" | "list" | "item" | "search"
tab: For mcp tabs use relative path "info", "workspace/proj1". For external use full name "&clipboard"
scope: "mcp" (default) | "all" | "external" - controls which tabs are accessible
index: Item index (for mode=item)
query: Search regex (for mode=search)
Examples:
tree of mcp tabs: mode="tree"
tree of ALL tabs: mode="tree", scope="all"
read external tab: mode="list", tab="&clipboard", scope="external"
search everywhere: mode="search", query="pattern", scope="all"
Errors: TAB_NOT_FOUND, INDEX_OUT_OF_BOUNDS, INVALID_MODE
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| mode | Yes | Operation mode | |
| tab | No | Tab path: 'info', 'workspace/proj1' or full external name '&clipboard' | |
| scope | No | Tab scope: mcp (default, read/write), all (read external), external (only non-mcp, read-only) | mcp |
| index | No | Item index (for mode=item) | |
| query | No | Search regex (for mode=search) | |
| search_in | No | all | |
| max_depth | No | ||
| max_items | No | ||
| skip | No | ||
| include_text | No | ||
| include_tags | No | ||
| include_note | No |
Implementation Reference
- src/mcp_copyq/tools/read.py:22-93 (handler)The primary handler function `mcp_read` that validates parameters and dispatches to mode-specific read operations (_read_tree, _read_list, _read_item, _read_search).async def mcp_read( mode: str, tab: str = "", index: int | None = None, query: str | None = None, search_in: str = "all", scope: str = "mcp", max_depth: int = 2, max_items: int = 20, skip: int = 0, include_text: bool = True, include_tags: bool = True, include_note: bool = False, ) -> str: """ Universal read operation for CopyQ MCP. Modes: - tree: Get tab structure with metadata - list: Get items from a specific tab - item: Get single item fully - search: Search across tabs Scope: - mcp: Only mcp/* tabs (default, full access) - all: All CopyQ tabs (external tabs are read-only) - external: Only non-mcp tabs (read-only) Returns compact pipe-separated format. """ try: # Validate mode valid_modes = [m.value for m in ReadMode] if mode not in valid_modes: raise InvalidModeError(mode, valid_modes) # Validate scope valid_scopes = [s.value for s in Scope] if scope not in valid_scopes: raise MCPCopyQError(ErrorCode.INVALID_PARAM, f"Invalid scope '{scope}'. Valid: {', '.join(valid_scopes)}") if mode == "tree": return await _read_tree(max_depth, max_items, include_text, include_tags, scope) elif mode == "list": if not tab: raise MissingParamError("tab", mode) return await _read_list( tab, max_items, skip, include_text, include_tags, include_note, scope ) elif mode == "item": if not tab: raise MissingParamError("tab", mode) if index is None: raise MissingParamError("index", mode) return await _read_item(tab, index, include_text, include_tags, include_note, scope) elif mode == "search": if not query: raise MissingParamError("query", mode) return await _read_search( query, search_in, max_items, skip, include_text, include_tags, scope ) 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/models.py:61-76 (schema)Pydantic model defining the input parameters and validation schema for the mcp_read tool.class ReadRequest(BaseModel): """Parameters for mcp_read tool.""" mode: ReadMode = Field(..., description="Operation mode: tree, list, item, search") tab: str = Field(default="", description="Tab path: 'info', 'workspace/proj1' or full path like '&clipboard'") index: int | None = Field(default=None, description="Item index (for mode=item)") query: str | None = Field(default=None, description="Search query regex (for mode=search)") search_in: SearchIn = Field(default=SearchIn.ALL, description="Where to search") scope: Scope = Field(default=Scope.MCP, description="Tab scope: mcp (default), all, external") max_depth: int = Field(default=2, ge=1, le=10, description="Max depth for tree") max_items: int = Field(default=20, ge=1, le=100, description="Max items to return") skip: int = Field(default=0, ge=0, description="Skip N items (pagination)") include_text: bool = Field(default=True, description="Include text/preview") include_tags: bool = Field(default=True, description="Include tags") include_note: bool = Field(default=False, description="Include note")
- src/mcp_copyq/server.py:21-117 (registration)MCP tool registration in `list_tools()` including name, description, and JSON inputSchema for mcp_read.Tool( name="mcp_read", description="""Read from CopyQ clipboard manager. MCP tabs (full read/write access): - mcp/info - general information storage - mcp/заметки - notes storage - mcp/workspace - projects (supports subtabs like workspace/myproject) External tabs (READ-ONLY access with scope="all" or scope="external"): - All other CopyQ tabs like "&clipboard", personal tabs, etc. - Use scope="all" to see all tabs, scope="external" for non-mcp only Modes: - tree: Get tab structure with previews. Use FIRST to see available tabs. - list: Get items from tab with pagination - item: Get single item with full content (text, tags, note) - search: Search by regex across tabs Parameters: - mode (required): "tree" | "list" | "item" | "search" - tab: For mcp tabs use relative path "info", "workspace/proj1". For external use full name "&clipboard" - scope: "mcp" (default) | "all" | "external" - controls which tabs are accessible - index: Item index (for mode=item) - query: Search regex (for mode=search) Examples: - tree of mcp tabs: mode="tree" - tree of ALL tabs: mode="tree", scope="all" - read external tab: mode="list", tab="&clipboard", scope="external" - search everywhere: mode="search", query="pattern", scope="all" Errors: TAB_NOT_FOUND, INDEX_OUT_OF_BOUNDS, INVALID_MODE""", inputSchema={ "type": "object", "properties": { "mode": { "type": "string", "enum": ["tree", "list", "item", "search"], "description": "Operation mode" }, "tab": { "type": "string", "description": "Tab path: 'info', 'workspace/proj1' or full external name '&clipboard'" }, "scope": { "type": "string", "enum": ["mcp", "all", "external"], "default": "mcp", "description": "Tab scope: mcp (default, read/write), all (read external), external (only non-mcp, read-only)" }, "index": { "type": "integer", "description": "Item index (for mode=item)" }, "query": { "type": "string", "description": "Search regex (for mode=search)" }, "search_in": { "type": "string", "enum": ["text", "note", "tags", "all"], "default": "all" }, "max_depth": { "type": "integer", "minimum": 1, "maximum": 10, "default": 2 }, "max_items": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, "skip": { "type": "integer", "minimum": 0, "default": 0 }, "include_text": { "type": "boolean", "default": True }, "include_tags": { "type": "boolean", "default": True }, "include_note": { "type": "boolean", "default": False } }, "required": ["mode"] } ),
- src/mcp_copyq/server.py:238-240 (registration)Dispatch logic in `call_tool()` that invokes the mcp_read handler when the tool is called.if name == "mcp_read": result = await mcp_read(**arguments) elif name == "mcp_write":
- src/mcp_copyq/tools/read.py:95-186 (helper)Helper function for 'tree' mode that recursively builds tab structure with previews.async def _read_tree( max_depth: int, max_items: int, include_text: bool, include_tags: bool, scope: str = "mcp", ) -> str: """Build tree structure of tabs.""" # Get tabs based on scope if scope == "mcp": tabs = await client.list_tabs() elif scope == "external": tabs = await client.list_external_tabs() else: # "all" tabs = await client.list_all_tabs() lines = [] async def format_tab( path: str, depth: int, indent: str = "", is_external: bool = False ) -> None: if depth > max_depth: return count = await client.get_count(path, external=is_external) # Clean path for display if path.startswith(f"{MCP_ROOT}/"): display_path = path[len(MCP_ROOT) + 1:] else: display_path = path # Mark external tabs external_marker = " [external]" if is_external else "" lines.append(f"{indent}{display_path} [{count}]{external_marker}") # Add preview items preview_count = min(count, max_items) for i in range(preview_count): preview = await client.read_item_preview(path, i, external=is_external) parts = [f"{indent} {i}:"] if include_text: parts.append(f'"{preview["text_preview"]}"') if include_tags and preview["tags"]: parts.append(f"[{','.join(preview['tags'])}]") if preview["has_note"]: parts.append("+note") lines.append("|".join(parts) if len(parts) > 1 else parts[0]) # Find child tabs child_tabs = [t for t in tabs if t.startswith(path + "/") and t.count("/") == path.count("/") + 1] for child in child_tabs: child_is_external = not child.startswith(f"{MCP_ROOT}/") await format_tab(child, depth + 1, indent + " ", child_is_external) if scope == "mcp": # Start from mcp root tabs for root in ALLOWED_ROOT_TABS: full_path = f"{MCP_ROOT}/{root}" if full_path in tabs or any(t.startswith(full_path + "/") for t in tabs): await format_tab(full_path, 1, "", False) else: # For all/external scope, show root-level tabs root_tabs = [t for t in tabs if "/" not in t or (t.count("/") == 1 and t.startswith(f"{MCP_ROOT}/"))] # Also get direct children of mcp/ for "all" scope if scope == "all": # Get unique root tabs (either no "/" or first level of mcp/) seen_roots = set() for t in tabs: if t.startswith(f"{MCP_ROOT}/"): # For mcp tabs, get first level after mcp/ parts = t.split("/") if len(parts) >= 2: seen_roots.add(f"{MCP_ROOT}/{parts[1]}") else: # For external tabs, get root root = t.split("/")[0] seen_roots.add(root) root_tabs = sorted(seen_roots) for tab in root_tabs: is_external = not tab.startswith(f"{MCP_ROOT}/") await format_tab(tab, 1, "", is_external) return "\n".join(lines) if lines else "empty|no tabs found"