← mcp atlas

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"])

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:

fieldmeaning
name *short name, also used as the record key
description *what the server does — this powers english search
createdAt *ISO timestamp
transporthttp (hosted at url) or stdio (run locally from repo)
urlremote endpoint for hosted servers
reposource repository
languagepython, typescript, javascript, go, rust, zig
toolslist of {name, description} — also powers search
environmentlist of {name, required, description} env vars clients must set
packageslist of {registry, identifier, version} — pypi, npm, oci, …