{
  "openapi": "3.1.0",
  "info": {
    "title": "SaaSCity Public API",
    "version": "1.0.0",
    "description": "Public endpoints of SaaSCity, the launch directory for SaaS and AI tools on a live city map. Agent work goes through the MCP server at /api/mcp (keyless). All errors are JSON: {\"error\": string}.",
    "contact": { "name": "SaaSCity support", "email": "support@saascity.io", "url": "https://saascity.io/about" },
    "termsOfService": "https://saascity.io/terms",
    "x-logo": { "url": "https://saascity.io/apple-touch-icon.png" }
  },
  "servers": [{ "url": "https://saascity.io" }],
  "externalDocs": { "description": "MCP server docs and agent guide", "url": "https://saascity.io/mcp" },
  "tags": [
    { "name": "mcp", "description": "Model Context Protocol server (streamable HTTP, JSON-RPC 2.0)." },
    { "name": "nlweb", "description": "NLWeb natural-language search over live listings." }
  ],
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": { "error": { "type": "string", "description": "Human-readable reason." } }
      },
      "JsonRpcRequest": {
        "type": "object",
        "required": ["jsonrpc", "method"],
        "properties": {
          "jsonrpc": { "type": "string", "const": "2.0" },
          "id": { "oneOf": [{ "type": "string" }, { "type": "integer" }, { "type": "null" }] },
          "method": { "type": "string", "enum": ["initialize", "ping", "tools/list", "tools/call"] },
          "params": { "type": "object", "additionalProperties": true }
        }
      },
      "JsonRpcResponse": {
        "type": "object",
        "required": ["jsonrpc", "id"],
        "properties": {
          "jsonrpc": { "type": "string", "const": "2.0" },
          "id": { "oneOf": [{ "type": "string" }, { "type": "integer" }, { "type": "null" }] },
          "result": { "type": "object", "additionalProperties": true },
          "error": { "type": "object", "properties": { "code": { "type": "integer" }, "message": { "type": "string" } } }
        }
      },
      "NlwebResult": {
        "type": "object",
        "properties": {
          "url": { "type": "string", "format": "uri" },
          "name": { "type": "string" },
          "site": { "type": "string" },
          "score": { "type": "number" },
          "description": { "type": "string" },
          "schema_object": { "type": "object", "additionalProperties": true }
        }
      },
      "NlwebResponse": {
        "type": "object",
        "properties": {
          "query_id": { "type": "string" },
          "results": { "type": "array", "items": { "$ref": "#/components/schemas/NlwebResult" } },
          "_meta": { "type": "object", "properties": { "response_type": { "type": "string" }, "version": { "type": "string" } } }
        }
      }
    },
    "responses": {
      "BadRequest": { "description": "Malformed input.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": { "description": "Rate limit hit. Retry-After header is set.", "headers": { "Retry-After": { "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    }
  },
  "paths": {
    "/api/mcp": {
      "get": {
        "operationId": "describeMcpServer",
        "tags": ["mcp"],
        "summary": "Describe the MCP server",
        "description": "Pointer for clients that probe with GET: server name, transport, endpoint, docs URL.",
        "responses": { "200": { "description": "Server pointer.", "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "transport": { "type": "string" }, "endpoint": { "type": "string" }, "docs": { "type": "string" } } } } } } }
      },
      "post": {
        "operationId": "callMcp",
        "tags": ["mcp"],
        "summary": "MCP JSON-RPC endpoint",
        "description": "Stateless streamable-HTTP MCP: one JSON-RPC message per POST. Serves MCP 2026-07-28 (per-request _meta, server/discover) and the initialize-based versions 2025-11-25 back to 2024-11-05. Methods: server/discover, initialize, ping, tools/list, tools/call, prompts/list, prompts/get. Tools: check_launch_readiness, recommend_launch_directories, find_launch_directories, get_launch_directory, submit_project, check_badge, get_submission_status, get_launch_weeks, get_offers, search_listings, get_listing, get_leaderboard, find_alternatives. Keyless; see https://saascity.io/.well-known/mcp/server-card.json.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JsonRpcRequest" } } } },
        "responses": {
          "200": { "description": "JSON-RPC result or error object.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JsonRpcResponse" } } } },
          "202": { "description": "Notification acknowledged." }
        }
      }
    },
    "/ask": {
      "get": {
        "operationId": "nlwebAsk",
        "tags": ["nlweb"],
        "summary": "Natural-language search (NLWeb)",
        "description": "Microsoft NLWeb /ask: searches live SaaSCity listings for a natural-language query. Set streaming=true (or Accept: text/event-stream) for SSE events start, result, complete.",
        "parameters": [
          { "name": "query", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "site", "in": "query", "schema": { "type": "string" } },
          { "name": "streaming", "in": "query", "schema": { "type": "boolean", "default": false } },
          { "name": "mode", "in": "query", "schema": { "type": "string", "enum": ["list"], "default": "list" } },
          { "name": "query_id", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Results.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NlwebResponse" } }, "text/event-stream": { "schema": { "type": "string" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "operationId": "nlwebAskPost",
        "tags": ["nlweb"],
        "summary": "Natural-language search (NLWeb, POST)",
        "description": "Same as GET /ask with a JSON body. `prefer: { streaming: true }` selects SSE.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["query"], "properties": { "query": { "type": "string" }, "site": { "type": "string" }, "streaming": { "type": "boolean" }, "prefer": { "type": "object", "properties": { "streaming": { "type": "boolean" } } }, "query_id": { "type": "string" } } } } } },
        "responses": {
          "200": { "description": "Results.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NlwebResponse" } }, "text/event-stream": { "schema": { "type": "string" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  }
}
