selfmcp
README.md
# selfMCP
A personal [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server. It exposes a
profile — bio, skills, projects, work experience — as structured tools that any MCP-compatible AI
client (Claude, ChatGPT, Codex) can query directly, secured with OAuth 2.1 and Dynamic Client
Registration (DCR, RFC 7591) so clients can connect without any manually-issued API key.
## Stack
- **Node.js + TypeScript + Express**
- **SQLite via `node:sqlite`** (Node's built-in driver, stable as of Node 22.5+) — no native
addon compilation required, so `npm install` works out of the box on any machine, including one
without build tools installed
- **[`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk)** for
the MCP protocol itself (Streamable HTTP transport) and for the full OAuth 2.1 authorization
server implementation (`mcpAuthRouter`), including Dynamic Client Registration
- **`jose`** for signing/verifying access tokens (stateless JWTs)
- **EJS** for the landing page and OAuth consent screen — no frontend build step
## Project layout
```
src/
config/env.ts Environment variable loading + validation (zod)
db/ SQLite schema, seed script, and typed repository functions
mcp/
server.ts Builds an McpServer and registers tools
tools/ get_bio, list_skills, list_projects, search_projects, list_experience
router.ts Mounts the Streamable HTTP transport at POST /mcp
oauth/
store.ts SQLite-backed OAuthRegisteredClientsStore (handles DCR persistence)
provider.ts Implements the SDK's OAuthServerProvider interface
tokens.ts JWT access tokens + hashed, rotating refresh tokens
consent.ts Signs/verifies the short-lived consent-screen round trip
web/
routes.ts Landing page (GET /) and consent decision handler (POST /consent)
views/ landing.ejs, consent.ejs
app.ts Wires everything into one Express app
server.ts Entry point
public/style.css Landing page styling
```
## Getting started
```bash
npm install
cp .env.example .env
```
Generate a signing secret and put it in `.env` as `TOKEN_SIGNING_SECRET`:
```bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
```
Edit `src/db/seed.ts` with your real name, bio, skills, projects, and experience, then run:
```bash
npm run seed
npm run dev
```
The server listens on `http://localhost:3000` by default. Visit it in a browser to see the landing
page, or run `npm run inspector` in another terminal to poke the MCP endpoint directly with the
official [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
## Scripts
| Script | Purpose |
| ------------------- | ------------------------------------------------ |
| `npm run dev` | Run with hot reload (`tsx watch`) |
| `npm run build` | Compile TypeScript to `dist/` |
| `npm start` | Run the compiled build (`node dist/server.js`) |
| `npm run seed` | (Re-)populate the database from `src/db/seed.ts` |
| `npm run typecheck` | `tsc --noEmit` |
| `npm run lint` | ESLint |
| `npm run format` | Prettier (writes) |
| `npm run inspector` | Launch the MCP Inspector dev tool |
## How the OAuth / DCR flow works
This server is both the MCP resource server and its own OAuth 2.1 authorization server, built on
the SDK's `mcpAuthRouter`:
1. **Discovery** — clients fetch `/.well-known/oauth-authorization-server` and
`/.well-known/oauth-protected-resource/mcp` to learn where to register and authenticate.
2. **Registration (DCR)** — `POST /register` lets any client dynamically obtain a `client_id`
(RFC 7591), no pre-registration or manual API key needed.
3. **Authorization** — `GET /authorize` renders a consent screen (`src/web/views/consent.ejs`).
The pending request (client, redirect URI, PKCE challenge, scope, state) is carried in a
short-lived signed JWT in a hidden form field rather than server-side session state, since
there's nothing else to persist between the redirect and the user's click.
4. **Consent decision** — `POST /consent` issues a one-time authorization code (stored in SQLite,
60s TTL by default) and redirects back to the client's `redirect_uri`.
5. **Token exchange** — `POST /token` exchanges the code (with PKCE verification handled by the
SDK itself) for an **access token** (a signed, stateless JWT, 1 hour TTL by default) and a
**refresh token** (opaque, hashed at rest, single-use with rotation — reusing a spent refresh
token fails immediately).
6. **Calling the server** — `POST /mcp` requires `Authorization: Bearer <access_token>`, verified
by `requireBearerAuth` against the same provider.
Trade-off worth knowing: because access tokens are stateless JWTs, they can't be individually
revoked before they expire — only refresh tokens are revocable. Given the short (1 hour) default
TTL, this is a standard and acceptable trade-off, not an oversight.
### Helmet gotchas for OAuth servers specifically
Helmet's secure-by-default headers ship configured for a typical web app, not an OAuth
authorization server, and two of its defaults will silently break real MCP/OAuth clients while
looking completely fine under `curl` (since `curl` doesn't enforce any browser-side policy):
- **CSP `form-action 'self'`** blocks the browser from following the redirect `/consent` issues
after an Allow/Deny decision, since that redirect must go to whatever `redirect_uri` the
connecting client registered — a different origin, by design. The POST completes correctly
server-side (you'll see a `302` in your logs) but the browser silently refuses to follow it and
reverts to the previous page. Fixed here by allowing `formAction: ["'self'", '*']`.
- **`Cross-Origin-Opener-Policy: same-origin`** severs the `window.opener` relationship the moment
a popup/redirected window navigates to this origin, which breaks clients that track flow
completion via `window.opener` or `postMessage`. Fixed here by disabling COOP entirely
(`crossOriginOpenerPolicy: false`).
Both are set in `src/app.ts`. If you're deploying behind a proxy (Render, most PaaS hosts),
also see the `trust proxy` note there — without it, `express-rate-limit` can't safely read
`X-Forwarded-For` and throws on every rate-limited route.
## Testing / connecting an MCP client
All of the methods below point at the same URL — `<deployed-url>/mcp` (e.g.
`https://selfmcp.onrender.com/mcp`) — and each one auto-discovers the OAuth metadata and completes
DCR + the consent flow on first connection; no manual API key needed.
If the server has been idle, the first request can take 50+ seconds to wake up — see
[Render (free tier)](#render-free-tier) below. That's expected, not a failure.
### Fastest: no account needed (MCP Inspector)
The official [MCP Inspector](https://github.com/modelcontextprotocol/inspector) can call every
tool directly, with no Claude subscription, ChatGPT plan, or OpenAI account required:
```bash
npx @modelcontextprotocol/inspector
```
In the UI: **Add Server** → transport **Streamable HTTP** → paste the `/mcp` URL → toggle the
server on → click **Allow** on the consent screen that opens → open the **Tools** tab and try
`get_bio` or `list_skills` directly.
### Claude Desktop / Claude Code
Add a remote connector via Settings → Connectors, or in `claude_desktop_config.json`:
```json
{
"mcpServers": {
"selfmcp": { "url": "https://your-deployed-url/mcp" }
}
}
```
### ChatGPT
This one has a real, non-obvious prerequisite step — custom connectors are gated behind
"Developer mode", which is off by default:
1. Settings → Security and login → scroll to **Developer mode** → turn it on (labeled "elevated
risk" — it just means you can add your own connectors, not a security downgrade for your account)
2. Go to the **Plugins** page → click the **+** next to the search box → **Add manually**
3. Server URL: the `/mcp` URL; Authentication: **OAuth** (default) → check the risk
acknowledgment → **Create**
4. Click **Sign in**, then **Allow** on the consent screen
### Codex CLI
```bash
npm install -g @openai/codex
codex mcp add selfmcp --url https://your-deployed-url/mcp
```
This registers the server via DCR and opens a browser to the consent screen automatically — click
**Allow** and it's connected. (`codex mcp get selfmcp` confirms it afterward.)
### Once connected, try asking
- "What's this person's professional background?"
- "List their skills relevant to a backend engineering role."
- "Search their projects for anything related to TypeScript or MCP."
## Deployment
Any host that can run a persistent Node process works (Render, Railway, Fly.io, a small VM). Avoid
serverless/functions platforms — the Streamable HTTP transport and SSE want a long-lived
connection, and the SQLite file needs a persistent disk.
1. Set `BASE_URL` to your real HTTPS URL (OAuth requires HTTPS in production; only `localhost` is
exempted).
2. Set `TOKEN_SIGNING_SECRET` to a real random secret (never reuse the `.env.example` placeholder).
3. Make sure the volume/disk backing `DATABASE_PATH` persists across restarts.
4. Run `npm run build && npm run seed && npm start`, or use the included `Dockerfile`.
### Render (free tier)
A `render.yaml` Blueprint is included: on Render, **New + → Blueprint**, connect this repo, and it
configures the service automatically (build command, start command, and a generated
`TOKEN_SIGNING_SECRET`).
The free plan has no persistent disk — its filesystem resets not just on every deploy but every
time the service wakes up from idle (it spins down after ~15 minutes of inactivity). Two
consequences of that, and how this project handles them:
- **Profile/skills/projects/experience data** would be lost on every wake-up. `render.yaml` works
around this by running `npm run seed` as part of the start command (not just at build time), so
this static data is restored every time the process boots.
- **OAuth client registrations and refresh tokens** are dynamic, so they can't be "reseeded" the
same way — they're genuinely wiped on each wake-up. In practice this just means an MCP client
that registered before a spin-down will get an `invalid_client` error on its next call and needs
to redo Dynamic Client Registration, which is expected, spec-compliant behavior for a client
talking to a server that doesn't guarantee permanent client storage — not a bug. If this matters
for your use case, move to a paid Render plan with a persistent disk instead.
## Known dependency advisory
`npm audit` flags a moderate path-traversal issue in `@hono/node-server` (a transitive dependency
of `@modelcontextprotocol/sdk`'s static-file-serving helper). This project never uses that
static-serving code path — only the SDK's protocol/transport and auth modules — so it isn't
exploitable here. Revisit this once the SDK bumps its `@hono/node-server` dependency.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues