{"openapi":"3.1.0","info":{"title":"Presify public API","version":"26.9.4","summary":"Microsoft Teams presence analytics: OAuth 2.0 authorization server, MCP endpoint, discovery documents.","description":"Presify turns Microsoft Teams presence history into work-pattern analytics for Microsoft 365 admins and team leads.\n\nThis document describes the PUBLIC machine surface. Tenant data is read through the Model Context Protocol endpoint (`POST /api/mcp`), which authenticates with OAuth 2.0 (authorization code + PKCE, dynamic client registration) or a personal access token. Every MCP tool is a pure read, scoped to what the connected person may see in Presify. There is no REST resource API for tenant data; the browser app's session-cookie routes are not a supported integration surface and are not listed here.\n\nPublic documents: `/llms.txt` (site index for AI assistants, with when-to-use guidance), `/sitemap.xml`, `/.well-known/security.txt`. Public pages answer `Accept: text/markdown` with a Markdown representation (`Vary: Accept`). Unknown paths return a real 404 (JSON under `/api/`, Markdown for machine clients, HTML for browsers); an unsupported method on a `/api/` route returns a JSON 405 with an `Allow` header.\n\n## Versioning and deprecation\n\nThe paths in this document are permanent contracts: `/api/mcp`, the `/api/oauth/*` endpoints and the `/.well-known/*` documents never move or change shape incompatibly. There is no URL version segment. The MCP endpoint is versioned by the `MCP-Protocol-Version` header (the server negotiates down to the newest revision at or below the one requested, and keeps serving older supported revisions). If an operation or protocol revision is ever retired, every affected response carries `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers plus a `Link` with rel=\"deprecation\" for at least 90 days before removal, the operation is marked `deprecated: true` here first, and after the sunset date it answers 410 Gone. Those headers are declared on every 2xx response below; none is sent today. New optional fields and new operations are added without notice. Full policy: https://presify.io/docs/api-versioning\n\n## Rate limits\n\nEvery `/api/mcp` response, including the unauthenticated handshake (initialize, ping, resources/list, resources/read) and the 401 challenge, carries IETF RateLimit header fields: `RateLimit-Policy` (both windows: 60 requests per minute per grant and 300 per minute per tenant, 2x burst) and `RateLimit` (remaining requests and seconds to full refill for the tighter window), plus the legacy `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` trio. A 429 adds `Retry-After`. Client registration is throttled per IP (429 with `Retry-After`).\n\nSupport: support@presify.io, Monday to Friday, 9am to 5pm US Eastern. Legal: https://presify.io/legal/terms, https://presify.io/legal/privacy.","termsOfService":"https://presify.io/legal/terms","contact":{"name":"Presify support","url":"https://presify.io/contact","email":"support@presify.io"}},"externalDocs":{"description":"Presify documentation","url":"https://presify.io/docs"},"servers":[{"url":"https://app.presify.io","description":"Canonical API host: OAuth 2.0 issuer and MCP resource."},{"url":"https://presify.io","description":"Public website. The discovery and document endpoints also answer here."}],"tags":[{"name":"discovery","description":"Machine-readable metadata (RFC 8414, RFC 9728, RFC 9116)."},{"name":"oauth","description":"OAuth 2.0 authorization server for the MCP connector."},{"name":"mcp","description":"Model Context Protocol endpoint (stateless Streamable HTTP, JSON responses)."},{"name":"health","description":"Liveness and readiness probes."},{"name":"documents","description":"Agent-facing documents and Markdown representations."}],"paths":{"/.well-known/oauth-authorization-server":{"get":{"operationId":"getAuthorizationServerMetadata","tags":["discovery"],"summary":"OAuth 2.0 authorization server metadata (RFC 8414)","description":"Endpoints, grant types and PKCE methods of the Presify authorization server. The issuer is https://app.presify.io; the same document is served on the public website host for discovery convenience.","responses":{"200":{"description":"Authorization server metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorizationServerMetadata"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}}}}},"/.well-known/oauth-authorization-server/api/mcp":{"get":{"operationId":"getAuthorizationServerMetadataForMcp","tags":["discovery"],"summary":"Authorization server metadata, path-inserted variant","description":"Identical to /.well-known/oauth-authorization-server. Served because several MCP clients apply RFC 9728 path insertion to the authorization-server URL when the protected resource lives at a path.","responses":{"200":{"description":"Authorization server metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorizationServerMetadata"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}}}}},"/.well-known/oauth-protected-resource":{"get":{"operationId":"getProtectedResourceMetadata","tags":["discovery"],"summary":"Protected resource metadata for the MCP endpoint (RFC 9728)","description":"Names the MCP resource URL, the authorization server that protects it, the bearer method and the supported scope.","responses":{"200":{"description":"Protected resource metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProtectedResourceMetadata"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}}}}},"/.well-known/oauth-protected-resource/api/mcp":{"get":{"operationId":"getProtectedResourceMetadataForMcp","tags":["discovery"],"summary":"Protected resource metadata, path-inserted variant","description":"Identical to /.well-known/oauth-protected-resource; this is the URL the 401 WWW-Authenticate challenge from /api/mcp points at.","responses":{"200":{"description":"Protected resource metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProtectedResourceMetadata"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}}}}},"/.well-known/mcp/server-card.json":{"get":{"operationId":"getMcpServerCard","tags":["discovery"],"summary":"MCP server card (pre-connection discovery)","description":"Server URL, transport, protocol revision, capabilities, OAuth details and the full read-only tool catalog with input schemas, so an agent can decide to connect before its first request. The live tools/list is still gated by plan and role.","responses":{"200":{"description":"Server card.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerCard"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}}}}},"/.well-known/mcp":{"get":{"operationId":"getMcpServerCardAlias","tags":["discovery"],"summary":"MCP server card, directory-style alias","description":"Identical to /.well-known/mcp/server-card.json, served at the path some clients probe first.","responses":{"200":{"description":"Server card.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerCard"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}}}}},"/.well-known/security.txt":{"get":{"operationId":"getSecurityTxt","tags":["discovery"],"summary":"Vulnerability disclosure policy (RFC 9116)","description":"Contact, policy and expiry lines for security researchers.","responses":{"200":{"description":"security.txt document.","content":{"text/plain":{"schema":{"type":"string"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}}}}},"/api/oauth/register":{"post":{"operationId":"registerOAuthClient","tags":["oauth"],"summary":"Dynamic client registration (RFC 7591)","description":"Open registration: no initial access token is required. Redirect URIs must be https, or http on localhost for development. Public clients (token_endpoint_auth_method \"none\") receive no secret; confidential clients receive a client_secret exactly once. Registration is throttled per IP and unused clients are cleaned up nightly.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientRegistrationRequest"}}}},"responses":{"201":{"description":"Client created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientRegistrationResponse"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}},"400":{"description":"Invalid registration request (invalid_client_metadata, invalid_redirect_uri).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Registration throttled for this IP.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/oauth/authorize":{"get":{"operationId":"authorize","tags":["oauth"],"summary":"Authorization endpoint (authorization code + PKCE)","description":"Browser entry point of the authorization-code flow. Validates the request, then sends the person to Microsoft sign-in (if needed) and the Presify consent page; on approval the browser is redirected to redirect_uri with `code` and `state`. PKCE with S256 is required. An unverified client_id or redirect_uri renders a local error page and never redirects (open-redirect guard); every later error redirects back to the client with `error` and `error_description`.","parameters":[{"name":"response_type","in":"query","required":true,"description":"Must be `code`.","schema":{"type":"string","enum":["code"]}},{"name":"client_id","in":"query","required":true,"description":"Client identifier from dynamic client registration (UUID).","schema":{"type":"string","format":"uuid"}},{"name":"redirect_uri","in":"query","required":true,"description":"One of the redirect URIs registered for the client (exact match).","schema":{"type":"string","format":"uri","maxLength":500}},{"name":"code_challenge","in":"query","required":true,"description":"PKCE code challenge (base64url, 43 to 128 characters).","schema":{"type":"string","minLength":43,"maxLength":128}},{"name":"code_challenge_method","in":"query","required":true,"description":"Must be `S256`.","schema":{"type":"string","enum":["S256"]}},{"name":"state","in":"query","required":false,"description":"Opaque client state, echoed back on the redirect.","schema":{"type":"string","maxLength":1024}},{"name":"scope","in":"query","required":false,"description":"Requested scope. Empty or `presify:read`; anything else is rejected.","schema":{"type":"string","maxLength":200}},{"name":"resource","in":"query","required":false,"description":"RFC 8707 resource indicator. Must normalize to https://app.presify.io/api/mcp.","schema":{"type":"string","format":"uri","maxLength":500}}],"responses":{"302":{"description":"Redirect to sign-in / consent, or back to redirect_uri with `code` + `state` (or `error`)."},"400":{"description":"Unverified client or redirect_uri: a local HTML error page (no redirect).","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/oauth/token":{"post":{"operationId":"token","tags":["oauth"],"summary":"Token endpoint","description":"Exchanges an authorization code (with PKCE code_verifier) or a refresh token for tokens. Accepts application/x-www-form-urlencoded or application/json. Client authentication: none (public clients), client_secret_basic, or client_secret_post. Authorization codes are single-use and live 5 minutes; a code presented twice revokes everything it minted (RFC 9700). Access tokens live 1 hour, refresh tokens rotate and live 30 days.","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/TokenRequest"}},"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}},"responses":{"200":{"description":"Token response (RFC 6749 §5.1).","headers":{"Cache-Control":{"description":"no-store","schema":{"type":"string"}},"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"invalid_request, invalid_grant, invalid_target or unsupported_grant_type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_client: unknown, disabled or mis-authenticated client.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/oauth/revoke":{"post":{"operationId":"revokeToken","tags":["oauth"],"summary":"Token revocation (RFC 7009)","description":"Revokes an access or refresh token and the grant behind it. Returns 200 whether or not the token was live (RFC 7009 §2.2), so a client cannot probe for valid tokens.","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/RevokeRequest"}}}},"responses":{"200":{"description":"Revocation processed.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}},"400":{"description":"Malformed request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/mcp":{"post":{"operationId":"mcpMessage","tags":["mcp"],"summary":"Model Context Protocol endpoint (JSON-RPC 2.0 over HTTP)","description":"Stateless Streamable HTTP transport in JSON-response mode: one JSON-RPC request (or batch) per POST, one JSON response, no server-sent stream and no Mcp-Session-Id. Supported protocol revisions, newest first: 2026-07-28, 2025-06-18, 2025-03-26 (the server negotiates DOWN to the newest revision at or below the one requested). Send `Accept: application/json` (and optionally text/event-stream); send the negotiated revision in `MCP-Protocol-Version` on every request after initialize.\n\nAuthenticate with a Bearer token: an OAuth access token from /api/oauth/token, or a personal access token minted in Settings, then Connections (365-day lifetime). The handshake and the public resources need no token: initialize, ping, resources/list, resources/templates/list, resources/read and notifications are served anonymously (rate limited per container). Every other method without a valid token answers 401 with a `WWW-Authenticate: Bearer resource_metadata=\"...\"` challenge (RFC 9728 §5.1). Authorization is re-checked live on every call: subscription state, the integrations.mcp plan feature, and the caller's current role.\n\nMethods: initialize, ping, tools/list, tools/call, prompts/list, prompts/get, resources/list, resources/templates/list, resources/read. Every tool is a pure read (readOnlyHint: true) and the tool list varies by plan and role, so call tools/list rather than assuming the catalog below. Resources are the public documents (llms.txt, the documentation pages as Markdown, this OpenAPI description); each URI is the document's real URL on the website. Tool results carry the payload both as text and as structuredContent. Dates are ISO 8601; durations are seconds.\n\nRate limits (token bucket, 2x burst): 60 requests per minute per grant and 300 per minute per tenant. A 429 carries Retry-After.\n\nCatalog (subject to plan/role gating):\n- `get_workspace_info`: Returns the connected Presify workspace at a glance: workspace name, plan tier with seat and retention limits, subscription and trial state, monitored-user counts (active, paused, pending enrollment), presence-collection health, and what the connected person may see. Call this first to orient yourself.\n- `list_monitored_users`: Lists the people monitored in this Presify workspace with their status, department, timezone, business hours, and flags (collection paused, legal hold). Filter by monitoring status (active by default; former users appear under removed statuses), department, or a name/email search. Paginated: up to 200 per page with offset. Use this to find a person before calling per-user tools.\n- `get_user`: Returns one monitored person: profile (name, email, department, timezone, business hours, monitoring status, flags), their latest presence reading, and their last 7 daily presence totals. Accepts a name, email, or Microsoft user id. For deeper trends use get_user_stats.\n- `get_user_stats`: Computes rich presence statistics for one person over the last 14 weeks (bounded by plan retention): this week and month with honest same-elapsed-point comparisons to the prior period, a weekly trend, business-hours versus after-hours split, typical day rhythm (median start and end, drift), uninterrupted focus blocks, meeting time, state mix, and longest online stretch. Accepts a name, email, or Microsoft user id.\n- `get_user_timeline`: Returns one person's presence timeline for one local calendar day: ordered segments with local start and end times, duration in seconds, and the presence status held in each (\"Available\", \"Busy\", \"In a call\", \"Away\", \"Offline\", and so on). \"No data\" marks spans where collection was not running, which is different from a confirmed Offline reading. Times are wall-clock in the person's own timezone; the response names the zone and the business-hours window. Days outside the plan retention window return no segments.\n- `get_current_presence`: Returns the latest Microsoft Teams presence reading for each actively monitored person: availability, activity, presence bucket, when it last changed, and whether they count as online right now (fresh reading within the last few minutes). Includes data-freshness info so you can caveat stale results. Optionally filter by department. Up to 500 people per call.\n- `get_presence_summary`: Sums presence time over a date range from nightly per-person daily rollups, grouped by person or by day. Returns seconds in each presence bucket (online, away, offline or no signal, in a call or presenting). Use for questions like \"how much was the team online last week\" or \"show daily totals for June\". Range up to 366 days; optionally restrict to specific people (Microsoft user ids) or one department. Days outside the plan retention window have no data.\n- `compare_users`: Compares 2 to 10 people side by side over a date range using whole-day presence totals from the nightly rollups: online, away, offline or no signal, and in-call seconds, plus days with data and daily averages. Range up to 92 days. Each entry accepts a name, email, or Microsoft user id. Note: totals are whole-day (not clipped to business hours); the in-app Compare page offers business-window clipping.\n- `get_coverage`: Builds a typical-week coverage grid: for each weekday hour, how many of the selected people are typically online (fractional headcount and percent of team), plus best and thinnest hours. Answers \"when is the team reachable\" and \"where are our coverage gaps\". Hard caps: at most 31 days of history and 50 people per call; scope the users list or department for bigger teams. Requires a plan with the Coverage feature.\n- `get_connection_health`: Reports the health of this workspace connection to Microsoft 365: whether presence collection is running (active, delayed, paused for reconsent, or suspended), the last successful poll time, how reliably Microsoft Graph has been answering over the chosen window (overall and per day), and when the Microsoft app credential expires. Use when data looks missing or stale, or for an operations checkup. Window up to 90 days.\n- `list_notifications`: Lists recent in-app workspace notices for admins: billing events, connection health changes, enrollment reminders, digest highlights. Up to 50, newest first; optionally unread only. Useful for \"anything needing attention in Presify?\".\n- `list_anomalies`: Lists flagged presence patterns (anomalies) with factual descriptions of what was measured: unbroken Available runs, missing idle time, overnight presence, no activity variation, robotic regularity, weekend uniformity. Filter by person, pattern type, date range, or open/dismissed. Up to 200, newest first. Descriptions state the observed pattern and computation only, never a cause or intent. To inspect a flagged day the way the app does, follow up with get_user_timeline using the entry's person and date.\n- `get_anomaly_summary`: Counts flagged presence patterns by type over a date range (open and dismissed separately), with the latest detection per type. Use for triage and trend questions like \"are overnight flags increasing\". Range up to 366 days.\n- `list_reports`: Lists recently generated report jobs (PDF scorecards and Excel workbooks): type, status, who requested them, when. Up to 50, newest first. Use get_report_download for a finished report file link.\n- `get_report_download`: Returns a fresh download link for a completed report by reportId (from list_reports). Links are valid for 15 minutes and are generated per request; ask again if one expires.\n- `list_scheduled_reports`: Lists this workspace recurring report schedules: report type, cadence in plain words, window, recipients, status, and the next run time. Answers \"when does my next report go out\".\n- `list_audit_events`: Lists this workspace audit trail: who did what and when (settings changes, enrollments, report requests, connections). Filter by action prefix or date range. Up to 100, newest first. Owner only, on plans with the audit log feature.\n- `search`: Searches this Presify workspace across monitored people (name, email, department), flagged presence patterns, and generated reports. Returns up to 20 results with namespaced ids for fetch. Built for research-style clients; prefer the specific tools when you know what you need.\n- `fetch`: Fetches the full record behind a search result id (user:, anomaly:, or report: prefixed). Companion to search for research-style clients.","security":[{"oauth2":["presify:read"]},{"personalAccessToken":[]}],"parameters":[{"name":"MCP-Protocol-Version","in":"header","required":false,"description":"The negotiated protocol revision (YYYY-MM-DD). Any date-shaped value is accepted; the response echoes the revision the server is serving.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/JsonRpcRequest"},{"type":"array","items":{"$ref":"#/components/schemas/JsonRpcRequest"},"minItems":1}]}}}},"responses":{"200":{"description":"JSON-RPC response (or batch). Tool-level failures are JSON-RPC results with isError, transport-level failures are JSON-RPC errors.","headers":{"MCP-Protocol-Version":{"description":"Protocol revision served.","schema":{"type":"string"}},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/JsonRpcResponse"},{"type":"array","items":{"$ref":"#/components/schemas/JsonRpcResponse"}}]}}}},"202":{"description":"A notification (no id) was accepted; no body.","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}},"400":{"description":"Malformed JSON-RPC or unsupported protocol revision.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcErrorResponse"}}}},"401":{"description":"Missing, expired or revoked bearer token on a method that needs one (anything but initialize, ping, resources/* and notifications).","headers":{"WWW-Authenticate":{"description":"Bearer challenge pointing at the protected-resource metadata.","schema":{"type":"string"}},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcErrorResponse"}}}},"403":{"description":"Token valid but the tenant's plan, subscription or the caller's role no longer permits MCP.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcErrorResponse"}}}},"429":{"description":"Rate limited.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcErrorResponse"}}}},"503":{"description":"The MCP kill switch is engaged (fails closed).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcErrorResponse"}}}}}},"get":{"operationId":"mcpStreamNotSupported","tags":["mcp"],"summary":"Not supported: no server-initiated stream","description":"Stateless mode issues no session and opens no SSE stream, which the MCP specification permits. Always 405 with `Allow: POST, OPTIONS` and a JSON method_not_allowed body.","responses":{"405":{"description":"Method not allowed.","headers":{"Allow":{"description":"Supported methods.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MethodNotAllowedError"}}}}}},"delete":{"operationId":"mcpSessionTeardownNotSupported","tags":["mcp"],"summary":"Not supported: no session to tear down","description":"No Mcp-Session-Id is ever issued, so there is nothing to delete. Always 405 with `Allow: POST, OPTIONS` and a JSON method_not_allowed body.","responses":{"405":{"description":"Method not allowed.","headers":{"Allow":{"description":"Supported methods.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MethodNotAllowedError"}}}}}}},"/api/health":{"get":{"operationId":"getHealth","tags":["health"],"summary":"Liveness probe","description":"Reports the running version. 503 with status \"maintenance\" while the platform is in maintenance mode.","responses":{"200":{"description":"Service is up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}},"503":{"description":"Maintenance mode.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/api/ready":{"get":{"operationId":"getReadiness","tags":["health"],"summary":"Readiness probe","description":"Checks the database path. 503 with status \"not-ready\" when a dependency is unavailable.","responses":{"200":{"description":"Ready.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadyResponse"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}},"503":{"description":"Not ready.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadyResponse"}}}}}}},"/llms.txt":{"get":{"operationId":"getLlmsTxt","tags":["documents"],"summary":"Site index for AI assistants (llmstxt.org)","description":"Curated Markdown summary of Presify: what it is, when to use it, how an agent connects, and links to every public page. Generated from the same sources as the pricing page and the blog, so it cannot disagree with them.","responses":{"200":{"description":"llms.txt document (Markdown, served as text/plain).","content":{"text/plain":{"schema":{"type":"string"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}}}}},"/openapi.json":{"get":{"operationId":"getOpenApiDocument","tags":["documents"],"summary":"This document","description":"OpenAPI 3.1 description of the public machine surface.","responses":{"200":{"description":"OpenAPI document.","content":{"application/json":{"schema":{"type":"object"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}}}}},"/sitemap.xml":{"get":{"operationId":"getSitemap","tags":["documents"],"summary":"XML sitemap of the public website","description":"Every public, indexable page, including the runtime-visible blog posts.","responses":{"200":{"description":"Sitemap.","content":{"application/xml":{"schema":{"type":"string"}}},"headers":{"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}}}}}},"/markdown/{path}":{"get":{"operationId":"getMarkdownRepresentation","tags":["documents"],"summary":"Markdown representation of a public page","description":"The Markdown body a public page serves when asked with `Accept: text/markdown` on its own URL (the preferred way). This direct URL is an alternate representation: it carries `X-Robots-Tag: noindex` and a rel=\"canonical\" Link header to the HTML page. Supported paths: the marketing pages, /blog and /blog/{slug}, /comparisons and /comparisons/{slug}, /legal/{doc}.","parameters":[{"name":"path","in":"path","required":true,"description":"The page path without the leading slash, e.g. `docs`, `legal/privacy`, `blog/how-to-get-a-teams-presence-report`.","schema":{"type":"string","pattern":"^[a-z0-9/-]+$"}}],"responses":{"200":{"description":"Markdown document.","headers":{"Vary":{"description":"Accept","schema":{"type":"string"}},"Link":{"description":"rel=\"canonical\" to the HTML page.","schema":{"type":"string"}},"Deprecation":{"$ref":"#/components/headers/Deprecation"},"Sunset":{"$ref":"#/components/headers/Sunset"}},"content":{"text/markdown":{"schema":{"type":"string"}}}},"404":{"description":"No such page. The body points at llms.txt, openapi.json and the sitemap.","content":{"text/markdown":{"schema":{"type":"string"}}}}}}}},"components":{"securitySchemes":{"oauth2":{"type":"oauth2","description":"OAuth 2.0 authorization code with PKCE (S256 required). Clients register at https://app.presify.io/api/oauth/register; metadata at https://app.presify.io/.well-known/oauth-authorization-server. Revocation at https://app.presify.io/api/oauth/revoke.","flows":{"authorizationCode":{"authorizationUrl":"https://app.presify.io/api/oauth/authorize","tokenUrl":"https://app.presify.io/api/oauth/token","refreshUrl":"https://app.presify.io/api/oauth/token","scopes":{"presify:read":"Read presence analytics the connected person may see in Presify. The only scope; every tool is read-only."}}}},"personalAccessToken":{"type":"http","scheme":"bearer","description":"Personal access token minted by a Presify owner or admin in Settings, then Connections (365-day lifetime, revocable). Same authorization as an OAuth token."}},"schemas":{"AuthorizationServerMetadata":{"type":"object","description":"RFC 8414 document.","required":["issuer","authorization_endpoint","token_endpoint","response_types_supported"],"properties":{"issuer":{"type":"string","format":"uri"},"authorization_endpoint":{"type":"string","format":"uri"},"token_endpoint":{"type":"string","format":"uri"},"registration_endpoint":{"type":"string","format":"uri"},"revocation_endpoint":{"type":"string","format":"uri"},"response_types_supported":{"type":"array","items":{"type":"string"}},"grant_types_supported":{"type":"array","items":{"type":"string"}},"code_challenge_methods_supported":{"type":"array","items":{"type":"string"}},"token_endpoint_auth_methods_supported":{"type":"array","items":{"type":"string"}},"revocation_endpoint_auth_methods_supported":{"type":"array","items":{"type":"string"}},"scopes_supported":{"type":"array","items":{"type":"string"}},"service_documentation":{"type":"string","format":"uri","description":"Public setup guide for connecting an MCP client."}}},"ProtectedResourceMetadata":{"type":"object","description":"RFC 9728 document.","required":["resource","authorization_servers"],"properties":{"resource":{"type":"string","format":"uri"},"authorization_servers":{"type":"array","items":{"type":"string","format":"uri"}},"bearer_methods_supported":{"type":"array","items":{"type":"string"}},"scopes_supported":{"type":"array","items":{"type":"string"}},"resource_name":{"type":"string"}}},"McpServerCard":{"type":"object","description":"MCP server card (SEP-1649 draft fields plus registry-style remotes).","required":["name","description","version","serverUrl","protocolVersion","transport","tools"],"properties":{"name":{"type":"string","description":"Reverse-DNS server name."},"title":{"type":"string"},"description":{"type":"string"},"version":{"type":"string"},"serverUrl":{"type":"string","format":"uri"},"websiteUrl":{"type":"string","format":"uri"},"documentationUrl":{"type":"string","format":"uri"},"openapiUrl":{"type":"string","format":"uri"},"protocolVersion":{"type":"string"},"transport":{"type":"object","required":["type","endpoint"],"properties":{"type":{"type":"string","enum":["streamable-http"]},"endpoint":{"type":"string","format":"uri"},"responseMode":{"type":"string","enum":["json"]}}},"capabilities":{"type":"object","additionalProperties":true},"authentication":{"type":"object","additionalProperties":true},"remotes":{"type":"array","items":{"type":"object","additionalProperties":true}},"tools":{"type":"array","items":{"type":"object","required":["name","description","inputSchema"],"properties":{"name":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"inputSchema":{"type":"object","additionalProperties":true},"annotations":{"type":"object","additionalProperties":true}}}}}},"ClientRegistrationRequest":{"type":"object","description":"RFC 7591 client metadata. Unknown fields are ignored.","required":["redirect_uris"],"properties":{"redirect_uris":{"type":"array","minItems":1,"items":{"type":"string","format":"uri"},"description":"https URIs, or http on localhost/127.0.0.1 for development. Exact-match on authorize."},"client_name":{"type":"string","maxLength":200},"client_uri":{"type":"string","format":"uri"},"token_endpoint_auth_method":{"type":"string","enum":["none","client_secret_basic","client_secret_post"],"default":"none"},"grant_types":{"type":"array","items":{"type":"string","enum":["authorization_code","refresh_token"]}},"response_types":{"type":"array","items":{"type":"string","enum":["code"]}},"scope":{"type":"string","description":"Only `presify:read` is issued."},"software_id":{"type":"string"},"software_version":{"type":"string"}}},"ClientRegistrationResponse":{"type":"object","required":["client_id","client_id_issued_at","redirect_uris","grant_types","response_types","token_endpoint_auth_method","scope"],"properties":{"client_id":{"type":"string","format":"uuid"},"client_id_issued_at":{"type":"integer","description":"Unix seconds."},"client_name":{"type":"string"},"redirect_uris":{"type":"array","items":{"type":"string","format":"uri"}},"grant_types":{"type":"array","items":{"type":"string"}},"response_types":{"type":"array","items":{"type":"string"}},"token_endpoint_auth_method":{"type":"string"},"scope":{"type":"string"},"client_secret":{"type":"string","description":"Confidential clients only. Shown once; only a hash is stored."},"client_secret_expires_at":{"type":"integer","description":"0 = never expires (rotate by re-registering)."}}},"TokenRequest":{"type":"object","description":"RFC 6749 token request. grant_type selects the shape.","required":["grant_type"],"properties":{"grant_type":{"type":"string","enum":["authorization_code","refresh_token"]},"code":{"type":"string","maxLength":300,"description":"authorization_code grant."},"redirect_uri":{"type":"string","format":"uri","description":"authorization_code grant; must equal the authorize redirect_uri."},"code_verifier":{"type":"string","minLength":43,"maxLength":128,"description":"authorization_code grant (PKCE)."},"refresh_token":{"type":"string","maxLength":300,"description":"refresh_token grant."},"client_id":{"type":"string","format":"uuid","description":"Required unless sent via HTTP Basic."},"client_secret":{"type":"string","description":"Confidential clients (client_secret_post)."},"scope":{"type":"string","maxLength":200},"resource":{"type":"string","format":"uri","maxLength":500}}},"TokenResponse":{"type":"object","required":["access_token","token_type","expires_in","scope"],"properties":{"access_token":{"type":"string"},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer","description":"Seconds (3600)."},"refresh_token":{"type":"string","description":"Rotating; the previous refresh token is invalidated."},"scope":{"type":"string"}}},"RevokeRequest":{"type":"object","required":["token"],"properties":{"token":{"type":"string","maxLength":300},"token_type_hint":{"type":"string","enum":["access_token","refresh_token"]}}},"OAuthError":{"type":"object","description":"RFC 6749 §5.2 error body.","required":["error"],"properties":{"error":{"type":"string","description":"Error code (invalid_request, invalid_client, invalid_grant, invalid_target, invalid_redirect_uri, invalid_client_metadata, unsupported_grant_type)."},"error_description":{"type":"string"}}},"JsonRpcRequest":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"oneOf":[{"type":"string"},{"type":"integer"}],"description":"Omit for a notification."},"method":{"type":"string","enum":["initialize","ping","tools/list","tools/call","prompts/list","prompts/get","notifications/initialized"]},"params":{"type":"object","additionalProperties":true}}},"JsonRpcResponse":{"type":"object","required":["jsonrpc","id"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"oneOf":[{"type":"string"},{"type":"integer"},{"type":"null"}]},"result":{"type":"object","additionalProperties":true},"error":{"$ref":"#/components/schemas/JsonRpcError"}}},"JsonRpcError":{"type":"object","required":["code","message"],"properties":{"code":{"type":"integer","description":"-32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error, -32000 to -32099 server errors."},"message":{"type":"string"},"data":{"description":"Optional structured detail."}}},"JsonRpcErrorResponse":{"type":"object","description":"Transport-level failure: a JSON-RPC error with id null.","required":["jsonrpc","id","error"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"type":"null"},"error":{"$ref":"#/components/schemas/JsonRpcError"}}},"HealthResponse":{"type":"object","required":["status","timestamp"],"properties":{"status":{"type":"string","enum":["ok","maintenance"]},"version":{"type":"string"},"message":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}}},"ReadyResponse":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["ready","not-ready"]}},"additionalProperties":true},"NotFoundError":{"type":"object","description":"Body of every 404 under /api/ (also served for other paths when the client prefers application/json).","required":["error","status","message","hint","links"],"properties":{"error":{"type":"string","enum":["not_found"]},"status":{"type":"integer","enum":[404]},"message":{"type":"string"},"hint":{"type":"string","description":"Where to look instead."},"links":{"type":"object","additionalProperties":{"type":"string","format":"uri"}}}},"MethodNotAllowedError":{"type":"object","description":"Body of the 405 middleware returns for a method a /api route does not export; the Allow header lists the supported methods.","required":["error","status","message","allow"],"properties":{"error":{"type":"string","enum":["method_not_allowed"]},"status":{"type":"integer","enum":[405]},"message":{"type":"string"},"allow":{"type":"array","items":{"type":"string"}},"links":{"type":"object","additionalProperties":{"type":"string","format":"uri"}}}},"BadRequestError":{"type":"object","description":"Body of the middleware 400 for malformed percent-encoding or a NUL byte in the URL, under /api/.","required":["error","status","message"],"properties":{"error":{"type":"string","enum":["bad_request"]},"status":{"type":"integer","enum":[400]},"message":{"type":"string"}}}},"headers":{"RateLimit-Policy":{"description":"IETF RateLimit policy: quota (q) per window (w seconds) and burst (b) for the \"grant\" and \"tenant\" windows.","schema":{"type":"string"},"example":"\"grant\";q=60;w=60;b=120, \"tenant\";q=300;w=60;b=600"},"RateLimit":{"description":"IETF RateLimit field for the tighter window: remaining requests (r) and seconds until the window is fully refilled (t).","schema":{"type":"string"},"example":"\"grant\";r=118;t=1"},"RateLimit-Limit":{"description":"Legacy: requests per minute for the tighter window.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Legacy: requests remaining in the tighter window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Legacy: seconds until the tighter window is fully refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"Deprecation":{"description":"RFC 9745. Present only once the operation is scheduled for retirement: the moment the deprecation took effect, as an IMF-fixdate or a structured-field date (@ + Unix seconds). Sent at least 90 days before the Sunset date. Not sent today: nothing is deprecated.","schema":{"type":"string"},"example":"@1767225600"},"Sunset":{"description":"RFC 8594. Present only once the operation is scheduled for retirement: the HTTP date after which it stops responding (410 Gone). Accompanied by a Link header with rel=\"deprecation\" pointing at the policy page.","schema":{"type":"string"},"example":"Wed, 01 Apr 2027 00:00:00 GMT"}},"responses":{"NotFound":{"description":"Unknown path under /api/.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}},"MethodNotAllowed":{"description":"Unsupported method on a /api route.","headers":{"Allow":{"description":"Supported methods.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MethodNotAllowedError"}}}},"BadRequest":{"description":"Malformed URL encoding.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}}},"x-versioning-policy":{"scheme":"permanent-contract","policyUrl":"https://presify.io/docs/api-versioning","urlVersioning":"none","mcpProtocolVersionHeader":"MCP-Protocol-Version","mcpProtocolVersions":["2026-07-28","2025-06-18","2025-03-26"],"deprecationHeaders":["Deprecation","Sunset","Link; rel=\"deprecation\""],"minimumNoticeDays":90,"afterSunset":"410 Gone with the JSON error shape; retired MCP tools leave tools/list.","currentlyDeprecated":[],"deprecationNotice":"Deprecation (RFC 9745) and Sunset (RFC 8594) headers at least 90 days before removal, announced in this document first."},"x-mcp":{"serverCard":"https://app.presify.io/.well-known/mcp/server-card.json","endpoint":"https://app.presify.io/api/mcp","transport":"streamable-http","responseMode":"json","protocolVersions":["2026-07-28","2025-06-18","2025-03-26"],"authorizationServer":"https://app.presify.io","protectedResourceMetadata":"https://app.presify.io/.well-known/oauth-protected-resource/api/mcp","scope":"presify:read","readOnly":true,"tools":[{"name":"get_workspace_info","title":"Workspace overview","description":"Returns the connected Presify workspace at a glance: workspace name, plan tier with seat and retention limits, subscription and trial state, monitored-user counts (active, paused, pending enrollment), presence-collection health, and what the connected person may see. Call this first to orient yourself."},{"name":"list_monitored_users","title":"Monitored people","description":"Lists the people monitored in this Presify workspace with their status, department, timezone, business hours, and flags (collection paused, legal hold). Filter by monitoring status (active by default; former users appear under removed statuses), department, or a name/email search. Paginated: up to 200 per page with offset. Use this to find a person before calling per-user tools."},{"name":"get_user","title":"Person overview","description":"Returns one monitored person: profile (name, email, department, timezone, business hours, monitoring status, flags), their latest presence reading, and their last 7 daily presence totals. Accepts a name, email, or Microsoft user id. For deeper trends use get_user_stats."},{"name":"get_user_stats","title":"Person deep stats","description":"Computes rich presence statistics for one person over the last 14 weeks (bounded by plan retention): this week and month with honest same-elapsed-point comparisons to the prior period, a weekly trend, business-hours versus after-hours split, typical day rhythm (median start and end, drift), uninterrupted focus blocks, meeting time, state mix, and longest online stretch. Accepts a name, email, or Microsoft user id."},{"name":"get_user_timeline","title":"Person day timeline","description":"Returns one person's presence timeline for one local calendar day: ordered segments with local start and end times, duration in seconds, and the presence status held in each (\"Available\", \"Busy\", \"In a call\", \"Away\", \"Offline\", and so on). \"No data\" marks spans where collection was not running, which is different from a confirmed Offline reading. Times are wall-clock in the person's own timezone; the response names the zone and the business-hours window. Days outside the plan retention window return no segments."},{"name":"get_current_presence","title":"Who is online now","description":"Returns the latest Microsoft Teams presence reading for each actively monitored person: availability, activity, presence bucket, when it last changed, and whether they count as online right now (fresh reading within the last few minutes). Includes data-freshness info so you can caveat stale results. Optionally filter by department. Up to 500 people per call."},{"name":"get_presence_summary","title":"Presence summary","description":"Sums presence time over a date range from nightly per-person daily rollups, grouped by person or by day. Returns seconds in each presence bucket (online, away, offline or no signal, in a call or presenting). Use for questions like \"how much was the team online last week\" or \"show daily totals for June\". Range up to 366 days; optionally restrict to specific people (Microsoft user ids) or one department. Days outside the plan retention window have no data."},{"name":"compare_users","title":"Compare people","description":"Compares 2 to 10 people side by side over a date range using whole-day presence totals from the nightly rollups: online, away, offline or no signal, and in-call seconds, plus days with data and daily averages. Range up to 92 days. Each entry accepts a name, email, or Microsoft user id. Note: totals are whole-day (not clipped to business hours); the in-app Compare page offers business-window clipping."},{"name":"get_coverage","title":"Team coverage grid","description":"Builds a typical-week coverage grid: for each weekday hour, how many of the selected people are typically online (fractional headcount and percent of team), plus best and thinnest hours. Answers \"when is the team reachable\" and \"where are our coverage gaps\". Hard caps: at most 31 days of history and 50 people per call; scope the users list or department for bigger teams. Requires a plan with the Coverage feature."},{"name":"get_connection_health","title":"Microsoft 365 connection health","description":"Reports the health of this workspace connection to Microsoft 365: whether presence collection is running (active, delayed, paused for reconsent, or suspended), the last successful poll time, how reliably Microsoft Graph has been answering over the chosen window (overall and per day), and when the Microsoft app credential expires. Use when data looks missing or stale, or for an operations checkup. Window up to 90 days."},{"name":"list_notifications","title":"Workspace notices","description":"Lists recent in-app workspace notices for admins: billing events, connection health changes, enrollment reminders, digest highlights. Up to 50, newest first; optionally unread only. Useful for \"anything needing attention in Presify?\"."},{"name":"list_anomalies","title":"Flagged presence patterns","description":"Lists flagged presence patterns (anomalies) with factual descriptions of what was measured: unbroken Available runs, missing idle time, overnight presence, no activity variation, robotic regularity, weekend uniformity. Filter by person, pattern type, date range, or open/dismissed. Up to 200, newest first. Descriptions state the observed pattern and computation only, never a cause or intent. To inspect a flagged day the way the app does, follow up with get_user_timeline using the entry's person and date."},{"name":"get_anomaly_summary","title":"Flagged-pattern summary","description":"Counts flagged presence patterns by type over a date range (open and dismissed separately), with the latest detection per type. Use for triage and trend questions like \"are overnight flags increasing\". Range up to 366 days."},{"name":"list_reports","title":"Generated reports","description":"Lists recently generated report jobs (PDF scorecards and Excel workbooks): type, status, who requested them, when. Up to 50, newest first. Use get_report_download for a finished report file link."},{"name":"get_report_download","title":"Report download link","description":"Returns a fresh download link for a completed report by reportId (from list_reports). Links are valid for 15 minutes and are generated per request; ask again if one expires."},{"name":"list_scheduled_reports","title":"Scheduled reports","description":"Lists this workspace recurring report schedules: report type, cadence in plain words, window, recipients, status, and the next run time. Answers \"when does my next report go out\"."},{"name":"list_audit_events","title":"Audit trail","description":"Lists this workspace audit trail: who did what and when (settings changes, enrollments, report requests, connections). Filter by action prefix or date range. Up to 100, newest first. Owner only, on plans with the audit log feature."},{"name":"search","title":"Search this workspace","description":"Searches this Presify workspace across monitored people (name, email, department), flagged presence patterns, and generated reports. Returns up to 20 results with namespaced ids for fetch. Built for research-style clients; prefer the specific tools when you know what you need."},{"name":"fetch","title":"Fetch a search result","description":"Fetches the full record behind a search result id (user:, anomaly:, or report: prefixed). Companion to search for research-style clients."}]},"x-agent-guidance":{"llmsTxt":"https://presify.io/llms.txt","sitemap":"https://presify.io/sitemap.xml","markdownNegotiation":"Send Accept: text/markdown on any public page URL.","notFound":"Unknown paths return HTTP 404: JSON under /api/, Markdown for non-browser clients."}}