AI Server Commander is a self-hosted control plane that lets approved AI assistant clients use bounded capabilities on machines and authenticated local services you control. Its production core today is terminal execution; the project is designed to grow through optional capability adapters without turning client-specific behavior into core infrastructure. OpenAI/ChatGPT and Anthropic/Claude are first-class client families; additional clients and protocols are welcome when they do not compromise functionality, reliability, or performance for those two.
It exposes the same execution core through two primary client adapters:
The server does not provide model access or credits. It receives authenticated requests, applies local policy and limits, invokes an explicitly enabled capability, and returns structured state or results. In v1.2.0 the production capability is the bounded host command executor.
AI Server Commander is self-hosted. Each user installs Commander for their own machine, gets their own public HTTPS origin and authentication credentials, and connects their own AI assistant to that deployment.
For ChatGPT Custom GPT Actions, each user configures their own Custom GPT with their own Commander deployment's /openapi.json and authToken. A GPT Action configured for one Commander deployment does not automatically switch to another person's server.
The repository is what you share, not a preconfigured GPT. If another person installs Commander on another machine, they use their own Commander hostname, their own token, and their own GPT or other client connection.
Start with Connecting OpenAI and Claude, which covers the URL, authentication and first test for each client.
| Client | Connection | URL to enter |
|---|---|---|
| ChatGPT Custom GPT | Action with Bearer API-key authentication | https://YOUR-COMMANDER-DOMAIN/openapi.json |
| ChatGPT custom MCP connection | Remote MCP with OAuth | https://YOUR-COMMANDER-DOMAIN/mcp |
| Claude web / Desktop | Custom remote connector with OAuth | https://YOUR-COMMANDER-DOMAIN/mcp |
| Claude Code | HTTP MCP server with OAuth | https://YOUR-COMMANDER-DOMAIN/mcp |
Replace the example hostname with your server's public HTTPS origin. The browser watcher is optional: all four connections work with it disabled. See watcher on/off and browser recovery for the separate browser lifecycle integration.
The longer-term direction is broader than terminal access: Commander should remain a small, auditable control plane that can expose typed capabilities such as read-only filesystem operations, remote-host adapters and browser/session automation while keeping authentication, policy, activity state and client transports at clear boundaries. A future mobile or chat UI should consume these capabilities rather than become a dependency of the core server.
[!CAUTION] AI Server Commander can execute real shell commands with the permissions of its operating-system user. It is not a sandbox. Run it as a dedicated unprivileged user, keep it behind HTTPS, enable
SAFE_MODE, and expose it only to clients and users you trust.
activityId.SAFE_MODE denylist for obviously destructive commands./openapi.json./mcp.ChatGPT Custom GPT / REST client
│ HTTPS + Bearer token
▼
REST / OpenAPI adapter ─────┐
│
Claude / remote MCP client ├── shared bounded executor ── host shell
│ HTTPS + OAuth │ │
▼ │ ├── SAFE_MODE
MCP adapter ──────────────┘ ├── timeout/output caps
├── activity log
└── notices
That diagram is the current production baseline. The extension model keeps REST/MCP and future clients thin while adding optional typed capabilities behind the same control-plane boundary. The optional browser watcher follows this pattern; future adapters can extend filesystem and remote-host access.
See docs/architecture.md for request flows, trust boundaries and the module map.
Set chatgptWeb.enabled to true or false in config.json and restart Commander to enable or disable the browser watcher. CHATGPT_WEB_ENABLED overrides that flag. Disabling preserves pending responses and deduplication state. See docs/chatgpt-web-mvp1.md for modes, automatic consent behavior, and recovery.
Windows may work for basic commands, but process-group termination is POSIX-specific.
git clone https://github.com/Jhacarreiro/ai-server-commander.git
cd ai-server-commander
npm install
The first npm start launches an interactive setup and writes config.json.
npm start
For non-interactive deployment:
cp config.example.json config.json
chmod 600 config.json
Minimal configuration:
{
"port": 3000,
"productionDomain": "https://commander.example.com",
"authToken": "replace-me",
"mcpToken": "replace-me-too"
}
The placeholder values shown above are invalid on purpose: the server rejects placeholder secrets at startup instead of running with publicly-known credentials. Generate tokens with a cryptographically secure tool:
openssl rand -hex 32
SAFE_MODE=true npm start
Local checks:
curl http://127.0.0.1:3000/openapi.json
npm run check
npm test
Use a reverse proxy such as Nginx, Caddy or a managed tunnel. Forward the original host and scheme so OAuth metadata contains the correct public URL.
See docs/deployment.md for systemd, Nginx, upgrades and rollback.
config.json| Key | Required | Purpose |
|---|---|---|
port |
Yes | Local TCP port used by the Node server. |
host |
No | Listen hostname or IP address (no scheme or port). Omitted: preserves Node's all-interface default (:: where IPv6 is available, otherwise 0.0.0.0). Use 127.0.0.1 for an IPv4 loopback-only listener. |
productionDomain |
Yes | Exact public origin, such as https://commander.example.com. Required for correct remote OAuth metadata behind a proxy. |
authToken |
Yes | Bearer token for REST and approval code for the built-in OAuth consent page. |
confirmationPolicy |
No | Per-category human-confirmation policy advertised to MCP clients. Defaults: read=false, write=false; delete, restart, permissions, credentials = true. |
mcpToken |
No | Separate pre-shared token for MCP clients that support token auth. Falls back to authToken when omitted. |
config.json contains secrets and is ignored by Git. Keep it mode 600 and never paste it into issues or logs.
The setup wizard leaves host unset for compatibility. To restrict the listener,
add "host": "127.0.0.1" to your private configuration before starting the server.
Use "host": "::1" for IPv6 loopback; IPv6 addresses are not bracketed in this field.
An empty or non-string host is rejected. productionDomain describes the public
URL and does not control the bind address. See the deployment guide
before restricting access through a container or remote proxy.
LocalTunnel support was removed in v1.0.8 because its pinned HTTP dependency chain could not be updated safely. Existing configurations with useLocalTunnel: true now fail with migration guidance. Use a maintained HTTPS reverse proxy or tunnel and set productionDomain explicitly.
| Variable | Default | Purpose |
|---|---|---|
SAFE_MODE |
false |
Enables the built-in destructive-command denylist. Recommended for production. |
COMMAND_TIMEOUT_MS |
120000 |
Server-wide maximum command duration. Client requests can ask for less, not more. |
MAX_OUTPUT_CHARS |
12000 |
Server-wide maximum returned output. |
MAX_SCRIPT_BODY_BYTES |
524288 |
Maximum script/request body size. |
COMMAND_OPERATIONS_PATH |
runtime/command-operations.json |
Persistent operationId state used for idempotent recovery. |
COMMAND_OPERATION_TTL_SECONDS |
86400 |
How long a completed or accepted operationId is retained before it can be reused. |
OAUTH_STATE_PATH |
runtime/oauth-state.json |
Persistent OAuth client and token-hash state. |
OAUTH_AUTH_CODE_TTL_SECONDS |
300 |
Authorization-code lifetime. |
OAUTH_ACCESS_TOKEN_TTL_SECONDS |
3600 |
Access-token lifetime. |
OAUTH_REFRESH_TOKEN_TTL_SECONDS |
2592000 |
Refresh-token lifetime. |
MAX_OAUTH_CLIENTS |
200 |
Maximum persisted dynamically registered OAuth clients. At the limit, the oldest client without a live code or token is evicted; registration returns 429 only when every client holds an active grant. |
MAX_CLIENT_NAME_CHARS |
128 |
Maximum stored length of a registered OAuth client_name; longer names are truncated. |
RESTART_FORCE_EXIT_MS |
30000 |
Upper bound for /api/restart, SIGTERM and SIGINT to wait for in-flight responses before the process exits anyway. Running commands are interrupted first; a second signal exits immediately. |
MAX_CONCURRENT_COMMANDS |
8 |
Maximum commands running at once across REST and MCP. Further requests get 429 and do not consume their operationId. |
MAX_INLINE_COMMAND_BYTES |
65536 |
Maximum inline command size in bytes; larger inline commands are rejected with 413. Send larger payloads in script mode. |
MAX_NOTICE_TEXT |
8192 |
Maximum /api/notices text length; longer notices are rejected with 400. |
MAX_NOTICE_SOURCE |
256 |
Maximum /api/notices source length; longer values are rejected with 400. |
MAX_ACTIVITY_CONTEXTS |
500 |
Maximum saved conversation contexts, and maximum conversation and task activity directories; the least recently used ones are removed first. |
MAX_ACTIVITY_LOG_BYTES |
8388608 |
Size at which each activity log file is rotated to <file>.1 (one previous file is kept). |
ACTIVITY_LOG_DIR |
runtime/activity |
Directory for activity logs, status files and saved contexts. |
MAX_ACTIVITY_FIELD |
256 |
Conversation ID, task ID and task title values are truncated to this length before they are stored. |
MAX_MCP_BATCH |
64 |
Maximum JSON-RPC batch size on /mcp; larger batches are rejected with 400 / -32600. |
MAX_EDIT_FILE_BYTES |
2097152 |
Largest file /api/read-or-edit-file reads or edits; larger files get 413. |
MAX_ACCESS_FILE_BYTES |
8388608 |
Largest file served or diffed through an /access/<token> share link; larger files get 413. |
MAX_TOKEN_STORE_ENTRIES |
500 |
Maximum share-link tokens kept in tokenStore.json; the ones closest to expiry are dropped first. |
MAX_REPLACEMENTS |
50 |
Maximum replacements in one /api/read-or-edit-file request. |
MAX_FUZZY_QUERY_CHARS / MAX_FUZZY_HAYSTACK_CHARS |
256 / 262144 |
Above these sizes a search text that is not found exactly is reported as not found instead of fuzzy-matched. |
SHELL |
/bin/bash |
Shell for inline commands and the script-mode default, for REST and MCP alike. When unset, /bin/bash is used, or /bin/sh if Bash is not installed. |
NODE_ENV |
unset | Standard Node environment label. |
See .env.example. The application does not automatically load .env; set variables through your shell, process manager or service unit.
Custom GPT Actions use the REST/OpenAPI adapter for accounts where Custom GPT editing and Actions are available.
https://YOUR-COMMANDER-DOMAIN/openapi.json from your own Commander deployment.authToken value from your own deployment's config.json.pwd && hostname.For the full walkthrough and OAuth-based ChatGPT/Claude connections, use the client setup guide.
Legacy GET request:
GET /api/runTerminalScript?command=pwd%20%26%26%20hostname
Authorization: Bearer <authToken>
Preferred POST request:
POST /v1/commands/execute
Authorization: Bearer <authToken>
Content-Type: application/json
{
"mode": "inline",
"command": "pwd && hostname",
"cwd": "/srv/project",
"timeoutMs": 45000,
"maxOutputChars": 12000,
"operationId": "deploy-config-2026-09-23T0945Z"
}
For Claude web/Desktop, Claude Code and ChatGPT, start with the client setup guide. It explains adding the connection and completing Commander authorization.
The remote MCP endpoint is:
https://commander.example.com/mcp
The server implements MCP protocol version 2025-03-26. initialize always returns that version and does not echo a different client-requested version. Clients that cannot continue on 2025-03-26 disconnect during negotiation, which matches the MCP lifecycle rules.
An empty JSON-RPC batch ([]) is invalid and returns HTTP 400 with JSON-RPC -32600. A POST that contains only notifications (no id) still returns HTTP 202 with no body.
The primary tool is run_terminal_command.
| Field | Type | Notes |
|---|---|---|
command |
string | Exact command for inline mode. Mutually exclusive with script. |
script |
string | Multi-line script body. Supplying it defaults the mode to script. Mutually exclusive with command. |
mode |
inline or script |
Optional explicit mode. |
cwd |
string | Must be an existing readable directory. Invalid paths are rejected. |
shell |
string | Script-mode shell, for example /bin/sh. |
timeoutMs |
integer | Requested timeout, capped by server policy. |
maxOutputChars |
integer | Requested output limit, capped by server policy. |
The server publishes:
/.well-known/oauth-protected-resource
/.well-known/oauth-protected-resource/mcp
/.well-known/oauth-authorization-server
/.well-known/openid-configuration
/oauth/register
/oauth/authorize
/oauth/token
/oauth/revoke
The built-in flow supports dynamic client registration, authorization code + PKCE, refresh-token rotation and RFC-style token revocation. The authorization page asks for the server authToken as the approval code.
OAuth state is persisted atomically at OAUTH_STATE_PATH. Client secrets, authorization codes, access tokens and refresh tokens are stored only as SHA-256 hashes; raw values are returned to the client only when issued. The state file is forced to mode 600 on supported POSIX filesystems. After upgrading from an in-memory-only release, existing clients must authorize once; credentials issued by v1.0.8 or later survive normal restarts.
The MCP descriptor includes OAuth security schemes, a compatibility mirror in _meta, risk annotations, an output schema and structuredContent. Whether a specific ChatGPT account or surface can add a custom remote MCP server depends on the current ChatGPT plan and client capabilities. Keep the REST Action path available until the target workflow is validated.
curl -sS \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"mode":"inline","command":"pwd && hostname","timeoutMs":5000}' \
https://commander.example.com/v1/commands/execute
curl -sS \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"mode":"script",
"shell":"/bin/sh",
"script":"set -e\npwd\nhostname\n",
"timeoutMs":5000
}' \
https://commander.example.com/v1/commands/execute
{
"message": "Command executed successfully.",
"activityId": "cmd_...",
"output": "...",
"exitCode": 0,
"timedOut": false,
"interrupted": false,
"blocked": false,
"outputTruncated": false,
"maxOutputChars": 12000,
"mode": "inline",
"notices": []
}
Treat terminal execution as a sequence of bounded operations rather than one large request:
For multi-file changes, logical atomicity matters more than one-request-per-file: stage the complete patch or helper script, apply it once, then validate in separate calls.
If a transport, reverse-proxy, or WAF error occurs after a mutating request, do not automatically resend a payload that had no operationId. The request may have reached the origin even when the client did not receive the response. Probe state with a small read-only request first, then continue from the observed state. Requests that carried an operationId can be recovered safely as described in the next section.
Clients should also:
operationId with every mutating request;tail, grep, or equivalent filters;A proxy-generated WAF page can be produced before the request reaches AI Server Commander, so normalization of that page belongs in the client/action layer rather than in the Commander origin.
operationIdFor mutating POST requests, clients may provide an optional operationId (1-128 characters: letters, digits, ., _, :, -). Commander persists only a command fingerprint and bounded execution summary; it does not store stdout in the operation record.
Reusing the same operationId with the same command does not execute the command again while the record is retained. Reusing it with a different command returns HTTP 409.
Operation records are kept for COMMAND_OPERATION_TTL_SECONDS (default 24 hours) and for at most 512 recent operations; after that the same operationId is treated as new. IDs are scoped per adapter, so REST and MCP clients cannot replay or block each other's operations. Generate a fresh, unique operationId for every distinct mutation.
If Commander rejects a request before anything runs (for example a SAFE_MODE block or a failure to start the process), the response reports operationState: "not_executed" and the operationId is released for a clean retry.
After a lost transport response, probe the operation before deciding whether to retry:
GET /v1/commands/operations/deploy-config-2026-09-23T0945Z
Authorization: Bearer <authToken>
The probe covers REST operations. The returned state is one of running, finished, indeterminate, or unknown. MCP clients can recover by resending the same arguments with the same operationId: the result reports replayed: true and the operation state without running the command again. indeterminate is deliberately conservative: Commander accepted the operation previously, but the current process cannot prove whether it completed, so the client should inspect target state rather than resubmit blindly.
When exactly one command is active:
POST /api/interrupt
Authorization: Bearer <authToken>
When several commands may be active, target one explicitly:
POST /api/interrupt
Authorization: Bearer <authToken>
Content-Type: application/json
{
"activityId": "cmd_..."
}
Activity endpoints:
GET /api/activity
GET /api/activity/status
GET /api/activity/index
POST /api/activity/context
Notice endpoints:
POST /api/notices
GET /api/notices/pending
POST /api/notices/:id/ack
Activity records use command hashes, byte counts and redacted previews rather than complete script bodies by default. Treat generated runtime/ data as potentially sensitive operational metadata.
AI Server Commander provides controls, not isolation:
SAFE_MODE blocks a small set of obviously destructive patterns./access?...diff=1 requests discover the target file repository, read indexed blob data through bounded Git subprocesses, and compare isolated temporary snapshots with external diff/textconv execution disabled. Indexed and working-tree inputs are capped at 8 MiB each, and each Git subprocess is capped at 5 seconds; files or repositories that exceed those limits fail closed with an HTTP 500 response rather than falling back to unbounded Git behavior.It does not provide:
Recommended production controls:
docker, sudo or other privileged groups unless explicitly required.SAFE_MODE=true, but do not treat it as a sandbox.authToken and mcpToken values.confirmationPolicy for terminal commands that need human confirmation. By default, reads/inspection and ordinary writes are allowed without an extra confirmation (read: false, write: false), while delete, restart, permission-change and credential-access categories require confirmation.See SECURITY.md for vulnerability reporting.
npm run check
npm test
The smoke suite covers:
SAFE_MODE results;CI runs checks on supported Node versions for every push and pull request.
http:// or the wrong hostnameSet productionDomain to the exact external HTTPS origin and forward Host and X-Forwarded-Proto from the reverse proxy.
That is expected. /api/runTerminalScript executes on GET and POST only; a HEAD request is rejected before it is parsed, because Express would otherwise route it to the GET handler and run the ?command=. Probe /openapi.json for liveness instead.
Confirm that every release uses the same OAUTH_STATE_PATH and that the service user can read and write it. Upgrading from v1.0.7 or earlier requires one new authorization because those releases kept OAuth state only in memory. A missing, moved or deleted state file also requires reauthorization.
The server only implements MCP protocol version 2025-03-26 and always returns that version from initialize. A client that cannot continue on 2025-03-26 is expected to disconnect. Confirm the client supports that version rather than expecting the server to echo an older or newer request.
Pass an explicit cwd. It must exist and be readable by the service user.
The effective timeout is the lower of the client request and COMMAND_TIMEOUT_MS.
Check outputTruncated. Increase the requested maxOutputChars and, if needed, the server-wide MAX_OUTPUT_CHARS cap. Prefer commands that filter output before returning it.
Check these URLs from outside your network:
curl -i https://commander.example.com/.well-known/oauth-protected-resource/mcp
curl -i https://commander.example.com/.well-known/oauth-authorization-server
curl -i https://commander.example.com/mcp
The unauthenticated /mcp request should return 401 with a WWW-Authenticate challenge pointing to protected-resource metadata.
AI Server Commander is a small self-hosted project maintained on a best-effort basis. The command-execution surface is intentionally narrow. New capabilities should normally be implemented once in shared core code and exposed through both REST and MCP adapters with matching safety semantics.
Contributions are welcome. Read CONTRIBUTING.md before opening a pull request. Do not include deployment secrets, private hostnames, personal paths, access tokens, logs or production state.
Licensed under the MIT License. See NOTICE.md for project attribution.