workspace_read_text
Read a UTF-8 text file from the workspace root safely, with built-in traversal protection to prevent directory escapes. Specify the relative path and optionally limit bytes.
Instructions
Read a UTF-8 text file under the workspace root with traversal protection.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| relative_path | Yes | ||
| max_bytes | No |
Implementation Reference
- The MCP tool handler function that receives relative_path and optional max_bytes, calls the workspace service's read_text method, and converts security/file-not-found errors to ValueError.
@server.tool( name="workspace_read_text", annotations=ToolAnnotations(readOnlyHint=True, idempotentHint=True, openWorldHint=False), ) async def workspace_read_text(relative_path: str, max_bytes: int | None = None) -> object: """Read a UTF-8 text file under the workspace root with traversal protection.""" with container.metrics.observe_tool("workspace_read_text"): try: return container.workspace.read_text( relative_path=relative_path, max_bytes=max_bytes or container.settings.max_workspace_read_bytes, ) except (WorkspaceSecurityError, FileNotFoundError) as exc: raise ValueError(str(exc)) from exc - The output schema for workspace_read_text, containing the path, bytes_read, truncated flag, and text content.
class WorkspaceDocument(TemplateModel): path: str bytes_read: int truncated: bool content: str - src/mcp_template/modules/workspace.py:29-42 (registration)The @server.tool decorator registers workspace_read_text as an MCP tool. The tool is also listed in the ModuleDescriptor returned by the register() function.
@server.tool( name="workspace_read_text", annotations=ToolAnnotations(readOnlyHint=True, idempotentHint=True, openWorldHint=False), ) async def workspace_read_text(relative_path: str, max_bytes: int | None = None) -> object: """Read a UTF-8 text file under the workspace root with traversal protection.""" with container.metrics.observe_tool("workspace_read_text"): try: return container.workspace.read_text( relative_path=relative_path, max_bytes=max_bytes or container.settings.max_workspace_read_bytes, ) except (WorkspaceSecurityError, FileNotFoundError) as exc: raise ValueError(str(exc)) from exc - The underlying WorkspaceService.read_text method that resolves the path, reads the file (with byte limit), handles truncation, and decodes UTF-8 content.
def read_text(self, relative_path: str, max_bytes: int) -> WorkspaceDocument: resolved = self.resolve_path(relative_path) if not resolved.is_file(): raise FileNotFoundError(f"{relative_path!r} is not a file under {self.root}") with resolved.open("rb") as handle: payload = handle.read(max_bytes + 1) truncated = len(payload) > max_bytes content = payload[:max_bytes].decode("utf-8", errors="replace") return WorkspaceDocument( path=str(resolved.relative_to(self.root)), bytes_read=min(len(payload), max_bytes), truncated=truncated, content=content, ) - Helper method that resolves a relative path against the workspace root and enforces the path does not escape the root directory (traversal protection).
def resolve_path(self, relative_path: str) -> Path: candidate = Path(relative_path) resolved = candidate.resolve() if candidate.is_absolute() else (self.root / candidate).resolve() if not resolved.is_relative_to(self.root): raise WorkspaceSecurityError(f"Path {relative_path!r} escapes workspace root {self.root}") return resolved