Skip to main content
MCP

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/mcp

OAuth discovery is served at /.well-known/oauth-protected-resource and the authorization server metadata lives at your Lovable Cloud auth issuer.

OAuth consent flow

  1. Client (ChatGPT/Claude) fetches protected-resource metadata from https://your-app.lovable.app/.well-known/oauth-protected-resource.
  2. Client performs dynamic client registration against the discovered authorization server.
  3. User is redirected to https://your-app.lovable.app/.lovable/oauth/consent?authorization_id=….
  4. If not signed in, they land on /auth with a preserved nextparameter and return to the same authorization_id after magic-link sign-in.
  5. User taps Approve. Supabase issues an authorization code that the client exchanges for a bearer token.
  6. 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_id from 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 check has_role(uid, 'admin').
  • You cannot swipe on or endorse yourself.

Tool catalog

get_my_profile

Read the signed-in user's profile row.

Request
{}
Response
{
  "profile": {
    "id": "…",
    "name": "Priya",
    "bio_polished": "…",
    "onboarding_complete": true
  }
}

list_my_matches

List active matches for the caller.

Request
{}
Response
{
  "matches": [
    { "id": "…", "user_a": "…", "user_b": "…", "created_at": "…" }
  ]
}

list_messages

Read messages in a match the caller participates in.

Request
{ "match_id": "b1e2…-uuid" }
Response
{
  "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.

Request
{
  "match_id": "b1e2…-uuid",
  "body": "Would love to grab coffee next week.",
  "include_context": true,
  "context_limit": 5,
  "metadata": { "source": "chatgpt", "intent": "reply" }
}
Response
{
  "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.

Request
{ "only_unread": true, "limit": 20 }
Response
{
  "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.

Request
{ "status": "open", "limit": 25 }
Response
{
  "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.

Request
{
  "report_id": "8f6a…-uuid",
  "status": "resolved",
  "resolution_note": "Verified duplicate account; account removed."
}
Response
{
  "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.

Request
{
  "endorsed_id": "9c1d…-uuid",
  "relationship": "college friend, 8 years",
  "note": "Priya is genuine, grounded, and knows what she wants in a partner."
}
Response
{
  "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.

Request
{
  "swipee_id": "4a2b…-uuid",
  "direction": "like",
  "reason": "Shared values on family and career."
}
Response
{
  "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
}