{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://enoniva.com/connectors/schema.json",
  "title": "Eno’niva data connector: tools eno_get_contact_changes, eno_connect, eno_disconnect (protocol enoniva.contacts/1)",
  "description": "eno_get_contact_changes: use $defs.input as the tool's inputSchema and $defs.output as its outputSchema. Directory services also list eno_connect ($defs.connect_input / $defs.connect_output) and, optionally, eno_disconnect ($defs.disconnect_input / $defs.disconnect_output). Eno’niva ignores unknown fields and drops values that break these rules (the rest of the change is kept when it is still usable).",
  "$defs": {
    "input": {
      "type": "object",
      "additionalProperties": false,
      "required": ["protocol", "user", "sequence", "since_sequence"],
      "properties": {
        "protocol": { "const": "enoniva.contacts/1" },
        "user": { "$ref": "#/$defs/user" },
        "sequence": { "type": "integer", "minimum": 1, "description": "This call's number: last acknowledged sequence + 1. A retry repeats the same number." },
        "since_sequence": { "type": "integer", "minimum": 0, "description": "Return changes made after the call with this number. 0 = full export of everything that exists now." },
        "page_cursor": { "type": "string", "maxLength": 1000, "description": "The next_page_cursor of the previous page of the same call." }
      }
    },
    "user": {
      "type": "object",
      "additionalProperties": false,
      "description": "Who the call is for. Personal and per-member calls send a verified phone and/or email; workspace-mode calls send only workspace_domain; directory connections send only enoniva_user (linked in eno_connect).",
      "properties": {
        "phone": { "type": "string", "pattern": "^\\+[1-9][0-9]{6,14}$", "description": "E.164, verified by Eno’niva" },
        "phone_verified": { "const": true },
        "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Lowercase, verified by Eno’niva" },
        "email_verified": { "const": true },
        "workspace_domain": { "type": "string", "maxLength": 253, "description": "Workspace mode (company connectors, optional for providers): the company's primary domain" },
        "enoniva_user": { "$ref": "#/$defs/enoniva_user" }
      },
      "anyOf": [
        { "required": ["phone", "phone_verified"] },
        { "required": ["email", "email_verified"] },
        { "required": ["workspace_domain"] },
        { "required": ["enoniva_user"] }
      ]
    },
    "enoniva_user": {
      "type": "string", "pattern": "^enu_[A-Za-z0-9_-]{32}$",
      "description": "Directory services: an opaque id for one Eno’niva member, stable for your service and different for every other service. Not an Eno’niva database id; store it with the account you linked in eno_connect."
    },
    "connect_input": {
      "type": "object",
      "additionalProperties": false,
      "required": ["protocol", "enoniva_user", "emails", "phones"],
      "description": "eno_connect: a member clicked Connect in Eno’niva and agreed to share these details with you. Every email and phone was verified by Eno’niva (unverified ones are never sent). Never return data for identifiers that aren't in this call.",
      "properties": {
        "protocol": { "const": "enoniva.contacts/1" },
        "enoniva_user": { "$ref": "#/$defs/enoniva_user" },
        "emails": { "type": "array", "maxItems": 5, "items": { "type": "string", "format": "email", "maxLength": 254 }, "description": "Verified, lowercase (may be empty)" },
        "phones": { "type": "array", "maxItems": 5, "items": { "type": "string", "pattern": "^\\+[1-9][0-9]{6,14}$" }, "description": "Verified, E.164 (may be empty)" },
        "user_name": { "type": "string", "maxLength": 200, "description": "The member's name in Eno’niva (display only; never match on it)" }
      }
    },
    "connect_output": {
      "type": "object",
      "required": ["status"],
      "properties": {
        "status": { "enum": ["not_registered", "consent_required", "connected"], "description": "not_registered: no account here matches. consent_required: the account must confirm on your side first (recommended for anything sensitive). connected: you linked enoniva_user to the account; Eno’niva starts importing with eno_get_contact_changes." },
        "consent_url": { "type": "string", "pattern": "^https://", "maxLength": 1000, "description": "Required with consent_required: a page in your product where the signed-in account approves the link. Eno’niva opens it and calls eno_connect again until you answer connected." },
        "account_label": { "type": "string", "maxLength": 100, "description": "connected: how the member recognizes the account (\"Dana @ Acme team\")" },
        "total_contacts": { "type": "integer", "minimum": 0, "description": "connected: how many contacts the first import will bring (for the progress bar)" }
      },
      "if": { "properties": { "status": { "const": "consent_required" } } },
      "then": { "required": ["consent_url"] }
    },
    "disconnect_input": {
      "type": "object",
      "required": ["protocol", "enoniva_user"],
      "description": "eno_disconnect (optional tool): the member disconnected your service in Eno’niva. Forget the link; Eno’niva won't call for this enoniva_user again until they connect anew.",
      "properties": { "protocol": { "const": "enoniva.contacts/1" }, "enoniva_user": { "$ref": "#/$defs/enoniva_user" } }
    },
    "disconnect_output": { "type": "object", "properties": { "ok": { "const": true } } },
    "output": {
      "type": "object",
      "required": ["sequence", "changes"],
      "properties": {
        "sequence": { "type": "integer", "description": "Echo of the input sequence" },
        "next_page_cursor": { "type": "string", "maxLength": 1000, "description": "Present when more pages follow for this call. Omit (or null/empty) on the last page." },
        "changes": { "type": "array", "maxItems": 5000, "items": { "$ref": "#/$defs/change" } }
      }
    },
    "change": {
      "type": "object",
      "required": ["op", "external_id", "updated_at"],
      "properties": {
        "op": { "enum": ["upsert", "delete"] },
        "external_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "Your stable id for this contact (never reused for someone else)" },
        "updated_at": { "type": "string", "format": "date-time" },
        "contact": { "$ref": "#/$defs/contact", "description": "Required for upsert: the full current record, not a diff" }
      },
      "if": { "properties": { "op": { "const": "upsert" } } },
      "then": { "required": ["contact"] }
    },
    "contact": {
      "type": "object",
      "description": "Needs at least one of full_name, phones, emails.",
      "properties": {
        "external_id": { "type": "string", "maxLength": 200, "description": "Optional; same as the change's external_id" },
        "full_name": { "type": "string", "maxLength": 200 },
        "phones": { "type": "array", "maxItems": 20, "items": { "type": "string", "pattern": "^\\+[1-9][0-9]{6,14}$" } },
        "emails": { "type": "array", "maxItems": 20, "items": { "type": "string", "format": "email", "maxLength": 254 } },
        "company": { "type": "string", "maxLength": 200 },
        "title": { "type": "string", "maxLength": 200 },
        "linkedin_url": { "type": "string", "pattern": "^https://([a-z]{2,3}\\.)?(www\\.)?linkedin\\.com/(in|pub|company)/", "maxLength": 300 },
        "city": { "type": "string", "maxLength": 100 },
        "country": { "type": "string", "description": "ISO 3166-1 alpha-2 preferred (\"IL\"); English names are accepted" },
        "notes": { "type": "string", "maxLength": 2000, "description": "Accepted but ignored by default (v1 never imports notes)" },
        "tags": { "type": "array", "maxItems": 20, "items": { "type": "string", "maxLength": 50 } },
        "last_interaction_at": { "type": "string", "format": "date-time" },
        "interaction_counts": {
          "type": "object",
          "description": "Totals so far (not increments), 0..1,000,000",
          "properties": {
            "calls": { "type": "integer", "minimum": 0 },
            "messages": { "type": "integer", "minimum": 0 },
            "emails": { "type": "integer", "minimum": 0 },
            "meetings": { "type": "integer", "minimum": 0 }
          }
        },
        "owner_relationship": { "enum": ["customer", "client", "prospect", "lead", "partner", "vendor", "supplier", "investor", "advisor", "colleague", "ex_colleague", "friend", "family", "acquaintance", "community", "other"] },
        "url": { "type": "string", "pattern": "^https://", "maxLength": 500, "description": "Deep link to this contact in your system (shown only to its owner)" }
      }
    },
    "error": {
      "description": "Return a tool result with isError: true and structuredContent { error: {...} }",
      "type": "object",
      "required": ["error"],
      "properties": {
        "error": {
          "type": "object",
          "required": ["code", "message"],
          "properties": {
            "code": { "enum": ["unknown_user", "not_authorized", "resync_required", "cursor_expired", "rate_limited", "unsupported_protocol", "bad_request", "internal"] },
            "message": { "type": "string", "maxLength": 300 },
            "retry_after": { "type": "integer", "minimum": 0, "maximum": 86400, "description": "Seconds (rate_limited)" }
          }
        }
      }
    }
  }
}
