Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions plugins/target/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "target",
"version": "0.1.0",
"description": "Search Target products, check store-scoped prices and nearby availability, and build shopping lists and menus.",
"author": {"name": "Walker Hughes", "url": "https://github.com/walkerhughes"},
"license": "MIT",
"homepage": "https://github.com/walkerhughes/agents/tree/main/plugins/target",
"repository": "https://github.com/walkerhughes/agents",
"keywords": ["target", "shopping", "prices", "pickup", "meal-planning", "mcp"],
"skills": "./skills/",
"mcpServers": {"target": {"command": "${CLAUDE_PLUGIN_ROOT}/scripts/start-server.sh"}},
"interface": {
"displayName": "Target",
"shortDescription": "Store-aware products, prices, availability, and lists.",
"longDescription": "Search Target's public catalog, compare store-scoped prices, find nearby pickup availability, price shopping lists, and plan practical Target runs.",
"developerName": "Walker Hughes",
"category": "Lifestyle",
"capabilities": ["Read", "Interactive"],
"websiteURL": "https://www.target.com/",
"defaultPrompt": ["Search Target products and compare prices.", "Find nearby pickup availability for a product.", "Price and organize my Target shopping list."],
"brandColor": "#CC0000"
}
}
33 changes: 33 additions & 0 deletions plugins/target/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "target",
"version": "0.1.0",
"description": "Search Target products, check store-scoped prices and nearby availability, and build shopping lists and menus.",
"author": {
"name": "Walker Hughes"
},
"license": "MIT",
"homepage": "https://github.com/walkerhughes/agents/tree/main/plugins/target",
"repository": "https://github.com/walkerhughes/agents",
"keywords": ["target", "shopping", "prices", "pickup", "meal-planning", "mcp"],
"skills": "./skills/",
"interface": {
"displayName": "Target",
"shortDescription": "Store-aware products, prices, availability, and lists.",
"longDescription": "Search Target's public catalog, compare store-scoped prices, find nearby pickup availability, price shopping lists, and plan practical Target runs.",
"developerName": "Walker Hughes",
"category": "Lifestyle",
"capabilities": ["Read", "Interactive"],
"websiteURL": "https://www.target.com/",
"defaultPrompt": [
"Search Target products and compare prices.",
"Find nearby pickup availability for a product.",
"Price and organize my Target shopping list."
],
"brandColor": "#CC0000"
},
"mcpServers": {
"target": {
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/start-server.sh"
}
}
}
6 changes: 6 additions & 0 deletions plugins/target/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.venv/
.pytest_cache/
.ruff_cache/
.mypy_cache/
.coverage
__pycache__/
5 changes: 5 additions & 0 deletions plugins/target/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"target": {
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/start-server.sh"
}
}
1 change: 1 addition & 0 deletions plugins/target/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
6 changes: 6 additions & 0 deletions plugins/target/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Target plugin

- Keep the integration read-only. Do not add cart, account, order, or checkout mutations.
- Treat Redsky availability as a point-in-time signal, not a reservation or guarantee.
- Preserve explicit store and ZIP context for every price or availability claim.
- Generate eval task directories from `evals/generate_tasks.py`; do not edit generated task files directly.
1 change: 1 addition & 0 deletions plugins/target/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
23 changes: 23 additions & 0 deletions plugins/target/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.PHONY: check lint format typecheck test coverage selftest

check: lint typecheck selftest test

lint:
uv run ruff check .
uv run ruff format --check .

format:
uv run ruff check --fix .
uv run ruff format .

typecheck:
uv run mypy src

selftest:
uv run python -c "from src.server import mcp; assert len(mcp._tool_manager._tools) == 4"

test:
uv run pytest -m unit

coverage:
uv run pytest --cov=src --cov-report=term-missing
37 changes: 37 additions & 0 deletions plugins/target/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Target

An unofficial, read-only MCP plugin for Target's public website data. It searches
products with store context, checks published prices, finds nearby stores carrying
a selected item, prices draft shopping lists, and guides menu planning.

No Target account is required.

## What it can do

- Search products using a selected Target store and ZIP code.
- Look up product details and published prices by TCIN.
- Find nearby stores and pickup signals for a selected product.
- Batch-price shopping-list phrases and show alternatives.
- Build menus and trip plans from the matched products.

## Price and availability boundaries

Prices come from Target's public website APIs for the supplied pricing-store context.
They can differ by store, fulfillment method, promotion eligibility, sign-in state,
or Target Circle offer. Availability is a point-in-time website signal and does not
reserve an item or guarantee that it will be on the shelf when you arrive.

Target's Redsky service may present an automated-traffic challenge on some networks.
The plugin reports that condition directly instead of returning stale or invented data.

## Development

Requires Python 3.13 and [uv](https://docs.astral.sh/uv/).

```text
make check
make coverage
```

The implementation uses undocumented website request contracts that may change. This
project is not affiliated with or endorsed by Target Corporation.
30 changes: 30 additions & 0 deletions plugins/target/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[project]
name = "target-mcp"
version = "0.1.0"
description = "Read-only MCP server for Target public website data"
readme = "README.md"
requires-python = ">=3.13"
dependencies = ["mcp[cli]>=2.0", "httpx>=0.27"]

[tool.uv]
package = false

[dependency-groups]
dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "pytest-cov>=6.0", "ruff>=0.9", "mypy>=1.0"]

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
markers = ["unit: Unit tests"]
addopts = "-v"

[tool.ruff]
extend = "../../ruff.toml"
target-version = "py313"

[tool.coverage.run]
source = ["src"]

[tool.coverage.report]
fail_under = 85
show_missing = true
5 changes: 5 additions & 0 deletions plugins/target/scripts/start-server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail

cd "$(dirname "$0")/.."
exec uv run --no-dev python -m src.server
37 changes: 37 additions & 0 deletions plugins/target/skills/target-shopping/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: target-shopping
description: Plan Target shopping lists and menus using store-scoped products, public prices, package details, and nearby pickup availability. Use for Target searches, price checks, store selection, shopping trips, and meal planning.
---

# Target Shopping and Menu Planning

Build store-specific shopping plans from Target's public website data while keeping
price and pickup claims tied to the location the user selected.

## Establish Store Context

Use a store already selected in the conversation. Otherwise search for the first
needed product and call `find_stores_with_item` with the user's ZIP code. Let the user
choose when distance, availability, and pickup options conflict. Carry the chosen
`store_id` and ZIP code into product searches. Never imply device-location access.

## Search and Verify

Use focused `search_products` calls to discover candidates and `get_product` for a
known TCIN. Use `find_stores_with_item` when pickup availability matters. Preserve
package details, current and regular prices, promotion context, and the selected store.

Treat availability and quantities as point-in-time website signals. Do not describe
them as reservations or guarantees. Distinguish current public prices from prices that
require sign-in, Target Circle eligibility, a particular fulfillment method, or an
in-store visit.

## Build Lists and Menus

Call `price_shopping_list`, inspect every first match and its alternatives, and flag
unmatched or unpriced requests. Adjust written quantities for servings and package
sizes. Reuse perishable ingredients across meals and call out likely surplus.

Separate pantry staples from purchases and group the final list by practical
departments. Do not infer allergy safety from product names. For severe allergies,
ask the user to verify the current package label.
Empty file added plugins/target/src/__init__.py
Empty file.
112 changes: 112 additions & 0 deletions plugins/target/src/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Async client for Target's public Redsky website contracts."""

import os
from typing import Any

import httpx

from .errors import TargetError

BASE = "https://redsky.target.com/redsky_aggregations/v1/web"
SEARCH_URL = os.getenv("TARGET_SEARCH_URL", f"{BASE}/plp_search_v2")
PRODUCT_URL = os.getenv("TARGET_PRODUCT_URL", f"{BASE}/pdp_client_v1")
AVAILABILITY_URL = os.getenv("TARGET_AVAILABILITY_URL", f"{BASE}/fiats_v1")
REDSKY_KEY = os.getenv("TARGET_REDSKY_KEY", "9f36aeafbe60771e321a7cc95a78140772ab3e96")
VISITOR_ID = "0000000000000000000000000000000000"


class TargetClient:
def __init__(self, transport: httpx.AsyncBaseTransport | None = None) -> None:
self.http = httpx.AsyncClient(
timeout=20,
transport=transport,
headers={
"Accept": "application/json",
"Origin": "https://www.target.com",
"Referer": "https://www.target.com/",
"User-Agent": "Mozilla/5.0 AppleWebKit/537.36 Chrome/144 Safari/537.36",
},
)

async def close(self) -> None:
await self.http.aclose()

async def _get(self, url: str, params: dict[str, Any]) -> dict[str, Any]:
response = await self.http.get(url, params=params)
if response.status_code == 403:
raise TargetError(
"Redsky presented an automated-traffic challenge on this network. "
"Retry from a normal residential connection; no product data was returned."
)
response.raise_for_status()
try:
payload = response.json()
except ValueError as exc:
raise TargetError("Target returned a non-JSON response") from exc
if not isinstance(payload, dict) or not isinstance(payload.get("data"), dict):
raise TargetError("Target returned an unexpected response shape")
return payload["data"]

async def search_products(self, query: str, store_id: str, zip_code: str, limit: int) -> dict[str, Any]:
params: dict[str, Any] = {
"key": REDSKY_KEY,
"channel": "WEB",
"keyword": query,
"page": f"/s/{query}",
"visitor_id": VISITOR_ID,
"pricing_store_id": store_id,
"store_ids": store_id,
"default_purchasability_filter": "true",
"include_sponsored": "false",
"platform": "desktop",
"count": limit,
"offset": 0,
}
if zip_code:
params["zip"] = zip_code
data = await self._get(SEARCH_URL, params)
search = data.get("search")
if not isinstance(search, dict) or not isinstance(search.get("products"), list):
raise TargetError("Target search did not include a product list")
return search

async def get_product(self, tcin: str, store_id: str, zip_code: str = "", state: str = "") -> dict[str, Any] | None:
params = {
"key": REDSKY_KEY,
"channel": "WEB",
"tcin": tcin,
"store_id": store_id,
"pricing_store_id": store_id,
"has_pricing_store_id": "true",
"visitor_id": VISITOR_ID,
"page": f"/p/A-{tcin}",
}
if zip_code:
params["zip"] = zip_code
if state:
params["state"] = state.upper()
data = await self._get(PRODUCT_URL, params)
product = data.get("product")
return product if isinstance(product, dict) else None

async def find_stores_with_item(
self, tcin: str, zip_code: str, radius: int, limit: int, requested_quantity: int
) -> dict[str, Any]:
data = await self._get(
AVAILABILITY_URL,
{
"key": REDSKY_KEY,
"channel": "WEB",
"tcin": tcin,
"nearby": zip_code,
"radius": radius,
"limit": limit,
"requested_quantity": requested_quantity,
"include_only_available_stores": "false",
"visitor_id": VISITOR_ID,
},
)
fiats = data.get("fulfillment_fiats")
if not isinstance(fiats, dict) or not isinstance(fiats.get("locations"), list):
raise TargetError("Target availability did not include store locations")
return fiats
28 changes: 28 additions & 0 deletions plugins/target/src/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Error normalization for Target tools."""

from collections.abc import Awaitable, Callable
from functools import wraps
from typing import Any

import httpx


class TargetError(Exception):
"""Expected error with guidance suitable for an MCP response."""


def guarded_tool(function: Callable[..., Awaitable[str]]) -> Callable[..., Awaitable[str]]:
@wraps(function)
async def wrapped(*args: Any, **kwargs: Any) -> str:
try:
return await function(*args, **kwargs)
except TargetError as exc:
return f"Target request could not be completed: {exc}"
except httpx.TimeoutException:
return "Target request timed out. Try again shortly or reduce the number of items."
except httpx.HTTPStatusError as exc:
return f"Target returned HTTP {exc.response.status_code}. Try again shortly."
except httpx.HTTPError as exc:
return f"Target could not be reached: {exc}"

return wrapped
Loading
Loading