Terminal MCP Server
by AcosX
README.md
# Terminal MCP Server
Terminal MCP Server exposes one MCP tool, `run_command`, that runs shell
commands on the host and returns stdout, stderr, exit code, duration, timeout
status, and truncation status.
I personally use it with ChatGPT, so ChatGPT can access my local system in a
Codex-like way.
This is intentionally powerful. If the service runs as root, the MCP client has
root shell capability. Put it behind HTTPS, require OAuth/OIDC authentication,
restrict who can receive the required scope, and review audit logs.
## Features
- Streamable HTTP MCP transport.
- `run_command` tool implemented with `bash -lc`.
- JWT access-token validation through any OAuth/OIDC server that publishes JWKS.
- Optional direct Microsoft Entra ID validation.
- Optional bearer-token and in-memory OAuth modes for local testing.
- OAuth protected-resource metadata for ChatGPT and other MCP clients.
- JSON-lines audit log with command, result metadata, source IP, session ID, and
non-secret token claims.
## Demo

## Recommended Architecture
Use a real OAuth/OIDC authorization server in front of this MCP server:
```text
MCP client -> Terminal MCP Server
|
validates JWT access token
|
OAuth/OIDC authorization server
```
The authorization server should:
- authenticate users with your preferred identity providers;
- decide which users may receive the command-execution scope;
- issue JWT access tokens with a stable issuer, audience, and scope;
- expose a JWKS endpoint.
The MCP server only validates tokens. It does not need client secrets for the
OAuth client used by ChatGPT or another MCP client.
## Quick Start
```bash
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
set -a
. ./.env
set +a
python server.py
```
The MCP endpoint is:
```text
http://127.0.0.1:8767/mcp
```
For production, put HTTPS in front of the service and set
`MCP_RESOURCE_BASE_URL` to the public origin, for example:
```text
MCP_RESOURCE_BASE_URL=https://mcp.example.com
```
The advertised MCP resource becomes:
```text
https://mcp.example.com/mcp
```
## Configuration
Common settings:
```text
MCP_HOST=127.0.0.1
MCP_PORT=8767
MCP_TRANSPORT=streamable-http
MCP_COMMAND_TIMEOUT=30
MCP_COMMAND_MAX_TIMEOUT=120
MCP_MAX_OUTPUT_BYTES=131072
MCP_AUDIT_LOG=/var/log/terminal-mcp/audit.log
```
Recommended JWT mode:
```text
MCP_AUTH_MODE=jwt
MCP_JWKS_URI=https://auth.example.com/jwks
MCP_JWT_ISSUER=https://auth.example.com
MCP_JWT_AUDIENCE=https://mcp.example.com/mcp
MCP_JWT_ALGORITHM=RS256
MCP_REQUIRED_SCOPES=terminal.run
MCP_ADVERTISED_SCOPES=terminal.run
MCP_OAUTH_ISSUER=https://auth.example.com
MCP_RESOURCE_BASE_URL=https://mcp.example.com
```
The token must contain the required scope. Depending on your provider this is
usually represented as `scp`, `scope`, or a compatible claim understood by
FastMCP's JWT verifier.
## OAuth Discovery Endpoints
The server exposes:
```text
/.well-known/oauth-protected-resource
/.well-known/oauth-protected-resource/mcp
/.well-known/oauth-authorization-server
```
`MCP_OAUTH_ISSUER` controls the authorization-server issuer advertised to MCP
clients. For a generic OAuth/OIDC provider, that issuer is normally your auth
server origin.
## ChatGPT Connector Setup
Use these values when creating a ChatGPT connector:
```text
Server URL: https://mcp.example.com/mcp
OAuth client ID: <client id from your authorization server>
OAuth client secret: <client secret from your authorization server>
Authorization URL: https://auth.example.com/authorize
Token URL: https://auth.example.com/token
Scope: terminal.run
```
If ChatGPT sends a `resource` parameter during OAuth, configure
`MCP_JWT_AUDIENCE` to match the access token audience issued for that resource,
often the full MCP resource URL:
```text
MCP_JWT_AUDIENCE=https://mcp.example.com/mcp
```
## Microsoft Entra ID Direct Mode
You can validate Entra ID tokens directly:
```text
MCP_AUTH_MODE=entra
MCP_ENTRA_TENANT_ID=<tenant-id>
MCP_ENTRA_AUDIENCE=api://<api-application-client-id>
MCP_REQUIRED_SCOPES=terminal.run
MCP_ENTRA_ALLOWED_OIDS=<allowed-user-object-id>[,<another-object-id>]
```
Use two app registrations:
```text
1. API app: exposes the terminal.run delegated scope.
2. Client app: has the MCP client's redirect URI and a client secret.
```
`MCP_ENTRA_ALLOWED_OIDS` is optional but strongly recommended. When set, only
those Entra user object IDs can call `run_command`.
## Local Testing Modes
Bearer-token mode:
```text
MCP_AUTH_MODE=bearer
MCP_BEARER_TOKEN=<random token>
```
Development OAuth mode:
```text
MCP_AUTH_MODE=memory_oauth
MCP_PUBLIC_BASE_URL=https://mcp.example.com
MCP_RESOURCE_BASE_URL=https://mcp.example.com
MCP_OAUTH_CLIENT_ID=terminal-mcp
MCP_OAUTH_CLIENT_SECRET=<random secret>
MCP_OAUTH_REDIRECT_URIS=https://chatgpt.com/connector/oauth/<connector-id>
```
Do not use `memory_oauth` in production. Tokens and client registrations are
in-memory and restart-sensitive.
## systemd
An example unit is available at:
```text
examples/terminal-mcp.service
```
Typical layout:
```text
/opt/terminal-mcp/server.py
/opt/terminal-mcp/.venv
/etc/terminal-mcp/env
```
Install and start:
```bash
sudo cp examples/terminal-mcp.service /etc/systemd/system/terminal-mcp.service
sudo systemctl daemon-reload
sudo systemctl enable --now terminal-mcp.service
```
## Audit Log
Command calls are appended as JSON lines to `MCP_AUDIT_LOG`. The audit record
contains command metadata and non-secret authentication context. It does not log
access tokens.
## Security Notes
- Prefer running with the minimum Linux privileges needed for your use case.
- Restrict `terminal.run` issuance in your authorization server.
- Put HTTPS and a WAF or reverse proxy in front of the service.
- Keep client secrets in the OAuth client or authorization server, not in this
MCP resource server.
- Rotate keys and review audit logs regularly.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues