MCP Server
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
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues