How to Build a Local MCP Server for Claude: Files, Commands, Screenshots, and App Control

@hrswatigupta
ENGLISH2 months ago · Jun 04, 2026
1.0M
72
16
2
224

TL;DR

This guide demonstrates how to create a Python-based local MCP server for Claude, enabling safe and controlled access to local files, commands, and desktop tools through a secure allowlist architecture.

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:

  1. Ask Claude what to do
  2. Copy the answer
  3. Open the folder yourself
  4. Run the command yourself
  5. Take the screenshot yourself
  6. 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:

https://bytebuilders.beehiiv.com/subscribe

Follow me for more AI tips

👇

The design we’re building

Swati Gupta - inline image

We are going to build a local server with five tools:

  1. list_files — see what exists inside approved folders
  2. read_file — open safe text-based files
  3. run_command — execute a small set of approved local commands
  4. take_screenshot — save a screenshot to a known location
  5. open_target — open an app, file, folder, or URL
Swati Gupta - inline image

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:

bash
1mkdir local-mcp-server
2cd local-mcp-server
3uv init --python 3.11
4uv 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:

text
1mkdir local-mcp-server
2cd local-mcp-server
3uv init --python 3.11
4uv 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.

python
1from __future__ import annotations
2
3import json
4import os
5import platform
6import shlex
7import subprocess
8from pathlib import Path
9from typing import Any
10
11import mss
12from mcp.server.fastmcp import FastMCP
13
14app = FastMCP("local-computer-control", json_response=True)
15
16HOME = Path.home()
17PROJECT_ROOT = Path(__file__).parent.resolve()
18CAPTURE_DIR = PROJECT_ROOT / "captures"
19CAPTURE_DIR.mkdir(exist_ok=True)
20
21ALLOWED_ROOTS = [
22 HOME / "Documents",
23 HOME / "Desktop",
24 PROJECT_ROOT,
25]
26
27ALLOWED_COMMANDS = {
28 "pwd",
29 "ls",
30 "git status",
31 "git diff --stat",
32 "python --version",
33 "node --version",
34 "npm --version",
35}
36
37READABLE_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}
52
53def _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 path
59 raise ValueError(f"Path not allowed: {path}")
60
61def _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 normalized
68
69@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}")
75
76 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 )
85
86 return {
87 "path": str(target),
88 "count": len(items),
89 "items": items,
90 }
91
92@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}")
100
101 content = target.read_text(encoding="utf-8", errors="replace")
102 truncated = len(content) > max_chars
103 content = content[:max_chars]
104
105 return {
106 "path": str(target),
107 "truncated": truncated,
108 "content": content,
109 }
110
111@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_ROOT
116
117 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 )
125
126 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 }
133
134@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"
138
139 with mss.mss() as sct:
140 sct.shot(output=str(output_path))
141
142 return {
143 "saved": True,
144 "path": str(output_path),
145 }
146
147@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()
151
152 if target.startswith("http://") or target.startswith("https://"):
153 resolved = target
154 else:
155 resolved = str(_resolve_path(target))
156
157 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)
163
164 return {
165 "opened": True,
166 "target": resolved,
167 }
168
169if __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:

json
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:

json
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.

Swati Gupta - inline image

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.

Swati Gupta - inline image

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.

Swati Gupta - inline image

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:

  1. Centralized policy file

Move your rules into config/policy.json so changes are declarative.

Example:

json
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}
  1. Structured logging

Log tool calls, timestamps, arguments, and results to logs/server.log or a JSONL file.

  1. 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.

  1. Better screenshot handling

You can evolve from “save a screenshot to disk” into:

  • timestamped captures
  • active-window capture
  • region capture
  • file retention rules
  1. 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.

One-click save

Use YouMind for AI deep reading of viral articles

Save the source, ask focused questions, summarize the argument, and turn a viral article into reusable notes in one AI workspace.

Explore YouMind
For creators

Turn your Markdown into a clean 𝕏 article

When you publish your own long-form writing, images, tables, and code blocks make 𝕏 formatting painful. YouMind turns a full Markdown draft into a clean, ready-to-post 𝕏 article.

Try Markdown to 𝕏

More patterns to decode

Recent viral articles

Explore more viral articles