# SavedMD MCP Server

Connect SavedMD to an AI agent with Model Context Protocol (MCP). The MCP server lets an authenticated agent publish, read, update, list, and delete pages in the user's SavedMD account.

MCP is the recommended path for outside agents. Every page it creates is owned by the signed-in SavedMD user behind the MCP connection; there is no anonymous MCP publishing path.

Agents should treat `SavedMD`, `saved.md`, and `savedmd` as the same product. If a user says "Use SavedMD" or "publish a SavedMD page", use this MCP server when it is connected.

## Endpoint

```
https://mcp.saved.md/api/mcp
```

The endpoint is remote Streamable HTTP MCP and requires authorization.

Use this exact `mcp.saved.md` URL in MCP clients. OAuth clients bind tokens to the connector host, and redirects can prevent authorization from being sent.

ChatGPT, Claude, Gemini CLI, Codex, VS Code/Copilot, Claude Code, Cursor, and OAuth-capable custom MCP clients use OAuth. Add the MCP URL in the app's connector setup, then sign in to SavedMD and approve access when prompted.

Token-based setup remains available for clients that do not support OAuth yet. For those clients, send:

```http
Authorization: Bearer smd_mcp_...
```

## Setup from the app

1. Sign in to SavedMD.
2. Open **Dashboard -> Connections**.
3. Pick the target app.
4. Copy the connector URL or token-based setup shown for that app.
5. Run the test prompt shown in the dashboard.

The dashboard currently includes paths for ChatGPT, Claude, Gemini CLI, Codex, VS Code/Copilot, Claude Code, Cursor, and custom MCP clients. Listed first-class clients use OAuth through the connector URL. Token-based setup is only a fallback for custom clients that do not support MCP OAuth.

## ChatGPT OAuth setup

1. Open ChatGPT **Settings -> Apps**.
2. Enable **Advanced settings -> Developer mode**.
3. Click **Create app** and paste:

```text
https://mcp.saved.md/api/mcp
```

4. For **Client registration**, choose **Dynamic Client Registration (DCR)**.
5. Do not use **User-Defined OAuth Client**; SavedMD creates the OAuth client through DCR.
6. **CIMD unavailable** is expected unless SavedMD later advertises CIMD support.
7. Leave **OpenID Connect** disabled. **OIDC unavailable** is expected; it is only needed for ChatGPT domain-claiming flows, not for SavedMD MCP OAuth.
8. When ChatGPT asks to connect, sign in to SavedMD and approve access.
9. Test with: `Use SavedMD to publish a short page titled Hello from ChatGPT.`

## Claude OAuth setup

1. Open Claude **Settings -> Connectors**.
2. Click **Add custom connector** and paste:

```text
https://mcp.saved.md/api/mcp
```

3. Add the connector.
4. When Claude asks to connect, sign in to SavedMD and approve access.
5. Test with: `Use SavedMD to publish a short page titled Hello from Claude.`

## Gemini CLI OAuth setup

1. Add SavedMD as a remote HTTP MCP server:

```bash
gemini mcp add --transport http savedmd https://mcp.saved.md/api/mcp
```

2. Verify it appears:

```bash
gemini mcp list
```

3. Open Gemini CLI and authenticate savedmd:

```text
/mcp auth savedmd
```

4. Complete the browser OAuth flow.
5. Test with: `Use SavedMD to publish a short markdown note titled Hello from Gemini CLI.`

## Codex OAuth setup

```bash
codex mcp add savedmd --url https://mcp.saved.md/api/mcp
codex mcp login savedmd
codex mcp list
```

After the browser OAuth flow completes, test with: `Use SavedMD to publish a short page titled Hello from my agent.`

## VS Code / Copilot OAuth setup

Create or update `.vscode/mcp.json`:

```json
{
  "servers": {
    "savedmd": {
      "type": "http",
      "url": "https://mcp.saved.md/api/mcp"
    }
  }
}
```

Open Copilot Agent mode, enable `savedmd` in the tools picker, and approve SavedMD access when VS Code prompts. Test with: `Use SavedMD to publish a summary of the current file.`

## Claude Code OAuth setup

```bash
claude mcp add --transport http savedmd https://mcp.saved.md/api/mcp
claude mcp list
```

Open Claude Code, run `/mcp`, choose `savedmd`, and follow the browser OAuth flow. Test with: `Use SavedMD to publish a markdown release note for this project.`

## Cursor OAuth setup

Create or update `~/.cursor/mcp.json` for a global setup, or `.cursor/mcp.json` in a project:

```json
{
  "mcpServers": {
    "savedmd": {
      "type": "http",
      "url": "https://mcp.saved.md/api/mcp"
    }
  }
}
```

Reload Cursor and connect `savedmd` from MCP settings, or authenticate from Cursor CLI:

```bash
cursor-agent mcp login savedmd
```

Test with: `Use SavedMD to publish a concise architecture summary for this repo.`

## Custom MCP clients

OAuth-capable MCP clients should connect to the Streamable HTTP URL without a static auth header:

```json
{
  "mcpServers": {
    "savedmd": {
      "type": "http",
      "url": "https://mcp.saved.md/api/mcp"
    }
  }
}
```

The client should follow MCP authorization discovery from the `WWW-Authenticate` response and the `.well-known` metadata, use PKCE, and send the resulting OAuth access token as `Authorization: Bearer <access-token>`.

## Token-only fallback

For clients that do not support MCP OAuth yet, send:

```http
Authorization: Bearer smd_mcp_...
```

## Agent workflow

When a user asks an AI agent to create a polished SavedMD page, the agent should do the whole flow through MCP:

1. If the user provides a specific SavedMD template URL, parse the page ID from the URL and call `get_page(id)`. Use that exact page's returned `content` and `contentType` as the canonical source and structure. Do not select a different template unless the URL cannot be read or the user asks for alternatives.
2. If the user does not provide a specific template URL, call `select_template(request)` with the user's goal, source material, audience, format preferences, and constraints.
3. If `select_template` cannot find a suitable match, ask the user to describe the desired layout, style, or outcome in more detail, or to share an image/reference for inspiration. Do not publish a from-scratch page unless the user explicitly asks for that.
4. Generate the final Markdown, HTML, or JSX source. For selected templates, use `selectedTemplate.outputType`, `templateUrl`, `description`, `usageNotes`, and `agentInstructions`; for provided template URLs, adapt the source returned by `get_page`.
5. Call `publish_page(content, contentType, remixSourcePageId)` with the generated source. Set `remixSourcePageId` to the selected template's `templatePageId`, or to the page ID parsed from the user-provided template URL.
6. Return the public SavedMD URL to the user.

Example: for `Publish a new SavedMD page based on the template "Bold Sidebar Creative Resume" found at https://www.saved.md/3fjwSRWaGzusu4ZiPG9-B21CczXZzEXe.`, call `get_page("3fjwSRWaGzusu4ZiPG9-B21CczXZzEXe")`, generate the new page from that template source, then call `publish_page` with `remixSourcePageId: "3fjwSRWaGzusu4ZiPG9-B21CczXZzEXe"`.

Do not ask the user to copy/paste a prompt into SavedMD when MCP is available. The copy-prompt UI on the website is a fallback for users who are not connected through MCP.

## Tools

The server exposes six tools.

| Tool | Purpose |
|------|---------|
| `select_template` | Choose the best SavedMD template for the user's request and return generation instructions |
| `publish_page` | Create a new account-owned Markdown, HTML, or JSX page and return its public URL |
| `get_page` | Read the current source for any public SavedMD page by ID or URL |
| `update_page` | Add a new version to an account-owned page without changing its public URL |
| `list_pages` | List pages owned by the authenticated account |
| `delete_page` | Delete an account-owned page |

## `select_template(request, limit?)`

Choose the best DB-backed SavedMD template before creating a new page.

Use `select_template` only when the user did not already provide a specific SavedMD template URL. If the request includes a URL such as `https://saved.md/{id}` or `https://www.saved.md/{id}`, call `get_page` for that ID instead and publish the result as a remix.

Arguments:

- `request` (string, required) — the user's goal plus any source material, audience, content type, style, or constraints.
- `limit` (number, optional) — number of candidates to return, from 1 to 5. Defaults to 3.

Returns:

```json
{
  "selectedTemplate": {
    "slug": "client-qbr",
    "title": "Client QBR page",
    "summary": "A client-facing recap with outcomes, roadmap, and next steps.",
    "category": "Customer success",
    "outputType": "html",
    "score": 12,
    "prompt": "You are helping me create a SavedMD page...",
    "agentInstructions": "Create a client-ready QBR page...",
    "templatePageId": "abc123",
    "templateUrl": "https://www.saved.md/abc123",
    "description": "Use this for a polished client QBR.",
    "usageNotes": "Preserve the executive summary, KPI strip, and next-step hierarchy."
  },
  "alternatives": []
}
```

Use this first for new page creation unless the user explicitly asks for a blank/freehand page. `select_template` is read-only; it does not publish anything. If it returns `no_suitable_template`, ask the user for more direction or a visual reference before publishing.

## `publish_page(content, contentType?, remixSourcePageId?)`

Create a new page owned by the authenticated account.

Arguments:

- `content` (string, required) — complete source, max 100 KB.
- `contentType` (string, optional) — `"markdown"` by default; may be `"markdown"`, `"html"`, or `"jsx"`.
- `remixSourcePageId` (string, optional) — set this when the new page is a remix of another SavedMD page. A SavedMD URL is accepted by the MCP tool and normalized to its page ID.

Returns:

```json
{
  "id": "abc123xyz",
  "url": "https://saved.md/abc123xyz",
  "contentType": "markdown",
  "ownedByAccount": true
}
```

Use `publish_page` after `select_template` when the user asks for a new shareable page. Do not use it to overwrite an existing URL.

## `get_page(id)`

Read the current source for a public page.

Arguments:

- `id` (string, required) — the page ID from `https://saved.md/{id}` or the full SavedMD URL.

Returns:

```json
{
  "id": "abc123xyz",
  "content": "# Page Title\n\n...",
  "contentType": "markdown",
  "url": "https://saved.md/abc123xyz",
  "currentVersion": 2
}
```

Use `get_page` before updating, summarizing, remixing, or creating a new page from a specific SavedMD template URL. The returned `content` is the source string for the current version.

## `update_page(id, content, contentType?)`

Add a new version behind the same public URL. The page must belong to the authenticated account.

Arguments:

- `id` (string, required) — the account-owned page ID or full SavedMD URL.
- `content` (string, required) — complete replacement source, max 100 KB.
- `contentType` (string, optional) — guard value. If provided, it must match the page's original content type.

`contentType` may be `"markdown"`, `"html"`, `"jsx"`, or `"slides"` for existing owned pages. New MCP publishes only create Markdown, HTML, or JSX pages.

Returns:

```json
{
  "id": "abc123xyz",
  "url": "https://saved.md/abc123xyz",
  "contentType": "markdown",
  "versionNumber": 2,
  "createdAt": "2026-05-13T10:05:00.000Z",
  "updatedInPlace": true
}
```

The public URL stays the same. Tell the user the page was updated in place and include the returned URL.

## `list_pages()`

List pages owned by the authenticated account.

Returns:

```json
{
  "pages": [
    {
      "id": "abc123xyz",
      "title": "AI Trends Report",
      "contentType": "markdown",
      "currentVersion": 1,
      "createdAt": "2026-05-13T10:00:00.000Z",
      "visitCount": 0,
      "url": "https://saved.md/abc123xyz"
    }
  ]
}
```

Use this when the user asks what they have saved, or when they need help finding the page ID to update or delete.

## `delete_page(id)`

Delete a page owned by the authenticated account.

Arguments:

- `id` (string, required) — the page ID or full SavedMD URL.

Returns:

```json
{
  "success": true,
  "id": "abc123xyz"
}
```

Only use this when the user explicitly asks to delete a page. Deletion is destructive.

## Editing rules

For an owned page:

1. `get_page(id)`.
2. Edit the returned `content`.
3. `update_page(id, editedContent, contentType)`.
4. Return the same `url` and mention the new `versionNumber`.

For a page not owned by the authenticated MCP account:

1. `get_page(id)`.
2. Edit the returned `content`.
3. `publish_page(editedContent, contentType, remixSourcePageId)`.
4. Return the new URL and state that it is a remix.

Never claim an unowned page was changed in place.

## MCP vs direct HTTP

| Scenario | Recommended path |
|----------|------------------|
| Agent creating a polished page for a signed-in user | MCP `select_template`, generate source, then `publish_page` |
| Agent creating from a specific SavedMD template URL | MCP `get_page`, generate from the returned source, then `publish_page` with `remixSourcePageId` |
| Agent creating a blank/freehand page for a signed-in user | MCP `publish_page` |
| Agent editing an owned page | MCP `update_page` |
| Agent remixing an unowned page | MCP `get_page` then `select_template` if useful, then `publish_page` |
| Script or app that wants raw HTTP control | Direct API in [`llms/core.md`](llms/core.md): `POST /api/templates/select`, then authenticated `POST /api/pages` |

## Constraints and errors

- Max source size is 100 KB.
- `publish_page` supports Markdown, HTML, and JSX only.
- `update_page` cannot change the original content type.
- New pages are always account-owned. Anonymous publishing is not supported.
- Account credit limits can return `usage_limit_reached` or `variant_limit_reached`.
- Keep MCP bearer tokens private.

## Troubleshooting

**Missing or invalid MCP authorization** — Sign in, open **Dashboard -> Connections**, and copy fresh setup for the app.

**The server is not listed in the client** — Re-run the setup command or inspect the client's MCP configuration.

**The page is not owned** — Use remix flow: `get_page` then `publish_page`.

**The content type does not match** — Keep the page's original `contentType` when calling `update_page`.

## More info

For direct API details, content guidelines, chart widgets, HTML sanitization, and JSX runtime rules, read [`install.md`](install.md) and [`llms/core.md`](llms/core.md).
