Tattoo Feed
An MCP server that lets an LLM client browse and curate posts from a hand-picked list of Instagram tattoo artists via Instagram's Business Discovery API.
README
Tattoo Feed
An MCP (Model Context Protocol) server that lets an LLM client (e.g. Claude Desktop) browse and curate posts from a hand-picked list of Instagram tattoo artists, via Instagram's Business Discovery API.
You point it at the artists you follow, and from your chat client you can pull a merged feed, discover one post at a time, bookmark the ones you like, and record notes about your taste so a future session remembers them.
Architecture
A deliberate two-layer split so a future GUI can reuse the logic without a rewrite:
core(src/tattoo_feed/excludingserver/) — all real logic: domain models, typed errors, JSON-file repositories, the Graph API client, image processing, and the services that orchestrate them. Knows nothing about MCP.server(src/tattoo_feed/server/) — a thin FastMCP adapter that exposescoreas MCP tools. Holds no business logic.
src/tattoo_feed/
config.py # lazy env config (IG_ACCESS_TOKEN, IG_USER_ID)
models.py # Pydantic v2 value objects
errors.py # typed error hierarchy
imaging.py # preview downscale + EXIF strip
repositories/ # Repository ABC + JSON-file stores (atomic writes)
graph/client.py # Business Discovery client
services/ # FeedService, ArtistService, InspirationService, PreferenceService
server/app.py # FastMCP tools + stdio entrypoint
Setup
Requirements: Python 3.12 and uv.
uv sync # create the venv and install pinned deps
cp .env.example .env # then edit .env with your real credentials
Environment variables
| Variable | Meaning |
|---|---|
IG_ACCESS_TOKEN |
A long-lived Instagram Graph API access token. |
IG_USER_ID |
The Instagram Business/Creator account id that owns the token. |
TATTOO_FEED_DATA_DIR |
Optional. Where the JSON stores live (default ./data). |
.env is gitignored and must never be committed. Only .env.example (with
placeholders) is in the repo.
Getting credentials is a one-time manual step on Meta's side: create a Meta app, connect an Instagram Business/Creator account, and mint a long-lived access token with Business Discovery permission. Both the querying account and the artists you look up must be professional accounts.
Running
Locally (stdio)
uv run python -m tattoo_feed.server.app
The server speaks the MCP stdio protocol, so you normally don't run it by hand — you register it with an MCP client. For Claude Desktop, add to its config:
{
"mcpServers": {
"tattoo-feed": {
"command": "uv",
"args": ["run", "python", "-m", "tattoo_feed.server.app"],
"cwd": "/absolute/path/to/tattoo-feed",
"env": {
"IG_ACCESS_TOKEN": "your-token",
"IG_USER_ID": "your-business-user-id"
}
}
}
}
In Docker
A dev image (Dockerfile) bundles Python, uv, Node, and the toolchain. Build
and open a shell with only this folder mounted to /workspace:
docker build -t tattoo-feed-dev .
./run-loop.sh # mounts $PWD to /workspace, nothing else on your machine
Inside the container you have the full gate and can run the server exactly as
above. The volume mount means anything written under /workspace lands back in
this folder on your host.
The tools (MCP surface)
| Tool | What it does |
|---|---|
list_artists |
List tracked artists. |
add_artist(handle) |
Validate the handle is a reachable professional account, then track it. |
remove_artist(handle) |
Stop tracking a handle. |
get_feed(limit_per_artist=10) |
Merged, newest-first feed. Metadata + permalinks only (no images). |
next_inspiration() |
One not-yet-seen post, marked seen, with a rendered preview image. |
save_to_inspiration(post_id, notes=None) |
Bookmark a post into the saved collection. |
list_inspiration() |
The saved collection, in save order. |
remove_from_inspiration(post_id) |
Remove a saved item. |
reset_seen() |
Clear the seen-set so inspiration starts fresh. |
record_preference(observation) |
Persist a taste note (propose-then-confirm, see below). |
get_preference_summary() |
All recorded preferences, to reload taste in a fresh session. |
Design decisions
- Two-layer split (core / server). MCP concepts never leak into
core; business logic never leaks intoserver. This is what makes a phase-2 GUI a bolt-on rather than a rewrite. - JSON-file persistence behind a
Repositoryinterface. Simple, inspectable, and swappable. Writes are atomic (temp file +os.replace) so a crash mid-write can never corrupt a store. - Pydantic v2 frozen models for everything crossing a boundary, so external data is validated once and treated as immutable values thereafter.
- Typed error hierarchy (
TattooFeedErrorand friends). Every external failure maps to a typed error; nothing raises bare exceptions across a boundary, so the client always gets a readable message instead of a stack trace. - Lazy credentials. The server boots and lists its tools with no network and no real credentials; the token is only read when a tool actually calls Instagram.
- Hermetic tests. All Instagram HTTP is mocked with
respx; there are zero live network calls in the test suite. (mypy --strict,ruff, and a 90% coverage floor are enforced.) - Images only where they earn their context. Only
next_inspirationreturns a rendered image — the one-at-a-time conversational moment.get_feedstays metadata-only to keep the context window light.
Limitations (by design)
- No video. Video posts are filtered out entirely at the Graph-client layer and never enter the feed, inspiration, or stores.
- Carousels show the first image only. Multi-image expansion is out of scope.
- Manual token refresh. There is no automatic token refresh. When the token
expires, tools fail with a clear
TokenExpiredErrortelling you to mint a new long-lived token and updateIG_ACCESS_TOKEN. - Preview sizing is fixed. Previews are capped at 640px on the long edge, aspect ratio preserved, never upscaled, re-encoded as JPEG quality 85.
record_preferenceis propose-then-confirm. The tool persists whatever it is given; the discipline of proposing the observation to you and getting your explicit confirmation before the tool is called lives in the tool's description, so the calling assistant honours it.- No write access to Instagram. No posting, commenting, or messaging — this is strictly read-and-curate.
Attribution & copyright
Posts belong to the artists who made them. This tool is for personal discovery and curation, not redistribution:
- Previews are downscaled copies (≤640px, EXIF stripped), not full-resolution downloads.
- Every image and saved item carries the artist's handle and the post's permalink, so attribution travels with the content and you can always open the original on Instagram.
- Respect each artist's rights: don't repost or reuse their work without permission.
Development
The full gate (must all exit 0):
uv run ruff format --check .
uv run ruff check .
uv run mypy --strict src
uv run pytest -q --cov=src/tattoo_feed --cov-report=term-missing --cov-fail-under=90
License
MIT — see LICENSE.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。