A local Model Context Protocol (MCP) file server for ChatGPT. It lets ChatGPT read and edit files inside a workspace you choose. Broader raw file access is disabled by default and guarded by explicit environment variables.
This repository is source-only. It includes a Windows STDIO tunnel launcher, but it does not include generated build output, downloaded tunnel-client binaries, API keys, tunnel IDs, local tokens, or saved credential files.
The server supports two local MCP transports:
- HTTP streamable MCP at
http://127.0.0.1:3333/mcp - STDIO MCP through
node dist/src/stdio.js
For ChatGPT web, use a Secure MCP Tunnel. Create or manage tunnels from Platform tunnel settings, then connect the tunnel from ChatGPT Settings -> Plugins or chatgpt.com/plugins.
- Node.js 20 or newer
- An OpenAI API key from the API keys page, with permission to use the target tunnel
- A Secure MCP Tunnel ID from Platform tunnel settings
start-tunnel.exefrom the OpenAI tunnel-client releases, placed in the project root for the Windows launcher to copy locally, or a separately installedtunnel-clienton PATH- Optional: ripgrep for faster search
Keep the API key, tunnel ID, local .env, generated keys.bat, and
tunnel-client binary outside source control.
npm install
npm run buildThe build output is written to dist/. It is generated locally and ignored by
Git.
Create a local .env from the example:
Copy-Item .env.example .envAt minimum, set the workspace folder ChatGPT may read and edit:
WORKSPACE_ROOT=C:\Users\YourName\Documents\ChatGPT-editableFor HTTP tunnel mode, also set a local bearer token. This is your own local secret, not an OpenAI-provided value:
MCP_LOCAL_TOKEN=replace-with-a-long-random-local-tokennpm run doctorThe doctor script checks the configured workspace, backup directory, raw read/write flags, search settings, and write-safety settings.
STDIO mode is the simplest tunnel path because it does not require starting the local HTTP server or configuring an HTTP bearer token.
ChatGPT web -> Secure MCP Tunnel -> tunnel-client -> node dist/src/stdio.js
On Windows, the included launcher can prompt for the API key and tunnel ID,
build the project if needed, and start tunnel-client:
& ".\Run Tunnel Client STDIO.bat"The launcher writes saved local credentials to keys.bat. That file is ignored
by Git and must not be committed.
You can also run the same STDIO tunnel command manually from this project root
after npm run build:
$env:CONTROL_PLANE_API_KEY="your-api-key"
$env:CONTROL_PLANE_TUNNEL_ID="your-tunnel-id"
tunnel-client run --control-plane.api-key "env:CONTROL_PLANE_API_KEY" --control-plane.tunnel-id "$env:CONTROL_PLANE_TUNNEL_ID" --mcp.command "command=node dist/src/stdio.js,channel=main"The STDIO server still reads this project's local .env, so WORKSPACE_ROOT,
named workspaces, backups, search settings, and raw access flags still apply.
Use HTTP mode if you specifically want the local HTTP MCP endpoint:
ChatGPT web -> Secure MCP Tunnel -> tunnel-client -> http://127.0.0.1:3333/mcp
Start the local server:
npm run start:httpIt listens on:
http://127.0.0.1:3333/mcp
Health/status is available at:
http://127.0.0.1:3333/health
In a second terminal, from this project root:
$env:CONTROL_PLANE_API_KEY="your-api-key"
$env:CONTROL_PLANE_TUNNEL_ID="your-tunnel-id"
$env:MCP_LOCAL_TOKEN="replace-with-the-same-token-from-dotenv"
tunnel-client run --control-plane.api-key "env:CONTROL_PLANE_API_KEY" --control-plane.tunnel-id "$env:CONTROL_PLANE_TUNNEL_ID" --mcp.server-url "http://127.0.0.1:3333/mcp" --mcp.extra-headers "Authorization: Bearer env:MCP_LOCAL_TOKEN"MCP_LOCAL_TOKEN must match the value in your local .env file so
tunnel-client can authenticate to the local HTTP server.
npm run build
npm run start:http
npm run start:stdio
npm run doctorWorkspace-safe tools are enabled by default:
get_file_tool_status
set_workspace_root
switch_workspace
list_workspace_dir
search_workspace
read_workspace_file
read_workspace_files
list_workspace_tree
write_workspace_file
replace_in_file
append_to_file
insert_after
stat_any_path
cleanup_backups
Terminal/development tools are also available:
run_command
run_windows_command
run_commands
check_project
start_process
read_process
write_process
stop_process
list_processes
run_command and every entry in run_commands retain a bounded amount of
ANSI-stripped stdout and stderr so huge test/build logs do not become huge MCP
responses. The default is 65,536 bytes per stream and can be changed with:
COMMAND_MAX_OUTPUT_BYTES=65536When output exceeds the limit, the buffer preserves the beginning and the tail.
With the 64 KiB default it keeps about 8 KiB from the beginning and 48 KiB from
the end, leaving room for the truncation marker while keeping the entire returned
stream below the configured cap. Results include stdout_truncated /
stderr_truncated plus total, retained, and omitted byte counts. A per-command
max_output_bytes argument can override the environment default.
run_command and run_commands use cross-spawn and take an executable plus an
argument array. Call tools such as npm, npx, pnpm, git, node, and
python directly instead of wrapping them in PowerShell, cmd.exe, or another
shell. The executor uses shell: false; on Windows, cross-spawn handles
PATHEXT, shebangs, and .cmd/.bat shims internally. For example:
{
"command": "npm",
"args": ["test"],
"cwd": ".",
"timeout_ms": 120000
}Command working directories must stay inside the active workspace. The active
workspace's node_modules/.bin is prepended to PATH for command execution.
On Windows, run_windows_command is available for commands that intentionally
need cmd.exe, including .bat/.cmd scripts, CMD built-ins, command chaining,
pipes, and redirection. For example, with a workspace containing Build.bat:
{
"command_line": "Build.bat --no-pause",
"cwd": ".",
"timeout_ms": 120000
}The Windows launcher rebuilds the TypeScript project every time it starts, so a
previously generated dist/ directory cannot leave ChatGPT connected to a stale
tool schema after source changes.
Terminal stdout and stderr are ANSI-sanitized before buffering and before they are
returned through MCP. Color/style sequences such as ESC[31m, OSC hyperlinks/title
sequences, and related terminal control strings are removed. The sanitizer is
streaming, so an escape sequence split across multiple process-output chunks is still
removed cleanly. This also means ANSI bytes do not consume the configured output
buffer limits.
After editing code, check_project can detect configured verification and run it
in one MCP round trip instead of first discovering scripts and then issuing separate
terminal calls. Detection is intentionally conservative:
- Node.js: runs only package scripts that actually exist:
test, a known typecheck script (typecheck,type-check,check-types, orcheck:types), andbuild. The package manager comes frompackageManageror the project lockfile (pnpm, Yarn, Bun, or npm). - Python: runs
pytest, Ruff, and/or mypy only when their configuration or dependency is detected. A project.venv/venvPython is preferred when present. - Rust:
Cargo.tomlenablescargo checkandcargo test. - Go:
go.modenablesgo test ./....
Example:
{
"cwd": ".",
"mode": "sequential",
"stop_on_error": false
}The default is to continue through all detected checks so a single response reports
as many failures as possible. Set stop_on_error=true for fail-fast sequential
verification, or mode="parallel" when the project's checks are known to be safe
to run concurrently. dry_run=true returns the detected plan without launching
anything. Per-check stdout and stderr default to 8 KiB each, using the same
ANSI-stripped head+tail buffering as run_command, so the compound response remains
compact while retaining useful failure context.
When several commands are known up front, use run_commands so ChatGPT sends one
MCP request and receives one combined result instead of paying a round trip per
command. Sequential mode is intended for dependent verification pipelines:
{
"commands": [
{ "command": "git", "args": ["status", "--short"] },
{ "command": "npm", "args": ["test"] },
{ "command": "npm", "args": ["run", "build"] }
],
"mode": "sequential",
"stop_on_error": true
}With stop_on_error=true, sequential execution stops at the first failed command
and reports the remaining commands as skipped. Use parallel mode for independent
diagnostics that can safely run at the same time:
{
"commands": [
{ "command": "node", "args": ["--version"] },
{ "command": "git", "args": ["status", "--short"] },
{ "command": "npm", "args": ["--version"] },
{ "command": "python", "args": ["--version"] }
],
"mode": "parallel",
"stop_on_error": false
}Parallel mode starts all commands concurrently. Because they are already running,
stop_on_error cannot cancel sibling commands in parallel mode. Results retain
the same indexes/order as the input command list and include batch counts, timing,
first-failure index, and skipped-command information.
start_process is intended for long-running development servers, watchers, and
interactive stdin/stdout workflows. The child stays alive inside the running MCP
server and is tracked in an in-memory process map. Start it once and keep the
returned process_id while editing files:
{
"command": "npm",
"args": ["run", "dev"],
"cwd": "."
}The response includes process_id, pid, status, command/args, and unread
stdout/stderr byte counts. Output is ANSI-cleaned and kept in bounded in-memory
buffers. read_process consumes only output that has arrived since previous
reads:
{
"process_id": "proc_0123456789ab",
"wait_ms": 5000
}wait_ms is optional (maximum 30 seconds). When there is no unread output yet,
it can wait for the next stdout/stderr chunk or process exit, which avoids rapid
MCP polling while a watcher rebuilds. If more output remains than one read can
return, hasMoreOutput is true.
Use write_process to send stdin without restarting the process, and
stop_process to terminate it. stop_process first requests a graceful process-
tree shutdown, waits up to grace_ms (3 seconds by default), then force-kills the
tree if force=true and it is still alive. list_processes reports both active
and exited managed processes; pass include_exited=false to show only active
ones.
Terminal security: workspace-relative
cwdvalidation is not a command sandbox. A program launched by these tools runs with the same operating-system permissions as the MCP server and can access paths outsideWORKSPACE_ROOT. Treat terminal access as full command execution on the host account.
Raw local-file tools are disabled unless explicitly enabled:
list_any_dir
read_any_file
write_any_file
Enable raw reads only if you understand the privacy risk:
ALLOW_RAW_READ_ANY_FILE=I_UNDERSTAND_THIS_CAN_READ_PRIVATE_FILESEnable raw writes only if you understand the data-loss risk:
ALLOW_RAW_WRITE_ANY_FILE=I_UNDERSTAND_THIS_CAN_DESTROY_FILESRaw write access also enables raw reads for backwards compatibility.
Search uses ripgrep when available:
SEARCH_USE_RIPGREP=true
SEARCH_USE_RIPGREP_FILES=true
SEARCH_INDEX_CACHE_TTL_MS=300000
SEARCH_CONCURRENCY=16If the server cannot find rg on PATH, set:
RIPGREP_PATH=C:\full\path\to\rg.exeWrites create backups and compute before/after SHA-256 hashes by default:
WRITE_CREATE_BACKUP_DEFAULT=true
WRITE_COMPUTE_SHA256=trueBackups and audit logs default to:
CHATGPT_FILE_MCP_HOME=~/.chatgpt-local-file-mcpDo not commit local secrets, generated output, dependencies, or downloaded binaries. These are ignored by Git:
.env
.env.*
.mcp-local-token
.mcp-server-port
keys.bat
keys.txt
*.key
*.pem
*.p12
*.pfx
tunnel-client.exe
start-tunnel.exe
dist/
node_modules/
*.tgz
*.zip
If an API key, local token, or tunnel credential is ever committed, pasted into an issue, shared in logs, or exposed in a terminal transcript, revoke it and create a new one.