. The features had grown too lengthy and the variable names made no sense anymore. Each time I wished suggestions on a file, I finished, opened the chat, copied the entire thing in, and waited. Then went again to the editor, utilized the change, opened the subsequent file, and did it once more.
In some unspecified time in the future I counted. Six recordsdata. Eleven pastes. Twenty minutes of switching earlier than I wrote a single new line.
The plain repair was to offer the AI software direct entry to my undertaking folder. That’s after I bumped into MCP — the Mannequin Context Protocol — which is strictly constructed for this. A server runs domestically, exposes instruments, and the AI consumer calls these instruments immediately as an alternative of ready for me to stick issues.
So I checked out current implementations. Most required FastAPI, uvicorn, LangChain, or the official MCP SDK. Earlier than writing a single line of enterprise logic I had 5 packages in my necessities file and a server I wasn’t assured would run on Home windows with out a struggle.
I stepped again and skim the precise MCP spec [1]. The protocol is JSON-RPC 2.0 [2] over a transport layer. One JSON object per line. Consumer sends, server responds. The spec defines precisely two transports: stdio for native single-client connections, and HTTP with Server-Despatched Occasions for concurrent purchasers.
That’s the entire protocol.
I requested a special query: what does this really need that Python’s customary library doesn’t already present? sys.stdin, sys.stdout, http.server, threading, queue, pathlib, json. That’s it. Not a single pip set up.
This text is that implementation — each transports, a manufacturing safety mannequin, 50 assessments, and the numbers from operating it.
TL;DR
Most MCP implementations really feel heavier than they need to. The spec solely defines two transports, stdio and HTTP/SSE, however in apply they’re often wrapped in frameworks and additional dependencies.
I constructed each transports from scratch utilizing solely the Python customary library.
It runs as a single file with one runtime flag. No installs, no setup.
For native work, it makes use of stdio with a single consumer. Whenever you want concurrency, it switches to HTTP/SSE and handles a number of purchasers with out altering anything.
Underneath the hood, all the pieces stays constant. Similar dispatcher, similar instruments, similar safety mannequin.
As a result of it touches the filesystem, I added strict path checks early on. Widespread escape patterns like ../../, symlink methods, and Home windows UNC paths are blocked.
5 concurrent purchasers. Underneath 50ms whole wall time. Verified on Home windows 11, Python 3.12.6, CPU solely.
Full code: https://github.com/Emmimal/local-mcp-server/
The Mistake That Formed the Complete Design
Earlier than the structure, I need to let you know in regards to the factor that just about made me quit on the entire thing.
Early in improvement I used to be testing the search software. I pointed the server at C:UsersAdmin and ran it on the lookout for Python recordsdata. The server began. The demo began operating. Then it simply stored operating.
Thirty seconds. A minute. 5 minutes. I assumed there was an infinite loop someplace. I went again via the code thrice. Every part appeared appropriate. I killed the method and restarted. Similar consequence.
Ten minutes in I lastly understood what was taking place. The search software was utilizing rglob() by default. I had pointed it at my whole consumer listing and it was scanning all the pieces — digital environments, AppData, each cached file on the machine. Tens of 1000’s of recordsdata, separately.
I killed the method and altered one line:
# Earlier than — recursive by default, scans all the pieces
for match in goal.rglob(sample):
# After — shallow by default, opt-in for recursion
for match in goal.glob(sample):
And made recursive=False the default parameter. The consumer has to cross recursive=True explicitly. The server won’t ever scan recursively by itself.
That single change is why search completes in below 30ms on an actual undertaking folder right this moment as an alternative of operating without end. And it turned the rule I utilized all over the place: no conduct that destroys efficiency ought to ever be the default.
What MCP Truly Is
The Mannequin Context Protocol [1] is a standardised manner for AI purchasers to name instruments on exterior servers. It makes use of JSON-RPC 2.0 [2] as its message format.
In apply, this implies AI purchasers like Claude or ChatGPT can immediately entry and motive over native recordsdata as an alternative of counting on copy-paste.
The handshake has three phases. First the consumer initializes, then it asks what instruments can be found, then it begins calling them:
Every part after that’s the transport carrying messages forwards and backwards.
The spec defines two transports. stdio runs over customary enter and output — one JSON object per line, flushed instantly. HTTP/SSE runs requests over HTTP POST, with responses streamed again over a persistent Server-Despatched Occasions connection [3].
Most implementations choose one. This one implements each, with the identical dispatcher and the identical 4 instruments sitting behind every.
Here’s what the demo exhibits at startup — each transports register the identical instruments:
[2] Accessible instruments
[list_directory ] Record recordsdata and directories. Returns title, kind, dimension...
[read_file ] Learn a file's contents. Max 1 MB. Binary recordsdata returned...
[search_files ] Search recordsdata by glob sample. Use recursive=true for...
[get_file_info ] Get metadata for a file or listing: dimension, kind, ext...
Structure: 4 Layers
The system has 4 layers.

Safety layer — validates each path earlier than any filesystem operation. It runs earlier than anything, on each single name.
Instruments layer — 4 instruments for the precise file system work: list_directory, read_file, search_files, get_file_info.
Dispatcher — a stateless JSON-RPC router. Parses the strategy, calls the correct handler, returns the response. It has no concept which transport is operating and it doesn’t must.
Transport layer — two implementations. StdioTransport for native AI purchasers. HTTPSSETransport for concurrent connections. The dispatcher has no concept which one is operating.
The entry level selects the transport at startup:
dispatcher = MCPDispatcher(root)
if args.http:
HTTPSSETransport(dispatcher, host=args.host, port=args.port).run()
else:
StdioTransport(dispatcher).run()
One flag. That’s it.
The Safety Mannequin
The very first thing I had to consider when constructing a server that reads native recordsdata was what stops a consumer from studying recordsdata it shouldn’t. The plain assault is path traversal — as an alternative of sending README.md, a consumer sends ../../and so on/passwd and a server that doesn’t examine follows it straight out of the sandbox.
The repair was to resolve each paths totally earlier than evaluating them. The important thing line:
goal.resolve().relative_to(base.resolve())
Path.resolve() expands all symlinks and collapses all .. segments. relative_to() raises ValueError if the consequence lands outdoors the bottom. [6] No string parsing, no counting .. manually. The OS resolves the trail; Python checks the consequence.
MCP_ROOT units the sandbox root by way of surroundings variable. I set it to my undertaking folder particularly, not my house listing. Each software runs this examine earlier than touching the filesystem. If it fails, the error goes again to the consumer instantly.
The safety assessments confirm this on each construct:
| Assault | Outcome |
|---|---|
../../and so on/passwd |
Entry denied |
| Symlink pointing outdoors root | Entry denied |
Home windows UNC path servershare |
Entry denied |
src/most important.py inside root |
Allowed |
The 4 Instruments
list_directory
Lists all the pieces in a listing — title, kind, dimension, modified timestamp, relative path. Directories earlier than recordsdata, hidden entries excluded by default.
Pointing it on the undertaking folder:
[3] list_directory
8 entries:
[F] concurrent_demo.py 4,711B
[F] demo.py 10,451B
[F] http_client.py 5,140B
[F] local_desktop_config.json 228B
[F] README.md 7,542B
[F] server.py 29,222B
[F] test_server.py 17,500B
Eight entries, sizes, all contained in the sandbox. The type order places directories first as a result of the type key makes use of p.is_file() — False < True in Python, so directories naturally float up.
One factor that bit me on Home windows: a file can seem in a listing itemizing whereas being locked by one other course of. merchandise.stat() raises PermissionError on that entry. The software wraps every stat name in its personal strive/besides and skips locked entries silently as an alternative of crashing your entire itemizing.
read_file
Reads file contents with a tough 1 MB cap. Textual content recordsdata returned as plain UTF-8. Binary recordsdata returned as base64.
read_file
concurrent_demo.py:
#!/usr/bin/env python3
"""
concurrent_demo.py
============================
Proves the HTTP/SSE transport handles a number of concurrent purchasers.
Spins up 5 purchasers concurrently, every operating
... (4509 extra chars)
I added the binary fallback after pointing the server at an actual undertaking folder for the primary time. Python undertaking folders include .pyc recordsdata, compiled extensions, SQLite databases. The primary model refused all of them with UnicodeDecodeError. The repair: if read_text() fails on decode, fall again to read_bytes() and return base64. The consumer will get a structured response with a binary: true flag as an alternative of a crash.
The 1 MB cap exists as a result of one early check by accident learn a 200 MB SQLite database and froze the method for thirty seconds. MAX_FILE_BYTES is a continuing on the prime of server.py — change it in case your workflow wants bigger recordsdata.
search_files
After the rglob() incident, this software works like this:
[6] search_files — *.py (shallow)
Discovered 5 file(s):
-> concurrent_demo.py 4,711B
-> demo.py 10,451B
-> http_client.py 5,140B
-> server.py 29,222B
-> test_server.py 17,500B
5 recordsdata, below 30ms. The identical name on C:UsersAdmin with recursive=True would nonetheless scan all the pieces — however now that may be a deliberate selection the consumer has to make, not one thing the server does routinely.
The truncated flag tells the consumer when outcomes had been reduce off at max_results. The primary model silently dropped outcomes with no sign — I added truncated after realising the consumer had no option to comprehend it wasn’t getting all the pieces.
get_file_info
Returns metadata with out studying file contents — helpful when the consumer must examine permissions earlier than deciding whether or not to learn.
[4] get_file_info
title local-mcp-server
path .
kind listing
dimension 4096
modified 1780246573
created 1780227648
extension None
readable True
writable True
os.entry() checks actual permissions, not simply existence. On Home windows a file could be seen in a list whereas being locked. Realizing it’s unreadable earlier than attempting to learn it saves a spherical journey.
The Dispatcher
I didn’t need to reinvent the wheel or rewrite my core logic simply to deal with totally different community setups, so I constructed a central dispatcher to deal with all the pieces as an alternative. It features as a primary, stateless engine. A uncooked JSON string is available in, the dispatcher parses it to see precisely what the consumer wants, after which it drops a response again.
I explicitly stored all community and file I/O out of this element. It doesn’t know something about stdin, stdout, or HTTP. All of that messy communication is left fully to the transport layers. The transports do the heavy lifting with the precise sockets or streams and easily cross the clear information alongside to the dispatch() operate.
To maintain the system lean, the engine solely listens for 4 spec strategies: initialize, instruments/listing, instruments/name, and ping. If anything hits the dispatcher, it shuts the request down instantly with a typical JSON-RPC error.
The one exception is dealing with notifications. When a message comes via with out an id area, the MCP specification dictates that no response is required. The dispatcher processes the occasion internally and simply returns None. As a result of the core engine is totally unbiased of how information travels, transferring from native stdio to an HTTP server requires zero inner code adjustments. The transport layer adjustments on the skin, however the principle dispatcher stays precisely the identical.
Transport 1: stdio
For the native setup, the stdio transport is only a uncooked for line in self._stdin loop. I fully skipped async, threads, and occasion loops to maintain it so simple as potential.
The Home windows repair truly took me longer than writing the transport itself. By default, Python opens stdin and stdout in textual content mode on Home windows, which routinely adjustments each n to rn everytime you write information. That little change fully corrupts the JSON stream. The second a consumer reads }rn{, it hits a parse error on the very subsequent message, breaking your entire connection.
if platform.system() == "Home windows":
import msvcrt
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
Setting O_BINARY disables the interpretation. [8] With out this the server works on macOS and Linux and silently breaks on Home windows.
write_through=True on the stdout wrapper ensures each write flushes instantly. The AI consumer is obstructing synchronously ready for the response — any buffering stalls the interplay.
Right here is the total stdio demo output from my machine:
============================================================
local-mcp-server demo [stdio transport]
Root: C:UsersAdminPycharmProjectspythonProjectlocal-mcp-server
============================================================
[1] Initialize
Server : local-mcp-server v1.0.0
Protocol: 2024-11-05
[2] Accessible instruments
[list_directory ] Record recordsdata and directories...
[read_file ] Learn a file's contents. Max 1 MB...
[search_files ] Search recordsdata by glob sample...
[get_file_info ] Get metadata for a file or listing...
[3] list_directory 8 entries
[4] get_file_info readable: True writable: True
[5] read_file first small file learn efficiently
[6] search_files Discovered 5 .py recordsdata
============================================================
All checks handed. Prepared to attach Native Desktop.
============================================================
Transport 2: HTTP/SSE
Every consumer opens a GET /sse connection (constructed on Python’s http.server [4]) that stays open for your entire period of the session, permitting the server to push responses down that pipeline as server-sent occasions. Every connection receives a singular client_id [9] on join. When a consumer wants to speak again or ship a request, it fires off a separate POST /message.
The movement per consumer appears like this:

To deal with concurrency cleanly, every consumer will get its personal unbiased message queue. [7] The POST handler dispatches the decision, drops the consequence immediately onto that consumer’s queue, and instantly returns a 202 standing. It doesn’t look forward to the SSE supply to complete. The consumer simply picks up the response from its personal open stream. That’s what makes the concurrency work.
I arrange 16 daemon employee threads to handle incoming requests. Since every lively SSE connection holds onto one thread, having 5 lively SSE purchasers leaves 11 threads fully free to deal with incoming POST requests at any second. There isn’t a async/await syntax and no occasion loop—simply customary library threading. [5]
The Concurrent Demo
That is the output that solutions whether or not the HTTP/SSE transport truly works:
============================================================
Concurrent Consumer Demo — 5 purchasers, 5 simultaneous calls
============================================================
Launching 5 concurrent purchasers...
Consumer Device Outcome Time
---------- -------------------- ---------- --------
1 list_directory OK ~0.034s
2 get_file_info OK ~0.021s
3 list_directory OK ~0.038s
4 search_files OK ~0.023s
5 search_files OK ~0.021s
Whole wall time: ~0.04s for five concurrent purchasers
Outcome: ALL PASSED
============================================================
5 purchasers. 5 totally different software calls. Underneath 50ms whole wall time throughout all runs. None blocked one another. Measured on Home windows 11, Python 3.12.6, CPU solely.
What Broke Throughout Improvement
The ten-minute hold I already described. Three different issues broke earlier than the server was secure.
The Home windows rn drawback. The primary time I related an precise AI consumer it obtained a parse error on the second message. Every part appeared high quality in testing. The difficulty was the stdout translation — n changing into rn on Home windows. I spent an hour wanting on the dispatcher earlier than I discovered it. Two traces fastened it.
The binary file crash. First model of read_file referred to as read_text() on all the pieces. First actual undertaking folder it hit a .pyc file and raised UnicodeDecodeError. Added the base64 fallback after that.
The 200 MB database freeze. Earlier than the 1 MB cap, a check by accident learn a SQLite database. The method froze for thirty seconds. The cap went in instantly after.
Every of those solely appeared when the server ran in opposition to an actual machine, not a check listing.
The Check Suite
50 assessments throughout seven lessons. Safety runs first.
| Class | What it covers |
|---|---|
| TestSecurity | Traversal assaults, symlink escapes, empty paths |
| TestListDirectory | Hidden recordsdata, type order, locked entries, errors |
| TestReadFile | Textual content, binary/base64, 1 MB cap, permission errors |
| TestSearchFiles | Shallow vs recursive, max_results, truncation flag |
| TestGetFileInfo | File vs listing, permissions, timestamps |
| TestDispatcher | All strategies, notifications, parse errors, unknown strategies |
| TestHTTPTransport | Well being endpoint, SSE connection, 400/404 error codes |
Run the check suite with pytest in verbose mode. To skip integration assessments, cross the not integration marker flag.
Connecting to a Native AI Consumer
macOS: ~/Library/Software Assist/Claude/local_desktop_config.json
Home windows: %APPDATApercentClaudelocal_desktop_config.json
{
"mcpServers": {
"local-desktop": {
"command": "python",
"args": ["C:/absolute/path/to/local-mcp-server/server.py"],
"env": {
"MCP_ROOT": "C:/absolute/path/to/your/workspace"
}
}
}
}
For HTTP/SSE:
# Terminal 1 — begin the server
python server.py --http --port 8765
# Terminal 2 — run the instance consumer
python examples/http_client.py
Sincere Design Choices
A pool of 16 employee threads is lots for native improvement, however I didn’t design this to scale right into a shared server dealing with tons of of simultaneous connections. Should you want that type of scale, it’s best to most likely swap this out for asyncio and a devoted async framework. For native AI tooling operating a handful of purchasers by yourself machine, 16 threads is greater than sufficient.
The safety mannequin trusts the sandbox boundary itself, fully ignoring file sorts. I didn’t write an allowlist of protected extensions or a blocklist of harmful ones. If a path resolves inside MCP_ROOT, it’s readable. One rule is more durable to get round than ten.
I additionally deliberately omitted token counting. This server merely returns uncooked file contents. Managing your token price range belongs within the execution layer between the server and the mannequin. Including a counter right here would power a tokenizer dependency—breaking the zero-dependency purpose—or power an approximation with its personal messy edge instances.
Lastly, search is shallow by default. A ten-minute hold throughout testing made this choice for me. Any conduct that silently destroys efficiency like that ought to by no means be the default choice.
What This Truly Teaches
I anticipated constructing an MCP server to be difficult. The tutorials made it look difficult. Each implementation I discovered had FastAPI, uvicorn, and three different packages earlier than a single software was registered. So I assumed that complexity was needed.
It wasn’t. After I lastly learn the precise spec, the protocol was a loop. Learn a line. Parse JSON. Name a operate. Write a line. That’s it. The frameworks weren’t fixing MCP issues — they had been fixing HTTP issues that MCP over stdio doesn’t have.
The usual library was sufficient as a result of the issue was small. I didn’t want a framework. I wanted http.server for TCP connections, threading for parallel requests, queue to decouple SSE from POST dealing with, and pathlib for path decision. One module per drawback. Nothing left over.
The factor that stunned me most was how a lot the defaults mattered. Each actual failure on this codebase — the ten-minute hold, the 200 MB freeze, the Home windows JSON corruption — got here from a default that labored high quality in testing and broke on an actual machine. rglob() was high quality on a small check folder. Textual content mode stdout was high quality on Linux. The default that feels handy in improvement is commonly the one which silently destroys issues in manufacturing.
Full code: https://github.com/Emmimal/local-mcp-server/
References
[1] Mannequin Context Protocol. (n.d.). Mannequin Context Protocol Specification. https://modelcontextprotocol.io
[2] JSON-RPC Working Group. (2010). JSON-RPC 2.0 Specification. https://www.jsonrpc.org/specification
[3] WHATWG. (n.d.). Server-sent occasions. HTML Dwelling Commonplace. https://html.spec.whatwg.org/multipage/server-sent-events.html
[4] Python Software program Basis. http.server — HTTP servers. Python 3 Documentation. https://docs.python.org/3/library/http.server.html
[5] Python Software program Basis. threading — Thread-based parallelism. Python 3 Documentation. https://docs.python.org/3/library/threading.html
[6] Python Software program Basis. pathlib — Object-oriented filesystem paths. Python 3 Documentation. https://docs.python.org/3/library/pathlib.html
[7] Python Software program Basis. queue — A synchronized queue class. Python 3 Documentation. https://docs.python.org/3/library/queue.html
[8] Python Software program Basis. msvcrt — Helpful routines from the MS VC++ runtime. Python 3 Documentation. https://docs.python.org/3/library/msvcrt.html
[9] Python Software program Basis. (n.d.). uuid — UUID objects in line with RFC 4122. Python 3 Documentation. https://docs.python.org/3/library/uuid.html
[10] Python Software program Basis. subprocess — Subprocess administration. Python 3 Documentation. https://docs.python.org/3/library/subprocess.html
Disclosure
All code on this article was written by me and is unique work, developed and examined on Python 3.12.6, Home windows 11, CPU solely. No GPU was used at any stage. All benchmark numbers — response occasions, concurrent consumer outcomes, check counts — are from precise runs on my native machine and are totally reproducible by cloning the repository and operating demo.py and concurrent_demo.py as described above. The complete implementation makes use of solely the Python customary library. No third-party packages are required or used at any level. All structure choices, implementation selections, design tradeoffs, debugging experiences, and the failures described in “What Broke Throughout Improvement” are my very own. I’ve no monetary relationship with any software, library, framework, or firm talked about on this article. The MCP protocol is an open specification printed by Anthropic [1]; this implementation is unbiased and isn’t affiliated with or endorsed by Anthropic.
Should you construct manufacturing AI methods and need to go deeper — tutorials, studying tracks, and hands-on tasks at EmiTechLogic, my AI and Python studying platform.

