local-mcp-simple-server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@local-mcp-simple-serverUse read_source_file to read examples/source/CustomerService.vb and explain its validation behavior"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
local-mcp-simple-server
A deliberately small MCP server that isolates one complete interaction:
one selected file → one MCP tool call → bounded source text → Copilot explains itIt exposes exactly one tool, read_source_file. There is no inventory, no
search, no Git integration, no model client, and no second tool. This is
the MCP counterpart to local-llm-simple-harness: it exists so the
mechanics of one MCP round trip are easy to see end-to-end, before
looking at a larger system (modernization-evidence-harness) that builds
inventory, search, excerpts, provenance, and artifact-reference validation
on top of the same basic shape.
This is an independent learning project. It does not import, depend on, or require either the simple LLM harness or the evidence harness.
What this is not
Not a sandbox. The containment checks in
server.js(resolving..-free relative paths, then following symlinks withfs.realpathto confirm the real file is still under the configured root) are ordinary application-level checks, not an OS-level security boundary. They stop a well-formed MCP client from reading outside the configured root; they do not defend against a hostile process racing the filesystem underneath this one (a file swapped out between the containment check and the read that follows). Run this server with the same trust you'd give any local process that reads files on your behalf.Not a text-content guarantee. "UTF-8 text file" is enforced by two cheap, honest checks: reject any file containing a NUL byte, then require the remaining bytes to decode as strict UTF-8 (
TextDecoderwithfatal: true). This reliably rejects binary formats (images, archives, compiled binaries) but cannot prove a file is meaningful source code — a valid UTF-8 file full of nonsense still passes, and in principle a binary format could (rarely) decode as valid UTF-8 too. Good enough to keep this tool from handing binary garbage to an LLM; not a general file-type classifier.Not versioned or attributed. The tool reads whatever is on disk right now. There is no Git subprocess, no commit pinning, no hash, and no provenance record — if you need "prove this exact text existed at this exact revision," that's what the larger evidence harness is for.
Related MCP server: Project Files Read-only MCP
Requirements
Node.js 18 or newer, native ESM.
npm (a lockfile is committed; use
npm cifor a reproducible install).
Setup
cd local-mcp-simple-server
npm ciConfigure the one thing this server needs — an absolute path to the
directory it's allowed to read from — via SIMPLE_MCP_SOURCE_ROOT. The
tool caller (Copilot) never sees or changes this value; it is fixed for
the life of the server process.
Run it directly to confirm it starts (single-line PowerShell, no heredocs):
$env:SIMPLE_MCP_SOURCE_ROOT = "C:\path\to\local-mcp-simple-server\examples\source"; node server.jsWhy this appears to hang
server.js prints a one-line "ready" banner to stderr and then waits.
That's correct, not broken: a stdio MCP server has no interactive prompt —
it is a subprocess meant to be launched and driven by an MCP client (VS
Code / Copilot, or the test client in this repo) that writes JSON-RPC
requests to its stdin and reads responses from its stdout. Run standalone
in a terminal, it will sit there silently (aside from that one stderr
line) until something speaks the protocol to it, or until you close its
stdin (Ctrl+C, or closing the terminal) to stop it.
VS Code / Copilot setup (Windows)
Copy
examples/vscode.mcp.jsoninto your workspace's.vscode/mcp.json(single-line PowerShell):Copy-Item "examples\vscode.mcp.json" ".vscode\mcp.json" -ForceEdit the copied file so
SIMPLE_MCP_SOURCE_ROOTpoints at an absolute path on your machine (the checked-in example uses${workspaceFolder}/examples/source, which VS Code expands for you, so this step may already be correct if you keep the same layout).Open the Command Palette and run MCP: List Servers, confirm
local-mcp-simple-serveris listed, and start it from there if it isn't already running.Open Copilot Chat in Agent mode. Ask it to use the
read_source_filetool (name it explicitly) to readexamples/source/CustomerService.vband explain its validation behavior.Check the tool-call record before trusting the answer. Copilot's chat UI shows which tool actually ran for a turn. Confirm it says
read_source_filefromlocal-mcp-simple-server— not a built-in workspace file read. A built-in read can produce a similar-looking explanation without ever going through this server, which would silently defeat the point of the smoke test.
Verified 2026-09-21 against live VS Code Copilot Chat (Agent mode,
Windows) reading this repo's own CustomerService.vb fixture. The MCP
output log showed the server start under VS Code's MCP client
(Discovered 1 tools), and the chat transcript's tool-call record showed
tool: "mcp_local-mcp-sim_read_source_file" with
args: {"path":"CustomerService.vb"} — the mcp_-prefixed tool name
confirms the call went through this server rather than a built-in
workspace file read. The raw response was
{"path":"CustomerService.vb","text":"..."}, the complete file text
untouched (CRLF preserved), and Copilot's explanation of the validation
logic (name non-blank; email needs exactly one interior @; phone reduces
to digit characters via Char.IsDigit and requires exactly 10, correctly
noting this includes Unicode digits, not just ASCII) matched the actual
code.
What the SDK handles vs. what this code handles
Concern | Handled by |
JSON-RPC framing, request/response correlation |
|
Tool discovery ( | SDK, from the config passed to |
Argument shape validation (missing/wrong-typed | SDK, via the Zod |
Output shape validation | SDK, via the Zod |
Path safety, root containment, symlink-escape checks | This code ( |
File-size enforcement, streaming read | This code |
UTF-8 text policy | This code |
Turning a failure into a non-crashing tool result | This code ( |
We never hand-write JSON-RPC or MCP framing — the SDK owns the wire format entirely. Our job is exactly the part inside the tool callback: turn one validated path into either a result or a clear, catchable error.
Why no model client lives in this server
Copilot supplies its own reasoning; this server supplies bytes. VS Code's Copilot already has a configured chat model and calls this MCP server as a tool mid-conversation — the server's job ends the moment it returns text. There is deliberately no API key, no model SDK, and no prompt construction here: adding one would blur exactly the boundary this milestone exists to make visible (server = context, Copilot = reasoning).
Where the text you return actually goes
Whatever read_source_file returns is inserted into Copilot's ongoing
chat context and sent to whatever model backs your Copilot subscription
and its currently configured processing environment/region — the same as
if you'd pasted the file into the chat yourself. Only put synthetic,
non-sensitive files under a directory you point SIMPLE_MCP_SOURCE_ROOT
at for real use; the fixtures in this repo (examples/source/) are
intentionally synthetic for exactly this reason.
How this maps onto the larger evidence harness
modernization-evidence-harness performs the same kind of step — hand a
bounded slice of source to an LLM — but wraps it with repository inventory
(which files exist), search (which file is relevant), excerpting (which
lines matter), and provenance (proving what revision the text came from).
This server is that one step by itself: given a path someone already
picked, read it safely and hand back the whole thing. If you can explain
how a call reaches server.js's tool callback and how its result gets
back to Copilot, you've understood the piece the bigger harness builds on.
Function-by-function walkthrough of one call
Given a request to read CustomerService.vb:
VS Code / Copilot sends a
tools/callJSON-RPC request over the server's stdin. The MCP SDK'sStdioServerTransportreads and parses it; this code never touches raw stdin.McpServer(SDK) looks up the registeredread_source_filetool, validates{ path: "CustomerService.vb" }against the ZodinputSchema, and — only if that passes — invokes the callback registered inregisterReadSourceFileTool.readSourceFile(sourceRootReal, rawPath)is the composition of the actual policy, in order:normalizeRelativePath(rawPath)— pure, no filesystem access. Splits on/or\, rejects an absolute-looking path, and rejects any..segment. Returns a normalized, forward-slash relative path.resolveRealPathWithinRoot(sourceRootReal, normalizedPath)— joins the path onto the root, callsfs.realpath(which follows every symlink/junction in the chain), and confirms the real result is still inside the real root viapath.relative.assertRegularFile(realPath, normalizedPath)—fs.statand rejects anything that isn't a plain file (a directory, in particular).readBoundedFile(realPath)— streams the file in 16 KiB chunks, throwing the moment the running total passes 64 KiB, so an oversized file is never fully buffered before being rejected.decodeUtf8Text(buffer, normalizedPath)— rejects a NUL byte, then strictly decodes the rest as UTF-8.
On success, the callback returns an ordinary object —
{ structuredContent: { path, text }, content: [{ type: "text", ... }] }— straight back toMcpServer, which validates it against theoutputSchemaand serializes it as thetools/callresponse.On any failure at any step above, the callback's
catchcallstoToolErrorResult(error), which returns{ isError: true, content: [...] }instead of throwing. This is what satisfies "return understandable tool errors without terminating the server": the exception never reaches the SDK or the transport, so the process and the stdio connection to it both stay alive for the next call.StdioServerTransport(SDK) serializes that result and writes it to stdout; Copilot reads it back as the tool's output and continues the conversation with it in context.
Testing
npm testUses Node's built-in test runner (node --test) against
test/server.test.js. That file starts one real server subprocess via the
SDK's StdioClientTransport/Client — exactly the way a real MCP client
would — and reuses it across the whole suite (bounded by
--test-timeout=15000; the client and its fixtures are torn down in an
after hook regardless of pass/fail).
Covered:
discovery lists exactly
read_source_file;a synthetic fixture round-trips its expected relative path and text;
malformed arguments (missing
path, wrong type);missing file, a directory instead of a file, an absolute path,
..traversal;an oversized file is rejected, not truncated (
structuredContentis absent on that result);unsupported (binary) content is rejected;
a symlink escaping the source root is rejected — skipped with an explanation if the test environment can't create a symlink (e.g. Windows without permission to create one), rather than failing;
the server answers a normal call correctly after several failed calls;
the server's stderr banner is captured and asserted on directly, and every other test having round-tripped valid JSON-RPC over stdout is itself evidence that no stray diagnostic corrupted the protocol stream.
Known acceptance gaps
Symlink/junction-escape rejection is exercised on Linux in this environment; Windows junction behavior relies on Node's
fs.realpathresolving junctions the same way it resolves symlinks, which is documented Node behavior but has not been separately verified on Windows here.
Non-goals (out of scope for this milestone, on purpose)
Repository inventory or search, multiple source roots, Git subprocesses or
revision pinning, evidence catalogs or artifact validation, a model
client or API key, MCP sampling/prompts/resources/HTTP transport/auth,
file writes, shell tools, database storage, a UI, Docker/deployment, or
any plugin/agent/extensibility scaffolding. All of that belongs to
modernization-evidence-harness, not here.
Available Tools
1 toolread_source_fileRead source fileA
Reads one UTF-8 text file from the server's configured source root and returns its relative path and complete text. Files over 64 KiB are rejected, not truncated. The caller cannot choose or change the root; only a path relative to it.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | Path to the file, relative to the configured source root, e.g. "CustomerService.vb". |
Output Schema
| Name | Required | Description |
|---|---|---|
| path | Yes | |
| text | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the behavioral disclosure burden. It proactively states the 64 KiB rejection policy, the UTF-8 requirement, and the immutability of the source root, which are meaningful behavioral details beyond what the schema provides.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, each carrying distinct information: what is read, the file-size policy, and the root constraint. The core action and return value are front-loaded with no filler or repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple single-parameter read tool with an output schema present, the description covers the essential behaviors: path semantics, root restriction, size limit, and return content. Nothing an agent needs to invoke it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema already documents the 'path' parameter with a clear relative-to-root description and an example. The description reinforces the relative-root constraint but does not materially add new parameter-specific meaning beyond the schema, so the baseline of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly identifies the operation ('Reads one UTF-8 text file'), the resource ('server's configured source root'), and the return value ('relative path and complete text'). It is specific and unambiguous even without sibling tools to distinguish from.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives clear usage constraints: the path must be relative to the configured root, the root cannot be changed, and files over 64 KiB are rejected. It does not explicitly name alternatives, but no siblings exist, so the context is sufficient for correct selection.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
1 tool update
v1.0.0- First observed
read_source_file
TDQS
Scored across 1 tool
Only one tool exists, so there is no possibility of confusion or overlap. Every call is clearly directed to the single available operation.
The tool name 'read_source_file' follows a clear verb_noun pattern, which is consistent even though it is the only tool. The naming is descriptive and predictable.
A single tool feels thin for a server, but the purpose is explicitly simple (reading one file at a time), so it borders on acceptable. It would benefit from at least one companion tool like list_source_files.
The server only supports reading one file and offers no listing, writing, or directory traversal capabilities. Agents must know exact relative paths in advance, making workflows fragile and incomplete.
Maintenance
Related MCP Connectors
Manage files and folders directly from your workspace. Read and write files, list directories, cre…
Browse and manage files in your Moxt AI workspace from any MCP client.
Safe folder access for ChatGPT and Claude: read, write and search files, risky tools opt-in.
11Securely search and manage workspace context files for AI agents and teams.
Related MCP Servers
- FlicenseNot gradedqualityBmaintenanceEnables local ChatGPT/OpenAI MCP clients to read files and search within explicitly authorized directories using read-only, policy-constrained tools.1-
- AlicenseNot gradedqualityCmaintenanceEnables secure, read-only access to local project files (including text, DOCX, PDF, and XLSX) through MCP, with strict directory whitelisting and no write, edit, or command-execution tools.1MIT

PC FileBridgeofficial
AlicenseNot gradedqualityAmaintenanceEnables ChatGPT, Codex, and compatible MCP clients to read bounded UTF-8 text and create files or directories in operator-selected folders, with strict protections against overwrites, deletes, and path escapes.Apache 2.0- FlicenseNot gradedqualityBmaintenanceEnables AI assistants to safely read and search project files within a configured root directory, supporting regex search, file listing, line-range reading, and symbol usage lookup.-