Skip to main content
Glama
README.md
# MCP Server

Node.js + Express MCP server for ChatGPT with:

- OAuth 2.0 Authorization Code Flow + PKCE `S256`
- refresh token rotation
- Supabase Postgres persistence
- MCP Streamable HTTP endpoint at `/mcp`
- sample tools: `calculator`, `get_weather`, `search_web`
- cleanup job for expired OAuth records

## Structure

```text
mcp-server/
|-- .env.example
|-- IMPLEMENTATION_REPORT.md
|-- package.json
|-- README.md
|-- supabase/
|   `-- schema.sql
`-- src/
    |-- app.js
    |-- server.js
    |-- config/
    |-- mcp/
    |-- middleware/
    |-- routes/
    |-- scripts/
    |-- services/
    |-- tools/
    `-- utils/
```

## 1. Supabase setup

1. Create a Supabase project.
2. Open the SQL editor.
3. Run `supabase/schema.sql`.
4. Copy the Postgres connection string into `.env` as `DATABASE_URL`.

For IPv4-only networks, use the Supabase Session Pooler URI instead of the direct IPv6 connection string.

### Seed an OAuth client

You need at least one record in `oauth_clients`.

For local development the server can seed one automatically from env:

- `AUTO_SEED_CLIENT_ID`
- `AUTO_SEED_CLIENT_NAME`
- `AUTO_SEED_CLIENT_SECRET`
- `AUTO_SEED_REDIRECT_URIS`

If `AUTO_SEED_CLIENT_SECRET` is empty, the client is treated as public and may use `none` auth at the token endpoint.
If it is set, the client supports both `client_secret_basic` and `client_secret_post`.

## 2. Local setup

```bash
cd mcp-server
npm install
copy .env.example .env
```

Edit `.env`:

- `BASE_URL`
- `DATABASE_URL`
- `CORS_ORIGINS`
- `MCP_ALLOWED_ORIGINS`
- `MCP_ALLOWED_HOSTS`
- `ENABLE_CLEANUP_JOB`
- `CLEANUP_INTERVAL_MINUTES`
- `AUTO_SEED_*`

Check database connectivity:

```bash
npm run db:check
npm run db:init
```

Run locally:

```bash
npm run dev
```

Health check:

```bash
GET http://localhost:3000/healthz
```

Run cleanup manually:

```bash
npm run cleanup:run
```

## 3. Expose with ngrok

```bash
ngrok http 3000
```

Take the HTTPS URL from ngrok and set it as:

- `BASE_URL`
- add the ngrok origin to `MCP_ALLOWED_ORIGINS`
- add the ngrok host to `MCP_ALLOWED_HOSTS`
- one of the registered redirect URIs in `oauth_clients`

Restart the server after updating `.env`.

## 4. OAuth endpoints

### Authorization endpoint

- `GET /authorize`
- `POST /authorize/decision`
- demo consent screen with optional auto-approve mode
- validates:
  - `client_id`
  - `redirect_uri`
  - `response_type=code`
  - `code_challenge`
  - `code_challenge_method=S256`
  - scopes

### Token endpoint

- `POST /token`
- supports:
  - `grant_type=authorization_code`
  - `grant_type=refresh_token`

Client authentication:

- `none`
- `client_secret_basic`
- `client_secret_post`

### Discovery

- `GET /.well-known/oauth-authorization-server`
- `GET /.well-known/openid-configuration`

### Revoke

- `POST /revoke`

## 5. Refresh token rotation

This server rotates refresh tokens on every refresh request:

1. the current refresh token row is locked
2. the old refresh token is revoked
3. a new access token is issued
4. a new refresh token is issued
5. rotation lineage is stored in Postgres

If a revoked or replaced refresh token is reused:

- the backing `mcp_session` is marked `compromised`
- session tokens are revoked
- the request fails with `invalid_grant`

## 6. MCP endpoint

Main MCP endpoint:

- `POST /mcp`

Behavior:

- uses MCP Streamable HTTP transport
- path is `/mcp`
- bearer access token is required
- host and origin allowlists are enforced
- the implementation is stateless on the transport layer
- each HTTP request creates a fresh MCP server and transport instance
- no in-memory MCP transport session map is retained across requests or restarts

Compatibility note:

- `GET /mcp` and `DELETE /mcp` currently return `405`
- this server is using the stateless streamable HTTP pattern, not SSE session transport

## 7. Sample tools

- `calculator`
- `get_weather`
- `search_web`

These are in `src/tools/` and registered via `src/mcp/tool-registry.js`.

## 8. Cleanup job

The server starts a background cleanup job on boot when `ENABLE_CLEANUP_JOB=true`.

It removes:

- expired or stale authorization codes
- expired or revoked access tokens older than 1 day
- expired or revoked refresh tokens older than 7 days

Interval is controlled by `CLEANUP_INTERVAL_MINUTES`.

## 9. Register in ChatGPT

Use your deployed server values:

- Authorization URL: `https://your-domain/authorize`
- Token URL: `https://your-domain/token`
- Revoke URL: `https://your-domain/revoke`
- MCP URL: `https://your-domain/mcp`

Recommended scopes:

- `mcp.tools.call`
- `offline_access`

PKCE:

- required
- method: `S256`

If ChatGPT or your connector uses a confidential client:

- create the client in `oauth_clients`
- store the secret hash
- allow `client_secret_basic` and/or `client_secret_post`

## 10. Security notes

- token values are stored hashed in Postgres
- client secrets are stored hashed
- rate limiting is applied to `/authorize`, `/token`, `/revoke`
- OAuth errors use standard fields:
  - `error`
  - `error_description`
- raw secrets and tokens are not logged
- refresh token reuse triggers session compromise handling

## 11. Current status

Verified against real Supabase:

- `npm run db:check`
- `npm run db:init`
- `npm run cleanup:run`

What is still intentionally basic:

1. the consent page is a demo flow, not a real user login system
2. access tokens are opaque and require a DB lookup
3. there is no automated test suite yet
4. rate limits and allowlists should be tightened before production