{
  "openapi": "3.1.0",
  "info": {
    "title": "Magnus Rødseth — site API",
    "version": "1.0.0",
    "summary": "Machine-readable representations of a personal site and blog.",
    "description": "Read-only and unauthenticated. No API key, no quota, no rate limit; call it as often as you need.\n\n**Versioning.** JSON endpoints live under `/api/v1/`. Within a version, changes are additive only: new fields may appear, existing fields keep their name, type and meaning. A breaking change gets a new prefix (`/api/v2/`). A retired version keeps answering for at least six months, sending `Deprecation` and `Sunset` headers (RFC 9745 and RFC 8594) on every response for that whole window, so an agent learns of the retirement from the response itself.\n\n**Other representations.** Every page also answers `Accept: text/markdown` with a Markdown twin of itself, reachable directly at the `/md/...` paths below. Start at `/llms.txt` for an index of everything.",
    "contact": {
      "name": "Magnus Rødseth",
      "url": "https://www.magnusrodseth.com",
      "email": "magnus.rodseth@gmail.com"
    },
    "license": {
      "name": "All rights reserved",
      "url": "https://www.magnusrodseth.com/"
    }
  },
  "servers": [
    {
      "url": "https://www.magnusrodseth.com"
    }
  ],
  "tags": [
    {
      "name": "content",
      "description": "Page and post content."
    },
    {
      "name": "discovery",
      "description": "Indexes, feeds, and well-known documents."
    },
    {
      "name": "api",
      "description": "JSON endpoints."
    }
  ],
  "paths": {
    "/llms.txt": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "getLlmsTxt",
        "summary": "Site index for language models (llmstxt.org).",
        "responses": {
          "200": {
            "description": "Index of pages and posts with Markdown links.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/llms-full.txt": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "getLlmsFullTxt",
        "summary": "Every page and post concatenated into one document.",
        "responses": {
          "200": {
            "description": "Full site content as plain text.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/md/index.md": {
      "get": {
        "tags": [
          "content"
        ],
        "operationId": "getHomeMarkdown",
        "summary": "Home page as Markdown.",
        "responses": {
          "200": {
            "description": "Home page content.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/md/blog.md": {
      "get": {
        "tags": [
          "content"
        ],
        "operationId": "getBlogIndexMarkdown",
        "summary": "Blog index as Markdown.",
        "responses": {
          "200": {
            "description": "List of every post.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/md/blog/{slug}.md": {
      "get": {
        "tags": [
          "content"
        ],
        "operationId": "getBlogPostMarkdown",
        "summary": "One blog post as Markdown.",
        "description": "`GET /blog/{slug}` with `Accept: text/markdown` returns the same body.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Post slug, as listed in /llms.txt or /sitemap.xml.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Post front matter and body.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No such post; body links to the site map.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/md/projects.md": {
      "get": {
        "tags": [
          "content"
        ],
        "operationId": "getProjectsMarkdown",
        "summary": "Projects and work history as Markdown.",
        "responses": {
          "200": {
            "description": "Projects page content.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/md/daily-drivers.md": {
      "get": {
        "tags": [
          "content"
        ],
        "operationId": "getDailyDriversMarkdown",
        "summary": "Tools and hardware in daily use, as Markdown.",
        "responses": {
          "200": {
            "description": "Daily drivers page content.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts": {
      "get": {
        "tags": [
          "api"
        ],
        "operationId": "listPosts",
        "summary": "Every blog post, newest first.",
        "description": "The blog index as JSON. Each entry links to the HTML page, the Markdown twin, and the single-post JSON.",
        "responses": {
          "200": {
            "description": "Every published post.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostList"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts/{slug}": {
      "get": {
        "tags": [
          "api"
        ],
        "operationId": "getPost",
        "summary": "One blog post, body included.",
        "description": "`content` is the post's Markdown source, the same text `/md/blog/{slug}.md` serves.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Post slug, as returned by listPosts.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The post and its Markdown body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "404": {
            "description": "Structured error. `error.code` is stable; branch on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/spotify/recent": {
      "get": {
        "tags": [
          "api"
        ],
        "operationId": "getRecentSpotifyTracks",
        "summary": "Recently played Spotify tracks.",
        "description": "Decorative. Fails soft: the site is fully readable without it.",
        "responses": {
          "200": {
            "description": "Up to five recently played tracks, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/RecentTrack"
                  }
                }
              }
            }
          },
          "500": {
            "description": "Structured error. `error.code` is stable; branch on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Structured error. `error.code` is stable; branch on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "getSitemap",
        "summary": "Every public URL.",
        "responses": {
          "200": {
            "description": "urlset document.",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/feed.xml": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "getRssFeed",
        "summary": "Blog RSS feed. Also served at /rss.xml.",
        "responses": {
          "200": {
            "description": "RSS 2.0 document.",
            "content": {
              "application/rss+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/atom.xml": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "getAtomFeed",
        "summary": "Blog Atom feed.",
        "responses": {
          "200": {
            "description": "Atom 1.0 document.",
            "content": {
              "application/atom+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/api-catalog": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "getApiCatalog",
        "summary": "RFC 9727 linkset pointing at every machine-readable document.",
        "responses": {
          "200": {
            "description": "Linkset document.",
            "content": {
              "application/linkset+json": {
                "schema": {
                  "$ref": "#/components/schemas/Linkset"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/mcp": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "getMcpDescriptor",
        "summary": "MCP server descriptor.",
        "description": "Names the Streamable HTTP endpoint and the tools it exposes. Also served at /.well-known/mcp.json.",
        "responses": {
          "200": {
            "description": "Server descriptor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpDescriptor"
                }
              }
            }
          }
        }
      }
    },
    "/api/mcp": {
      "get": {
        "tags": [
          "api"
        ],
        "operationId": "getMcpEndpointInfo",
        "summary": "What the MCP endpoint is, for anyone opening it in a browser.",
        "description": "The transport offers no server-to-client stream, so GET returns the descriptor instead of an SSE channel.",
        "responses": {
          "200": {
            "description": "Server descriptor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpDescriptor"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "api"
        ],
        "operationId": "callMcp",
        "summary": "MCP over Streamable HTTP (JSON-RPC 2.0).",
        "description": "Stateless and unauthenticated. Supports `initialize`, `ping`, `tools/list` and `tools/call`. Tools: `list_posts`, `search_posts`, `get_post`, `get_page`. All read-only. Responses are `application/json`; there is no SSE stream and no session id.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonRpcRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "202": {
            "description": "Notification accepted; no body."
          },
          "400": {
            "description": "Malformed JSON-RPC.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/agent-skills/index.json": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "getAgentSkills",
        "summary": "Agent skills discovery index.",
        "responses": {
          "200": {
            "description": "Skills index.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentSkillsIndex"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "status",
              "resolution"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "not_found",
                  "method_not_allowed",
                  "upstream_unavailable",
                  "internal_error"
                ],
                "description": "Stable identifier. Branch on this, not on message."
              },
              "message": {
                "type": "string"
              },
              "status": {
                "type": "integer"
              },
              "resolution": {
                "type": "string",
                "description": "What the caller should do next."
              },
              "documentation": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "OriginalPublication": {
        "type": [
          "object",
          "null"
        ],
        "description": "Where the post first ran, when it is a republication. Null for posts written for this site.",
        "required": [
          "name",
          "url"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "PostSummary": {
        "type": "object",
        "required": [
          "slug",
          "title",
          "description",
          "date",
          "language",
          "readingTimeMinutes",
          "url",
          "markdownUrl",
          "jsonUrl",
          "originalPublication"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9-]+$"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "language": {
            "type": "string",
            "description": "BCP 47 tag for the body, e.g. \"no\" or \"en\"."
          },
          "readingTimeMinutes": {
            "type": "integer",
            "minimum": 1
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "markdownUrl": {
            "type": "string",
            "format": "uri"
          },
          "jsonUrl": {
            "type": "string",
            "format": "uri"
          },
          "originalPublication": {
            "$ref": "#/components/schemas/OriginalPublication"
          }
        }
      },
      "PostList": {
        "type": "object",
        "required": [
          "count",
          "posts"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "minimum": 0
          },
          "posts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PostSummary"
            }
          }
        }
      },
      "Post": {
        "type": "object",
        "required": [
          "slug",
          "title",
          "description",
          "date",
          "language",
          "readingTimeMinutes",
          "url",
          "markdownUrl",
          "originalPublication",
          "contentFormat",
          "content"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9-]+$"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "language": {
            "type": "string"
          },
          "readingTimeMinutes": {
            "type": "integer",
            "minimum": 1
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "markdownUrl": {
            "type": "string",
            "format": "uri"
          },
          "originalPublication": {
            "$ref": "#/components/schemas/OriginalPublication"
          },
          "contentFormat": {
            "type": "string",
            "const": "text/markdown"
          },
          "content": {
            "type": "string",
            "description": "The post body, as Markdown."
          }
        }
      },
      "McpDescriptor": {
        "type": "object",
        "required": [
          "name",
          "protocolVersion",
          "endpoint",
          "tools"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "protocolVersion": {
            "type": "string"
          },
          "instructions": {
            "type": "string"
          },
          "transport": {
            "type": "string",
            "const": "streamable-http"
          },
          "endpoint": {
            "type": "string",
            "format": "uri"
          },
          "servers": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "name",
                "transport",
                "url"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "transport": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "authentication": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "capabilities": {
            "type": "object",
            "additionalProperties": true
          },
          "tools": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "name",
                "description"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "inputSchema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          },
          "contact": {
            "type": "string"
          }
        }
      },
      "Linkset": {
        "type": "object",
        "required": [
          "linkset"
        ],
        "description": "RFC 9264 linkset. Each context object holds typed link arrays.",
        "properties": {
          "linkset": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "anchor"
              ],
              "properties": {
                "anchor": {
                  "type": "string",
                  "format": "uri"
                }
              },
              "additionalProperties": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "href"
                  ],
                  "properties": {
                    "href": {
                      "type": "string",
                      "format": "uri"
                    },
                    "type": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "AgentSkillsIndex": {
        "type": "object",
        "required": [
          "version",
          "site",
          "skills"
        ],
        "properties": {
          "$schema": {
            "type": "string",
            "format": "uri"
          },
          "version": {
            "type": "string"
          },
          "site": {
            "type": "object",
            "required": [
              "name",
              "url"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              },
              "description": {
                "type": "string"
              }
            }
          },
          "skills": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "name",
                "type",
                "description",
                "url",
                "sha256"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "sha256": {
                  "type": "string",
                  "pattern": "^[0-9a-f]{64}$"
                }
              }
            }
          }
        }
      },
      "JsonRpcRequest": {
        "type": "object",
        "required": [
          "jsonrpc",
          "method"
        ],
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "type": [
              "string",
              "integer",
              "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": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "result": {
            "type": "object",
            "additionalProperties": true
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "integer"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "RecentTrack": {
        "type": "object",
        "required": [
          "title",
          "artist",
          "album",
          "albumArt",
          "songUrl",
          "playedAt"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "artist": {
            "type": "string"
          },
          "album": {
            "type": "string"
          },
          "albumArt": {
            "type": "string",
            "format": "uri"
          },
          "songUrl": {
            "type": "string",
            "format": "uri"
          },
          "playedAt": {
            "type": "string",
            "format": "date-time"
          },
          "context": {
            "type": "object",
            "description": "Where the track was played from, when Spotify reports it.",
            "required": [
              "type",
              "url"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "playlist",
                  "album",
                  "artist",
                  "show"
                ]
              },
              "name": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      }
    }
  }
}