local-code-mcp
A security-first MCP server that enables clients to inspect and edit multiple local repositories via configured aliases, with read-only Git access and allowlisted test commands.
README
local-code-mcp
A security-first Model Context Protocol (MCP) server for inspecting and editing multiple local repositories over STDIO. Repository roots are configured on the server and exposed to clients only through explicit project aliases.
Tiếng Việt · WSL + ChatGPT Secure MCP Tunnel runbook
Design goals
- No arbitrary shell tool.
- No client-supplied repository roots.
- No mutable “current project” state.
- Independent path sandboxing for every configured project.
- Read-only Git access.
- Atomic text-file writes with secret and path protections.
- Project-specific, allowlisted test commands executed with
spawn()andshell: false.
Requirements
- Node.js 18 or newer
- npm
- Git for the Git tools
- Optional: ripgrep for faster
search_code; the server has a built-in literal-search fallback whenrgis unavailable
End-to-end flow
From a clean checkout to a client that can call tools:
| Step | Command | Result |
|---|---|---|
| 1. Install and verify | npm install && npm run typecheck && npm test && npm run build |
dist/index.js exists |
| 2. Configure roots | copy start-mcp.example.sh to start-mcp.sh, edit PROJECTS_CONFIG |
aliases mapped to absolute roots |
| 3. Smoke-test locally | ./start-mcp.sh |
Configured projects: … on stderr, then it waits for JSON-RPC; stop with Ctrl+C |
| 4a. Use from Codex CLI | codex mcp add local-code -- /absolute/path/to/start-mcp.sh |
tools available in Codex |
| 4b. Use from ChatGPT | tunnel-client run --profile <profile> |
tools available in a developer-mode app — full setup in the Secure MCP Tunnel runbook |
| 5. Verify from the client | list_projects → list_files → write_file to a throwaway path, then delete it |
reads and writes reach the right repository |
| 6. After changing this server | npm run typecheck && npm test && npm run build, then restart whatever spawns dist/index.js |
clients pick up the new build |
Step 6 matters: tunnel-client and Codex spawn dist/index.js once and keep that process, so a rebuild alone changes nothing until the parent is restarted.
For ChatGPT specifically, the runbook covers the parts this README does not: installing tunnel-client, creating the tunnel and runtime key, initializing and running the profile, the developer-mode app, verification, and troubleshooting.
Install and verify
npm install
npm run typecheck
npm test
npm run build
The build output is dist/. The server entry point is dist/index.js.
Configure projects
Set PROJECTS_CONFIG to a non-empty JSON object whose keys are aliases and whose values are absolute repository paths:
export PROJECTS_CONFIG='{
"project-a": "/home/example/projects/project-a",
"project-b": "/home/example/projects/project-b",
"project-c": "/home/example/projects/project-c"
}'
At startup, the server:
- accepts aliases matching
[a-zA-Z0-9_-]+; - requires every root to be an existing absolute directory;
- canonicalizes every root with
realpath; - rejects duplicate canonical roots; and
- exits before accepting requests if the configuration is missing or invalid.
Only aliases are logged or returned to clients. Clients cannot submit raw repository roots.
Run
node dist/index.js
This is a STDIO server. It normally waits silently for JSON-RPC input; operational logs go to stderr, while stdout is reserved for MCP traffic.
For Codex CLI, copy start-mcp.example.sh to an untracked start-mcp.sh, replace the example paths, make it executable, and register it:
chmod 0755 /absolute/path/to/local-code-mcp/start-mcp.sh
codex mcp add local-code -- /absolute/path/to/local-code-mcp/start-mcp.sh
codex mcp list
To connect this private STDIO server to ChatGPT, follow the Secure MCP Tunnel runbook. In short: tunnel-client init --profile <profile> …, tunnel-client doctor --profile <profile> --explain, then keep tunnel-client run --profile <profile> in the foreground while ChatGPT discovers and calls tools.
After changing server code
npm run typecheck && npm test && npm run build
Then restart the process that spawns dist/index.js — tunnel-client run (Ctrl+C, start it again) or the Codex session. Until that restart, clients keep talking to the previously loaded build.
Tool reference
Except for list_projects, every tool requires a project alias.
| Tool | Inputs | Behavior and limits |
|---|---|---|
list_projects |
none | Returns configured aliases only. |
list_files |
project, optional path, optional max_depth |
Lists from path (default .). Depth defaults to 4 and is capped at 20. Output is capped at 10,000 entries. Skips symlinks and every denylisted path, so blocked names are not disclosed. |
read_file |
project, path, optional start_line, optional end_line |
Reads UTF-8 text with numbered lines. Rejects binary files and files larger than 2,000,000 bytes. Response text is capped at 500,000 characters. |
search_code |
project, query, optional path, optional file_pattern, optional max_results |
Performs a literal search from path (default .). query and file_pattern are limited to 500 characters and cannot contain NUL or newlines. The requested match limit defaults to 100 and is capped at 1,000, and means the same total number of matching lines on both the rg path and the built-in fallback. Hits are reported relative to the project root, and denylisted files are excluded on both paths. Response text is capped at 500,000 characters. |
write_file |
project, path, content |
Creates or replaces one UTF-8 text file, up to 2,000,000 bytes, using a same-directory temporary file and atomic rename. Replaced files keep their previous mode and ownership; new files are created 0644. The parent directory must already exist. |
git_status |
project |
Runs git status --short in the selected root. |
git_diff |
project, optional staged |
Runs git diff, or git diff --staged when staged is true. |
git_log |
project, optional limit |
Runs git log --oneline; the default limit is 20 and the maximum is 50. |
run_tests |
project, test, optional args |
Runs only a server-defined command for the selected alias. Timeout: 120 seconds. Combined stdout/stderr capture is capped independently at 500,000 characters. |
Test allowlist
The allowlist is the TEST_COMMANDS map in src/git-tools.ts. Each entry is a fixed argv, never a shell string, and deployments edit the map for their own aliases:
| Project alias | Test name | Command |
|---|---|---|
project-a |
unit |
deployment-defined argv |
project-b |
unit |
deployment-defined argv |
project-c |
test |
deployment-defined argv |
Other aliases can use repository and Git tools, but run_tests returns COMMAND_NOT_ALLOWED until a command is explicitly added for that alias. Test arguments are passed as an argv array, limited to 20 strings, and reject shell metacharacters, newlines, absolute paths, and .. path components.
Security model
Path containment
Every repository path is resolved relative to the selected canonical root. Lexical traversal, absolute paths outside the root, and symlink escapes are rejected with PATH_OUTSIDE_PROJECT_ROOT. Directory listings do not follow symlinks.
Blocked content
Direct paths passed to read_file and write_file reject these names anywhere in the relative path:
- directories:
.git,.ssh,.aws,.gnupg,node_modules, andvendor; - files:
.env,.env.*,*.pem,*.key,*.p12,*.pfx,id_rsa,id_ed25519, andauthorized_keys.
.git is blocked in full rather than only .git/objects: git executes the values of core.fsmonitor, core.pager, and aliases from .git/config, and runs .git/hooks/*, so a writable .git would turn this server's own Git tools into arbitrary command execution.
The denylist is applied by read_file, write_file, list_files, and both search_code implementations, so blocked files are neither read, written, listed, nor matched. It is still a last line of defense, not a substitute for choosing narrow project roots and running the process with least privilege.
Commands and Git
All child processes use spawn(command, args, { shell: false }) with a fixed command and argument structure. There is no generic command-execution tool. Git exposure is intentionally limited to status, diff, and log; the server does not expose commit, push, reset, clean, checkout, merge, or rebase operations.
Child processes do not inherit the server environment. They receive only PATH, HOME, LANG, LC_ALL, TZ, TERM, USER, and LOGNAME, so PROJECTS_CONFIG and any tunnel credentials stay in the server process. A deployment that needs another variable (for example COMPOSER_HOME) lists its name in MCP_CHILD_ENV_ALLOWLIST, which can never re-add PROJECTS_CONFIG.
Writes
write_file validates the destination before writing, refuses blocked paths and symlink escapes, writes a mode-0600 temporary file in the destination directory, and atomically renames it into place. Because rename replaces the inode, the mode and ownership of the file being replaced are restored explicitly before the rename; otherwise an edit would narrow an existing 0644 repository file to 0600 and break the runtime user that serves it. New files are created 0644. The file and its directory are synced so the replacement survives power loss, not only a process crash. It does not create parent directories. Filesystem errors are mapped to safe error codes without returning stack traces.
Logging
Each tool call writes one JSON record to stderr containing timestamp, alias, tool, the requested path (or .), success status, and duration; failures also carry the returned code and an operator-only detail field. File contents, configured roots, and environment values are not included. Because rejected client input is still logged, operators must protect stderr logs appropriately.
Error codes
Expected failures include:
PROJECT_CONFIG_INVALID,PROJECT_ALIAS_INVALID,PROJECT_ROOT_INVALIDPROJECT_NOT_FOUND,INVALID_ARGUMENT,UNKNOWN_TOOLPATH_OUTSIDE_PROJECT_ROOT,BLOCKED_FILEFILE_TOO_LARGE,BINARY_FILECOMMAND_NOT_ALLOWED,COMMAND_FAILED,COMMAND_TIMEOUTWRITE_PERMISSION_DENIED,WRITE_FAILEDINTERNAL_ERRORfor anything unmapped
MCP tool failures are returned with isError: true and a text payload in the form CODE: message. Only the codes above are ever returned: an unmapped failure becomes INTERNAL_ERROR: Tool execution failed, because a raw errno such as EACCES carries a Node message containing server-side absolute paths, and reporting an internal fault as INVALID_ARGUMENT sends the client into retrying arguments that were never wrong. Messages are redacted of configured roots and truncated to 2,000 characters; the full cause is written to stderr only.
Development
npm run typecheck
npm test
npm run build
npm run typecheck checks src/ and tests/ against tsconfig.json; npm run build emits dist/ from tsconfig.build.json.
Add or update tests whenever path handling, blocked content, command execution, writes, or project isolation changes. Keep Git access read-only and never add a generic shell interface.
Files
src/index.ts— MCP tool registration, dispatch, and client-safe error mappingsrc/config.ts— project-alias configurationsrc/security.ts— root containment, blocked-path policy, root redactionsrc/fs-tools.ts—list_files,read_file,search_codesrc/git-tools.ts— read-only Git tools and theTEST_COMMANDSallowlistsrc/write-file.ts— validated atomic writessrc/command.ts— boundedspawn()wrapper and child-environment allowlistsrc/errors.ts— error-code allowlist and redactionsrc/tool-input.ts,src/limits.ts— argument validation and shared boundssrc/logger.ts— structuredstderraudit recordstests/— security, write, filesystem-tool, error-mapping, command, and isolation testsstart-mcp.example.sh— local launcher template
Handling secrets
Never commit API keys, .env files, tunnel credentials, local tunnel profiles, private certificates, or a machine-specific start-mcp.sh. Review repository changes before committing.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.