Skip to main content
Glama

local-terminal-mcp

A small, security-first MCP server that lets an AI assistant — ChatGPT, Claude Desktop, Claude Code, or any other MCP client — run a set of allowlisted commands and read files from a local directory on your machine.

The motivating use case: point ChatGPT (or Claude) at a local codebase so it can run git, rg, cat, etc. and analyse the repo — without copying files around by hand, and, for ChatGPT, using your flat-rate subscription instead of metered API/agent tokens.

⚠️ This runs commands on your computer. It is built to be safe by default (read-only, allowlisted, no shell, path-contained, auth-required over the network), but you are responsible for how you configure and expose it. Read Security model before exposing it to the internet.


Table of contents


Related MCP server: ChampCity GPT MCP Launcher

Why this exists

Running a coding agent against a large repo through a metered API burns tokens fast. If you already pay for a ChatGPT or Claude subscription, you can instead give the assistant a tool to read files and run read-only commands on your machine, and let it analyse the code conversationally. That is what this server provides, with a security model strong enough that you can leave write access turned off and expose only read access.

What you can do with it

  • Work on your codebase from ChatGPT without spending Codex/API tokens. Let ChatGPT run git, rg, cat, find, etc. to read, search and reason about a local repo — billed to your flat ChatGPT subscription instead of metered agent/API usage.

  • Run local generators from ChatGPT — images, sprites, assets — unmetered. The server runs any program you allowlist. Allowlist your own generation CLI or script and ChatGPT can trigger it locally, as many times as you like, without using ChatGPT's built-in image quota. For example, to let ChatGPT drive a local sprite/image script:

    local-terminal-mcp --transport http --port 3003 \
      --root /path/to/assets-project \
      --auth path --mcp-path "/mcp/$SECRET" \
      --allow-commands "python,node,convert,aseprite" \
      --allow-write

    Then ask ChatGPT to call run_command with e.g. python gen_sprite.py --seed 42 --out sprites/hero.png, and read_file / list_directory to inspect the results. (--allow-write is only needed if the generator writes into the root; keep it off for read-only analysis.)

You decide exactly which programs are reachable. The default allowlist is read-only; everything beyond it is opt-in.

How it works

  MCP client (ChatGPT / Claude Desktop / Claude Code)
        │
        │   stdio  ── local, no network  ────────────┐
        │                                            │
        │   HTTP (Streamable) + bearer token         │
        ▼                                            ▼
  cloudflared / reverse proxy  ──────────►  local-terminal-mcp
        (only needed for ChatGPT)                    │
                                                     ▼
                                     policy engine  →  git / rg / cat / ...
                                     (allowlist, no shell, path containment)
  • Local clients (Claude Desktop, Claude Code) talk to the server over stdio — the server is a child process, nothing is exposed to the network. This is the most secure mode and needs no tunnel.

  • ChatGPT can only reach servers over public HTTPS, so for ChatGPT you run the server in HTTP mode behind a tunnel (e.g. cloudflared) and protect it with a bearer token (ideally plus Cloudflare Access).

Security model

The server never trusts the model's judgement. A deterministic policy layer (src/local_terminal_mcp/policy.py) gates every request:

  1. Allowlist, deny by default. Only commands whose program is on the allowlist run. Anything else is rejected. The default allowlist is read-only (git, rg, grep, ls, cat, head, tail, wc, find, tree, stat, file, pwd, echo, diff).

  2. One command, no shell. Commands are parsed with shlex and executed with shell=False. Pipes (|), chaining (&&, ;), redirects (>), background (&) and command substitution ($(...), backticks) are all rejected — so git status && rm -rf / never runs.

  3. Path containment. Every file path is fully resolved (symlinks and .. included) and must land inside the configured root directory.

  4. Writes are opt-in. write_file is disabled unless you start with --allow-write. The default is read-only.

  5. Fail closed on exposure. The server refuses to start in HTTP mode without an auth token of at least 16 characters.

  6. Bounded output and time. Output is truncated to a byte limit and every command has a timeout.

Layers you add around it:

  • Put Cloudflare Access (or equivalent) in front of the tunnel. A bearer token is the app-level check; Access is the network-level wall. Obscure tunnel URLs are not security.

  • Run the server as a low-privilege user, ideally inside a container or VM, not as your main account.

  • Keep write access off unless you truly need it, and when you do, keep the root scoped to a single project directory.

Install

Requires Python 3.10+.

git clone https://github.com/luckysolanki/local-terminal-mcp.git
cd local-terminal-mcp
python3 -m venv .venv && source .venv/bin/activate
pip install -e .

Quick start (local / stdio)

Run it read-only against a project directory:

local-terminal-mcp --root /path/to/your/repo

That starts the server on stdio, waiting for an MCP client. See the sections below to connect a specific client.

Connect to Claude Desktop

Add the server to Claude Desktop's config file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

  • Windows: %APPDATA%\Claude\claude_desktop_config.json

{
  "mcpServers": {
    "local-terminal": {
      "command": "/path/to/local-terminal-mcp/.venv/bin/local-terminal-mcp",
      "args": ["--root", "/path/to/your/repo"]
    }
  }
}

Restart Claude Desktop. The tools appear under the connectors / tools menu.

Connect to Claude Code

claude mcp add local-terminal -- \
  /path/to/local-terminal-mcp/.venv/bin/local-terminal-mcp --root /path/to/your/repo

Then /mcp inside Claude Code will list the server.

Connect to ChatGPT (over the internet)

📄 For the exact, step-by-step tested procedure, see docs/CHATGPT_SETUP.md. The summary below covers the same flow.

ChatGPT can only reach servers over public HTTPS, so run in HTTP mode behind a tunnel. The flow is: (1) start the server, (2) expose it with a tunnel (ngrok or cloudflared), (3) add the connector in ChatGPT.

Why path auth for ChatGPT? ChatGPT's Create custom MCP server dialog only offers OAuth or No authentication — there is no field for a static bearer token. So instead of a header, we put an unguessable secret in the URL path (a capability URL) and select No authentication in ChatGPT. The MCP route exists only at that secret path; probes of the bare /mcp return 404. This is appropriate for personal use; note the secret appears in the URL (and thus in edge/proxy logs). For a shared or higher-value deployment, implement OAuth instead.

The server also validates the incoming Host header for DNS-rebinding protection. Because tunnels forward an arbitrary public hostname, the server accepts any host by default; pin it to your tunnel hostname with --allowed-hosts for defense in depth.

Step 1 — start the server in HTTP mode (path auth)

SECRET="$(openssl rand -hex 16)"
echo "MCP URL path: /mcp/$SECRET"
local-terminal-mcp --transport http --host 127.0.0.1 --port 8000 \
  --root /path/to/your/repo \
  --auth path --mcp-path "/mcp/$SECRET"

Step 2 — expose it with a tunnel

Install ngrok and add your authtoken (one-time, from the ngrok dashboard):

brew install ngrok            # or: https://ngrok.com/download
ngrok config add-authtoken <YOUR_NGROK_AUTHTOKEN>

Start the tunnel pointing at the local port:

ngrok http 8000

ngrok prints a forwarding URL like https://a1b2-34-56.ngrok-free.app. Your MCP endpoint is that URL + your secret path, e.g. https://a1b2-34-56.ngrok-free.app/mcp/<SECRET>.

Notes for the free tier:

  • The URL is random and changes every restart — you'll re-paste it into ChatGPT each session. (A paid plan gives a stable --domain.)

  • ngrok shows a browser interstitial on the free tier for browser traffic; ChatGPT's MCP client sends API requests, so it is not affected.

  • Optionally lock the Host header to the tunnel domain:

    local-terminal-mcp --transport http --port 8000 --root /path/to/repo \
      --auth path --mcp-path "/mcp/$SECRET" \
      --allowed-hosts a1b2-34-56.ngrok-free.app

With a named tunnel on your own domain (see examples/cloudflared-config.yml):

tunnel: lucky-tunnel
credentials-file: /Users/you/.cloudflared/<tunnel-id>.json
ingress:
  - hostname: mcp.yourdomain.com
    service: http://localhost:8000
  - service: http_status:404
cloudflared tunnel run lucky-tunnel

Your MCP endpoint is https://mcp.yourdomain.com/mcp/<SECRET>. (If you also front it with Cloudflare Access, note ChatGPT cannot complete an Access login, so use a reserved port/hostname routed directly to the server, as here, rather than an Access-gated one.)

Step 3 — add the connector in ChatGPT

Requires a plan that has the custom MCP feature (Plus/Pro/Team/Enterprise/Edu):

  • Plugins → Add ▾ → Create custom MCP server.

  • Name: e.g. Local Terminal.

  • Server URL: your tunnel URL + secret path (e.g. https://a1b2-34-56.ngrok-free.app/mcp/<SECRET>).

  • Authentication: No authentication (the secret path is the credential).

  • Tick I understand and want to continue, then Create as a plugin and Connect.

ChatGPT discovers the tools automatically. A GET /healthz endpoint (no auth) is available for tunnel/uptime checks. To use the tools in a chat, ask ChatGPT to use the connector by name.

Configuration reference

Every option has a CLI flag and an LTMCP_-prefixed environment variable. CLI flags win over environment variables.

CLI flag

Env var

Default

Meaning

--root

LTMCP_ROOT

cwd

Directory the server is confined to

--transport

LTMCP_TRANSPORT

stdio

stdio or http

--host

LTMCP_HOST

127.0.0.1

HTTP bind host

--port

LTMCP_PORT

8000

HTTP bind port

--auth

LTMCP_AUTH_MODE

bearer

HTTP auth mode: bearer (header) or path (secret in URL)

--auth-token

LTMCP_AUTH_TOKEN

—

Bearer token (required for bearer mode)

--mcp-path

LTMCP_MCP_PATH

/mcp

Path the MCP endpoint is served at; for path auth, end it with a long random segment

--allowed-hosts

LTMCP_ALLOWED_HOSTS

any

Comma-separated Host header allowlist (e.g. your tunnel hostname)

--allow-commands

LTMCP_ALLOW_COMMANDS

read-only set

Comma-separated allowlist

--allow-write

LTMCP_ALLOW_WRITE

false

Enable write_file

--max-output-bytes

LTMCP_MAX_OUTPUT_BYTES

100000

Output truncation limit

--timeout

LTMCP_TIMEOUT

120

Per-command timeout (seconds)

Tools exposed

Tool

Available when

Description

run_command

always

Run one allowlisted command (no shell).

read_file

always

Read a file inside the root.

list_directory

always

List a directory inside the root.

write_file

--allow-write

Write a file inside the root.

Development

pip install -e ".[dev]"
pytest            # run the test suite
ruff check .      # lint

The security-critical logic lives in policy.py and is covered by tests/test_policy.py. If you change the policy, add a test for it.

FAQ

Can it run any command? No — only programs on the allowlist, one at a time, with no shell. Expand the allowlist with --allow-commands if you need more.

How is the HTTP endpoint protected? With a credential, never obscurity of the tunnel hostname alone. Use bearer auth (header token) for clients that support custom headers, or path auth (a long random secret in the URL, a capability URL) for ChatGPT, whose UI only offers OAuth or no-auth. For a shared or higher-value deployment, implement OAuth and/or front it with Cloudflare Access.

Why no pipes or &&? Because allowing shell composition is the easiest way to smuggle a dangerous command past an allowlist. Run multiple tool calls instead.

License

MIT — see LICENSE.

Available Tools

3 tools
list_directoryB

List the entries of a directory inside the project root.

ParametersJSON Schema
NameRequiredDescriptionDefault
pathNo.

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

B3.1/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden, and it does disclose one meaningful constraint: paths are confined to the project root, implying a sandbox. However, it says nothing about recursion, symlink handling, error behavior for missing directories, or whether the listing is shallow, leaving notable gaps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single, front-loaded sentence with no filler. Every word earns its place and the scoping constraint appears immediately.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be explained, and the tool is simple with one optional parameter. Still, the description omits when-to-use guidance and any meaning for the 'path' argument, which are the remaining pieces an agent needs for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% and the single parameter 'path' (with default '.') is never mentioned in the description. The description does not clarify whether the path is relative to the project root, whether an absolute path is accepted, or how the default is applied, so it fails to compensate for the schema gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('List the entries of a directory') with a scope qualifier ('inside the project root'). It is clear what the tool does, but it does not distinguish itself from siblings like read_file or run_command, so it falls short of a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no guidance on when to use this tool versus read_file or run_command, nor any prerequisites or exclusions. Usage can only be inferred from the name and the phrase 'inside the project root'; the description offers no routing help.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

read_fileA

Read a UTF-8 text file located inside the project root.

ParametersJSON Schema
NameRequiredDescriptionDefault
pathYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A3.6/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden. It usefully discloses two real constraints (UTF-8 only, project-root sandbox), but says nothing about error behavior for missing/binary files, file size or truncation limits, or whether the entire file is returned.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

One sentence, front-loaded with the action and immediately qualified by the two constraints. No filler or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a one-parameter read tool with an output schema (so return values need no explanation) and no annotations, the description is nearly sufficient. Only minor gaps remain around failure modes and size limits.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The single 'path' parameter has 0% schema description coverage, so the description must compensate. It partially does by stating the path refers to a UTF-8 text file inside the project root, but gives no format details (relative vs absolute, extension handling).

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb+resource ('Read a UTF-8 text file') with two meaningful scope qualifiers: encoding (UTF-8) and location (inside the project root). This clearly distinguishes it from siblings like run_command, though it never names or contrasts them explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No explicit when-to-use or when-not-to-use guidance, but the 'inside the project root' and 'UTF-8' constraints implicitly bound applicability (no binary files, no paths outside the project). Usage is implied rather than stated, so this lands at the minimum-viable level.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

run_commandA

Run a single allowlisted command inside the project root and return its output. Exactly one program per call: pipes, chaining (&&, ;), redirects (>) and command substitution are not allowed. Allowed programs: cat, diff, echo, file, find, git, grep, head, ls, pwd, rg, stat, tail, tree, wc. Optionally set 'cwd' to a subdirectory of the root.

ParametersJSON Schema
NameRequiredDescriptionDefault
cwdNo
commandYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden and does disclose important constraints: exactly one program per call, no pipes/chaining/redirects/substitution, confinement to the project root, and an explicit allowlist. It does not state whether any allowed commands can mutate files or repository state, nor does it mention timeouts, output limits, or authentication requirements.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose and then immediately lists the hard restrictions and allowlist. The longer allowlist is necessary and earns its place; there is no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a command-execution tool with two parameters, no annotations, and an output schema, the description covers the essential invocation constraints well. The main remaining gap is the absence of a clear safety profile, such as whether commands are read-only or can modify project files.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate for both parameters, and it does: it explains that 'command' must be a single allowlisted program with no shell metacharacters, and that 'cwd' optionally points to a subdirectory of the project root. It would be stronger with explicit mention of the default root behavior or example command syntax.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Run') and resource ('a single allowlisted command inside the project root'), and clearly states that it returns output. It is plainly distinct from the read-only file and directory siblings, even without naming them.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives clear constraints for what kinds of commands are acceptable, including the allowlist and prohibition on shell chaining, but it never states when an agent should choose run_command over read_file or list_directory. Usage is therefore implied rather than explicitly guided.

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. 3 tool updatesv0.1.0
    • First observedlist_directory
    • First observedread_file
    • First observedrun_command

TDQS

A3.6/5.0

Scored across 3 tools

Disambiguation4/5

The three tools have mostly clear roles, but run_command's allowlist includes cat, ls, find, and grep, which functionally overlaps with read_file and list_directory. An agent could read a file or list a directory via either path, though the dedicated tools are clearly documented as the simpler option.

Naming Consistency5/5

All three names follow a clean verb_noun snake_case pattern (run_command, read_file, list_directory) with no deviations or mixed conventions.

Tool Count4/5

Three tools is on the lean side, but the server is deliberately a narrow, sandboxed read-only surface, so each tool earns its place and nothing is redundant padding.

Completeness3/5

The read surface is coherent, but there is no write, create, edit, or delete capability anywhere in the set, and redirects/chaining are blocked in run_command, so agents hit a hard dead end for any mutation task. This may be intentional sandboxing but leaves the lifecycle incomplete.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    B
    maintenance
    Zero-dependency MCP server that provides AI models with secure read/write/exec access to local files and directories over HTTP and SSE, designed to be tunneled via ngrok for integration with Claude Web.
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    A lightweight local coding MCP server that exposes a single project directory to ChatGPT via Streamable HTTP, enabling file operations, command execution, search, and web fetching without authentication.
    71 npm
    25
    MIT