OurGroceries MCP
<!-- mcp-name: io.github.OriginalByteMe/our-groceries-mcp -->
# OurGroceries MCP
An **unofficial** [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for an OurGroceries account. It relies on an unofficial API client; upstream behavior, availability, and compatibility can change without notice. Review every result and verify important changes in OurGroceries.
> [!WARNING]
> The Streamable HTTP server has **no built-in application authentication**. Never expose it directly to the internet: anyone who can reach it can use the configured grocery account. Bind it to localhost, or put it behind an **authenticated, trusted tunnel or reverse proxy** that enforces authentication before forwarding requests.
## Prerequisites
- An OurGroceries account and its credentials.
- For a source install: [uv](https://docs.astral.sh/uv/) and Python 3.12 or later.
- For the container option: Docker.
- An MCP client for stdio or Streamable HTTP.
The server reads `OURGROCERIES_EMAIL` and `OURGROCERIES_PASSWORD` from its process environment. `.env` is a local template, not an automatically loaded configuration file. Keep it private and never commit it.
## Install and configure
### Source with uv
```bash
git clone https://github.com/OriginalByteMe/our-groceries-mcp.git
cd our-groceries-mcp
cp .env.example .env
# Edit .env locally; do not commit it.
uv sync --locked
```
Set placeholders in `.env` only on your machine:
```dotenv
OURGROCERIES_EMAIL=""
OURGROCERIES_PASSWORD=""
```
For a shell-launched server, export the file first:
```bash
set -a
. ./.env
set +a
```
### Docker
The image starts the Streamable HTTP transport. Build it and publish the port only on localhost:
```bash
docker build -t our-groceries-mcp .
docker run --rm --env-file .env -p 127.0.0.1:8000:8000 our-groceries-mcp
```
Its MCP endpoint is `http://127.0.0.1:8000/mcp`. This is an MCP endpoint, not a browser UI. If remote access is necessary, retain the localhost binding and use an authenticated trusted tunnel or reverse proxy; do not forward the port unauthenticated.
### Install the agent skill
For agents that support Vercel Skills, install the repository skill:
```bash
npx skills add OriginalByteMe/our-groceries-mcp --skill our-groceries-mcp
```
This installs guidance, not credentials. Configure credentials locally in the MCP client or environment.
## Connect an MCP client
### Stdio (recommended)
After `uv sync --locked`, add this JSON to your client's MCP server configuration. Replace the absolute checkout path and placeholders locally; do not put real credentials in a shared config file.
```json
{
"mcpServers": {
"our-groceries": {
"command": "uv",
"args": [
"run",
"--directory",
"/absolute/path/to/our-groceries-mcp",
"--locked",
"our-groceries-mcp"
],
"env": {
"OURGROCERIES_EMAIL": "",
"OURGROCERIES_PASSWORD": ""
}
}
}
}
```
Stdio keeps the server local to the client process and is the safest default.
### Streamable HTTP
For a source checkout, after exporting the environment variables, run:
```bash
uv run --locked our-groceries-mcp \
--transport streamable-http \
--host 127.0.0.1 \
--port 8000
```
Connect an MCP client to `http://127.0.0.1:8000/mcp`. The endpoint has no HTTP authentication. Do not use `0.0.0.0` or a public host unless an authenticated trusted tunnel or reverse proxy protects every request.
## Read-only smoke check
From a configured source checkout, this launches the server over stdio, initializes MCP, and calls the real read-only `list_grocery_lists` tool. It prints no grocery-list contents.
```bash
set -a
. ./.env
set +a
uv run --locked python - <<'PY'
import asyncio
import os
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main() -> None:
server = StdioServerParameters(
command="uv",
args=["run", "--locked", "our-groceries-mcp"],
env=dict(os.environ),
)
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("list_grocery_lists", {})
if result.isError:
raise RuntimeError("list_grocery_lists failed")
print("Read-only MCP smoke check passed.")
asyncio.run(main())
PY
```
This contacts the live OurGroceries account. Do not run it with credentials you do not intend to use.
## Tools and confirmation
List and item names are resolved by case-insensitive exact match. Resolve ambiguity with `list_grocery_lists` or `get_items` before a mutation.
| Tool | Parameters | Behavior |
| --- | --- | --- |
| `list_grocery_lists` | none | Read available lists. |
| `get_items` | `list_name` | Read items in one list. |
| `add_item` | `list_name`, `item`, optional `quantity`, `note` | Adds immediately; it has no confirmation flag. Ask before calling it. |
| `complete_item` | `list_name`, `item`, optional `confirm` | Returns a preview by default. Marking complete requires `confirm: true`. |
| `remove_item` | `list_name`, `item`, optional `confirm` | Returns a preview by default. Permanent removal requires `confirm: true`. |
| `move_item` | `source_list`, `destination_list`, `item`, optional `confirm` | Returns a preview by default. Moving requires `confirm: true`. |
For `complete_item`, `remove_item`, and `move_item`, first call with the default `confirm: false`, show the preview, and only call again with `confirm: true` after explicit user approval. `add_item` is an immediate write, so obtain approval before the first call.
> [!CAUTION]
> `move_item` is not atomic: it adds the item to the destination, then removes it from the source. If removal fails, the item can exist in both lists. Inspect both lists before retrying so a retry does not create another duplicate.
## Troubleshooting
- **Credentials are not configured:** pass both environment variables to the server process; `.env` alone is not loaded automatically.
- **Authentication failed or requests fail:** verify the account credentials and network, then retry later. This is an unofficial integration and upstream API changes can break it.
- **List or item not found / ambiguous:** use the read tools and provide one case-insensitive exact name; rename duplicate lists or items in OurGroceries if needed.
- **Cannot connect over HTTP:** ensure the server is running, use `/mcp` (not `/`), and keep the host/port local unless an authenticated proxy or tunnel is in place.
- **A move reports partial failure:** check both source and destination lists manually before deciding whether to remove, move, or retry the item.
## Development and testing
Install development dependencies and run the offline test suite with the lockfile:
```bash
uv sync --locked --all-groups
uv run --locked pytest -m "not live"
```
Tests marked `live` require a deliberately configured live account and are excluded by default. Do not add credentials to test output, fixtures, or committed files.
## Security
See [SECURITY.md](SECURITY.md) before enabling Streamable HTTP. In particular, direct unauthenticated internet exposure grants grocery-account access to callers.TDQS
Scored across 6 tools
Each tool has a clear, distinct purpose: listing lists, getting items, adding, completing, removing, and moving items. There is no overlap or ambiguity between these actions.
All tool names follow a consistent verb_noun pattern in snake_case (e.g., list_grocery_lists, add_item, complete_item). The naming is uniform and predictable.
With 6 tools, the server is well-scoped for grocery list management. Each tool addresses a distinct operation, and the count is neither too sparse nor overwhelming.
The tool set covers item-level operations (add, get, complete, remove, move) but lacks list-level create/delete operations and any way to update item details like quantity or note after creation. These are notable gaps for a grocery list management server.