Skip to main content
Glama
ryanDwright

pos-mcp-server

by ryanDwright

pos-mcp-server

A Model Context Protocol server that lets a restaurant operate its point-of-sale by talking to an LLM agent — edit the menu, 86 an item, check sales, restock, watch for voids.

Twenty tools, two transports, one codebase:

  • stdio — for local clients (Claude Desktop, Claude Code, Copilot in VS Code). One process, one merchant, credentials from the environment.

  • Streamable HTTP — for cloud agents that can only reach a public endpoint. One endpoint serves every merchant, each request scoped by the caller's own API key.

This is extracted from a multi-tenant POS running in production. It's published as a reference for three things that were not obvious when I built it: making one tool implementation serve both transports, doing per-request tenancy in ASGI without leaking between concurrent requests, and the DNS-rebinding default that breaks MCP behind any reverse proxy.


The interesting parts

1. Tenancy comes from the key, and nothing else

On the remote transport there is no tenant identifier on the wire. The caller sends their API key; the key alone determines which merchant's data they get, resolved server-side.

def _get(path, params=None):
    slug = _current_slug()
    merged = {**(params or {})}
    if slug:            # stdio mode cross-checks the slug…
        merged["slug"] = slug
    ...                 # …remote mode sends none, so there is nothing to tamper with

A client cannot ask for another merchant's data, because there is no field in which to ask.

2. Pure-ASGI middleware, because BaseHTTPMiddleware loses contextvars

The per-request key is bound to a contextvars.ContextVar so the tool coroutine can read it without threading an argument through every call site.

This does not work with Starlette's BaseHTTPMiddleware. That class runs the downstream app in a separate task, so a contextvar set in the middleware is not visible inside the tool — you get an empty key and a confusing 401, or worse, a fallback to the wrong tenant. Writing it as pure ASGI keeps the whole request in one task:

class ApiKeyAuth:
    async def __call__(self, scope, receive, send):
        ...
        token = server._REQ_KEY.set(key)
        try:
            await self.app(scope, receive, send)
        finally:
            server._REQ_KEY.reset(token)

Found by testing cross-tenant isolation, not by reading about it.

3. MCP's DNS-rebinding protection 421s behind a proxy

The SDK enables DNS-rebinding protection with a localhost-only host allowlist by default. Put the server behind any reverse proxy and every request returns:

421 Invalid Host header

with nothing indicating a host allowlist is the cause. The fix is to configure the allowlist explicitly rather than to disable the protection:

server.mcp.settings.transport_security = TransportSecuritySettings(
    enable_dns_rebinding_protection=True,
    allowed_hosts=_ALLOWED,
    allowed_origins=[...],
)

Set ALAMOAI_POS_MCP_ALLOWED_HOSTS to your public hostname(s). The endpoint stays bearer-gated either way.

4. Stateless JSON, deliberately

stateless_http = True and json_response = True. Every POST is self-contained, which keeps the auth contextvar bound to exactly one request's task — and it matches what cloud MCP clients expect, since SSE support was dropped from that ecosystem in late 2025.

5. Errors written for a model to act on

Tool errors are phrased as instructions, not status codes, because the consumer is a language model deciding what to do next:

if r.status_code == 404:
    raise PosError("Not found. Check the id or name — list the menu/inventory "
                   "first to see valid values.")

"404" makes a model guess. "List the menu first" makes it recover.


Related MCP server: MCP Coding Agents

Tools

Menu list_menu · add_menu_category · add_menu_item · update_menu_item · set_item_availability · set_item_online · publish_category_online · set_store_type

Inventory inventory_status · lookup_product · receive_stock · set_stock · restock_ingredient · record_waste

Operations sales_today · cost_report · open_orders · void_report

Payments payment_status · create_onboarding_link

Tools resolve items and ingredients by name, not by id, so the model doesn't have to carry identifiers across turns.


Running it

stdio (Claude Desktop, Claude Code, Copilot in VS Code)

pip install -r requirements.txt
{
  "mcpServers": {
    "pos": {
      "command": "python3",
      "args": ["/path/to/pos-mcp-server/server.py"],
      "env": {
        "ALAMOAI_POS_API_KEY": "alamopos_yourkeyhere",
        "ALAMOAI_POS_SLUG": "your-restaurant"
      }
    }
  }
}

Streamable HTTP (cloud agents)

pip install -r requirements-remote.txt
ALAMOAI_POS_MCP_ALLOWED_HOSTS=your.domain \
ALAMOAI_POS_MCP_PATH=/pos-mcp \
python3 remote_server.py

Callers authenticate per request:

Authorization: Bearer alamopos_…

or X-Alamoai-Key: alamopos_…. Health check at /healthz. Missing key returns 401 before anything else runs.

Environment

Variable

Default

Notes

ALAMOAI_POS_API_KEY

Required in stdio mode

ALAMOAI_POS_SLUG

Required in stdio mode; unused in remote mode

ALAMOAI_POS_API_BASE

https://alamoai.org/api

Backing REST API

ALAMOAI_POS_MCP_HOST

127.0.0.1

Remote mode bind address

ALAMOAI_POS_MCP_PORT

8791

Remote mode port

ALAMOAI_POS_MCP_PATH

/mcp

Public path for the endpoint

ALAMOAI_POS_MCP_ALLOWED_HOSTS

localhost,127.0.0.1:8000

Set to your public hostname


Design notes

Thin by intent. Every tool is one REST call against an API that already enforces tenancy and role permissions. This process holds no secrets beyond the one key and can grant nothing the key doesn't already carry. If it were compromised it would be a worse client, not a wider door.

API keys authenticate through the same path as interactive sessions. Because a key resolves through the identical session-resolution code as a human bearer token, every tenant-scoped, role-gated endpoint in the backing API accepted key auth with zero per-endpoint changes.

Build once, expose twice. The write functions behind these tools also back a web catalog editor. The MCP server was not a side integration — it was a second front end onto the same core.


License

Apache-2.0. See LICENSE.

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    -
    quality
    -
    maintenance
    A secure and pluggable MCP server to run terminal commands on your local machine or cloud server — remotely, safely, and with LLMs or agentic clients.
    Last updated
  • A
    license
    -
    quality
    C
    maintenance
    A transport-agnostic MCP server that integrates multiple AI coding agents (Claude Code, Gemini, and Codex) with built-in tools for command execution, calculations, and streaming capabilities. Supports both STDIO and HTTP transports for flexible deployment.
    Last updated
    57
    1
    MIT
  • A
    license
    -
    quality
    B
    maintenance
    A production-ready MCP server that gives an LLM agent standalone-equivalent control over a Mineflayer Minecraft bot — movement, mining, crafting, inventory, combat, containers, chat, and much more — exposed as 110 strongly-typed tools across 23 groups, with full bot lifecycle management and dual (poll + push) event streaming.
    Last updated
    64
    1
    MIT

View all related MCP servers

Related MCP Connectors

  • Hosted MCP server to manage a restaurant menu from AI agents - 39 tools over the DuckHub API.

  • Hosted MCP server connecting claude.ai, ChatGPT and other AI apps to your own computer

  • Cloud-hosted MCP server for durable AI memory

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ryanDwright/pos-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server