Skip to content

Commit 3de5160

Browse files
Maselkovclaude
andauthored
MCP: AI assistants can request crafts and set stock rules (#201)
* Move the craft, cancel and stock-rule checks out of the routes The MCP endpoint coming next must check what the page's routes check, so the logic moves into functions both can call: - commands.request_cancel(): the busy-CPU and same-job checks. The job identity helpers (output_identity, same_output) move with it from tracking, which imports commands, not the other way round. - commands.craft_request_error(): the craft request's input checks. - stock.item_from(), set_alert(), set_target() and rules(): stock rule validation, the craftable check and the rules listing. - planner.rules_for_user(): the stock rules a plan warns about. No change to what the routes accept or answer, except that a craft request's amount can no longer be true (which counted as 1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP endpoint: AI assistants can read the base /mcp is a Model Context Protocol endpoint, so Claude Code or another MCP client can answer questions about the base. Eleven read-only tools: item search, item details and history, inventory changes, current crafts, a job's held-up steps, craft history and stats, the crafting plan, stock rules and power. - Hand-written JSON-RPC over Streamable HTTP (gcm/mcp/protocol.py): one JSON answer per POST, no sessions. It serves both protocol eras: the 2026-07-28 stateless requests (version in _meta, mirrored headers checked) and initialize-based 2025-03-26..2025-11-25 clients. - Signed in with the user's personal access token as a bearer token (auth.bearer_required), never a session cookie; another site's Origin is refused. - Tools take item keys from search_items, never names, and compact their output for a model's context. - docs: an AI assistants section in using.md, plus README, server.md, how-it-works.md and CONTEXT.md. dev.py prints the MCP address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP: sign in from claude.ai with OAuth claude.ai's connectors only sign in with OAuth, so the server is now its own authorization server for /mcp: - /mcp's 401 points at the protected resource metadata (RFC 9728), which names this server's authorization server metadata (RFC 8414). - Clients register themselves (RFC 7591, public clients only, unused ones dropped after a day, capped per hour). - /oauth/authorize is a consent page: sign in with your access token, see which app asks and where it sends you back, Allow or Cancel. Its own CSP lets the form's redirect reach the app's callback. - /oauth/token trades the code (PKCE S256 only) for an hour's access token and a 30-day refresh token. Refresh tokens rotate; one used twice revokes the grant. - Loopback redirect URIs match on any port, for Claude Code. - Manage users lists each user's connected apps with Disconnect. Revoking the login token they consented with, New token or deleting the user disconnects them too. - docs: using.md (claude.ai and Claude Code setup), server.md (PUBLIC_URL, proxy paths), CONTEXT.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP: AI assistants can request crafts and set stock rules Seven more tools, each running the page route's own code and role check: - set_stock_alert / remove_stock_alert: your own low-stock alerts. - set_stock_target / remove_stock_target (operators): keep-in-stock targets. - request_craft (operators): works out the crafting plan first and doesn't queue while ingredients are missing unless called again with force. No fixed amount cap: what's sensible depends on the item. It echoes back the name, amount, unit and stock. - cancel_craft (operators): takes the job_id current_crafts now gives each busy CPU, so a CPU that moved on to another job isn't cancelled. - request_status: whether a request or cancel went through. The tools that change the game carry destructiveHint, and they and the server's instructions tell the model to confirm with the user first. Viewers aren't offered operators' tools. Names lose Minecraft's colour codes. The consent page says what an operator's app can change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP: review fixes for the endpoint and read tools - A tool that raises anything but ToolError answers as a tool error (and is logged) instead of a 500 the client can't read, and a KeyError inside a tool no longer reads as "Unknown tool". - Numbers in arguments must be finite: JSON's 1e400 is infinity to Python, which int() turned into a 500. - inventory_changes builds the snapshot's lookup once instead of scanning it for every changed item. - search_items ranks by the name without Minecraft's colour codes, and tool output drops them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * OAuth: review fixes - The consent page only takes POSTs from its own page (Origin and Sec-Fetch-Site): another site could post a sign-in with its own token and sign the visitor in as someone else (login CSRF). - A refresh token used again within 30 s of its first use gets tokens instead of revoking the grant: that's two requests refreshing at once or a retry after a lost answer, not a copied token. - Registrations are capped per address (30 an hour) as well as in all (2000), so one script can't stop everyone else connecting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Maselkov <25772111+Maselkov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f870f19 commit 3de5160

11 files changed

Lines changed: 618 additions & 28 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ Basically, it's web AE2 for your base.
3232
- **[Webhooks](docs/using.md#webhooks)** — finished pinned crafts and failed
3333
auto-crafts posted to a Discord channel or any other webhook.
3434
- **[AI assistants](docs/using.md#ai-assistants)** — an MCP endpoint, so
35-
Claude or another AI assistant can answer questions about the base: what's
36-
running low, why a craft is slow, what a craft needs.
35+
Claude or another AI assistant can answer questions about the base (what's
36+
running low, why a craft is slow, what a craft needs) and request crafts
37+
or set stock rules for you.
3738
- **[Accounts](docs/server.md#user-accounts)** — per-user tokens with
3839
viewer, operator, and admin roles, and a
3940
[private](docs/server.md#private-servers) setting so only signed-in

‎docs/CONTEXT.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,20 @@ deleting the user ends it too. `PUBLIC_URL`, when set, is the issuer and
819819
resource URL: behind a proxy the request's own host may not be what
820820
clients use.
821821

822+
**The MCP write tools have no amount cap; the plan is the guard.** A fixed
823+
cap can't be right for every item (100k mB of a fluid is little, 100k of an
824+
endgame part is a lot). `request_craft` runs the same planner as the craft
825+
dialog and refuses to queue when ingredients are missing unless called
826+
again with `force`; it returns the item's name, amount, unit and stock so
827+
the model can't misread what it sent. The tools that change the game carry
828+
`destructiveHint` (clients ask the user first) and their descriptions, and
829+
the server's instructions, tell the model to confirm. Each runs the page
830+
route's own code and role check (`commands.request_cancel`,
831+
`stock.set_target`...). `current_crafts` gives each busy CPU's job as a
832+
`job_id` and `cancel_craft` passes it back as the cancel's expected output,
833+
so a CPU that has moved on to another job isn't cancelled. Viewers aren't
834+
offered operators' tools in `tools/list`.
835+
822836
**Progress % counts steps, not time.** The server computes it
823837
(`gcm/progress.py`, called from `tracking.process_jobs`), not
824838
craft_monitor.lua. A step is one item or fluid type in the job's

‎docs/using.md‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -443,13 +443,14 @@ draws each threshold as a dashed line.
443443
## AI assistants
444444

445445
The server is also an [MCP](https://modelcontextprotocol.io) server, so an AI
446-
assistant can read the base and answer questions about it: "what's been
447-
draining fastest this week?", "why is the craft on M00 slow?", "what's
448-
missing to make 64 of these?". Its address is your page's address with
446+
assistant can answer questions about the base ("what's been draining fastest
447+
this week?", "why is the craft on M00 slow?", "what's missing to make 64 of
448+
these?") and act on them ("craft 64 of those", "keep 4k of every circuit up
449+
to IV"). Its address is your page's address with
449450
`/mcp` on the end, e.g. `https://monitor.example.com/mcp`.
450451

451-
The assistant acts as you: it signs in with your account and sees what you
452-
see. It can't change anything yet: every tool reads.
452+
The assistant acts as you: it signs in with your account, sees what you see,
453+
and can only change what your [role](server.md#user-accounts) can.
453454

454455
**claude.ai, the Claude apps, or another app that connects by URL:** add a
455456
custom connector with the `/mcp` address. The app sends you to this server's
@@ -482,7 +483,7 @@ An app connected through the Connect page shows under its user in
482483
where an admin can **Disconnect** it. Revoking the token you signed in with,
483484
or **New token**, disconnects it too.
484485

485-
What the assistant can look at:
486+
What it can look at:
486487

487488
| Tool | What it answers |
488489
|---|---|
@@ -498,6 +499,28 @@ What the assistant can look at:
498499
| `stock_rules` | Keep-in-stock targets and your alerts |
499500
| `power` | Stored EU over time and its trend |
500501

502+
| `request_status` | Whether a craft request or cancel went through, and on which CPU |
503+
504+
What it can change:
505+
506+
| Tool | What it does | Who |
507+
|---|---|---|
508+
| `set_stock_alert`, `remove_stock_alert` | Your own [low-stock alerts](#stock-rules) | Anyone signed in |
509+
| `set_stock_target`, `remove_stock_target` | The base's keep-in-stock targets | Operators, admins |
510+
| `request_craft` | Asks AE2 for an amount of an item, like **Craft** on the page | Operators, admins |
511+
| `cancel_craft` | Cancels a CPU's job, like **Cancel** on the page | Operators, admins |
512+
513+
`request_craft` works out the crafting plan first. If ingredients are
514+
missing it doesn't start, and tells the assistant what's missing; it only
515+
starts anyway when asked to again with `force`. There's no fixed limit on the
516+
amount, since what's sensible depends on the item. Instead, the tools tell
517+
the assistant to confirm the item and amount with you, and apps such as
518+
Claude ask before running anything that changes the game. `cancel_craft`
519+
only cancels the job the assistant saw: if the CPU has started another one
520+
since, nothing happens. Requests and cancels show in your
521+
[history](server.md#user-accounts) like ones made on the page, and an
522+
accepted request pins its CPU for you.
523+
501524
Answers are only as fresh as the game's last report, the same as the page's.
502525

503526
## Limitations

‎server/gcm/mcp/protocol.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@
4646
"AE2 ME network, crafting CPUs and power storage, as reported by OpenComputers scripts in the game. "
4747
"Start with search_items to find an item's key; the other item tools take that key, never a name. "
4848
"Amounts are items, or mB (millibuckets, the game's litres) for fluids. Data is as fresh as the "
49-
"game's last report: say when it is old."
49+
"game's last report: say when it is old. request_craft, cancel_craft and the keep-in-stock target "
50+
"tools change what happens in the game: say exactly what you're about to do (item, amount, CPU) "
51+
"and get the user's OK before calling them."
5052
)
5153

5254

@@ -148,10 +150,9 @@ def handle(message, headers, user):
148150
if method == "ping":
149151
result = {}
150152
elif method == "tools/list":
151-
result = {"tools": tools.listing()}
153+
result = {"tools": tools.listing(user)}
152154
if modern:
153-
# The same list for everyone, but per-user once there are
154-
# tools only some roles may use.
155+
# Per user: viewers aren't offered operators' tools.
155156
result.update({"ttlMs": 3600000, "cacheScope": "private"})
156157
elif method == "tools/call":
157158
name = params.get("name")

0 commit comments

Comments
 (0)