publish your MCP server
the atlas doesn't have a database of servers — it crawls
tech.waow.mcp.server records that authors publish on their own
atproto PDS. one record, on your identity, and every crawl finds you. no signup, no approval.
1get an app password
any atproto account works — a Bluesky account included.
create an app password at
settings → app passwords
(never your real password). your handle (like alice.bsky.social) is the other
thing you need.
2publish the record
edit the server details, then run it. the record key is your server's name, so re-running updates in place — safe to wire into your deploy.
# publish.py — run with: uv run publish.py
# /// script
# requires-python = ">=3.11"
# dependencies = ["httpx"]
# ///
import os, re
from datetime import datetime, timezone
import httpx
SERVER = {
"$type": "tech.waow.mcp.server",
"name": "my-server",
"description": "what your server does, one paragraph.",
"transport": "http", # or "stdio" for run-locally servers
"url": "https://my-server.example.com/mcp", # omit for stdio
"repo": "https://github.com/you/my-server",
"language": "python",
"tools": [
{"name": "my_tool", "description": "what it does, one line."},
],
"createdAt": datetime.now(timezone.utc).isoformat(),
}
# export BSKY_HANDLE=you.bsky.social BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
handle = os.environ["BSKY_HANDLE"]
password = os.environ["BSKY_APP_PASSWORD"]
# handle → DID → PDS
did = httpx.get(
"https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle",
params={"handle": handle},
).json()["did"]
doc = httpx.get(f"https://plc.directory/{did}").json()
pds = next(s["serviceEndpoint"] for s in doc["service"] if s["id"] == "#atproto_pds")
# sign in and put the record on YOUR pds
login = httpx.post(
f"{pds}/xrpc/com.atproto.server.createSession",
json={"identifier": handle, "password": password},
)
login.raise_for_status()
session = login.json()
rkey = re.sub(r"[^a-z0-9-]+", "-", SERVER["name"].lower()).strip("-")
resp = httpx.post(
f"{pds}/xrpc/com.atproto.repo.putRecord",
headers={"Authorization": f"Bearer {session['accessJwt']}"},
json={
"repo": did,
"collection": "tech.waow.mcp.server",
"rkey": rkey,
"record": SERVER,
},
)
resp.raise_for_status()
print(resp.json()["uri"])
// publish.ts — run with: bun run publish.ts (no dependencies, node 24+ works too)
const SERVER = {
$type: "tech.waow.mcp.server",
name: "my-server",
description: "what your server does, one paragraph.",
transport: "http", // or "stdio" for run-locally servers
url: "https://my-server.example.com/mcp", // omit for stdio
repo: "https://github.com/you/my-server",
language: "typescript",
tools: [
{ name: "my_tool", description: "what it does, one line." },
],
createdAt: new Date().toISOString(),
};
// export BSKY_HANDLE=you.bsky.social BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
const handle = process.env.BSKY_HANDLE;
const password = process.env.BSKY_APP_PASSWORD;
if (!handle || !password) throw new Error("set BSKY_HANDLE and BSKY_APP_PASSWORD");
// handle → DID → PDS
const { did } = await (await fetch(
"https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=" + handle,
)).json();
const doc = await (await fetch("https://plc.directory/" + did)).json();
const pds = doc.service.find((s) => s.id === "#atproto_pds").serviceEndpoint;
// sign in and put the record on YOUR pds
const login = await fetch(pds + "/xrpc/com.atproto.server.createSession", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ identifier: handle, password }),
});
if (!login.ok) throw new Error("login failed: " + await login.text());
const session = await login.json();
const rkey = SERVER.name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
const resp = await fetch(pds + "/xrpc/com.atproto.repo.putRecord", {
method: "POST",
headers: {
"content-type": "application/json",
authorization: "Bearer " + session.accessJwt,
},
body: JSON.stringify({
repo: did,
collection: "tech.waow.mcp.server",
rkey,
record: SERVER,
}),
});
if (!resp.ok) throw new Error(await resp.text());
console.log((await resp.json()).uri);
3you're on the map
the atlas refreshes every few minutes — new records reach it off
the firehose via microcosm's UFOs index, with a
full relay sweep every 6 hours reconciling the long tail. hosted servers get a
liveness probe against their url. inspect your record any time at
pdsls.dev/at://<your-did>/tech.waow.mcp.server.
record reference
full schema lives on the network as com.atproto.lexicon.schema. the useful fields:
| field | meaning |
|---|---|
| name * | short name, also used as the record key |
| description * | what the server does — this powers english search |
| createdAt * | ISO timestamp |
| transport | http (hosted at url) or stdio (run locally from repo) |
| url | remote endpoint for hosted servers |
| repo | source repository |
| language | python, typescript, javascript, go, rust, zig |
| tools | list of {name, description} — also powers search |
| environment | list of {name, required, description} env vars clients must set |
| packages | list of {registry, identifier, version} — pypi, npm, oci, … |