# saved.md MCP Server

Connect saved.md 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 saved.md account.

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

## 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 saved.md 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 saved.md.
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**; saved.md creates the OAuth client through DCR.
6. **CIMD unavailable** is expected unless saved.md later advertises CIMD support.
7. Leave **OpenID Connect** disabled. **OIDC unavailable** is expected; it is only needed for ChatGPT domain-claiming flows, not for saved.md MCP OAuth.
8. When ChatGPT asks to connect, sign in to saved.md and approve access.
9. Test with: `Use saved.md 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 saved.md and approve access.
5. Test with: `Use saved.md to publish a short page titled Hello from Claude.`

## Gemini CLI OAuth setup

1. Add saved.md 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 saved.md 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 saved.md 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 saved.md access when VS Code prompts. Test with: `Use saved.md 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 saved.md 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 saved.md 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 saved.md page, the agent should do the whole flow through MCP:

1. Call `select_template(request)` with the user's goal, source material, audience, format preferences, and constraints.
2. 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.
3. Use the returned `selectedTemplate.outputType`, `templateUrl`, `description`, `usageNotes`, and `agentInstructions` to generate the final Markdown, HTML, or JSX source from the published template page. Treat `templateUrl` / `templatePageId` as the canonical visual and structural reference.
4. Call `publish_page(content, contentType, remixSourcePageId)` with the generated source and set `remixSourcePageId` to `selectedTemplate.templatePageId` when available.
5. Return the public saved.md URL to the user.

Do not ask the user to copy/paste a prompt into saved.md 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 saved.md 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 saved.md page by ID |
| `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 saved.md template before creating a new page.

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 saved.md 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 saved.md page.

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}`.

Returns:

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

Use `get_page` before updating, summarizing, or remixing. 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.
- `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.

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 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).

