{
  "openapi": "3.1.0",
  "info": {
    "title": "saileshdahal.com.np API",
    "version": "1.0.0",
    "summary": "Free, read-only JSON API over Sailesh Dahal's published writing, plus an MCP server and the human contact endpoint.",
    "description": "# Overview\n\nThe public surface of saileshdahal.com.np. The site is a personal blog and its data is public by design, so every read endpoint is open and unauthenticated.\n\n# Authentication\n\nNone. No API keys exist because nothing here is gated. Do not POST to /api/contact programmatically: it is a human form and rejects scripted submissions.\n\n# Versioning and deprecation policy\n\nVersioned by URL path (/v1). Additions are non-breaking. Breaking changes ship only under /api/v2, never inside /v1. Deprecations are announced at least 12 months ahead, signalled on responses with the RFC 9745 Deprecation header followed by an RFC 8594 Sunset header carrying the removal date. Neither header is sent today because nothing is deprecated.\n\n# Errors\n\nEvery 4xx and 5xx from /api/* is RFC 9457 application/problem+json with a stable machine-readable code, human-readable title and detail, the HTTP status, usually a hint, and for validation failures an errors map of field name to message.\n\n# Rate limits\n\nReads carry a RateLimit-Policy header (draft-ietf-httpapi-ratelimit-headers-11 format) describing advisory fair use of roughly 600 requests per 60 seconds per client. It is guidance, not enforcement. The one enforced limit is POST /api/contact at 5 submissions per minute per IP; excesses get 429 with Retry-After: 60.\n\n# Content negotiation\n\nPage URLs serve text/html and negotiate text/markdown on Accept (Vary: Accept). Under /api/v1, GET /api/v1/posts/{slug} returns application/json by default and text/markdown when the Accept header names markdown without html."
  },
  "servers": [
    {
      "url": "https://saileshdahal.com.np"
    }
  ],
  "tags": [
    {
      "name": "content",
      "description": "Read-only access to posts"
    },
    {
      "name": "agents",
      "description": "Agent-facing surfaces: MCP server and machine-readable indexes"
    },
    {
      "name": "contact",
      "description": "The human contact form endpoint"
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "tags": [
          "content"
        ],
        "operationId": "getServiceIndex",
        "summary": "Get the API service index",
        "description": "Lists every endpoint on this API with its operationId, plus links to this spec, the developer docs and related machine-readable surfaces. A good first call for agents discovering the surface.",
        "responses": {
          "200": {
            "description": "Service index document",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceIndex"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/posts": {
      "get": {
        "tags": [
          "content"
        ],
        "operationId": "listPosts",
        "summary": "List all posts",
        "description": "Returns every post as JSON: slug, title, canonical URL, markdown URL, summary, tags and publication dates. Optionally filter by tag (display-name match, case-insensitive).",
        "parameters": [
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter to posts carrying this tag. Case-insensitive display-name match, e.g. Flutter.",
            "example": "Flutter"
          }
        ],
        "responses": {
          "200": {
            "description": "Post list, newest first as served by feed.json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/posts/{slug}": {
      "get": {
        "tags": [
          "content"
        ],
        "operationId": "getPost",
        "summary": "Get one post",
        "description": "Returns one post's metadata as JSON. Send Accept: text/markdown to receive the full post body as markdown instead; the response then has content type text/markdown and carries Vary: Accept.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Slug"
          },
          {
            "name": "Accept",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "text/markdown selects the markdown representation; anything else gets JSON.",
            "example": "text/markdown"
          }
        ],
        "responses": {
          "200": {
            "description": "Post metadata as JSON, or the post body as markdown when Accept names text/markdown",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Full post body in markdown"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/posts/{slug}/markdown": {
      "get": {
        "tags": [
          "content"
        ],
        "operationId": "getPostMarkdown",
        "summary": "Get one post as raw markdown",
        "description": "Returns the full text of one post as clean markdown with content type text/markdown. Equivalent to fetching /{slug}.md, kept inside the versioned namespace for convenience.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Slug"
          }
        ],
        "responses": {
          "200": {
            "description": "Post body in markdown",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Full post body in markdown"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/search": {
      "get": {
        "tags": [
          "content"
        ],
        "operationId": "searchPosts",
        "summary": "Search posts by keyword",
        "description": "Case-insensitive keyword search across post titles (weight 3), tags (weight 2) and summaries (weight 1). Results are ordered by score descending, then slug.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "description": "Keyword or phrase to search for.",
            "example": "flutter flavors"
          }
        ],
        "responses": {
          "200": {
            "description": "Matching posts with the query echoed back",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "agents"
        ],
        "operationId": "getOpenApiSpec",
        "summary": "Get this OpenAPI document",
        "description": "The OpenAPI 3.1 description of this API, served as static JSON with open CORS.",
        "responses": {
          "200": {
            "description": "This document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "An OpenAPI 3.1 document"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "tags": [
          "agents"
        ],
        "operationId": "getLlmsTxt",
        "summary": "Get llms.txt",
        "description": "Markdown index of the site for LLMs per llmstxt.org, including when-to-use guidance and links to every page's markdown twin.",
        "responses": {
          "200": {
            "description": "llms.txt overview",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    },
    "/feed.json": {
      "get": {
        "tags": [
          "agents"
        ],
        "operationId": "getFeed",
        "summary": "Get the JSON Feed index",
        "description": "JSON Feed 1.1 document listing every post; each item links its markdown twin. The source of truth /api/v1 reads are projected from.",
        "responses": {
          "200": {
            "description": "JSON Feed document",
            "content": {
              "application/feed+json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonFeed"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    },
    "/api/mcp": {
      "post": {
        "tags": [
          "agents"
        ],
        "operationId": "mcpStreamableHttp",
        "summary": "MCP server endpoint (Streamable HTTP)",
        "description": "A minimal read-only MCP server (protocol versions 2025-06-18 and 2025-03-26) exposing three tools over Streamable HTTP: list_posts, search_posts, get_post_markdown. POST JSON-RPC here; send Accept: application/json for JSON responses or text/event-stream for SSE framing. GET opens a short-lived event stream. Tools are read-only; no resources or prompts are exposed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "A single JSON-RPC 2.0 message or a batch array",
                "properties": {
                  "jsonrpc": {
                    "type": "string",
                    "const": "2.0"
                  },
                  "id": {
                    "type": [
                      "string",
                      "number"
                    ]
                  },
                  "method": {
                    "type": "string",
                    "examples": [
                      "initialize",
                      "tools/list",
                      "tools/call",
                      "ping"
                    ]
                  },
                  "params": {
                    "type": "object"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response as application/json, SSE-framed response as text/event-stream, or 202 with no body for notifications",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "SSE stream whose single message event carries the same JSON-RPC payload"
                }
              }
            }
          },
          "400": {
            "description": "Parse error, framed as a JSON-RPC error object",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcError"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "get": {
        "tags": [
          "agents"
        ],
        "operationId": "mcpEventStream",
        "summary": "Open the MCP event stream",
        "description": "Opens a short-lived text/event-stream used by clients probing transport compliance. This stateless server pushes nothing asynchronously, so the stream emits one comment ping and closes cleanly.",
        "responses": {
          "200": {
            "description": "Server-sent events stream that closes after one ping",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    },
    "/api/contact": {
      "post": {
        "tags": [
          "contact"
        ],
        "operationId": "submitContact",
        "summary": "Submit a project enquiry (human contact form)",
        "description": "Delivers a contact-form submission to the site owner's inbox. Intended for humans using /contact; programmatic submissions are rejected. Browser clients receive 303 redirects to /thanks or /contact-error; clients sending or accepting JSON instead receive problem+json responses: 202 on acceptance, 422 with an errors map on validation failure, 403 for a disallowed origin, 429 when rate limited. Defence layers (origin check, honeypots, content filters) that silently drop spam answer exactly like a success, so bots learn nothing.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/ContactSubmission"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/ContactSubmission"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted (JSON-mode clients): the message was queued for delivery",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactResult"
                }
              }
            }
          },
          "303": {
            "description": "See Other (browser flow): redirect to /thanks on success, /contact-error on rejection",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "description": "Origin not allowed (problem+json)",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed (problem+json with an errors map)",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "502": {
            "description": "Delivery failed (problem+json); nothing was sent",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "contact"
        ],
        "operationId": "submitContactProbe",
        "summary": "Method probe for /api/contact",
        "description": "Any method other than POST returns 405 with an Allow: POST header and a problem+json body.",
        "responses": {
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Slug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^[a-z0-9][a-z0-9-]*$"
        },
        "description": "Post slug, e.g. flavor-setup-flutter. Call listPosts for valid values.",
        "example": "flavor-setup-flutter"
      }
    },
    "headers": {
      "RateLimitPolicy": {
        "description": "Advisory quota policy, draft-ietf-httpapi-ratelimit-headers-11 format",
        "schema": {
          "type": "string"
        },
        "example": "\"fair-use\";q=600;w=60"
      },
      "RetryAfter": {
        "description": "Seconds until the client should retry",
        "schema": {
          "type": "integer"
        },
        "example": 60
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Malformed request (problem+json)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotFound": {
        "description": "Unknown resource (problem+json, code not_found)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Method not allowed (problem+json with an Allow header)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limited (problem+json)",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ServerError": {
        "description": "Unexpected server failure (problem+json)",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem detail. Every 4xx/5xx from /api/* uses this shape.",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri-reference",
            "description": "Identifies the problem type; anchors on the developers page.",
            "example": "https://saileshdahal.com.np/developers#error-not_found"
          },
          "title": {
            "type": "string",
            "description": "Short human-readable summary",
            "example": "Not Found"
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code",
            "example": 404
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable identifier for branching logic.",
            "enum": [
              "not_found",
              "method_not_allowed",
              "invalid_slug",
              "missing_query",
              "unsupported_filter",
              "invalid_form",
              "validation_failed",
              "origin_not_allowed",
              "rate_limited",
              "delivery_failed",
              "internal_error",
              "not_acceptable"
            ]
          },
          "detail": {
            "type": "string",
            "description": "Human-readable explanation"
          },
          "instance": {
            "type": "string",
            "description": "Path reference of the failing request"
          },
          "hint": {
            "type": "string",
            "description": "What the client could try next"
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "On validation_failed: field name to message"
          }
        },
        "required": [
          "type",
          "title",
          "status",
          "code"
        ]
      },
      "Post": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "example": "flavor-setup-flutter"
          },
          "title": {
            "type": "string",
            "example": "Flavor setup in Flutter"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "example": "https://saileshdahal.com.np/flavor-setup-flutter"
          },
          "markdownUrl": {
            "type": "string",
            "format": "uri",
            "example": "https://saileshdahal.com.np/flavor-setup-flutter.md"
          },
          "summary": {
            "type": "string",
            "description": "One-paragraph description of the post"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "Flutter",
              "Dart"
            ]
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "slug",
          "title",
          "url",
          "markdownUrl",
          "summary",
          "tags"
        ]
      },
      "PostList": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer",
            "description": "Number of posts returned"
          },
          "posts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Post"
            }
          }
        },
        "required": [
          "count",
          "posts"
        ]
      },
      "SearchResponse": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Post"
            }
          }
        },
        "required": [
          "query",
          "count",
          "results"
        ]
      },
      "ServiceIndex": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "const": "saileshdahal.com.np API"
          },
          "version": {
            "type": "string",
            "example": "1.0.0"
          },
          "description": {
            "type": "string"
          },
          "endpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "method": {
                  "type": "string",
                  "example": "GET"
                },
                "path": {
                  "type": "string",
                  "example": "/api/v1/posts"
                },
                "operationId": {
                  "type": "string",
                  "example": "listPosts"
                }
              },
              "required": [
                "method",
                "path",
                "operationId"
              ]
            }
          },
          "specUrl": {
            "type": "string",
            "format": "uri"
          },
          "docsUrl": {
            "type": "string",
            "format": "uri"
          },
          "related": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "format": "uri"
            }
          },
          "errors": {
            "type": "string",
            "example": "RFC 9457 application/problem+json"
          },
          "rateLimitPolicy": {
            "type": "string"
          }
        },
        "required": [
          "name",
          "version",
          "endpoints",
          "specUrl",
          "docsUrl"
        ]
      },
      "ContactSubmission": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120,
            "description": "Sender's full name"
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 200,
            "description": "Reply-to email address"
          },
          "company": {
            "type": "string",
            "maxLength": 200,
            "description": "Company or organisation (optional)"
          },
          "topic": {
            "type": "string",
            "maxLength": 200,
            "description": "What the enquiry is about"
          },
          "message": {
            "type": "string",
            "minLength": 10,
            "maxLength": 8000,
            "description": "The enquiry message"
          }
        },
        "required": [
          "name",
          "email",
          "message"
        ]
      },
      "ContactResult": {
        "type": "object",
        "properties": {
          "accepted": {
            "type": "boolean",
            "const": true
          }
        },
        "required": [
          "accepted"
        ]
      },
      "JsonRpcError": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "type": [
              "string",
              "number",
              "null"
            ]
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "integer",
                "description": "JSON-RPC error code, e.g. -32700 parse error, -32601 method not found, -32602 invalid params"
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "jsonrpc",
          "error"
        ]
      },
      "JsonFeed": {
        "type": "object",
        "description": "JSON Feed 1.1 top-level document (subset)",
        "properties": {
          "version": {
            "type": "string",
            "const": "https://jsonfeed.org/version/1.1"
          },
          "title": {
            "type": "string"
          },
          "home_page_url": {
            "type": "string",
            "format": "uri"
          },
          "feed_url": {
            "type": "string",
            "format": "uri"
          },
          "description": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uri"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "title": {
                  "type": "string"
                },
                "summary": {
                  "type": "string"
                },
                "content_text": {
                  "type": "string"
                },
                "date_published": {
                  "type": "string",
                  "format": "date-time"
                },
                "date_modified": {
                  "type": "string",
                  "format": "date-time"
                },
                "tags": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "_markdown": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  },
                  "required": [
                    "url"
                  ]
                }
              },
              "required": [
                "id",
                "url",
                "title",
                "summary"
              ]
            }
          }
        },
        "required": [
          "version",
          "title",
          "items"
        ]
      },
      "JsonRpcResponse": {
        "type": "object",
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "jsonrpc": {
                "type": "string",
                "const": "2.0"
              },
              "id": {
                "type": [
                  "string",
                  "number",
                  "null"
                ]
              },
              "result": {
                "type": "object",
                "description": "Method result; shape depends on method"
              }
            },
            "required": [
              "jsonrpc",
              "id",
              "result"
            ]
          },
          {
            "$ref": "#/components/schemas/JsonRpcError"
          }
        ]
      }
    }
  }
}
