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.
prefer a checked-in path? copy the
publisher,
GitHub Action,
or Tangled workflow.
Try iroh (experimental)
Host an MCP server without your own domain. Clients connect through an iroh bridge using your public endpoint ID. Your server must stay online.
Add an iroh object to your server record. Keep url and transport: "http" if you also offer HTTP. For iroh-only servers, omit url and use transport: "iroh".
"iroh": {
"endpointId": "YOUR_64_CHARACTER_HEX_PUBLIC_ENDPOINT_ID",
"alpn": "tech.waow/mcp-stdio-spike/0"
}
This is an app-level experiment, not a published lexicon change or standard MCP transport. Atlas displays these connections as not checked. An HTTP health check says nothing about the iroh connection.
Download the experimental bridge source (Rust 1.91+, Unix). Extract it and run cargo build --locked. The included README covers setup. The bridge only supports the protocol shown above; another ALPN needs a compatible client.
Each side creates its own key with iroh-mcp-spike keygen identity.key. Share only the printed public ID; retain the private key file to keep your ID stable. The server operator must allow the client's ID. Use one active bridge process per identity key; simultaneous processes sharing a key were unreliable in testing.
# publisher: allow one client, then run your existing stdio server
./target/debug/iroh-mcp-spike serve server.key CLIENT_ID auto ready.txt python server.py
# client: configure this as a standard MCP stdio command
./target/debug/iroh-mcp-spike connect client.key SERVER_ID auto
Use absolute paths in client configuration. This bridge requires a local subprocess; HTTP-only clients cannot use it directly. Anyone can see a published endpoint ID, but this bridge rejects clients outside its allowlist. Public access is not supported by this experimental bridge.
Tested between a Mac and Linux, including relay-only connections. Public iroh relays are for development and testing. Choose production relay infrastructure before relying on this for a production service.
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 ATPROTO_HANDLE=you.example ATPROTO_PASSWORD=xxxx-xxxx-xxxx-xxxx
handle = os.environ["ATPROTO_HANDLE"]
password = os.environ["ATPROTO_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 ATPROTO_HANDLE=you.example ATPROTO_PASSWORD=xxxx-xxxx-xxxx-xxxx
const handle = process.env.ATPROTO_HANDLE;
const password = process.env.ATPROTO_PASSWORD;
if (!handle || !password) throw new Error("set ATPROTO_HANDLE and ATPROTO_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, … |