Claude gets much more useful when it stops being just a chat interface.
A local MCP server lets Claude interact with your actual machine: local files, approved commands, screenshots, and app launches. The important part is not raw access. It’s controlled access.
In this guide, we’ll build a local MCP server for Claude that is practical, narrow, and safe enough to use in real workflows.
A quick note before we build anything
This article intentionally avoids the irresponsible version of “computer control.”
We are not going to give Claude unrestricted shell access, full filesystem authority, or permission to mutate your machine without guardrails. The fastest way to build a bad local MCP server is to expose a giant run_anything() tool and call it innovation.
The better pattern is:
- allowlisted directories
- allowlisted commands
- safe defaults
- human-readable logs
- explicit responses
- clear separation between read-only and action-taking tools
If Claude can do everything, you have built a demo.
If Claude can do the right things safely, you have built something usable.
Why this architecture is worth learning
The value of a local MCP server is not novelty. It is reduction of friction.
Without a local tool layer, your workflow looks like this:
- Ask Claude what to do
- Copy the answer
- Open the folder yourself
- Run the command yourself
- Take the screenshot yourself
- Paste the result back into chat
With a local MCP server, that loop becomes dramatically tighter. Claude can inspect the context it needs, use narrowly scoped tools, and return an answer grounded in your actual machine state.
That is useful for:
- development workflows
- log inspection
- content operations
- research pipelines
- desktop automation
- repeatable admin tasks
And because the tool layer is yours, you choose exactly where the model stops.
Want to learn AI and stay ahead in your career?
I share:
1,000+ AI prompts
AI tools & automations
Career and LinkedIn growth tips
Curated tech & productivity resources
Subscribe here:
Follow me for more AI tips
👇
The design we’re building

We are going to build a local server with five tools:
- list_files — see what exists inside approved folders
- read_file — open safe text-based files
- run_command — execute a small set of approved local commands
- take_screenshot — save a screenshot to a known location
- open_target — open an app, file, folder, or URL

That scope is deliberate.
It is enough to make Claude meaningfully useful on a local machine without collapsing into unsafe general-purpose automation.
The mental model should look like this:
Claude → MCP client → local MCP server → narrow tools → operating system
Claude should never talk directly to your operating system. Your MCP server is the control plane in the middle.
The stack
For a local build, Python is a clean choice because the official MCP SDK is mature, the FastMCP abstraction is concise, and Python remains the easiest language for filesystem, subprocess, and desktop scripting work 4 2.
We’ll use:
- Python 3.11+
- mcp[cli] for the MCP server runtime
- mss for cross-platform screenshots
- standard library modules for file access, subprocess calls, and OS handling
Set up a new project:
1mkdir local-mcp-server2cd local-mcp-server3uv init --python 3.114uv add "mcp[cli]>=1.0,<2.0" "mss>=9.0,<10.0"
The decorator-based FastMCP style keeps the protocol layer out of your way so you can focus on tool quality instead of wiring 4 5.
A simple project layout works well:
1mkdir local-mcp-server2cd local-mcp-server3uv init --python 3.114uv add "mcp[cli]>=1.0,<2.0" "mss>=9.0,<10.0"
You do not need a complex architecture for v1. What you need is clarity.
The actual server
Create server.py and start with a policy-driven implementation.
1from __future__ import annotations23import json4import os5import platform6import shlex7import subprocess8from pathlib import Path9from typing import Any1011import mss12from mcp.server.fastmcp import FastMCP1314app = FastMCP("local-computer-control", json_response=True)1516HOME = Path.home()17PROJECT_ROOT = Path(__file__).parent.resolve()18CAPTURE_DIR = PROJECT_ROOT / "captures"19CAPTURE_DIR.mkdir(exist_ok=True)2021ALLOWED_ROOTS = [22 HOME / "Documents",23 HOME / "Desktop",24 PROJECT_ROOT,25]2627ALLOWED_COMMANDS = {28 "pwd",29 "ls",30 "git status",31 "git diff --stat",32 "python --version",33 "node --version",34 "npm --version",35}3637READABLE_EXTENSIONS = {38 ".txt",39 ".md",40 ".json",41 ".py",42 ".js",43 ".ts",44 ".tsx",45 ".jsx",46 ".yaml",47 ".yml",48 ".toml",49 ".csv",50 ".log",51}5253def _resolve_path(raw_path: str) -> Path:54 path = Path(raw_path).expanduser().resolve()55 for root in ALLOWED_ROOTS:56 root = root.resolve()57 if path == root or root in path.parents:58 return path59 raise ValueError(f"Path not allowed: {path}")6061def _ensure_safe_command(command: str) -> str:62 normalized = " ".join(shlex.split(command))63 if normalized not in ALLOWED_COMMANDS:64 raise ValueError(65 "Command not allowed. Add it explicitly to ALLOWED_COMMANDS if you really need it."66 )67 return normalized6869@app.tool()70def list_files(path: str = "~") -> dict[str, Any]:71 """List files and folders inside an approved directory."""72 target = _resolve_path(path)73 if not target.is_dir():74 raise ValueError(f"Not a directory: {target}")7576 items = []77 for child in sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):78 items.append(79 {80 "name": child.name,81 "path": str(child),82 "type": "directory" if child.is_dir() else "file",83 }84 )8586 return {87 "path": str(target),88 "count": len(items),89 "items": items,90 }9192@app.tool()93def read_file(path: str, max_chars: int = 12000) -> dict[str, Any]:94 """Read a safe text file from an approved location."""95 target = _resolve_path(path)96 if not target.is_file():97 raise ValueError(f"Not a file: {target}")98 if target.suffix.lower() not in READABLE_EXTENSIONS:99 raise ValueError(f"Unsupported file type: {target.suffix}")100101 content = target.read_text(encoding="utf-8", errors="replace")102 truncated = len(content) > max_chars103 content = content[:max_chars]104105 return {106 "path": str(target),107 "truncated": truncated,108 "content": content,109 }110111@app.tool()112def run_command(command: str, cwd: str | None = None, timeout: int = 15) -> dict[str, Any]:113 """Run one allowlisted local command."""114 safe_command = _ensure_safe_command(command)115 working_dir = _resolve_path(cwd) if cwd else PROJECT_ROOT116117 completed = subprocess.run(118 safe_command,119 shell=True,120 cwd=str(working_dir),121 capture_output=True,122 text=True,123 timeout=timeout,124 )125126 return {127 "command": safe_command,128 "cwd": str(working_dir),129 "returncode": completed.returncode,130 "stdout": completed.stdout.strip(),131 "stderr": completed.stderr.strip(),132 }133134@app.tool()135def take_screenshot(name: str = "latest") -> dict[str, Any]:136 """Capture a screenshot and save it locally."""137 output_path = CAPTURE_DIR / f"{name}.png"138139 with mss.mss() as sct:140 sct.shot(output=str(output_path))141142 return {143 "saved": True,144 "path": str(output_path),145 }146147@app.tool()148def open_target(target: str) -> dict[str, Any]:149 """Open an approved file, folder, app, or URL using the local OS."""150 system = platform.system().lower()151152 if target.startswith("http://") or target.startswith("https://"):153 resolved = target154 else:155 resolved = str(_resolve_path(target))156157 if system == "darwin":158 subprocess.run(["open", resolved], check=True)159 elif system == "windows":160 os.startfile(resolved) # type: ignore[attr-defined]161 else:162 subprocess.run(["xdg-open", resolved], check=True)163164 return {165 "opened": True,166 "target": resolved,167 }168169if __name__ == "__main__":170 app.run(transport="stdio")
This is a compact server, but the important part is not its length. The important part is the shape of the interface:
- every tool has a very clear job
- every tool returns structured data
- command execution is gated
- file access is rooted
- screenshots go to a known folder
That is exactly what you want in a local MCP server.
Why these tools are designed this way
Senior content about agent tooling should never stop at “here is code.” Tool shape is the real lesson.
list_files
This tool gives Claude a safe discovery surface. It should be able to answer questions like:
- What’s in this project folder?
- Which notes exist in Documents?
- Is there already a log file I can inspect?
But it should not become a recursive whole-disk crawler.
read_file
This is often the most useful local tool of all. A huge percentage of real work is still hidden in local Markdown notes, logs, CSVs, docs, and project files.
The max_chars cap matters. Large files are a context and latency problem. Returning the entire contents of a giant log file is rarely useful.
run_command
This is where most people get sloppy.
The safe pattern is not “allow shell access, then hope for the best.” The safe pattern is “allow a tiny set of exact, reviewable commands.” That is why the example uses a strict allowlist.
take_screenshot
A screenshot tool is valuable because it lets Claude participate in desktop workflows. Even if your first version only saves the image to disk, that is already useful for reporting, UI debugging, documentation capture, and structured handoffs.
open_target
App control does not need to start with GUI automation. For many workflows, “open the right folder, file, or URL” is enough.
That is a more durable v1 than pretending you need full cursor automation on day one.
Connecting the server to Claude
Local MCP servers are commonly run over stdio, which means Claude launches the process locally and communicates with it directly over stdin/stdout. For a local computer-control server, that is the right default because it avoids unnecessary network exposure 4 5.
Claude Desktop supports local MCP servers via configuration, where it launches the server process for you. In practice, using absolute paths for the interpreter and script is the least fragile setup because local GUI app environments are often stricter than your terminal 2.
A minimal config looks like this:
1{2 "mcpServers": {3 "local-computer-control": {4 "command": "/absolute/path/to/python",5 "args": [6 "/absolute/path/to/local-mcp-server/server.py"7 ]8 }9 }10}
If you prefer uv, that is fine too:
1{2 "mcpServers": {3 "local-computer-control": {4 "command": "/absolute/path/to/uv",5 "args": [6 "--directory",7 "/absolute/path/to/local-mcp-server",8 "run",9 "python",10 "server.py"11 ]12 }13 }14}
After you save the config and restart Claude, the server tools should appear in the local MCP tools list. Claude Desktop’s local-MCP setup is built around exactly this model: launch a local process, connect over stdio, and expose the tools to the model 2 3.

Prompts that are actually useful for testing
Once the server is connected, do not start with complicated orchestration. Start with direct, boring checks.
Try prompts like:
- “List the files in my Desktop folder.”
- “Read ~/Documents/todo.md and summarize the top three priorities.”
- “Run git status in my local project folder and explain what changed.”
- “Take a screenshot called workspace-check.”
- “Open my project README.”
If those simple flows work consistently, you have a server worth expanding.
If they do not, adding more tools will only hide the real problems.
Where local MCP servers become genuinely valuable
The obvious use case is development, but that is only one lane.
Developer workflow
Claude can:
- inspect a repo folder
- read a config file
- run git status
- capture a screenshot of a bug state
- open the project directory
That already eliminates a lot of context-switching.
Research workflow
Claude can:
- list research folders
- open and summarize Markdown notes
- read structured CSVs
- save screenshots from tools or dashboards
- open source files or browser links
Content workflow
Claude can:
- scan a drafts folder
- read existing post ideas
- capture a screenshot of a design reference
- open the right writing file or URL
- run a limited command that generates a build artifact or draft export
Ops workflow
Claude can:
- inspect logs from approved directories
- run a read-only diagnostic command
- open the relevant folder or dashboard link
- save an evidence screenshot
This is the real point of the architecture: not “computer control” as a stunt, but workflow compression.
The security layer is the product
This is the section too many technical articles trivialize.
The dangerous part of local MCP is not the protocol. It is bad permission design.
If you want this server to be usable beyond a demo, build the safety model early.

Use directory allowlists
Claude should only be able to see paths you explicitly approve. That is why _resolve_path() is the core of the file tools.
Use command allowlists
Never expose arbitrary shell execution in a first version. Start with exact commands you can audit line by line.
Separate read tools from action tools
Read-only tools should be default. Action tools should be deliberately introduced.
Log everything
Even a simple append-only JSON log makes debugging and trust dramatically better.
Add a confirmation layer for writes
If you later add write_file, move_file, or delete_file, make those tools either require a second confirmation token or stay disabled by default.
Consider a dry-run mode
For action-taking tools, dry-run mode is underrated. It lets Claude explain what it would do before it does it.
Run under a restricted user when possible
If you are serious about local automation, do not give your MCP server more OS privilege than it needs.
A useful rule of thumb:
- Level 1: read-only tools
- Level 2: low-risk actions like open file / open app
- Level 3: confirmed write actions
- Level 4: destructive actions you probably should not expose casually
Most people never need Level 4.

What to improve after v1
A solid first version earns the right to become more capable.
Once the basic server is stable, the next sensible upgrades are:
- Centralized policy file
Move your rules into config/policy.json so changes are declarative.
Example:
1{2 "allowed_roots": [3 "~/Documents",4 "~/Desktop",5 "./"6 ],7 "allowed_commands": [8 "pwd",9 "ls",10 "git status",11 "git diff --stat",12 "python --version"13 ]14}
- Structured logging
Log tool calls, timestamps, arguments, and results to logs/server.log or a JSONL file.
- Safer command execution
Instead of a single generic command tool, split commands into narrower tools like:
- git_status
- show_current_directory
- list_project_files
That makes tool choice easier for Claude and safety easier for you.
- Better screenshot handling
You can evolve from “save a screenshot to disk” into:
- timestamped captures
- active-window capture
- region capture
- file retention rules
- OS-specific automation adapters
On macOS, you may later add AppleScript or Shortcuts. On Windows, PowerShell or UI Automation. On Linux, desktop-specific launchers and window tools.
But that should come after the boring local core is reliable.
Common mistakes people make with local MCP servers
The mistakes are predictable.
Mistake 1: Too much power, too early
People love the idea of total computer control. They hate debugging it. Start smaller.
Mistake 2: Vague tool names
If your tool names are ambiguous, Claude will use them badly. Be explicit.
Bad:
- system_action
- computer_control
Better:
- list_files
- read_file
- run_command
- take_screenshot
- open_target
Mistake 3: Unstructured outputs
A blob of mixed text is harder for Claude to reason over than a clean JSON object.
Mistake 4: No logging
If a tool fails and you cannot see why, the system becomes guesswork.
Mistake 5: Treating the model like the control layer
Claude is the reasoning layer. Your server must still be the enforcement layer.
That distinction is non-negotiable.
What this architecture does better than plain automation
Traditional desktop automation is usually one of two things:
- brittle GUI scripting
- isolated scripts that require a human to know exactly when to run them
A local MCP server changes that because Claude can decide which tool to use based on the user’s request and available context.
That means you are not just automating a command. You are building a local capability layer the model can reason over.
This is why MCP feels important. It is not merely another integration pattern. It is a cleaner way to expose tool use to the model without hardcoding every possible workflow in the application layer.
The limits you should respect
Even a good local MCP server has real limitations.
- Desktop automation can be flaky across operating systems.
- Screenshots are useful, but not magical.
- App launches are easy; reliable UI manipulation is harder.
- Generic shell access is dangerous.
- Context bloat is real if tool outputs are too large.
- Human approval is still valuable for anything consequential.
In other words: do not confuse “model can take actions” with “model should act without supervision.”
The more valuable pattern is collaborative control, not blind autonomy.





