Skip to main content
Glama
SongLei75

mcp-local-gateway

by SongLei75
README.md
# mcp-local-gateway

P0 goal: run a minimal local MCP server that exposes exactly one tool, `run_date`, through a Streamable HTTP `/mcp` endpoint.

This project is intentionally small. It does not expose arbitrary Bash. It does not use an OpenAI API key. It is designed for this validation chain:

```text
ChatGPT Web
  -> HTTPS /mcp
  -> Cloudflare Tunnel or another HTTPS tunnel
  -> 127.0.0.1:8787
  -> Hono
  -> MCP Server
  -> policy layer
  -> fixed date command
```

## Terms

- MCP: Model Context Protocol, 模型上下文协议,用来让 AI Host 通过标准协议发现并调用外部工具。
- Streamable HTTP: 可流式 HTTP 传输,适合远程 MCP server。
- Hono: 轻量 HTTP framework,HTTP 框架。
- OAuth: Open Authorization,开放授权。P0 只预留结构,不实现完整授权码流程。
- Policy layer: 策略层,本项目中用于拒绝任意命令,只允许 `run_date -> date`。

## Requirements

- Node.js >= 20.11
- npm
- Linux / WSL recommended

## Install

```bash
cd ~/project/mcp-local-gateway
npm install
cp .env.example .env
```

## Start

```bash
npm run dev
```

Expected startup lines:

```text
mcp-local-gateway listening on http://127.0.0.1:8787
health: http://127.0.0.1:8787/healthz
mcp:    http://127.0.0.1:8787/mcp
auth:   off
```

## Local validation

In another terminal:

```bash
cd ~/project/mcp-local-gateway
npm run typecheck
npm run build
npm test
npm run test:local
```

The `tools/list` response should include `run_date`. The `tools/call` response should include JSON text containing `stdout` from the local `date` command.

## Persistent user service / 用户级常驻服务

Use the user-level systemd service when you want the gateway to keep running after the terminal closes.

```bash
npm run service:install
npm run service:status
npm run service:logs
npm run service:uninstall
```

`service:install` runs `npm run build` and starts `dist/index.js`; it does not use `npm run dev`.
The service keeps running after the terminal exits.
Cloudflare Tunnel access to `mcp.songlei.me` depends on this service listening on `127.0.0.1:8787`.

## Static bearer mode

Use this before exposing through a tunnel if you are not using OAuth yet.

```bash
cat > .env <<'ENDENV'
HOST=127.0.0.1
PORT=8787
MCP_AUTH_MODE=static_bearer
MCP_STATIC_BEARER_TOKEN=replace-this-with-a-long-random-token
MCP_ALLOWED_ORIGINS=http://127.0.0.1:8787,http://localhost:8787,https://chatgpt.com
AUDIT_TO_STDOUT=true
AUDIT_LOG_FILE=logs/audit.jsonl
ENDENV

npm run dev
```

Validation with token:

```bash
MCP_STATIC_BEARER_TOKEN='replace-this-with-a-long-random-token' npm run test:local
```

## Cloudflare Tunnel sketch

Do not bind this server to `0.0.0.0`. Keep `HOST=127.0.0.1` and publish it with a tunnel.

Example target mapping:

```text
https://mcp.songlei.me/mcp -> http://127.0.0.1:8787/mcp
```

After that, create a ChatGPT connector and set Connector URL to:

```text
https://mcp.songlei.me/mcp
```

P0 recommendation: use static bearer only for a private short-lived test. P1 should implement OAuth / OIDC token validation or put the MCP server behind a compatible identity-aware proxy.

## Security constraints in P0

- No arbitrary Bash.
- `run_date` accepts no arguments.
- Command execution uses `spawn` with `shell: false`.
- Server refuses `HOST=0.0.0.0`.
- `/mcp` checks Origin when Origin is present.
- `/mcp` can enforce static Bearer token.
- Each tool call writes JSONL audit records.

## File map

```text
src/index.ts              entry point
src/config.ts             environment config and startup safety checks
src/http/app.ts           Hono app, health endpoint, /mcp route
src/http/origin.ts        Origin allowlist guard
src/auth/auth.ts          off/static_bearer/oauth_placeholder auth middleware
src/mcp/server.ts         MCP server creation and tool registration
src/mcp/transport.ts      Streamable HTTP transport creation
src/tools/run-date.ts     run_date MCP tool
src/policy/policy.ts      P0 allow/deny logic
src/shell/exec-fixed.ts   fixed date command execution
src/audit/audit.ts        JSONL audit logger
scripts/local-validate.sh curl-based validation script
test/*.test.ts            minimal unit tests
```