Connect an AI agent to 2nd Degree
2nd Degree exposes a Model Context Protocol server so ChatGPT, Claude, Codex, Cursor and other MCP clients can act as you inside the app. Every tool runs under your OAuth session, scoped by row-level security.
Endpoint
Add this URL as a Streamable HTTP MCP server in your client:
https://your-app.lovable.app/mcpOAuth discovery is served at /.well-known/oauth-protected-resource and the authorization server metadata lives at your Lovable Cloud auth issuer.
OAuth consent flow
- Client (ChatGPT/Claude) fetches protected-resource metadata from
https://your-app.lovable.app/.well-known/oauth-protected-resource. - Client performs dynamic client registration against the discovered authorization server.
- User is redirected to
https://your-app.lovable.app/.lovable/oauth/consent?authorization_id=…. - If not signed in, they land on
/authwith a preservednextparameter and return to the sameauthorization_idafter magic-link sign-in. - User taps Approve. Supabase issues an authorization code that the client exchanges for a bearer token.
- Every subsequent MCP call carries
Authorization: Bearer <token>; tools call Postgres as that user.
Deny returns the user to the client with an access_denied error. Tokens are verified against the direct Supabase issuer — never a proxy URL.
Authorization model
- All tools read the caller's
user_idfrom the verified token — never from tool input. - Data access uses your row-level security policies; tools cannot see rows you cannot see.
- Admin-only tools (e.g.
update_report_status) additionally checkhas_role(uid, 'admin'). - You cannot swipe on or endorse yourself.
Tool catalog
get_my_profile
Read the signed-in user's profile row.
{}{
"profile": {
"id": "…",
"name": "Priya",
"bio_polished": "…",
"onboarding_complete": true
}
}list_my_matches
List active matches for the caller.
{}{
"matches": [
{ "id": "…", "user_a": "…", "user_b": "…", "created_at": "…" }
]
}list_messages
Read messages in a match the caller participates in.
{ "match_id": "b1e2…-uuid" }{
"messages": [
{
"id": "…",
"sender_id": "…",
"body": "Hey, saw we both like hiking!",
"created_at": "2026-07-27T12:00:00Z",
"read_at": null
}
]
}send_message
Send a message in a match. Optionally return recent thread context.
{
"match_id": "b1e2…-uuid",
"body": "Would love to grab coffee next week.",
"include_context": true,
"context_limit": 5,
"metadata": { "source": "chatgpt", "intent": "reply" }
}{
"message": {
"id": "…",
"match_id": "b1e2…",
"sender_id": "…",
"body": "Would love to grab coffee next week.",
"delivered_at": "…"
},
"context": [ /* last 5 messages, oldest first */ ],
"metadata": { "source": "chatgpt", "intent": "reply" },
"attachments_supported": false
}Attachments and metadata are accepted for forward-compatibility. Only the body is persisted today; metadata is echoed back so agents can round-trip request state.
list_my_notifications
Show recent in-app notifications for the caller.
{ "only_unread": true, "limit": 20 }{
"notifications": [
{ "id": "…", "kind": "match", "title": "New match", "link": "/chat/…", "read_at": null }
]
}list_my_reports
List abuse/safety reports the caller can see. Admins see all; users see only reports they filed.
{ "status": "open", "limit": 25 }{
"reports": [
{
"id": "…",
"reporter_id": "…",
"reported_id": "…",
"reason": "spam",
"status": "open",
"created_at": "…"
}
]
}update_report_status
Admin-only. Move a report through the triage workflow and attach a resolution note.
{
"report_id": "8f6a…-uuid",
"status": "resolved",
"resolution_note": "Verified duplicate account; account removed."
}{
"report": {
"id": "8f6a…",
"status": "resolved",
"resolution_note": "Verified duplicate account; account removed.",
"updated_at": "…"
}
}Callers without the admin role receive: Forbidden: admin role required to update reports.
create_endorsement
Vouch for another user (seriousness signal). endorser_id is always the OAuth caller.
{
"endorsed_id": "9c1d…-uuid",
"relationship": "college friend, 8 years",
"note": "Priya is genuine, grounded, and knows what she wants in a partner."
}{
"endorsement": {
"id": "…",
"endorser_id": "…",
"endorsed_id": "9c1d…",
"relationship": "college friend, 8 years",
"created_at": "…"
}
}Endorsements are one-directional and cannot be self-directed.
create_swipe
Record a like or pass on a candidate profile.
{
"swipee_id": "4a2b…-uuid",
"direction": "like",
"reason": "Shared values on family and career."
}{
"swipe": {
"id": "…",
"swiper_id": "…",
"swipee_id": "4a2b…",
"direction": "like",
"created_at": "…"
},
"matched": true
}matched=true means the other user had already liked you — a match row exists. Call list_my_matches to fetch it.
Error shape
All tools return MCP content. Failures set isError: true:
{
"content": [{ "type": "text", "text": "Forbidden: admin role required to update reports." }],
"isError": true
}