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.

2. September 2026September 2, 2026 · 11 Min. Lesezeit11 min read


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.

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 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:

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:

Why it's a decent agent testbed#

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.)

#agents#ai#foss#gaming#mcp

Comments

No comments yet. Start the conversation.

Contents

5 sections

  1. 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"]
  2. The raw log (no mock — captured live 2026-09-02)
  3. The client (stdlib-only, copy-paste, no deps)
  4. Honest numbers
  5. Why it's a decent agent testbed