An MCP (Model Context Protocol) server that exposes information about Utkarsh — bio, skills, work experience, and portfolio projects — to any MCP-compatible client (Claude, Claude Code, ChatGPT, Codex, etc.).
It ships two transports from one shared server implementation:
| Transport | File | Used by |
|---|---|---|
| stdio | src/transports/stdio.ts |
Local clients that launch the server as a subprocess: Claude Desktop, Claude Code, Codex CLI |
| Streamable HTTP (remote) | src/transports/http.ts |
Remote clients that connect over a URL: ChatGPT connectors, remote Claude/Claude Code, any HTTP-based MCP client |
The remote HTTP transport is protected with OAuth 2.0, including Dynamic Client Registration (DCR, RFC 7591) — so a new MCP client can self-register and connect without any manual "create an API key" step, exactly as the MCP Authorization spec expects for remote servers.
Live deployment: https://about-utkarsh-mcp.onrender.com/mcp
Repo: https://github.com/utkarsh21123/about-utkarsh-mcp
src/
data/profile.ts # Single source of truth: bio, skills, experience, projects
mcpServer.ts # Shared MCP server: tools + one resource, used by both transports
transports/
stdio.ts # Local subprocess transport
http.ts # Remote Streamable HTTP transport (OAuth-protected)
oauth/
store.ts # In-memory client/auth-code store
router.ts # DCR, authorize (PKCE), token, discovery metadata endpoints
get_bio— name, title, location, experience summary, linksget_skills— skills by category (languages/frontend/backend/databases/infra/spoken)get_experience— work historylist_projects— all portfolio projects (summary)get_project_details— full detail for one named projectget_contact— how to reach out
Plus one resource: profile://utkarsh/full (the whole profile as JSON).
- Client discovers the server via
GET /.well-known/oauth-protected-resource(RFC 9728) andGET /.well-known/oauth-authorization-server(RFC 8414). - Client dynamically registers itself:
POST /register(RFC 7591) — no pre-shared client id/secret needed. - Client runs the standard Authorization Code + PKCE flow:
GET /authorize(a one-click "Approve" page — there's no personal login here, just consent to read a public profile) →POST /token. - Client calls
POST/GET/DELETE /mcpwithAuthorization: Bearer <token>.
This was verified end-to-end (registration → authorize → token exchange →
authenticated initialize and tools/call) both locally and against the live
Render deployment.
- Node.js 18+ (tested on Node 22) and npm
gitcurl(for manual testing) — optional but recommended
git clone https://github.com/utkarsh21123/about-utkarsh-mcp.git
cd about-utkarsh-mcp
npm installnpm run buildThis compiles src/ (TypeScript) to dist/ (JavaScript) via tsc. Re-run this
any time you edit a .ts file — the running server uses the compiled dist/ output.
npm run start:stdioYou should see, on stderr (not stdout — stdout is reserved for the MCP JSON-RPC protocol):
about-utkarsh-mcp: stdio server ready
It's now waiting for a client to talk to it over stdin/stdout. Two ways to test:
Option A — connect a real client. Add this to Claude Desktop's
claude_desktop_config.json or Claude Code's .mcp.json:
{
"mcpServers": {
"about-utkarsh": {
"command": "node",
"args": ["/absolute/path/to/about-utkarsh-mcp/dist/transports/stdio.js"]
}
}
}Or with the Claude Code CLI:
claude mcp add about-utkarsh -- node /absolute/path/to/about-utkarsh-mcp/dist/transports/stdio.jsRestart the client, then ask it something like "What are Utkarsh's skills?" and confirm it invokes a tool (not just answers from general knowledge).
Option B — manual JSON-RPC test, no client needed:
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}\n{"jsonrpc":"2.0","method":"notifications/initialized"}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_bio","arguments":{}}}\n' | node dist/transports/stdio.jsYou should get back an initialize response followed by a tools/call result
containing your bio as JSON.
npm run start:httpDefaults to port 3000. You'll see:
about-utkarsh-mcp: HTTP server listening on port 3000
MCP endpoint: http://localhost:3000/mcp
OAuth discovery: http://localhost:3000/.well-known/oauth-authorization-server
Auth required: true
To test without dealing with OAuth first (useful while developing):
REQUIRE_AUTH=false npm run start:http
curl -s -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'With the server running (REQUIRE_AUTH=true, the default), in a second terminal:
URL=http://localhost:3000
# 1. Health check
curl -s $URL/health
# 2. Discovery metadata
curl -s $URL/.well-known/oauth-authorization-server
curl -s $URL/.well-known/oauth-protected-resource
# 3. Confirm auth is enforced (expect 401)
curl -s -i -X POST $URL/mcp -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -6
# 4. Dynamic Client Registration
REG=$(curl -s -X POST $URL/register -H "Content-Type: application/json" -d '{
"client_name": "Test Client",
"redirect_uris": ["https://client.example.com/callback"],
"token_endpoint_auth_method": "none"
}')
echo "$REG"
CLIENT_ID=$(echo "$REG" | node -e "process.stdin.on('data',d=>console.log(JSON.parse(d).client_id))")
# 5. PKCE challenge/verifier
VERIFIER=$(node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))")
CHALLENGE=$(node -e "console.log(require('crypto').createHash('sha256').update('$VERIFIER').digest('base64url'))")
# 6. Authorize + approve (simulates clicking "Approve" in the browser)
LOC=$(curl -s -o /dev/null -w '%{redirect_url}' "$URL/authorize/approve?client_id=$CLIENT_ID&redirect_uri=https://client.example.com/callback&state=xyz&code_challenge=$CHALLENGE&code_challenge_method=S256")
CODE=$(node -e "console.log(new URL('$LOC').searchParams.get('code'))")
# 7. Exchange code for token
TOKEN_RESP=$(curl -s -X POST $URL/token -H "Content-Type: application/json" \
-d "{\"grant_type\":\"authorization_code\",\"code\":\"$CODE\",\"redirect_uri\":\"https://client.example.com/callback\",\"client_id\":\"$CLIENT_ID\",\"code_verifier\":\"$VERIFIER\"}")
echo "$TOKEN_RESP"
ACCESS_TOKEN=$(echo "$TOKEN_RESP" | node -e "process.stdin.on('data',d=>console.log(JSON.parse(d).access_token))")
# 8. Authenticated MCP call
curl -s -i -X POST $URL/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'Expect a 200 OK on the last call with an mcp-session-id header and the
server's initialize payload in the body.
npm run dev:stdio # tsx, runs src/transports/stdio.ts directly
npm run dev:http # tsx, runs src/transports/http.ts directlyThese use tsx to run the TypeScript source directly — no npm run build step
needed between edits.
The included Dockerfile builds a production image; render.yaml is a one-click
blueprint for Render, but this deploys the same way on
Railway, Fly.io, or any container host.
- Push this repo to GitHub.
- On Render: New → Blueprint, point it at the repo —
render.yamlconfigures everything (including generating a randomJWT_SECRET). - Once deployed, set
PUBLIC_BASE_URLto the assignedhttps://<app>.onrender.comURL in the service's environment settings and it'll redeploy automatically, so OAuth metadata advertises the correct absolute URLs. - Your MCP endpoint is
https://<app>.onrender.com/mcp.
Note on the free tier: Render's free instances spin down after a period of inactivity and take up to ~50 seconds to spin back up on the next request. If you need consistently fast/always-on responses, upgrade the Render service to a paid instance type.
docker build -t about-utkarsh-mcp .
docker run -p 3000:3000 \
-e JWT_SECRET=$(node -e "console.log(require('crypto').randomBytes(48).toString('hex'))") \
-e PUBLIC_BASE_URL=https://your-deployed-domain.example.com \
about-utkarsh-mcpSee step 3a above.
In Claude's connector settings, add a custom connector pointing at:
https://about-utkarsh-mcp.onrender.com/mcp
Claude auto-discovers the OAuth metadata, registers itself via DCR, and walks you through the one-click approval — no manual credentials required.
Same idea: add a custom connector pointing at the same /mcp URL. ChatGPT
performs the same discovery → DCR → OAuth flow.
- Codex CLI (local): same config as Claude Code, pointing at
node dist/transports/stdio.js. - Codex web/cloud (remote): use the deployed
/mcpURL as above.
- Storage for registered clients/auth codes is in-memory (
src/oauth/store.ts) — fine for an assessment/demo; swap in a real database for production so registrations survive restarts and multiple instances. - Access tokens are short-lived HS256 JWTs (1 hour). There's no user login screen because the "resource" being protected is a public profile with no per-user data — the approval screen exists to satisfy the OAuth consent step the MCP spec expects, not to gate a private account.
- PKCE (RFC 7636) is enforced for public clients (
token_endpoint_auth_method: none), which is what DCR-registered MCP clients typically use.
Everything the tools return lives in src/data/profile.ts. Update that file,
npm run build, and redeploy to change what the server tells clients about Utkarsh.