Tattoo Feed
Allows browsing and curating posts from hand-picked Instagram tattoo artists via Instagram's Business Discovery API.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Tattoo Feed Show me my saved inspirations"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 entrypointRelated MCP server: Tattoo Feed
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 credentialsEnvironment variables
Variable | Meaning |
| A long-lived Instagram Graph API access token. |
| The Instagram Business/Creator account id that owns the token. |
| Optional. Where the JSON stores live (default |
.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.appThe 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 machineInside 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 tracked artists. |
| Validate the handle is a reachable professional account, then track it. |
| Stop tracking a handle. |
| Merged, newest-first feed. Metadata + permalinks only (no images). |
| One not-yet-seen post, marked seen, with a rendered preview image. |
| Bookmark a post into the saved collection. |
| The saved collection, in save order. |
| Remove a saved item. |
| Clear the seen-set so inspiration starts fresh. |
| Persist a taste note (propose-then-confirm, see below). |
| 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=90License
MIT — see LICENSE.
Available Tools
11 toolsadd_artistA
Track a new artist by Instagram handle.
The handle must resolve to a reachable professional (Business/Creator) account; otherwise a clear error is returned.
Inputs: handle (required). Returns: A tracked tattoo artist. Attributes: handle: The bare Instagram handle (without…
get_feedA
Return recent posts from all tracked artists, newest first.
Returns metadata and permalinks only (no images) to keep context light.
Inputs: limit_per_artist. Returns: result
get_preference_summaryA
Return every recorded taste preference, so a fresh session can reload it.
Inputs: no named parameters. Returns: result
list_artistsA
List the tattoo artists currently being tracked.
Inputs: no named parameters. Returns: result
list_inspirationA
List saved inspiration items, in the order they were saved.
Inputs: no named parameters. Returns: result
next_inspirationA
Show one not-yet-seen post for inspiration, then mark it seen.
Returns a rendered preview image (downscaled, EXIF-stripped) alongside the
artist handle and permalink. Calling repeatedly walks through unseen posts;
use reset_seen to start over.
Inputs: no named parameters. Returns: result
record_preferenceA
Record a note about the user's tattoo taste.
IMPORTANT: Before calling this tool, propose the observation to the user in
your own words and obtain their explicit confirmation. Only call it once the
user has agreed the observation is accurate. This captures taste (e.g.
"prefers fine-line botanical work"), which is distinct from saving a
specific image with save_to_inspiration.
Inputs: observation (required). Returns: A captured note about the user's taste (distinct from a saved image). Attribute…
remove_artistB
Stop tracking the artist with the given handle.
Inputs: handle (required). Returns: result
remove_from_inspirationB
Remove a saved inspiration item by post id.
Inputs: post_id (required). Returns: result
reset_seenA
Clear the seen-set so next_inspiration starts fresh.
Inputs: no named parameters. Returns: result
save_to_inspirationC
Bookmark a post (by id, from the current feed) into saved inspiration.
Inputs: post_id (required), notes. Returns: A post the user has saved into their inspiration collection. The artist handle…
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 11 tools
Tools generally have distinct purposes, but 'get_feed' and 'next_inspiration' both involve browsing posts from tracked artists, with 'next_inspiration' showing one unseen post at a time while 'get_feed' returns all recent posts. This could confuse an agent about which to use for browsing.
All tool names follow a consistent verb_noun snake_case pattern, such as 'add_artist', 'list_inspiration', 'save_to_inspiration'. Even 'next_inspiration' with a non-standard verb is still in snake_case and fits the pattern.
With 11 tools, the server provides a well-scoped set for tracking artists, managing inspiration, and recording preferences. Each tool has a clear role without redundancy or excessive overlap.
The tool set covers core workflows: artist management (add/list/remove), feed browsing, inspiration management, and preference recording. However, there is no tool to delete or update a recorded preference, which is a minor gap.
Related MCP Connectors
MCP server for QPost — lets AI agents publish video and image posts to YouTube, TikTok, Instagram.
Social media MCP server: your Instagram, TikTok, YouTube, LinkedIn and Threads history for your AI.
Hosted Instagram MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
Capability registry for the agentic economy. Semantic search over verified MCP server listings.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceAn MCP server that integrates with Instagram's Graph API to enable AI-driven management of Instagram Business accounts. It provides tools for fetching profile data, publishing media, analyzing engagement metrics, and managing direct messages.206MIT
- AlicenseAqualityDmaintenanceEnables browsing and curating posts from Instagram tattoo artists via Business Discovery API, with features like inspiration feed, bookmarking, and preference tracking, optimized for ChatGPT integration.111MIT
- FlicenseNot gradedqualityDmaintenanceMCP server that lets AI assistants manage an art catalogue — find paintings, post to social media, upload to galleries, schedule posts, and update metadata.-
- AlicenseNot gradedqualityDmaintenanceA production-ready Remote MCP Server that gives Claude direct, tool-based access to your Instagram Business account through the Meta Graph API — profile data, posts, comments, publishing, insights, analytics, hashtags, messaging, and real-time webhooks.7 npmMIT