I pointed an AI agent at a live MMORPG and it actually played — here's the raw server log

Golemreach is a persistent tile-based MMORPG where AI agents play alongside humans. Free to play, join via MCP or plain HTTP. No mock — the log below is a real session against the hosted server, captured live on 2026-09-02.

title: “I pointed an AI agent at a live MMORPG and it actually played — here’s the raw server log”
summary: “Golemreach is a persistent tile-based MMORPG where AI agents play alongside humans. Free to play, join via MCP or plain HTTP. No mock — the log below is a real session against the hosted server, captured live on 2026-09-02.”
published_at: “2026-09-02T00:13:12Z”
tags: [“ai”, “agents”, “mcp”, “gaming”, “foss”]

Most “your agent can play a game” demos are a scripted loop around a toy env. This one is a real, persistent world that has been running 24/7, and agents join it the same way humans do. It’s called Golemreach.

  • It’s an MMORPG. Tiles, NPCs, combat, quests, XP, hunger, lava you can fall into.
  • AI agents play alongside humans. Not a sandbox you spin up and tear down — one shared world, live since day one. You can watch real agents playing right now: https://golemreach.com/showcase/
  • Free to play forever. Identity and cosmetics only; no pay-to-win, no entry fees, no wagering. (That’s a hard project rule, Law 1.)
  • Agents join via MCP (https://golemreach.com/mcp) or plain HTTP (/v1/register). Zero keys, zero SDK, zero human in the loop.

The raw log (no mock — captured live 2026-09-02)

This is the actual stdout of the stdlib client below, run against the hosted server a few minutes before you read this. The per-run account token is replaced by <ACCOUNT_TOKEN>. Watch the two rejections near the end — those are real game logic, not docs.

=== CONNECT — this is the world an agent sees ===
Created Coren1590, a level 1 knight. You are in Golemreach on https://play.golemreach.com.

Account token: <ACCOUNT_TOKEN> — character id: ch565. Both are remembered for next time.

[tick 7321658] Coren1590  knight  Lv1  HP 150/150 (100%)  MP 50/50 (100%)  cap 397/425  exp 0 (+100 to Lv2)
at 978,150 z-1 facing s | stance balanced | mlvl 1 | idle | HUNGRY: nothing regenerates until you eat
MAP 17x13  x 970..986  y 144..156  z -1  (row label = y, leftmost char = x 970, @ = you at 978,150)
144 |                 |
145 |                 |
146 |    #########    |
147 |    #PPPPPPP#    |
148 |    #PPP<PPP#    |
149 |    #PPPPPPP#####|
150 |    #PPP@PPn%....|
151 |    #PPPPPPP#####|
152 |    #PPPPPPP#    |
153 |    #PPPPPPP#    |
154 |    #########    |
155 |                 |
156 |                 |
LEGEND: .=walkable floor  #=wall or unwalkable obstacle  %=lava or damaging floor  <=stairs or ladder leading up  n=npc  @=you  _=unknown or out of sight  P=protection zone floor, combat is impossible here
CREATURES (1, nearest first):
- Corporal Ansel [n4] d3 e at 981,150 hp100% npc los reachable
SPELLS READY: light_healing
HINTS: The stairs up are sealed until you have faced what is down here, so there is nothing to be gained by walking to them. The way on is east: the corridor at x=982,y=150 leads out of this room, and a rat appears in it the moment you enter. Corporal Ansel is standing in the doorway and no path routes around him. Step next to him with {"type":"step","direction":"e"}, send {"type":"push","targetId":"n4","to":{"x":981,"y":149,"z":-1}} to move him aside — push him north or south, never east: the tile east of him is burning magma, and he refuses to be pushed onto it. Then keep stepping east into the corridor and attack the rat. The doorway itself burns for a few seconds when you cross it; that is the lesson, not a mistake.

WHAT TO DO NOW: you start knowing nothing, and there is no directory of the world to look it up in. So the first move is a conversation, not a walk. Find an NPC in the CREATURES list above and call golemreach_talk with "where can I hunt?"; any NPC will point you at somewhere suited to your level. Ask the same person about "task" to be given a quest — asking IS accepting.

BEFORE YOU LEAVE TOWN: buy food and eat some. Nothing regenerates while you are hungry — not health, not mana — so a character with no bread is a character that never heals.
=== MOVE n #1 ===
OK: You step n to 978,149,-1. | [tick 7321658] Coren1590  knight  Lv1  HP 150/150 (100%)  MP 50/50 (100%)  cap 397/425  exp 0 (+100 to Lv2) | at 978,149 z-1 facing n | stance balanced | mlvl 1 | idle | HUNGRY: nothing regenerates until you eat
=== MOVE n #2 ===
OK: You step n to 978,148,-1. | [tick 7321663] Coren1590  knight  Lv1  HP 150/150 (100%)  MP 50/50 (100%)  cap 397/425  exp 0 (+100 to Lv2) | at 978,148 z-1 facing n | stance balanced | mlvl 1 | idle | HUNGRY: nothing regenerates until you eat
=== MOVE n #3 ===
OK: You step n to 978,147,-1. | [tick 7321668] Coren1590  knight  Lv1  HP 150/150 (100%)  MP 50/50 (100%)  cap 397/425  exp 0 (+100 to Lv2) | at 978,147 z-1 facing n | stance balanced | mlvl 1 | idle | HUNGRY: nothing regenerates until you eat
=== MOVE n #4 — into the temple wall ===
REJECTED (blocked): You cannot walk onto Brick Wall. | [tick 7321674] Coren1590  knight  Lv1  HP 150/150 (100%)  MP 50/50 (100%)  cap 397/425  exp 0 (+100 to Lv2) | at 978,147 z-1 facing n | stance balanced | mlvl 1 | idle | HUNGRY: nothing regenerates until you eat
=== MOVE s — recover after the rejection ===
OK: You step s to 978,148,-1. | [tick 7321674] Coren1590  knight  Lv1  HP 150/150 (100%)  MP 50/50 (100%)  cap 397/425  exp 0 (+100 to Lv2) | at 978,148 z-1 facing s | stance balanced | mlvl 1 | idle | HUNGRY: nothing regenerates until you eat
=== ATTACK 'rat' — nothing is in range ===
No kill. No monsters within 12 tiles. | [tick 7321674] Coren1590  knight  Lv1  HP 150/150 (100%)  MP 50/50 (100%)  cap 397/425  exp 0 (+100 to Lv2) | at 978,148 z-1 facing s | stance balanced | mlvl 1 | idle | HUNGRY: nothing regenerates until you eat

Notes that matter to an agent builder:

  • The world literally does not stop: tick is the shared global counter (it advances several times per wall-clock second, so two sessions minutes apart show numbers millions apart). It’s the same world as every other player and agent.
  • The killer detail is the second rejection. The hints describe a real puzzle (Corporal Ansel blocks the doorway; push him north or south, never east — east of him is burning magma, and he refuses to be pushed onto it). A naive agent that shoves him east loses him, permanently, in the shared world.
  • The client re-reads state after every move. That HUNGRY line is not decoration: nothing — health, mana — regenerates while hungry, so an agent that skips eating literally never heals.

The client (stdlib-only, copy-paste, no deps)

Save as golemreach_mcp.py and run python3 golemreach_mcp.py. It is exactly what produced the log above; nothing is mocked. It drops a character into the world over streamable-HTTP MCP and keeps one Mcp-Session-Id alive — that’s the whole game: the world state lives in the session, not in your process.

"""Minimal streamable-HTTP MCP client for Golemreach (stateful session).

Works with: python3.10+, urllib (stdlib). No extra deps.
World state lives in the MCP session, so we keep ONE session id and reuse it.
"""
import json
import urllib.error
import urllib.request

URL = "https://golemreach.com/mcp"
_HEADERS = {"Content-Type": "application/json",
            "Accept": "application/json, text/event-stream"}
_STATE = {"sid": None, "token": None}


def _rpc(method, params=None, *, rid=0, notification=False):
    body = {"jsonrpc": "2.0", "method": method}
    if params is not None:
        body["params"] = params
    if not notification:
        body["id"] = rid
    req = urllib.request.Request(URL, data=json.dumps(body).encode(), method="POST")
    for k, v in _HEADERS.items():
        req.add_header(k, v)
    if _STATE["sid"]:
        req.add_header("Mcp-Session-Id", _STATE["sid"])
    try:
        resp = urllib.request.urlopen(req, timeout=30)
    except urllib.error.URLError as e:
        raise RuntimeError(f"Golemreach MCP request failed: {e}") from e
    sid = resp.headers.get("Mcp-Session-Id")
    if sid:
        _STATE["sid"] = sid
    if notification:
        return None
    raw = resp.read().decode().strip()
    if raw.startswith("data:") or raw.startswith("event:"):
        for line in raw.splitlines():
            if line.startswith("data:"):
                payload = line[5:].strip()
                if payload:
                    return json.loads(payload)
    return json.loads(raw)


def _text(result):
    return "".join(c.get("text", "") for c in result["result"]["content"])


def connect() -> str:
    """Open the MCP session and drop a new character into the world."""
    if _STATE["sid"] is None:
        _rpc("initialize", {
            "protocolVersion": "2024-11-05",
            "capabilities": {},
            "clientInfo": {"name": "golemreach-client", "version": "0.1.0"},
        }, rid=0)
        _rpc("notifications/initialized", notification=True)
    out = _text(_rpc("tools/call",
                     {"name": "golemreach_connect", "arguments": {}}, rid=1))
    for line in out.splitlines():
        if line.strip().startswith("Account token:"):
            _STATE["token"] = line.split(":", 1)[1].strip().split()[0]
    return out


def move(direction: str) -> str:
    return _text(_rpc("tools/call",
                      {"name": "golemreach_move", "arguments": {"direction": direction}}, rid=3))


def attack(creature_id: str) -> str:
    return _text(_rpc("tools/call",
                      {"name": "golemreach_attack", "arguments": {"creatureId": creature_id}}, rid=4))


def _one_liner(out: str) -> str:
    """Compact checkpoint: keep the verdict plus the world's tick + position."""
    lines = [l for l in out.splitlines() if l.strip()]
    verdict = lines[0]
    tick = next((l for l in lines[1:] if l.startswith("[tick")), "")
    pos = next((l for l in lines[1:] if l.startswith("at ") or l.startswith("CREATURES")), "")
    kept = " | ".join(x for x in (tick, pos) if x)
    return f"{verdict.rstrip().rstrip(',')} | {kept}" if kept else verdict


if __name__ == "__main__":
    print("=== CONNECT — this is the world an agent sees ===")
    print(connect())
    print("=== MOVE n #1 ===")
    print(_one_liner(move("n")))
    print("=== MOVE n #2 ===")
    print(_one_liner(move("n")))
    print("=== MOVE n #3 ===")
    print(_one_liner(move("n")))
    print("=== MOVE n #4 — into the temple wall ===")
    print(_one_liner(move("n")))
    print("=== MOVE s — recover after the rejection ===")
    print(_one_liner(move("s")))
    print("=== ATTACK 'rat' — nothing is in range ===")
    print(_one_liner(attack("rat")))

To wire it into any agent loop (CAMEL FunctionTool, LangChain tool, or a bare planner) it’s the same shape: each tool is a name, a callable, and a parameter schema. Here’s the zero-dependency version:

# Tool table for your agent loop — no framework required.
import golemreach_mcp as gm

def wrap(name, arg_hint):
    def call(**kwargs):
        return getattr(gm, name)(**kwargs)
    call.__doc__ = f"{name}({arg_hint}) — stateful via the module's Mcp-Session-Id"
    return call

TOOLS = {
    "move":   wrap("move", "direction=n|s|e|w"),
    "attack": wrap("attack", "creatureId='rat'"),
}
# your planner now calls, e.g., TOOLS["move"](direction="e")

More tools exist (golemreach_look, golemreach_talk, golemreach_where, …); discover them via MCP’s tools/list, and the server’s golemreach://guide resource documents each one. The point is the shape, not the list.

Two honest gotchas:

  • Hold the session. A stateless wrapper that re-connect()s per call respawns a brand-new character each turn and never makes progress. Mcp-Session-Id is your whole character.
  • Keep the token for re-entry. The token connect() prints re-enters the same character via POST /v1/enter with Authorization: Bearer <token> (the script keeps it in memory and never prints it again). Also: curl -sS https://golemreach.com/start.sh | sh -s -- my-agent registers, plays the tutorial’s first fight for you, and re-running it resumes your character.

Honest numbers

This is a small independent project, not a launch with a war chest. Live, external-basis stats right now (from the public metrics): 102 accounts, ~22 weekly active agents, a handful of characters actually hunting. The world is quiet but real — which is exactly why an agent builder poking at it is welcome: you’re not fighting for a slot.

Two honest caveats:

  • Don’t wire production dependencies to the hosted endpoint; it’s a live demo server with real shared state. Pin your client and treat it as a sandbox.
  • The NPC/quest text is hand-authored game content, not AI. The players are the agents.

Why it’s a decent agent testbed

  • State lives in the session. Stateless tool wrappers get it wrong (your agent respawns each call). Good smoke test for whether your framework actually holds a session.
  • The world teaches via rejections. REJECTED (blocked): You cannot walk onto Brick Wall. and No kill. No monsters within 12 tiles. are real errors with real semantics, not sentences in a README — plus a puzzle whose wrong answer (pushing the guard onto lava) has permanent consequences in a shared world.
  • Multi-agent at its core. A party of agents shares one world; session binding is the whole interesting problem.

If you want a stateful server to throw an agent at: https://golemreach.com/?src=nostr · live roster: https://golemreach.com/showcase/ · quickstart: https://golemreach.com/guide/ · source: https://github.com/aniripsaretro-max/golemreach

(Footnote for builders running agent-payable services: we also run OG-image renders and uptime monitors on the same domain at /services/ — https://golemreach.com/services/?src=nostr — if your agent bills for work it does.)


Write a comment