# Reolora MCP · Connect AI agents to your warehouse

Reolora has an MCP server, so Claude, ChatGPT, Codex, Cursor, Grok and other agents can see stock, order, move and count. They follow the same rules as your staff, and everything is in the event log under the agent’s name.

- Address: https://mcp.reolora.dk/mcp
- Transport: Streamable HTTP
- Sign-in: OAuth 2.1 or API key
- Tools: 43
- HTML: https://www.reolora.dk/en/mcp

## Connect

Pick your client. Interactive clients sign in with OAuth; bots and scripts use an API key.

### Claude Code

- Add the server in your terminal.
- Type /mcp in Claude Code and pick reolora to sign in in the browser.

```sh
claude mcp add --transport http reolora https://mcp.reolora.dk/mcp
```

With an API key instead:

```sh
claude mcp add --transport http reolora https://mcp.reolora.dk/mcp \
  --header "Authorization: Bearer rk_live_…"
```

### Claude.ai / Desktop

- Open Settings → Connectors → Add custom connector.
- Name: Reolora. URL: the address below. Click Add and sign in.
- Works in Claude.ai, Claude Desktop and Claude on mobile.

```
https://mcp.reolora.dk/mcp
```

Open Connectors in Claude: https://claude.ai/settings/connectors

### ChatGPT / Dots

- Settings → Apps & Connectors → Advanced → turn on Developer mode.
- Create a connector: name Reolora, the URL below, authentication OAuth.
- Needs a Business or Enterprise plan at OpenAI. Dots uses the same connectors; Deep Research uses the search and fetch tools.

```
https://mcp.reolora.dk/mcp
```

### Codex

- Add the server and sign in (opens the browser).

```sh
codex mcp add reolora --url https://mcp.reolora.dk/mcp
codex mcp login reolora
```

With an API key instead: (~/.codex/config.toml)

```toml
[mcp_servers.reolora]
url = "https://mcp.reolora.dk/mcp"
bearer_token_env_var = "REOLORA_API_KEY"
```

### Cursor

- Click the button, or put the block in ~/.cursor/mcp.json. Cursor asks you to sign in.

```json
{
  "mcpServers": {
    "reolora": {
      "url": "https://mcp.reolora.dk/mcp"
    }
  }
}
```

With an API key instead:

```json
{
  "mcpServers": {
    "reolora": {
      "url": "https://mcp.reolora.dk/mcp",
      "headers": {
        "Authorization": "Bearer rk_live_…"
      }
    }
  }
}
```

### Grok

- grok.com → Connectors → New connector → Custom. The URL below, then sign in.
- Grok Build CLI is added like Claude Code. From the xAI API you use the MCP tool:

```
https://mcp.reolora.dk/mcp
```

xAI API

```json
{
  "type": "mcp",
  "server_url": "https://mcp.reolora.dk/mcp",
  "server_label": "reolora",
  "authorization": "Bearer rk_live_…"
}
```

Open Grok connectors: https://grok.com/connectors

### VS Code

- Click the button, or put the block in .vscode/mcp.json. Copilot asks you to sign in.

```json
{
  "servers": {
    "reolora": {
      "type": "http",
      "url": "https://mcp.reolora.dk/mcp"
    }
  }
}
```

Or in the terminal:

```sh
code --add-mcp '{"name":"reolora","type":"http","url":"https://mcp.reolora.dk/mcp"}'
```

### Gemini CLI

- Add the server and sign in with /mcp auth reolora.

```sh
gemini mcp add --transport http reolora https://mcp.reolora.dk/mcp
```

With an API key instead: (~/.gemini/settings.json)

```json
{
  "mcpServers": {
    "reolora": {
      "httpUrl": "https://mcp.reolora.dk/mcp",
      "headers": {
        "Authorization": "Bearer rk_live_…"
      }
    }
  }
}
```

## Sign in

### OAuth 2.1

For Claude.ai, ChatGPT, Dots, Grok, Cursor, VS Code and the CLIs with a login. The client finds the sign-in page itself; you sign in with your e-mail and approve. The app can then do what you can.

### API key

For bots that run on their own and CLIs without a browser. A key belongs to an agent in Team, so the agent has its own name and role. Send it as Authorization: Bearer rk_live_…. The key is shown once; 120 calls a minute by default.

Create the key under Office → Team → Keys.

## Roles

What an agent may do depends on its role. You pick it when you create the agent and can change it any time.

| | agent_mode | |
|---|---|---|
| Read | `read` | Reads everything on its sites. Changes nothing, proposes nothing. |
| Suggest | `suggest` | Changes become proposals that a manager approves. A good place to start. |
| Work | `execute` | Daily work: receive, ship transfers, count, move, orders and returns. Purchasing and the catalogue become proposals. |
| Manage | `manage` | Like a manager: purchasing, min/max and the catalogue too. |

Signed in with OAuth, the agent has your own role.

## Tools

The list comes straight from the server. Every list takes limit and cursor; every change takes preview and request_id; site can be left out when you have one site.

| Name | What it does | Kind | Example |
|---|---|---|---|
| `warehouse_status` | Start here. Orders by status, items out/reorder/ok, open purchase orders and transfers, pending proposals, the latest events and `cursor` (the newest event n). Pass `since` (a cursor you kept) to also get summary_since: what happened after it, by kind and by who. | reads | `{"since": 1520}` |
| `find_items` | Find items by SKU, EAN, supplier SKU or part of the name, with where they are on this site. | reads | `{"query": "løbesko 42"}` |
| `stock_overview` | Stock per item: available (on hand minus reserved), on its way, min/max, sold in 30 days, days left and status (out, reorder, coming, over, ok, none = no min/max). Filter by status, group or text; most urgent first. | reads | `{"status": "out"}` |
| `item_detail` | Everything about one item: master data, stock on every site, places here, min/max, what is on its way, 12 weeks of sales, recent history. | reads | `{"sku": "SKU-1003"}` |
| `list_items` | The catalogue (all sites) with group and supplier: filter by group name, items without a group, or text. For grouping and clean-up. | reads | `{"without_group": true}` |
| `groups_and_suppliers` | The item group tree (id, name, parent) and the suppliers (id, name, lead time in days, e-mail). | reads | `{}` |
| `reorder_suggestions` | What to buy now, per supplier: items below min (counting what is on its way), filled up to max in whole packs, with value and stock at other sites. Filter by supplier name. | reads | `{"supplier": "FitGear"}` |
| `suggest_levels` | Min/max from sales: daily mean and spread over `days` (default 60), lead time, 95 % service level, two-week review, whole packs. Compare with current_min/current_max. | reads | `{"skus": ["SKU-1003"], "days": 90}` |
| `balance_suggestions` | Items below min here that another site has more of than its own min: move them with create_transfer instead of buying. | reads | `{}` |
| `purchase_orders` | Purchase orders for this site with lines (ordered and received), supplier, expected date and value. Open ones only unless open_only is false. | reads | `{"open_only": false}` |
| `transfers` | Transfers in and out of this site, with lines (asked, sent, received). Open ones only unless open_only is false. | reads | `{}` |
| `count_suggestions` | Locations worth counting now: ABC class, days since the last count, recent shortages, traffic. | reads | `{"limit": 15}` |
| `report` | Key figures over `days` (default 30): stock value, picks per day, short picks, orders shipped, best sellers, dead stock, ABC split, count differences. | reads | `{"days": 7}` |
| `get_location` | One location (code or the customer alias) and what it holds. | reads | `{"code": "A-01-02-1"}` |
| `list_orders` | Customer orders, newest first, with lines; optionally one status (new, allocated, picking, picked, production, produced, packing, packed, shipped, cancelled). | reads | `{"status": "new"}` |
| `events` | The event log in order. Pass `since` (the cursor you kept) for only new events, and `kind` for one area (move, adjust, receive, count, pick, short, order, status, purchase, transfer, return, levels, proposal). Keep the returned cursor for next time. | reads | `{"since": 1520, "kind": "short"}` |
| `proposals` | Changes agents have proposed for this site and their status (pending by default). | reads | `{"status": "approved"}` |
| `set_stock_levels` | Set min/max for items: levels [{sku, min, max}]; min and max null removes the level. source "suggested" when the numbers come from suggest_levels. Managers only; agents that may not, use propose. | changes | `{"levels": [{"sku": "SKU-1003", "min": 20, "max": 60}]}` |
| `create_purchase_order` | Create a DRAFT purchase order for a supplier (name or id): lines [{sku, qty, cost?}]. Nothing is sent until send_purchase_order. | changes | `{"supplier": "FitGear", "lines": [{"sku": "SKU-1003", "qty": 40}]}` |
| `update_purchase_order` | Change a draft purchase order: replace its lines, expected date or note. | changes | `{"po": "IK-0004", "lines": [{"sku": "SKU-1003", "qty": 50}]}` |
| `send_purchase_order` | Mark a draft purchase order as sent to the supplier; its quantities then count as on their way. | changes | `{"po": "IK-0004"}` |
| `cancel_purchase_order` | Cancel a purchase order (a partly received one is closed for the rest). | changes | `{"po": "IK-0004"}` |
| `receive_purchase` | Goods in against a purchase order line: qty units to a location (or the best free place when left out). | changes | `{"po": "IK-0004", "line": 1, "qty": 40}` |
| `create_transfer` | Create a DRAFT transfer between two sites of the company (codes or ids); from_site defaults to this site. Lines [{sku, qty}]. | changes | `{"to_site": "WH-02", "lines": [{"sku": "SKU-1002", "qty": 10}]}` |
| `ship_transfer` | Send a draft transfer from this site; units are taken from the places holding most. | changes | `{"transfer": "FL-0002"}` |
| `receive_transfer` | Receive a transfer on its way to this site; units go to the best places. | changes | `{"transfer": "FL-0002"}` |
| `register_return` | A customer return: back on a shelf (restock true, the default) or written off. Say why. | changes | `{"sku": "SKU-1003", "qty": 1, "reason": "Forkert størrelse"}` |
| `save_item` | Create or change an item by SKU; only the fields given change. group may be a path ("Sko / Løbesko", created when missing), supplier a name. Fields: sku, name, ean, group, supplier, supplier_sku, unit, pack_qty, cost, price, weight_grams, active, notes. | changes | `{"item": {"sku": "SKU-1003", "price": 159}}` |
| `save_group` | Create an item group (optionally under a parent, by name or id), or rename one by id. | changes | `{"name": "Kampagne", "parent": "Tilbehør"}` |
| `set_item_group` | Put many items (SKUs) in one group (name or id); null takes them out of any group. | changes | `{"skus": ["SKU-1002", "SKU-1003"], "group": "Kampagne"}` |
| `save_supplier` | Create or change a supplier: name, contact, email, phone, lead_days, notes (id to change an existing one). | changes | `{"supplier": {"name": "FitGear Europe", "lead_days": 10}}` |
| `move_stock` | Move units between two locations on this site; never mixes two items in one location. | changes | `{"from": "A-01-02-1", "to": "B-03-01-2", "qty": 10, "reason": "Tættere på pakning"}` |
| `adjust_stock` | Set the quantity at a location (0 empties it). Give sku when the location is empty. Say why. | changes | `{"location": "A-01-02-1", "qty": 12, "reason": "Optalt"}` |
| `count` | Record a blind count at a location; returns the difference to the system quantity. | changes | `{"location": "A-01-02-1", "qty": 11}` |
| `create_order` | Create a customer order. Lines [{sku, qty, location?}]; missing locations are allocated from stock. | changes | `{"ref": "WEB-1042", "customer": "Mia Holm", "lines": [{"sku": "SKU-1003", "qty": 2}]}` |
| `set_order_status` | Move an order forward (production, produced, packing, packed, shipped). Cancelling needs a manager. | changes | `{"order": "WEB-1042", "status": "packed"}` |
| `propose` | For agents that may not change things themselves: propose a change for a manager to approve in Forslag, with the database command, its arguments and why. Commands: set_stock_levels {levels, source}, create_purchase_order {supplier_id, lines, expected_at?, note?}, send_purchase_order {po}, create_transfer {from_site?, to_site, lines} (site ids), save_item {item}, set_item_group {skus, group_id}, move_stock {from, to, qty, reason}, adjust_stock {location, qty, sku?, reason}, create_order, set_order_status. | changes | `{"command": "move_stock", "args": {"from": "A-01-02-1", "to": "B-03-01-2", "qty": 10, "reason": "Fyld op"}, "reason": "Pladsen er næsten tom"}` |
| `whoami` | Who you are signed in as: your name, role (and agent mode), company and the sites you can work on, with which one is used when you leave site out. | reads | `{}` |
| `search` | Search the warehouse (items by SKU, EAN or name) and get ids for fetch. Returns {results: [{id, title, url}]}, the shape ChatGPT and Deep Research use. | reads | `{"query": "drikkedunk"}` |
| `fetch` | The full record behind a search id: an item with stock per site, places, min/max, what is on its way and recent sales. Returns {id, title, text, url, metadata}. | reads | `{"id": "item:SKU-1003@WH-01"}` |
| `subscribe` | Get a POST to your https URL whenever events of these kinds happen on this site (empty: all): move, adjust, receive, count, pick, short, order, status, purchase, transfer, return, levels, proposal, layout. Signed with the returned secret; failed deliveries are retried. | changes | `{"url": "https://example.com/hooks/reolora", "kinds": ["short", "proposal"]}` |
| `unsubscribe` | Stop a webhook by its id (from subscribe or webhooks). | changes | `{"webhook": "0b6d…"}` |
| `webhooks` | Your webhooks on this site (managers see all), with failures and the last answer. | reads | `{}` |

## For agents that run on their own

- **preview: true**: Run a change and see the result without saving anything. The same checks as the real thing.
- **request_id**: Give every change an id. If the agent retries with the same id, nothing happens twice.
- **Errors with a next step**: Errors come as {code, message, hint, next}, e.g. not_found with next: find_items.
- **since and cursor**: warehouse_status and events return a cursor. Next time: since = cursor, and summary_since tells what happened.
- **Webhooks**: subscribe(url, kinds) sends a signed POST when, say, an item runs short or a proposal arrives. Failed calls are retried.
- **Resources and prompts**: reolora://guide and status per site; prompts such as dagens-indkob (today’s purchasing) and ugens-optaelling (this week’s count).

Check a webhook’s signature:

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody: the request body exactly as received (a string), secret: whsec_… from subscribe
const expected = 'sha256=' + createHmac('sha256', secret)
  .update(`${req.headers['x-reolora-timestamp']}.${rawBody}`)
  .digest('hex');
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-reolora-signature']));
```

## Try it

With an API key in REOLORA_API_KEY:

```sh
curl -s https://mcp.reolora.dk/mcp \
  -H "Authorization: Bearer $REOLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
```

```sh
curl -s https://mcp.reolora.dk/mcp \
  -H "Authorization: Bearer $REOLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
```

### Three things to ask

- What should we order from Løbeland today? Make drafts, but send nothing.
- What happened in the warehouse since yesterday, and is anything sold out?
- Move 10 of SKU-1003 to the shop WH-02. Show me first what will happen.
