diff --git a/runtimes/basic/Makefile b/runtimes/basic/Makefile index 40dda49..5fdd6d5 100644 --- a/runtimes/basic/Makefile +++ b/runtimes/basic/Makefile @@ -2,16 +2,15 @@ # Targets for development, testing, and running all services # Uses uCore (unified uServer + uConnect backend) in Python -.PHONY: help install test build run-cli run-gateway run-ucore run-snackbar run-gridui run-all clean +.PHONY: help install test build run-cli run-ucore run-snackbar run-gridui run-all clean help: @echo "uCode1 Core Runtime — Development Targets" @echo "" - @echo " install Install Python dependencies (uCore + uCode2)" + @echo " install Install Python dependencies" @echo " test Run all tests (uCore + uCode1)" @echo " build Build uCore package (editable install)" @echo " run-cli Start uCode1 REPL" - @echo " run-gateway Start uCode2 MCP Gateway" @echo " run-ucore Start uCore daemon (snackbar + hivemind + API)" @echo " run-snackbar Alias for run-ucore" @echo " run-gridui Start gridui Vue dev server" @@ -21,7 +20,6 @@ help: install: pip install -e ../.. cd ../../backend && pip install -e . - cd ../uCode2 && pip install -e . test: python -m pytest tests/ -v @@ -33,9 +31,6 @@ build: run-cli: python -m ucode1.cli --repl -run-gateway: - cd ../uCode2 && python -m ucode2.mcp.gateway - run-ucore: cd ../../backend && python -m app --port 8484 @@ -45,16 +40,13 @@ run-gridui: run-all: @echo "Starting all services..." - @echo " [1/4] uCode2 MCP Gateway..." - @cd ../uCode2 && python -m ucode2.mcp.gateway & - @sleep 2 - @echo " [2/4] uCore daemon..." + @echo " [1/3] uCore daemon..." @cd ../../backend && python -m app --port 8484 & @sleep 2 - @echo " [3/4] uCore daemon (snackbar mode)..." + @echo " [2/3] uCore daemon (snackbar mode)..." @cd ../../backend && python -m app --port 8485 & @sleep 2 - @echo " [4/4] gridui Vue dev server..." + @echo " [3/3] gridui Vue dev server..." @cd ../uConnect/gridui && npm run dev & @echo "All services started!" @@ -63,4 +55,3 @@ clean: find . -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true find . -type d -name *.egg-info -exec rm -rf {} + 2>/dev/null || true cd ../../backend && find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true - diff --git a/runtimes/basic/core_py/__init__.py b/runtimes/basic/core_py/__init__.py index df9872f..806ff88 100644 --- a/runtimes/basic/core_py/__init__.py +++ b/runtimes/basic/core_py/__init__.py @@ -13,7 +13,6 @@ cell, # Cell System with UDX addressing (NEW) feed, # Feed event archiving to Cells (NEW) grid, # Python grid-core (NEW) - mcp_client, # MCP client for uCode2 communication (NEW) mdx, # MDX Runtime with Snack shortcode support (NEW) nugget, # Nugget module — binary executable units (renamed from relic) plugin, # Plugin system (NEW) @@ -41,20 +40,6 @@ GridSize, ) -# MCP Client -from .mcp_client import ( - McpClient, - McpClientError, - McpConnectionError, - McpRequest, - McpRequestType, - McpResponse, - McpTimeoutError, - get_default_socket_path, - socket_exists, - test_connection, -) - # Nugget System from .nugget.models import ( Nugget, @@ -220,7 +205,6 @@ "Coordinate", "CoordSystem", "text", - "mcp_client", # Text System "TextInjector", "TemplateEngine", @@ -233,17 +217,6 @@ "InjectionError", "MarkdownError", "FormattingError", - # MCP Client - "McpClient", - "McpClientError", - "McpConnectionError", - "McpTimeoutError", - "McpRequest", - "McpRequestType", - "McpResponse", - "get_default_socket_path", - "socket_exists", - "test_connection", # Plugin System "PluginDiscovery", "PluginRegistry", diff --git a/runtimes/basic/core_py/mcp_client.py b/runtimes/basic/core_py/mcp_client.py deleted file mode 100644 index 905862f..0000000 --- a/runtimes/basic/core_py/mcp_client.py +++ /dev/null @@ -1,491 +0,0 @@ -""" -MCP Client for uCode1 - -**DEPRECATED** — Superseded by `mcp_client` package (OkAgentDigital/mcp_client/). -This module now delegates to the new client via compatibility shim. - -To migrate: - # Old: from core_py.mcp_client import McpClient - # New: from mcp_client import McpClient - -This module provides a Python client for connecting to the uCode2 MCP server -via Unix domain sockets. The MCP server provides access to vault operations, -notes, intents, and other uCode2 functionality. - -The default socket path is ~/.local/mcp.sock, aligned with the uCode2 specification. -""" - -import json -import os -import socket -import typing -from dataclasses import dataclass, asdict -from enum import Enum -from pathlib import Path -from typing import Any, Dict, List, Optional, Union - - -class McpRequestType(Enum): - """Enumeration of MCP request types supported by the uCode2 server.""" - LIST_NOTES = "ListNotes" - READ_NOTE = "ReadNote" - SEARCH_NOTES = "SearchNotes" - CLASSIFY_INTENT = "ClassifyIntent" - STATUS = "Status" - PING = "Ping" - SHUTDOWN = "Shutdown" - # Vault operations - VAULT_READ = "VaultRead" - VAULT_WRITE = "VaultWrite" - VAULT_LIST = "VaultList" - VAULT_SEARCH = "VaultSearch" - VAULT_DELETE = "VaultDelete" - VAULT_METADATA = "VaultMetadata" - VAULT_WATCH = "VaultWatch" - - -@dataclass -class McpRequest: - """Represents an MCP request to be sent to the server.""" - request_type: McpRequestType - data: Optional[Dict[str, Any]] = None - - def to_dict(self) -> Dict[str, Any]: - """Convert request to dictionary for JSON serialization. - - The Rust server expects the variant name as the key (e.g., {"Ping": null}) - rather than {"type": "Ping"}. - """ - result = {self.request_type.value: self.data if self.data else None} - return result - - def to_json(self) -> str: - """Convert request to JSON string.""" - return json.dumps(self.to_dict()) - - -@dataclass -class McpResponse: - """Represents a response from the MCP server.""" - raw_data: Dict[str, Any] - - @property - def is_success(self) -> bool: - """Check if the response indicates success.""" - return "Error" not in self.raw_data - - @property - def is_error(self) -> bool: - """Check if the response indicates an error.""" - return not self.is_success - - @property - def error_message(self) -> Optional[str]: - """Get the error message if this is an error response.""" - if self.is_error: - error_data = self.raw_data.get("Error", {}) - if isinstance(error_data, dict): - return error_data.get("message", str(error_data)) - return str(error_data) - return None - - @property - def data(self) -> Optional[Dict[str, Any]]: - """Get the data from a Success response.""" - success_data = self.raw_data.get("Success", {}) - if isinstance(success_data, dict): - return success_data.get("data") - return success_data if success_data else None - - @property - def notes(self) -> List[str]: - """Get the list of notes from a Notes response.""" - notes_data = self.raw_data.get("Notes", {}) - if isinstance(notes_data, dict): - return notes_data.get("list", []) - return notes_data if isinstance(notes_data, list) else [] - - @property - def note_content(self) -> Optional[Dict[str, str]]: - """Get the note content from a NoteContent response.""" - return self.raw_data.get("NoteContent") - - @property - def intent(self) -> Optional[Dict[str, Any]]: - """Get the intent from an Intent response.""" - return self.raw_data.get("Intent") - - @property - def status_info(self) -> Optional[Dict[str, str]]: - """Get the status info from a StatusInfo response.""" - return self.raw_data.get("StatusInfo") - - @property - def vault_content(self) -> Optional[Dict[str, str]]: - """Get vault content from a VaultContent response.""" - return self.raw_data.get("VaultContent") - - @property - def vault_list(self) -> Optional[Dict[str, List[str]]]: - """Get vault list from a VaultList response.""" - return self.raw_data.get("VaultList") - - @property - def vault_search_results(self) -> Optional[Dict[str, List[str]]]: - """Get vault search results from a VaultSearchResults response.""" - return self.raw_data.get("VaultSearchResults") - - @property - def vault_metadata(self) -> Optional[Dict[str, Any]]: - """Get vault metadata from a VaultMetadata response.""" - return self.raw_data.get("VaultMetadata") - - -class McpClientError(Exception): - """Exception raised when an MCP client error occurs.""" - pass - - -class McpConnectionError(McpClientError): - """Exception raised when connection to MCP server fails.""" - pass - - -class McpTimeoutError(McpClientError): - """Exception raised when MCP request times out.""" - pass - - -class McpClient: - """ - MCP Client for connecting to uCode2's MCP server. - - This client communicates with the uCode2 MCP server via Unix domain sockets - and provides a Python-friendly interface to vault operations, notes, - intents, and other uCode2 functionality. - - Example usage: - ```python - client = McpClient(socket_path="~/.local/mcp.sock") - - # List all notes - response = client.list_notes() - for note in response.notes: - print(note) - - # Read a specific note - response = client.read_note("my-note") - print(response.note_content) - - # Classify intent - response = client.classify_intent("What is the weather today?") - print(response.intent) - - client.close() - ``` - """ - - DEFAULT_SOCKET_PATH = "~/.local/share/udos/mcp/core.sock" - SOCKET_TIMEOUT = 30.0 # seconds - - def __init__( - self, - socket_path: Optional[Union[str, Path]] = None, - timeout: float = SOCKET_TIMEOUT, - auto_connect: bool = True - ): - """ - Initialize the MCP client. - - Args: - socket_path: Path to the Unix domain socket. Defaults to ~/.local/mcp.sock - timeout: Connection and request timeout in seconds - auto_connect: Whether to connect immediately on initialization - """ - if socket_path is None: - socket_path = self.DEFAULT_SOCKET_PATH - - # Expand tilde and convert to Path - socket_path = Path(str(socket_path)).expanduser() - self.socket_path = socket_path - self.timeout = timeout - self._socket: Optional[socket.socket] = None - - if auto_connect: - self.connect() - - def __del__(self): - """Destructor - ensure connection is closed.""" - self.close() - - def connect(self) -> bool: - """ - Connect to the MCP server. - - Returns: - True if connection succeeded, False otherwise - - Raises: - McpConnectionError: If connection fails - """ - try: - # Disconnect if already connected - self.close() - - # Create Unix domain socket - self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self._socket.settimeout(self.timeout) - - # Connect to the server - self._socket.connect(str(self.socket_path)) - - return True - except socket.error as e: - self._socket = None - raise McpConnectionError( - f"Failed to connect to MCP server at {self.socket_path}: {e}" - ) from e - - def disconnect(self) -> None: - """Disconnect from the MCP server.""" - if self._socket: - try: - self._socket.close() - except socket.error: - pass - self._socket = None - - def close(self) -> None: - """Alias for disconnect.""" - self.disconnect() - - @property - def is_connected(self) -> bool: - """Check if the client is currently connected.""" - return self._socket is not None - - def _send_request(self, request: McpRequest) -> McpResponse: - """ - Send a request to the MCP server and return the response. - - Args: - request: The MCP request to send - - Returns: - The MCP response from the server - - Raises: - McpConnectionError: If not connected or send fails - McpTimeoutError: If request times out - """ - # Reconnect for each request since server closes connection after each response - if self.is_connected: - self.close() - - try: - self.connect() - # Send request as JSON followed by newline - request_json = request.to_json() + "\n" - self._socket.sendall(request_json.encode('utf-8')) - - # Read response - MCP server sends one JSON response per request, terminated by newline - response_lines = [] - buffer = "" - - while True: - data = self._socket.recv(4096) - if not data: - break - buffer += data.decode('utf-8') - # Check if we have a complete response (ends with newline) - if buffer.endswith('\n'): - break - - # Parse response(s) - responses = [] - for line in buffer.strip().split('\n'): - line = line.strip() - if line: - try: - responses.append(json.loads(line)) - except json.JSONDecodeError: - # Skip invalid lines - pass - - # Return the last response (most complete) - if responses: - # If there's only one response, return it - # If multiple, try to find the most complete one - if len(responses) == 1: - return McpResponse(raw_data=responses[0]) - else: - # Return the last non-empty response - for resp in reversed(responses): - if resp: - return McpResponse(raw_data=resp) - - # If no valid response, return empty - return McpResponse(raw_data={}) - - except socket.timeout as e: - raise McpTimeoutError(f"Request timed out: {e}") from e - except socket.error as e: - raise McpConnectionError(f"Socket error: {e}") from e - - # High-level API methods - - def list_notes(self) -> McpResponse: - """List all notes in the vault.""" - request = McpRequest(request_type=McpRequestType.LIST_NOTES) - return self._send_request(request) - - def read_note(self, name: str) -> McpResponse: - """Read a specific note by name.""" - request = McpRequest( - request_type=McpRequestType.READ_NOTE, - data={"name": name} - ) - return self._send_request(request) - - def search_notes(self, query: str) -> McpResponse: - """Search for notes matching the query.""" - request = McpRequest( - request_type=McpRequestType.SEARCH_NOTES, - data={"query": query} - ) - return self._send_request(request) - - def classify_intent(self, text: str) -> McpResponse: - """Classify the intent of the given text.""" - request = McpRequest( - request_type=McpRequestType.CLASSIFY_INTENT, - data={"text": text} - ) - return self._send_request(request) - - def status(self) -> McpResponse: - """Get the status of the MCP server.""" - request = McpRequest(request_type=McpRequestType.STATUS) - return self._send_request(request) - - def ping(self) -> McpResponse: - """Send a ping request to check server availability.""" - request = McpRequest(request_type=McpRequestType.PING) - return self._send_request(request) - - def shutdown(self) -> McpResponse: - """Request server shutdown.""" - request = McpRequest(request_type=McpRequestType.SHUTDOWN) - return self._send_request(request) - - # Vault operations - - def vault_read(self, path: str) -> McpResponse: - """Read content from the vault at the specified path.""" - request = McpRequest( - request_type=McpRequestType.VAULT_READ, - data={"path": path} - ) - return self._send_request(request) - - def vault_write(self, path: str, content: str) -> McpResponse: - """Write content to the vault at the specified path.""" - request = McpRequest( - request_type=McpRequestType.VAULT_WRITE, - data={"path": path, "content": content} - ) - return self._send_request(request) - - def vault_list(self, path: str) -> McpResponse: - """List items at the specified vault path.""" - request = McpRequest( - request_type=McpRequestType.VAULT_LIST, - data={"path": path} - ) - return self._send_request(request) - - def vault_search(self, query: str) -> McpResponse: - """Search the vault for items matching the query.""" - request = McpRequest( - request_type=McpRequestType.VAULT_SEARCH, - data={"query": query} - ) - return self._send_request(request) - - def vault_delete(self, path: str) -> McpResponse: - """Delete an item from the vault at the specified path.""" - request = McpRequest( - request_type=McpRequestType.VAULT_DELETE, - data={"path": path} - ) - return self._send_request(request) - - def vault_metadata(self, path: str) -> McpResponse: - """Get metadata for an item in the vault.""" - request = McpRequest( - request_type=McpRequestType.VAULT_METADATA, - data={"path": path} - ) - return self._send_request(request) - - def vault_watch(self, path: str) -> McpResponse: - """Watch for changes at the specified vault path.""" - request = McpRequest( - request_type=McpRequestType.VAULT_WATCH, - data={"path": path} - ) - return self._send_request(request) - - -def get_default_socket_path() -> Path: - """Get the default MCP socket path.""" - return Path(McpClient.DEFAULT_SOCKET_PATH).expanduser() - - -def socket_exists() -> bool: - """Check if the MCP server socket exists.""" - return get_default_socket_path().exists() - - -def test_connection() -> bool: - """ - Test if the MCP server is available and responding. - - Returns: - True if server is available, False otherwise - """ - try: - client = McpClient(auto_connect=True) - response = client.ping() - return response.is_success - except (McpConnectionError, McpTimeoutError): - return False - - -# Module exports -__all__ = [ - # Client and errors - 'McpClient', - 'McpClientError', - 'McpConnectionError', - 'McpTimeoutError', - # Request and response types - 'McpRequest', - 'McpRequestType', - 'McpResponse', - # Utility functions - 'get_default_socket_path', - 'socket_exists', - 'test_connection', -] - -# ── Compatibility bridge to new mcp_client package ── -# When the new package is installed, existing imports keep working. -try: - from mcp_client import McpClient as _NewMcpClient # noqa: F401 - from mcp_client import ( - McpRequest as _NewMcpRequest, - McpResponse as _NewMcpResponse, - ) -except ImportError: - pass # New package not installed; use this legacy module as-is diff --git a/runtimes/basic/examples/mcp_integration.py b/runtimes/basic/examples/mcp_integration.py deleted file mode 100644 index df07966..0000000 --- a/runtimes/basic/examples/mcp_integration.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python3 -""" -MCP Integration Example - -This example demonstrates how uCode1 (Python) communicates with uCode2 (Rust) -via the MCP server socket. - -Prerequisites: -1. uCode2 MCP server must be running -2. Socket must be available at ~/.local/mcp.sock - -Usage: - python examples/mcp_integration.py -""" - -import sys -import os -from pathlib import Path - -# Add parent directory to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent)) - -from core_py.mcp_client import ( - McpClient, - McpConnectionError, - test_connection, - get_default_socket_path -) - - -def print_header(text): - """Print a formatted header.""" - print("\n" + "=" * 60) - print(f" {text}") - print("=" * 60) - - -def print_result(label, value): - """Print a labeled result.""" - print(f" {label}: {value}") - - -def print_error(label, error): - """Print an error.""" - print(f" {label}: ERROR - {error}") - - -def main(): - """Main example function.""" - print_header("uCode1/uCode2 MCP Integration Demo") - - # Check socket path - socket_path = get_default_socket_path() - print_result("MCP Socket Path", str(socket_path)) - - # Test if server is available - print("\n Testing MCP server connection...") - if test_connection(): - print(" ✓ MCP server is running and responding") - else: - print(" ✗ MCP server is not available") - print("\n To start the MCP server:") - print(" cd uCode2 && cargo run --package ucode2-mcp") - print(" Or start uCode1 CLI:") - print(" cd uCode2 && cargo run --package ucode1-cli -- --status") - return 1 - - # Create client - print("\n Creating MCP client...") - try: - client = McpClient() - print(" ✓ Client created and connected") - except McpConnectionError as e: - print_error("Client connection", e) - return 1 - - # Test ping - print_header("Testing MCP Operations") - print("\n 1. Ping Test") - try: - response = client.ping() - print_result("Ping", "Pong!" if response.is_success else "Failed") - except Exception as e: - print_error("Ping", e) - - # Test status - print("\n 2. Status Test") - try: - response = client.status() - if response.status_info: - for key, value in response.status_info.items(): - print_result(f" Status {key}", value) - else: - print_result("Status", response.raw_data) - except Exception as e: - print_error("Status", e) - - # Test notes operations - print("\n 3. Notes Operations") - try: - response = client.list_notes() - if response.notes: - print_result("Notes count", len(response.notes)) - for note in response.notes[:5]: # Show first 5 - print(f" - {note}") - if len(response.notes) > 5: - print(f" ... and {len(response.notes) - 5} more") - else: - print(" No notes found") - except Exception as e: - print_error("List notes", e) - - # Test vault operations - print("\n 4. Vault Operations") - try: - response = client.vault_list("/") - if response.vault_list: - items = response.vault_list.get("items", []) - print_result("Vault items", len(items)) - for item in items[:10]: - print(f" - {item}") - if len(items) > 10: - print(f" ... and {len(items) - 10} more") - else: - print(" No vault items found") - except Exception as e: - print_error("Vault list", e) - - # Test intent classification - print("\n 5. Intent Classification") - try: - response = client.classify_intent("What is the weather today?") - if response.intent: - print_result("Intent", response.intent.get("intent", "unknown")) - print_result("Confidence", response.intent.get("confidence", 0)) - else: - print(" No intent detected") - except Exception as e: - print_error("Classify intent", e) - - # Close client - client.close() - print("\n ✓ Client disconnected") - - print_header("Demo Complete") - print("\n Next steps:") - print(" - Explore the MCP API in core_py/mcp_client.py") - print(" - Check uCode2/mcp/src/lib.rs for server implementation") - print(" - Run all tests: pytest uCode1/tests/ uCode1/test_*.py") - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/runtimes/basic/ucode1/mcp_client.py b/runtimes/basic/ucode1/mcp_client.py deleted file mode 100644 index 57b75ed..0000000 --- a/runtimes/basic/ucode1/mcp_client.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -MCP Client — Connect to uCode2 MCP Gateway from uCode1. - -Allows uCode1 scripts and snacks to call MCP methods on the uCode2 gateway, -accessing vault resources, feed spool, spatial grid, and other services. - -Usage: - from ucode1.mcp_client import McpClient - - client = McpClient() - result = client.call("vault.list_resources") - print(result) -""" - -import json -import logging -import socket -import struct -from pathlib import Path -from typing import Any, Optional - -logger = logging.getLogger(__name__) - -DEFAULT_SOCKET = Path.home() / ".local" / "share" / "udos" / "ucode2.sock" - - -class McpClientError(Exception): - """MCP client error.""" - - -class McpClient: - """Client for the uCode2 MCP Gateway. - - Connects to the gateway's Unix socket and sends JSON-RPC 2.0 requests - using the length-prefixed protocol. - """ - - def __init__(self, socket_path: Optional[Path] = None): - self.socket_path = socket_path or DEFAULT_SOCKET - self._next_id = 1 - - def _next_request_id(self) -> int: - rid = self._next_id - self._next_id += 1 - return rid - - def call(self, method: str, params: Optional[dict] = None) -> Any: - """Call an MCP method on the gateway. - - Args: - method: The method name (e.g. 'vault.list_resources', 'ping') - params: Optional parameters dict - - Returns: - The result from the gateway - - Raises: - McpClientError: If connection fails or the method returns an error - """ - request = { - "jsonrpc": "2.0", - "id": self._next_request_id(), - "method": method, - } - if params is not None: - request["params"] = params - - payload = json.dumps(request).encode("utf-8") - - try: - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.settimeout(10.0) - sock.connect(str(self.socket_path)) - - # Send length-prefixed request - sock.sendall(struct.pack("!I", len(payload)) + payload) - - # Read length-prefixed response (handle partial reads) - raw_len = sock.recv(4) - if not raw_len: - raise McpClientError("Empty response from gateway") - msg_len = struct.unpack("!I", raw_len)[0] - raw = b"" - while len(raw) < msg_len: - chunk = sock.recv(msg_len - len(raw)) - if not chunk: - raise McpClientError(f"Connection closed: received {len(raw)} of {msg_len} bytes") - raw += chunk - - response = json.loads(raw.decode("utf-8")) - - if "error" in response: - err = response["error"] - raise McpClientError( - f"MCP error [{err.get('code', '?')}]: {err.get('message', 'Unknown')}" - ) - - return response.get("result") - - except socket.timeout: - raise McpClientError( - f"Timeout connecting to gateway at {self.socket_path}" - ) - except FileNotFoundError: - raise McpClientError( - f"Gateway socket not found at {self.socket_path}. " - "Is the uCode2 MCP gateway running?" - ) - except ConnectionRefusedError: - raise McpClientError( - f"Connection refused at {self.socket_path}. " - "Is the uCode2 MCP gateway running?" - ) - except json.JSONDecodeError as e: - raise McpClientError(f"Invalid JSON response: {e}") - finally: - sock.close() - - def ping(self) -> dict: - """Check if the gateway is reachable.""" - return self.call("ping") - - def status(self) -> dict: - """Get gateway status.""" - return self.call("status") - - def list_methods(self) -> dict: - """List all available methods on the gateway.""" - return self.call("list_methods") - - def vault_list(self) -> list: - """List vault resources.""" - return self.call("vault.list_resources") - - def vault_read(self, path: str) -> str: - """Read a vault resource by path.""" - return self.call("vault.read", {"path": path}) - - def feed_write(self, channel: str, content: str) -> dict: - """Write an entry to the feed spool.""" - return self.call("feed.write", {"channel": channel, "content": content}) - - def feed_read(self, channel: str, limit: int = 10) -> list: - """Read entries from the feed spool.""" - return self.call("feed.read", {"channel": channel, "limit": limit})