Skip to main content
Glama
Restrackit

restrackit-pantry-mcp

Official
README.md
# restrackit-pantry-mcp

A remote [MCP](https://modelcontextprotocol.io) server that turns a snapshot
of a grocery receipt into an up-to-date household pantry. Talk to Claude,
snap a photo of your receipt or just say what you bought or used, and it
tracks quantities on your behalf — no barcode scanning, no manual data
entry, no rigid expiry-date bookkeeping.

Built on top of [restrackit-core](https://github.com/restrackit/restrackit-core),
restrackit's own inventory-traceability backend, reused here purely through
its public REST API — this project ships no changes to that codebase.

## How it works

Claude is multimodal, so it reads the receipt photo itself: no OCR pipeline
to build or maintain. For every line item it estimates a plausible expiry
date and storage location — `dispensa` (pantry), `frigo` (fridge), or
`congelatore` (freezer); other values are accepted but get a generic
fallback shelf life instead of a tuned one — from general knowledge, then
calls this server to persist it.
`restrackit-pantry-mcp` is a thin, fully stateless translation layer — it
holds no database of its own and defers every fact about inventory to
restrackit-core.

```text
Claude (Desktop/mobile)
   │  reads the receipt photo / listens to a spoken update
   │  estimates category + plausible expiry date per item
   ▼
restrackit-pantry-mcp (AWS Lambda + API Gateway, MCP Streamable HTTP)
   │  translates tool calls into authenticated REST requests
   ▼
restrackit-core REST API
```

## Tools exposed

| Tool | Purpose |
|---|---|
| `add_purchase` | Record a purchase — creates the product/category/storage method if missing, one batch per unit |
| `get_pantry_status` | Return how much of each product is currently on hand |
| `record_consumption` | Close out units of a product you've used up (oldest first) |

There's no dedicated "shopping list" tool — ask Claude what you're low on
and it reasons over `get_pantry_status` in the conversation.

## Multi-tenancy

A single deployment of this server can serve any number of independent
restrackit-core stores, each isolated from the others: one Lambda, one API
Gateway endpoint, no per-store infrastructure to provision, and no tenant
registry of any kind. Each tenant authenticates with **their own** Keycloak
account (created by restrackit-core's onboarding flow) via the OAuth login
flow described below — pantry-mcp holds no shared credential that can
address every store, so a routing bug here can misdirect a request to the
wrong store, but it can never authenticate as a different tenant's account.

Isolation is enforced per request, from the user's own access token:
- pantry-mcp validates the token Claude sends (issued by Keycloak to
  `pantry-mcp-connector` for that specific user) and reads the `store_id`
  claim from it (`pantry_mcp/jwks.py`, `_extract_store_id` in
  `pantry_mcp/server.py`);
- it then exchanges that token server-side, via RFC 8693 Standard Token
  Exchange, for one scoped to `restrackit-backend` (`pantry_mcp/auth.py`,
  `TokenExchanger`) and forwards it with `X-Target-Store: <store_id>` on every
  restrackit-core call.

There is no DynamoDB table, no per-tenant Secrets Manager credential, and no
`add_tenant.py`-style provisioning script — the only shared secret is the
token-exchange client's own credential (see "Deployment" below).

## One-time setup (per tenant)

Tenants onboard via OAuth token exchange: a tenant logs in for the first
time through the "Sign in now" button in the custom connector registered in
Claude (Desktop/mobile). No manual provisioning steps required.

1. Ensure restrackit-core has a store created for this tenant (requires an
   `ADMIN_ALL` account). Once the store exists, the tenant is ready to
   authenticate via the connector.

2. Tenant registers the custom connector in Claude:
   - Copy the `ApiUrl` from the CDK deployment output (its `/mcp` path is the
     MCP endpoint).
   - In Claude (Desktop/mobile), add the connector as a Custom Connector,
     pointing at `<ApiUrl>/mcp`.
   - On first use, click "Sign in now". Claude discovers Keycloak
     automatically via `GET /.well-known/oauth-protected-resource`, so no
     redirect URI entry is needed on the tenant's side. In the connector's
     Advanced settings, set **OAuth Client ID** to `pantry-mcp-connector`
     (no client secret) — without it Claude falls back to Dynamic Client
     Registration, which either fails or creates a client missing the
     `store_id`/audience mappers this flow requires.
   - The token is exchanged server-side; no manual token provisioning is
     required.

## Deployment

Infrastructure is defined as AWS CDK (Python) under `infra/`:

```bash
cd infra
uv venv .venv && source .venv/bin/activate
uv pip install --python .venv -r requirements.txt
cdk deploy \
  --parameters KeycloakUrl=... \
  --parameters KeycloakRealm=... \
  --parameters KeycloakConnectorClientId=... \
  --parameters KeycloakExchangeClientId=... \
  --parameters RestrackitBackendClientId=... \
  --parameters RestrackitBaseUrl=... \
  --parameters McpPublicBaseUrl=...  # the ApiUrl output, no trailing slash
```

Take the `ApiUrl` output for tenants' connector URLs, and pass it back in as
`McpPublicBaseUrl` (it is only known after the first deploy — redeploy once
with it set).

Before the first deploy, create the token-exchange client secret manually,
once:

```bash
aws secretsmanager create-secret \
  --name pantry-mcp/token-exchange-client \
  --secret-string '<the secret configured on Keycloak's pantry-mcp-token-exchange client>'
```

This value must be **identical** to the secret configured for the
`pantry-mcp-token-exchange` client on the Keycloak side (provisioned from
restrackit-core's `scripts/provision_keycloak_realm.sh`). Nothing keeps the
two in sync automatically — rotating one without the other breaks every
token exchange with `invalid_client`.

## Development

```bash
uv sync --extra dev
uv run pytest
```