From b4813df8b4d80240633b59bad3135ef24a62b4c4 Mon Sep 17 00:00:00 2001 From: Thomas <28439359+thoda-dev@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:23:30 +0200 Subject: [PATCH 1/3] feat(docs): document the MCP server and list GitHub releases --- .../app/components/mdc/GithubReleases.vue | 83 +++++++++++++++++ apps/docs/app/components/mdc/McpInstall.vue | 73 +++++++++++++++ apps/docs/content/1.getting-started/2.mcp.md | 89 +++++++++++++++++++ apps/docs/content/4.releases.md | 19 ++++ apps/docs/nuxt.config.ts | 19 ++++ apps/docs/server/api/releases.get.ts | 63 +++++++++++++ 6 files changed, 346 insertions(+) create mode 100644 apps/docs/app/components/mdc/GithubReleases.vue create mode 100644 apps/docs/app/components/mdc/McpInstall.vue create mode 100644 apps/docs/content/1.getting-started/2.mcp.md create mode 100644 apps/docs/content/4.releases.md create mode 100644 apps/docs/server/api/releases.get.ts diff --git a/apps/docs/app/components/mdc/GithubReleases.vue b/apps/docs/app/components/mdc/GithubReleases.vue new file mode 100644 index 0000000..a6614a2 --- /dev/null +++ b/apps/docs/app/components/mdc/GithubReleases.vue @@ -0,0 +1,83 @@ + + + diff --git a/apps/docs/app/components/mdc/McpInstall.vue b/apps/docs/app/components/mdc/McpInstall.vue new file mode 100644 index 0000000..68777e7 --- /dev/null +++ b/apps/docs/app/components/mdc/McpInstall.vue @@ -0,0 +1,73 @@ + + + diff --git a/apps/docs/content/1.getting-started/2.mcp.md b/apps/docs/content/1.getting-started/2.mcp.md new file mode 100644 index 0000000..ae20724 --- /dev/null +++ b/apps/docs/content/1.getting-started/2.mcp.md @@ -0,0 +1,89 @@ +--- +title: Doc MCP server +description: Plug this documentation into Cursor, VS Code, Claude or any other MCP client. +navigation: + icon: i-lucide-plug +--- + +This documentation is also served as a [Model Context Protocol](https://modelcontextprotocol.io) +server. Point an AI assistant at it and it answers from the pages you are reading right now, instead +of whatever it remembers about a self-hosted pastebin it saw once during training. + +The endpoint is one URL, over HTTP, with no account and no token: + +::mcp-install +:: + +## What it exposes + +| Tool | What it does | +|---|---| +| `list-pages` | Lists every documentation page with its title, description and path. | +| `get-page` | Returns the full markdown of one page, given its path. | + +Both are read-only. The server has no access to any shhh instance, no access to your pastes, and no +way to create anything — it reads this documentation and nothing else. + +## Adding it by hand + +The buttons above cover Claude, Cursor and VS Code. Everywhere else, add the URL yourself. + +::code-group + +```bash [Claude Code] +claude mcp add --transport http shhh-docs https://shhh-docs.thoda.dev/mcp +``` + +```json [.vscode/mcp.json] +{ + "servers": { + "shhh-docs": { + "type": "http", + "url": "https://shhh-docs.thoda.dev/mcp" + } + } +} +``` + +```json [~/.cursor/mcp.json] +{ + "mcpServers": { + "shhh-docs": { + "type": "http", + "url": "https://shhh-docs.thoda.dev/mcp" + } + } +} +``` + +:: + +**ChatGPT** has no install link of its own. Custom connectors sit behind *Developer mode*, in +**Settings → Apps & Connectors → Advanced**; turn it on, then add the URL as a connector. It is +web-only and needs a paid plan — OpenAI moves those menu labels around, so follow +[their help page](https://help.openai.com/en/articles/12584461) if they have shifted again. + +Other clients — Zed, Windsurf, Continue, and anything built on an MCP SDK — take the same URL as a +*streamable HTTP* server; check their own documentation for where the config file lives. + +::note +Restart the client after editing its config. Most of them only read MCP servers at startup. +:: + +::warning +Claude and ChatGPT call the server from their own infrastructure, so they need a URL that is +publicly reachable over HTTPS. Cursor, VS Code and Claude Code connect from your own machine, where +`http://localhost:3000/mcp` works fine. +:: + +## Running it from your own instance + +The MCP server is part of the documentation site, not of shhh itself. If you host the docs yourself +with `docker/docs.Dockerfile`, the endpoint is `/mcp` on your own origin — `https://docs.example.com/mcp` — +and the install buttons on this page already point at whichever host served it to you. + +::warning +shhh itself has **no public API and no MCP server**. An agent cannot create or read pastes on your +behalf; it can only read this documentation. Both are on the +[roadmap](https://github.com/thoda-dev/shhh/blob/master/ROADMAP.md) and neither is built yet. +:: diff --git a/apps/docs/content/4.releases.md b/apps/docs/content/4.releases.md new file mode 100644 index 0000000..8bc054e --- /dev/null +++ b/apps/docs/content/4.releases.md @@ -0,0 +1,19 @@ +--- +title: Releases +description: Every published version of shhh, pulled straight from GitHub. +navigation: + icon: i-lucide-tag +--- + +Release notes are generated from the commit history at tag time. This page reads the +[GitHub releases](https://github.com/thoda-dev/shhh/releases) of the repository directly, so it never +lags behind a publication. + +::note +Version numbers follow [semantic versioning](https://semver.org). Upgrading is a `docker compose pull` +away — migrations run on boot, so there is no separate step. Read the notes first when a major +version lands. +:: + +::github-releases +:: diff --git a/apps/docs/nuxt.config.ts b/apps/docs/nuxt.config.ts index c6f7ec6..104ea8a 100644 --- a/apps/docs/nuxt.config.ts +++ b/apps/docs/nuxt.config.ts @@ -20,6 +20,22 @@ export default defineNuxtConfig({ }, }, + // The docs double as an MCP server on /mcp — see content/1.getting-started/2.mcp.md. Without a + // name the handshake advertises an empty one, which is what MCP clients list the server under. + mcp: { + name: 'shhh docs', + description: 'Documentation for shhh, a self-hosted zero-knowledge pastebin.', + }, + + // Two pages have to be rendered per request rather than baked at build time. /releases reads the + // GitHub API, and prerendering would freeze the list; its handler caches, so this costs one call to + // GitHub every half hour, not one per hit. The MCP page builds absolute URLs from the request + // origin, and the prerenderer only knows `http://localhost`. + routeRules: { + '/getting-started/mcp': { prerender: false }, + '/releases': { prerender: false }, + }, + // Same reason as the app: bundle icons at build time rather than fetching them at runtime. icon: { clientBundle: { @@ -35,16 +51,19 @@ export default defineNuxtConfig({ 'lucide:file-lock', 'lucide:info', 'lucide:lock-keyhole', + 'lucide:plug', 'lucide:rocket', 'lucide:server', 'lucide:shield', 'lucide:shield-check', 'lucide:sliders-horizontal', + 'lucide:tag', 'lucide:timer', 'lucide:user', 'lucide:users', 'simple-icons:github', 'vscode-icons:file-type-dotenv', + 'vscode-icons:file-type-json', ], }, }, diff --git a/apps/docs/server/api/releases.get.ts b/apps/docs/server/api/releases.get.ts new file mode 100644 index 0000000..16b7e89 --- /dev/null +++ b/apps/docs/server/api/releases.get.ts @@ -0,0 +1,63 @@ +interface GitHubRelease { + name: string | null + tag_name: string + html_url: string + published_at: string | null + created_at: string + draft: boolean + prerelease: boolean + body: string | null +} + +interface MarkdownNode { + props?: Record + children?: MarkdownNode[] +} + +// Every release repeats the same `### 🚀 Enhancements` headings, and they all land on one page. +function prefixIds(node: MarkdownNode, prefix: string) { + if (typeof node.props?.id === 'string') { + node.props.id = `${prefix}-${node.props.id}` + } + node.children?.forEach(child => prefixIds(child, prefix)) +} + +async function renderNotes(body: string, tag: string) { + // changelogen opens each body with the version as a heading; the card already shows it. GitHub + // hands the notes back with CRLF endings, which `.` in a JS regex refuses to cross. + const notes = body.replace(/\r\n/g, '\n').trim().replace(/^##\s+.*\n+/, '') + const parsed = await parseMarkdown(notes || '_No release notes._') + prefixIds(parsed.body as MarkdownNode, tag) + return parsed.body +} + +// Unauthenticated GitHub allows 60 calls an hour per IP, shared by every visitor of the instance. +// Caching the whole list keeps a busy day at 48 calls and rides out a GitHub outage for half an hour. +export default defineCachedEventHandler(async () => { + const releases = await $fetch('https://api.github.com/repos/thoda-dev/shhh/releases', { + query: { per_page: 30 }, + headers: { + 'accept': 'application/vnd.github+json', + 'user-agent': 'shhh-docs', + 'x-github-api-version': '2022-11-28', + }, + }) + + return Promise.all(releases + .filter(release => !release.draft) + .map(async release => ({ + title: release.name || release.tag_name, + tag: release.tag_name, + url: release.html_url, + publishedAt: release.published_at || release.created_at, + prerelease: release.prerelease, + // Parsed here rather than in the browser so the page ships an AST the docs prose components + // render, instead of raw HTML that would need its own styling. + body: await renderNotes(release.body || '', release.tag_name), + }))) +}, { + name: 'github-releases', + maxAge: 60 * 30, + swr: true, + getKey: () => 'thoda-dev/shhh', +}) From eede0a914feee102b7e569b48794773707231d76 Mon Sep 17 00:00:00 2001 From: Thomas <28439359+thoda-dev@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:32:52 +0200 Subject: [PATCH 2/3] docs(mcp): drop the hardcoded localhost URL from the reachability note --- apps/docs/content/1.getting-started/2.mcp.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/docs/content/1.getting-started/2.mcp.md b/apps/docs/content/1.getting-started/2.mcp.md index ae20724..6d29339 100644 --- a/apps/docs/content/1.getting-started/2.mcp.md +++ b/apps/docs/content/1.getting-started/2.mcp.md @@ -71,9 +71,10 @@ Restart the client after editing its config. Most of them only read MCP servers :: ::warning -Claude and ChatGPT call the server from their own infrastructure, so they need a URL that is -publicly reachable over HTTPS. Cursor, VS Code and Claude Code connect from your own machine, where -`http://localhost:3000/mcp` works fine. +Claude and ChatGPT reach the server from their own infrastructure, so the URL has to be publicly +reachable over HTTPS. Cursor, VS Code and Claude Code connect from your own machine instead, so they +also work against an instance on your network, or on a `localhost` port while you run the docs +yourself. :: ## Running it from your own instance From 6e071761b703f4deed7b642f75a7611633be67b2 Mon Sep 17 00:00:00 2001 From: Thomas <28439359+thoda-dev@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:39:22 +0200 Subject: [PATCH 3/3] fix(docs): handle a failing clipboard write on the MCP page --- apps/docs/app/components/mdc/McpInstall.vue | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/docs/app/components/mdc/McpInstall.vue b/apps/docs/app/components/mdc/McpInstall.vue index 68777e7..a61c09d 100644 --- a/apps/docs/app/components/mdc/McpInstall.vue +++ b/apps/docs/app/components/mdc/McpInstall.vue @@ -14,7 +14,19 @@ const copied = ref(false) const claudeUrl = `https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=${encodeURIComponent(serverName)}&connectorUrl=${encodeURIComponent(serverUrl)}` async function copyUrl() { - await navigator.clipboard.writeText(serverUrl) + try { + await navigator.clipboard.writeText(serverUrl) + } catch { + // There is no clipboard outside a secure context, which a docs instance on plain HTTP is not. + toast.add({ + title: 'Could not copy', + description: 'Select the URL above and copy it by hand.', + icon: 'i-lucide-triangle-alert', + color: 'warning', + }) + return + } + copied.value = true setTimeout(() => copied.value = false, 2000) toast.add({ title: 'Server URL copied', icon: 'i-lucide-check-circle', color: 'success' })