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.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。