BareGit

Add instructions

Author: MetroWind <chris.corsair@gmail.com>
Date: Wed Sep 23 17:07:56 2026 -0700
Commit: 82f2f3428a55f9347c8d8e088205ea4dfa093dd0

Changes

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 828eff1..f04f140 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -190,6 +190,8 @@ set(NETHACK_MCP_SOURCES
     src/game_http_server.cpp)
 
 set(STATIC_FILES
+    "${CMAKE_CURRENT_SOURCE_DIR}/web/AGENT.md"
+    "${CMAKE_CURRENT_SOURCE_DIR}/web/copy_prompt.js"
     "${CMAKE_CURRENT_SOURCE_DIR}/web/home.html"
     "${CMAKE_CURRENT_SOURCE_DIR}/web/home.css"
     "${CMAKE_CURRENT_SOURCE_DIR}/web/local_time.js"
diff --git a/README.md b/README.md
index 1cf26bf..58dd645 100644
--- a/README.md
+++ b/README.md
@@ -30,8 +30,10 @@ compiled into the executable. Asset changes regenerate that translation unit.
 ```
 
 The MCP endpoint is `http://127.0.0.1:8765/mcp`; the read-only landing page is
-at `http://127.0.0.1:8765/`. Game records are stored by default at
-`~/.local/share/nethack-mcp/games.sqlite3`, outside the temporary runtime
+at `http://127.0.0.1:8765/`. The landing page includes a short prompt for
+agents, and `/AGENT.md` explains how to connect and play. Game records are
+stored by default at `~/.local/share/nethack-mcp/games.sqlite3`, outside the
+temporary runtime
 directory. Stop the server with Ctrl-C. A server restart ends all active
 games and records them as interrupted.
 
diff --git a/cmake/embed_assets.cmake b/cmake/embed_assets.cmake
index 2e560ba..419bf84 100644
--- a/cmake/embed_assets.cmake
+++ b/cmake/embed_assets.cmake
@@ -21,6 +21,8 @@ foreach(ASSET IN LISTS STATIC_FILES)
     get_filename_component(ASSET_EXTENSION "${ASSET}" LAST_EXT)
     if(ASSET_EXTENSION STREQUAL ".html")
         set(ASSET_TYPE "text/html; charset=utf-8")
+    elseif(ASSET_EXTENSION STREQUAL ".md")
+        set(ASSET_TYPE "text/markdown; charset=utf-8")
     elseif(ASSET_EXTENSION STREQUAL ".css")
         set(ASSET_TYPE "text/css; charset=utf-8")
     elseif(ASSET_EXTENSION STREQUAL ".js")
diff --git a/src/game_http_server.cpp b/src/game_http_server.cpp
index 42be3ba..4e961c6 100644
--- a/src/game_http_server.cpp
+++ b/src/game_http_server.cpp
@@ -273,6 +273,14 @@ void GameHttpServer::setup()
     server.Get("/", [this](const Request& request, Response& response) {
         servePage(request, response);
     });
+    server.Get("/AGENT.md", [this](const Request& request,
+                                    Response& response) {
+        serveStatic("/AGENT.md", request, response);
+    });
+    server.Get("/copy-prompt.js", [this](const Request& request,
+                                           Response& response) {
+        serveStatic("/copy_prompt.js", request, response);
+    });
     server.Get(R"(/g/[0-9a-f-]{36})",
                [this](const Request& request, Response& response) {
                    serveGamePage(request, response);
@@ -488,6 +496,14 @@ void GameHttpServer::serveStatic(std::string_view path,
         return;
     }
     response.set_header("Cache-Control", "public, max-age=3600");
+    if(path == "/AGENT.md")
+    {
+        std::string guide(asset->content);
+        replaceTemplateValue(guide, "{{MCP_URL}}",
+                             manager_.publicBaseUrl() + "mcp");
+        response.set_content(guide, std::string(asset->content_type));
+        return;
+    }
     response.set_content(asset->content.data(), asset->content.size(),
                          std::string(asset->content_type));
 }
@@ -755,6 +771,11 @@ std::string GameHttpServer::recentPage()
     std::string html(asset->content);
     replaceTemplateValue(html, "{{MCP_URL}}",
                          htmlEscape(manager_.publicBaseUrl() + "mcp"));
+    replaceTemplateValue(
+        html, "{{AGENT_PROMPT}}",
+        htmlEscape("Read and follow " + manager_.publicBaseUrl()
+                   + "AGENT.md. Connect to its MCP endpoint and play NetHack "
+                     "autonomously. Share the spectator link with me."));
     replaceTemplateValue(html, "{{RECENT_GAMES}}", rows);
     return html;
 }
diff --git a/web/AGENT.md b/web/AGENT.md
new file mode 100644
index 0000000..e1db85e
--- /dev/null
+++ b/web/AGENT.md
@@ -0,0 +1,62 @@
+# Play NetHack with this server
+
+You are playing a real NetHack game through the tools on this server. Play
+autonomously until the game ends, and tell the user what happened.
+
+## Connect
+
+The remote MCP endpoint is `{{MCP_URL}}`. Add it as a Streamable HTTP MCP
+server in your agent environment. The server exposes `new_game`, `observe`,
+`press`, `select_menu`, `respond`, and `quit_game`.
+
+Reading this page does not connect the MCP tools. If you cannot call them,
+tell the user that the remote MCP server needs to be connected. The
+spectator pages cannot control a game.
+
+## Start a game
+
+Call `new_game` once with `model_slug` set to a short, printable ASCII
+name for your model or agent. You may also provide a character `name`.
+The result contains `game_id`, `control_token`, `viewer_url`, and the
+first `state`. Keep `game_id` and `control_token` for subsequent tool
+calls. Share the `viewer_url` with the user before continuing, but do
+not publish the control token or put it in a URL. If creation is rate
+limited or the server is full, report that instead of repeatedly
+creating games.
+
+## Play the game
+
+Use the returned `state`, then call `observe` as needed. Read its `map`,
+`status`, `messages`, `inventory`, `lifecycle`, `operation`, and `pending`
+fields before choosing an action. NetHack movement uses `h`, `j`, `k`, `l`
+for left, down, up, right, and `y`, `u`, `b`, `n` for diagonals.
+
+Every gameplay call needs the current `game_id` and `control_token`. For
+`press`, `select_menu`, or `respond`, also pass the latest
+`pending.input_id`:
+
+- For `pending.kind = "key"`, call `press` with one printable ASCII character
+  or a named key such as `ENTER`, `ESC`, `SPACE`, or `CTRL_A`.
+- For `pending.kind = "menu"`, call `select_menu` with `selections` containing
+  the desired selectable entries, such as `[{"entry_id": 1}]`. Pass an empty
+  array if choosing none, or use `cancel: true` with an empty array to cancel.
+- For `pending.kind = "text"`, `"choice"`, `"command"`, or `"acknowledge"`,
+  call `respond` with exactly one matching field: `text`, `choice`,
+  `command`, or `acknowledge: true`. Use offered choices and commands.
+
+Read the state returned by each action. If an operation is still running or
+no input is pending, wait briefly and call `observe` again. Do not send an
+action for an old `input_id`. Only the ten most recent messages are retained;
+read each response and use `after_message_id` to avoid repeating messages.
+Use `quit_game` only when you intentionally want to end the run.
+
+When `lifecycle` becomes `ended` or `failed`, stop issuing gameplay actions
+and report the outcome to the user. Keep the spectator link available while
+the game is active.
+
+## Game knowledge
+
+The goal of the game is to go down to the lowest level of the dungeon,
+and then go up to ascend. You can more info on how to play NetHack at
+https://www.nethack.org/download/5.0.0/nethack-500-Guidebook.txt and
+https://nethackwiki.com/wiki/Guidebook .
diff --git a/web/copy_prompt.js b/web/copy_prompt.js
new file mode 100644
index 0000000..c81a715
--- /dev/null
+++ b/web/copy_prompt.js
@@ -0,0 +1,19 @@
+const prompt_input = document.getElementById("agent-prompt");
+const copy_button = document.getElementById("copy-agent-prompt");
+const copy_status = document.getElementById("agent-prompt-status");
+
+copy_button.addEventListener("click", async () =>
+{
+    try
+    {
+        await navigator.clipboard.writeText(prompt_input.value);
+        copy_status.textContent = "Copied to clipboard.";
+    }
+    catch
+    {
+        prompt_input.focus();
+        prompt_input.select();
+        copy_status.textContent =
+            "Copy failed. The prompt is selected; copy it manually.";
+    }
+});
diff --git a/web/home.css b/web/home.css
index aacd7d5..25fe34e 100644
--- a/web/home.css
+++ b/web/home.css
@@ -21,3 +21,26 @@ td {
 code {
   overflow-wrap: anywhere;
 }
+
+.agent-prompt
+{
+    display: flex;
+    gap: .5rem;
+}
+
+.agent-prompt input
+{
+    flex: 1;
+    min-width: 0;
+    padding: .55rem;
+}
+
+.agent-prompt button
+{
+    padding: .55rem .9rem;
+}
+
+#agent-prompt-status
+{
+    min-height: 1.5em;
+}
diff --git a/web/home.html b/web/home.html
index bd6f328..e112667 100644
--- a/web/home.html
+++ b/web/home.html
@@ -12,6 +12,17 @@
     <p>Agents play through the shared MCP endpoint. Spectators can watch
        live games, and recent results appear below.</p>
     <p>MCP endpoint: <code>{{MCP_URL}}</code></p>
+    <section>
+      <h2>Let an agent play</h2>
+      <p>Paste this into an agent that supports remote MCP servers:</p>
+      <div class="agent-prompt">
+        <input id="agent-prompt" type="text" readonly
+               aria-label="Prompt to paste into an agent"
+               value="{{AGENT_PROMPT}}">
+        <button id="copy-agent-prompt" type="button">Copy</button>
+      </div>
+      <p id="agent-prompt-status" role="status" aria-live="polite"></p>
+    </section>
     <h2>Ten most recent completed games</h2>
     <table>
       <thead>
@@ -31,5 +42,6 @@
     </table>
   </main>
   <script src="/local-time.js"></script>
+  <script src="/copy-prompt.js"></script>
 </body>
 </html>