Give your AI assistant an Instagram account it can operate safely.
instagram-mcp-ai is a locally-run TypeScript MCP server that turns an Instagram professional account into 28 well-annotated tools — talking only to the official Meta Graph API, with writes that preview before they touch anything.
See it in action #
You describe the outcome in plain language; the model picks the right tool and arguments. Here are three real tool calls the server exposes.
Publish an image — safely
instagram_post_image is a composite write: it builds a
media container and publishes it in one call. It previews by default —
nothing is posted until you add apply: true.
Single URL posts one photo; 2–10 URLs post a carousel. Only public HTTPS URLs — the server never uploads or fetches your media itself.
// 1) Preview — changes nothing
instagram_post_image {
"imageUrls": ["https://cdn.example.com/lake.jpg"],
"caption": "Golden hour at the lake 🌅 #travel"
}
// → { "preview": true, "wouldPost": "IMAGE",
// "caption_chars": 33, "hashtags": 1 }
// 2) Apply — actually publishes
instagram_post_image {
"imageUrls": ["https://cdn.example.com/lake.jpg"],
"caption": "Golden hour at the lake 🌅 #travel",
"apply": true
}
// → { "id": "1789…", "permalink": "https://…/p/Cx…/" }
Read the latest comments
instagram_list_comments reads a media object's comments, with threaded replies inlined. It is read-only and paginated.
All account-controlled free text (usernames, comment bodies) is returned fenced as untrusted input, so a hostile comment cannot smuggle instructions to the model.
instagram_list_comments {
"mediaId": "17895695668004550",
"limit": 5
}
// → {
// "items": [
// { "id": "17865…", "username": "‹untrusted›maya‹/›",
// "text": "‹untrusted›Where was this shot?‹/›",
// "like_count": 12, "replies": [ … ] }
// ],
// "paging": { "after": "QVF…", "truncated": false }
// }
Discover another business
instagram_discover_business reads another public business/creator profile and its recent media by handle.
Path B (Facebook Login) only, and part of the discovery package that ships
dark by default. It needs Meta's "Instagram Public Content Access",
which may be App-Review-gated.
instagram_discover_business {
"username": "naturehub",
"mediaLimit": 3
}
// → {
// "username": "‹untrusted›naturehub‹/›",
// "followers_count": 128400,
// "media_count": 512,
// "media": [ { "id": "1794…", "like_count": 3120,
// "comments_count": 88 }, … ]
// }
What is MCP? #
The Model Context Protocol is an open standard that lets AI assistants call external tools through a uniform interface. An MCP server advertises a set of tools — each with a name, a typed input schema, and safety annotations — and the assistant's client decides which to call and with what arguments.
instagram-mcp-ai is one such server. It runs locally on your machine, holds your Instagram access token, and turns Graph API operations into tools the model can invoke. Because it runs where you run it, your token never leaves your environment.
Typed tools
Every tool declares a zod input schema, so arguments are validated before any API call is spent.
Honest annotations
Tools are tagged read-only, idempotent, or destructive, so clients can gate risky actions.
Runs anywhere MCP does
Speaks MCP over stdio to Claude Desktop, Cursor, and other MCP-capable clients.
Capabilities #
One server, the full lifecycle of a professional account — read, publish, engage, measure, discover.
Account & media
Read the operated profile, list and inspect media (including carousel children), and check the linked accounts and token status.
Publishing
Post images, carousels, reels, and stories via the container → publish flow, or use the composite one-call helpers.
Comment management
List, reply, create, hide, unhide, and (double-gated) delete comments; toggle comments on a media object.
Insights
Post-2025 views-based account and media insights, audience demographics, and online-follower timing.
Discovery
Resolve hashtags, read public hashtag media, and profile other public businesses — Path B, dark by default.
Safety built in
Preview-by-default writes, a destructive double-gate, host allow-listing, and untrusted-text fencing.
Quickstart #
Three steps to a working server. You will need Node.js 22+ and an Instagram professional account.
Authenticate once
Run the login helper to obtain and persist a long-lived token (choose ig or fb).
Verify your setup
The doctor prints the resolved profile, packages, write mode, and token expiry.
Wire it into your client
Point your MCP client at the command. The server speaks MCP over stdio by default.
~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"instagram": {
"command": "npx",
"args": ["-y", "instagram-mcp-ai"],
"env": {
"IG_ENV_FILE": "~/.config/instagram-mcp-ai/.env"
}
}
}
}
.cursor/mcp.json
{
"mcpServers": {
"instagram": {
"command": "npx",
"args": ["-y", "instagram-mcp-ai"],
"env": {
"IG_ENV_FILE": "~/.config/instagram-mcp-ai/.env"
}
}
}
}
Writes preview by default. Even after setup, the server changes nothing on
Instagram until you pass apply: true and run with IG_WRITE_MODE=apply.
Start in preview mode and promote to apply when you trust the calls.
Credentials #
The server authenticates with a standard Meta Graph API access token. There is no scraping and no password automation — a live login requires a registered Meta app (app id + secret and a whitelisted OAuth redirect URI).
| You need | Why | Where it goes |
|---|---|---|
| Instagram professional account | Business or Creator — personal accounts aren't exposed by the Graph API | IG_ACCOUNT_ID (optional; auto-resolved when possible) |
| A Meta app (id + secret) | Required to run OAuth and, on Path B, to sign requests | IG_APP_ID, IG_APP_SECRET |
| An access token | Authorizes every Graph API call | IG_ACCESS_TOKEN (Path A) or IG_FB_ACCESS_TOKEN (Path B) |
The login subcommand writes the token to your XDG / APPDATA env
file (chmod 0600 on POSIX) and never prints it. Secrets are
also registered with a redactor so they are masked in all log output.
Authentication — two paths #
The server supports two official OAuth paths. It auto-detects which you configured; you can
also pin it with IG_AUTH_MODE.
| Path A — Instagram Login | Path B — Facebook Login | |
|---|---|---|
| Mode | ig-login | fb-login |
| Endpoint | graph.instagram.com | graph.facebook.com (pinned v25.0) |
| Request signing | None | appsecret_proof (HMAC-SHA256 of the token) |
| Token lifetime | 60-day long-lived, refreshable | Can be a never-expiring system-user token |
| Discovery package | Not available | Available (subject to Meta feature access) |
| Best for | The simplest publishing & engagement setup | Discovery, or a token that never expires |
Not sure? Start with Instagram Login (Path A). Switch to
Facebook Login (Path B) only when you need the discovery package or a
never-expiring token. The tool surface is otherwise the same — discovery tools are simply
absent on Path A.
Command-line interface #
The instagram-mcp-ai binary is both the MCP server and a small operator CLI.
Run it with no subcommand to start the configured transport; the three subcommands below
never start a transport — they act and exit.
| Command | What it does |
|---|---|
login --path <ig|fb> |
Interactive browser OAuth to obtain and persist a long-lived token. Options: --profile, --app-id, --app-secret, --redirect-uri, --account-id, --scopes. Requires a registered Meta app. |
doctor |
Health-check the active profile: prints auth path, transport, write mode, destructive gate, active packages, and a token-expiry verdict (valid / never / expiring_soon / expired / unknown). Exit code reflects health. |
refresh |
Refresh the active profile's long-lived token, rewrite the credentials file, and print the new expiry (token itself never printed). |
npx instagram-mcp-ai login --path ig --profile default
npx instagram-mcp-ai doctor
npx instagram-mcp-ai refresh
Configuration #
Everything is configured through IG_* environment variables (loaded from the
process env and, optionally, an env file via IG_ENV_FILE). A
* marks a value you'll usually need to set.
| Variable | Default | Purpose |
|---|---|---|
IG_AUTH_MODE | auto | Pin the auth path: ig-login or fb-login. Auto-detected from which token is present. |
IG_ACCESS_TOKEN * | — | Path A (Instagram Login) access token. |
IG_FB_ACCESS_TOKEN | — | Path B (Facebook Login) access token. |
IG_ACCOUNT_ID | resolved | The Instagram professional-account id to operate. |
IG_APP_ID / IG_APP_SECRET | — | Meta app credentials — required for login and for Path B request signing. |
IG_ENV_FILE | — | Path to an env file to load before resolving settings. |
IG_ACTIVE_PROFILE | default | Which named profile is active. |
IG_PROFILE_<NAME>_* | — | Per-profile overrides (token, account id, app id/secret) for multi-account setups. |
IG_TOOL_PACKAGES | core | A profile (core/reader/publisher/all) or a comma/space list of package names. |
IG_PACKAGES_DENY | — | Packages to remove from the selection. |
IG_PACKAGES_READONLY | — | Packages forced to expose only their read tools. |
IG_WRITE_MODE | preview | preview refuses to apply writes; apply allows them (still needs apply: true per call). |
IG_ALLOW_DESTRUCTIVE | false | Second gate required to delete a comment. |
IG_TRANSPORT | stdio | Transport: stdio or http. |
IG_HTTP_HOST / IG_PORT | 127.0.0.1 / 3000 | Bind address for the optional loopback HTTP transport. |
IG_HTTP_TOKEN | — | Bearer token required by the HTTP transport. |
IG_MAX_CONCURRENT | 4 | Max concurrent Graph API requests. |
IG_MAX_ITEMS | 200 | Server-side cap on items returned from paginated reads. |
IG_REFRESH_AFTER_DAYS | 45 | When the doctor starts warning that a token is expiring soon. |
IG_TIMEOUT_MS | 30000 | Per-request timeout in milliseconds. |
IG_LOG_LEVEL | info | Log verbosity (debug … error). Secrets are always redacted. |
IG_PRETTY_JSON | false | Pretty-print JSON in tool results. |
Write mode & the destructive gate
Two independent switches guard mutations. IG_WRITE_MODE is the server-wide gate
(default preview); each write tool additionally needs apply: true.
Deleting a comment is double-gated — it requires apply: true
and IG_ALLOW_DESTRUCTIVE=true.
# A read-mostly reviewer that can publish but never delete
IG_TOOL_PACKAGES=publisher
IG_WRITE_MODE=apply
IG_ALLOW_DESTRUCTIVE=false
Packages & profiles #
The 28 tools are grouped into six packages. You choose which to expose with
IG_TOOL_PACKAGES — either a named profile or an explicit list.
The default is core, which deliberately leaves discovery dark.
- Profiles —
core(default): account, media, publishing, comments, insights (25 tools).reader: account, media, insights, comments, discovery.publisher: account, media, publishing, comments.all: every package (28 tools). - Packages —
account,media,publishing,comments,insights,discovery.
# Named profile …
IG_TOOL_PACKAGES=reader
# … or an explicit package list
IG_TOOL_PACKAGES=account,media,insights
The discovery package is not in core. It reads the
public content graph, is Path B only, and needs Meta's "Instagram Public Content Access" —
so it ships dark and you opt in explicitly (e.g. the reader or all profile).
Tools #
All 28 tools, grouped by package. read tools never mutate;
write tools preview by default and act only with apply: true.
Path B marks tools available only on Facebook Login.
No tools match your filter.
account
instagram_get_account readRead the operated professional account's profile (username, name, biography, website, counts). Free-text fields are returned fenced.
No parameters.
instagram_list_linked_accounts readPath BList the Facebook Pages and Instagram accounts linked to the token. Facebook Login only.
No parameters.
instagram_token_status readReport the access token's type and expiry. Path B resolves expiry via debug_token; on Path A expiry may be reported as unknown.
No parameters.
media
instagram_list_media readList the account's media objects, newest first, with paging.
| Parameter | Type | Notes | |
|---|---|---|---|
limit | number | optional | Page-size hint, 1–100. |
after | string | optional | Paging cursor from a previous page. |
fetchAll | boolean | optional | Follow paging up to the server item cap. |
instagram_get_media readFetch one media object; carousel albums include their children.
| Parameter | Type | Notes | |
|---|---|---|---|
mediaId | string | required | The media object id. |
instagram_set_comments_enabled writeEnable or disable comments on a media object. Idempotent.
| Parameter | Type | Notes | |
|---|---|---|---|
mediaId | string | required | The media object to update. |
enabled | boolean | required | true enables comments; false disables them. |
apply | boolean | optional | Set true to perform the write; omitted/false previews only. |
publishing
instagram_create_media_container writeCreate a media container (step 1 of publish). Supports images, reels, stories, carousels, and carousel items.
| Parameter | Type | Notes | |
|---|---|---|---|
mediaType | enum | optional | One of: REELS · STORIES · CAROUSEL. Omit for a single image. |
imageUrl | string | optional | Public HTTPS image URL. |
videoUrl | string | optional | Public HTTPS video URL (reels/stories). |
caption | string | optional | ≤2200 chars, ≤30 hashtags, ≤20 mentions. |
children | string[] | optional | 2–10 carousel-item container ids. |
locationId | string | optional | Page/location id to tag. |
userTags | object[] | optional | Users to tag in the media. |
coverUrl / thumbOffset | string / number | optional | Reel cover image or thumbnail frame offset. |
shareToFeed | boolean | optional | Also share a reel to the main feed. |
isCarouselItem | boolean | optional | Mark this container as a carousel child. |
apply | boolean | optional | Set true to perform the write; omitted/false previews only. |
instagram_get_container_status readPoll a container's processing status before publishing.
| Parameter | Type | Notes | |
|---|---|---|---|
containerId | string | required | The container (creation) id to inspect. |
instagram_publish_media writePublish a ready container (step 2). Never auto-retried, to avoid accidental duplicate posts.
| Parameter | Type | Notes | |
|---|---|---|---|
creationId | string | required | The container id to publish. |
apply | boolean | optional | Set true to perform the write; omitted/false previews only. |
instagram_get_publishing_limit readReport how much of the account's rolling publishing quota is used.
No parameters.
instagram_post_image writeComposite: build the container(s) and publish a photo or carousel in one call.
| Parameter | Type | Notes | |
|---|---|---|---|
imageUrls | string[] | required | 1 URL = single photo; 2–10 = carousel. Public HTTPS only. |
caption | string | optional | ≤2200 chars, ≤30 hashtags, ≤20 mentions. |
locationId | string | optional | Location/page id to tag. |
userTags | object[] | optional | Users to tag. |
resumeContainerId | string | optional | Resume a previously-created container instead of rebuilding. |
apply | boolean | optional | Set true to perform the write; omitted/false previews only. |
instagram_post_reel writeComposite: build and publish a reel from a public video URL in one call.
| Parameter | Type | Notes | |
|---|---|---|---|
videoUrl | string | required | Public HTTPS video URL. |
caption | string | optional | ≤2200 chars, ≤30 hashtags, ≤20 mentions. |
coverUrl / thumbOffset | string / number | optional | Cover image URL or thumbnail frame offset. |
shareToFeed | boolean | optional | Also share the reel to the feed. |
locationId | string | optional | Location/page id to tag. |
resumeContainerId | string | optional | Resume a previously-created container. |
apply | boolean | optional | Set true to perform the write; omitted/false previews only. |
instagram_post_story writeComposite: publish a story from one image XOR one video. Stories expire after 24 hours.
| Parameter | Type | Notes | |
|---|---|---|---|
imageUrl | string | optional | Public HTTPS image URL (mutually exclusive with videoUrl). |
videoUrl | string | optional | Public HTTPS video URL (mutually exclusive with imageUrl). |
resumeContainerId | string | optional | Resume a previously-created container. |
apply | boolean | optional | Set true to perform the write; omitted/false previews only. |
comments
instagram_list_comments readList a media object's comments, with threaded replies inlined. Text is returned fenced.
| Parameter | Type | Notes | |
|---|---|---|---|
mediaId | string | required | The media object to read comments for. |
limit | number | optional | Page-size hint, 1–100. |
after | string | optional | Paging cursor. |
fetchAll | boolean | optional | Follow paging up to the server item cap. |
instagram_get_comment readFetch a single comment by id.
| Parameter | Type | Notes | |
|---|---|---|---|
commentId | string | required | The comment id. |
instagram_list_tagged_media readList media the account is tagged in. Note: tags are not @mentions.
| Parameter | Type | Notes | |
|---|---|---|---|
limit | number | optional | Page-size hint, 1–100. |
after | string | optional | Paging cursor. |
fetchAll | boolean | optional | Follow paging up to the server item cap. |
instagram_reply_to_comment writeReply to a comment.
| Parameter | Type | Notes | |
|---|---|---|---|
commentId | string | required | The comment to reply to. |
message | string | required | The reply text. |
apply | boolean | optional | Set true to perform the write; omitted/false previews only. |
instagram_create_comment writePost a top-level comment on a media object.
| Parameter | Type | Notes | |
|---|---|---|---|
mediaId | string | required | The media object to comment on. |
message | string | required | The comment text. |
apply | boolean | optional | Set true to perform the write; omitted/false previews only. |
instagram_hide_comment writeHide a comment. Idempotent.
| Parameter | Type | Notes | |
|---|---|---|---|
commentId | string | required | The comment to hide. |
apply | boolean | optional | Set true to perform the write; omitted/false previews only. |
instagram_unhide_comment writeUnhide a previously hidden comment. Idempotent.
| Parameter | Type | Notes | |
|---|---|---|---|
commentId | string | required | The comment to unhide. |
apply | boolean | optional | Set true to perform the write; omitted/false previews only. |
instagram_delete_comment writedestructivePermanently delete a comment. Irreversible, and double-gated: needs apply:true AND IG_ALLOW_DESTRUCTIVE=true.
| Parameter | Type | Notes | |
|---|---|---|---|
commentId | string | required | The comment to delete. This cannot be undone. |
apply | boolean | optional | Set true to perform the write; also requires IG_ALLOW_DESTRUCTIVE=true. |
insights
instagram_get_account_insights readAccount-level insights using the post-2025 metric set. Ranges are bounded by Meta's 90-day retention.
| Parameter | Type | Notes | |
|---|---|---|---|
metrics | enum[] | optional | Post-2025 metrics; defaults to all. Legacy names (impressions, profile_views, video_views) are rejected. |
period | enum | optional | Aggregation period; defaults to day. |
metric_type | enum | optional | total_value (default) or time_series. |
since / until | number | optional | Unix seconds. Fully-expired ranges are rejected; partial ranges are clamped and flagged. |
instagram_get_media_insights readInsights for one media object. The valid metric set depends on the media product type.
| Parameter | Type | Notes | |
|---|---|---|---|
media_id | string | required | The media object to measure. |
metrics | enum[] | optional | Defaults to views, reach, likes, comments, saved, shares, total_interactions. navigation/replies are story-only. |
media_product_type | string | optional | Hint (FEED / REELS / STORY) to reject invalid metric combinations client-side. |
instagram_get_audience_demographics readFollower / engaged-audience demographics. Requires a breakdown, a timeframe, and ≥100 followers.
| Parameter | Type | Notes | |
|---|---|---|---|
metrics | enum[] | optional | Defaults to follower_demographics; engaged_audience_demographics also available. |
breakdown | enum | required | The single dimension to break down by. |
timeframe | enum | required | The window demographics are computed over. Needs an account with ≥100 followers. |
instagram_get_online_followers readHourly distribution of when followers are online (last 30 days). On Meta's deprecation watch-list.
No parameters.
discovery
instagram_search_hashtag readPath BResolve a hashtag name to its hashtag id(s). Budget: 30 unique hashtags per account per rolling 7 days.
| Parameter | Type | Notes | |
|---|---|---|---|
hashtag | string | required | 1–150 chars, with or without a leading #. Counts against the 30/7-day budget. |
instagram_get_hashtag_media readPath BList public media under a hashtag id, top or recent. Captions are fenced; owners are not disclosed.
| Parameter | Type | Notes | |
|---|---|---|---|
hashtagId | string | required | From instagram_search_hashtag. |
edge | enum | required | One of: top · recent. |
limit | number | optional | Page-size hint, 1–100. |
instagram_discover_business readPath BFetch another public business/creator profile and recent media by handle. Free text is fenced.
| Parameter | Type | Notes | |
|---|---|---|---|
username | string | required | 1–30 chars, without a leading @. Private/personal/unknown handles error. |
mediaLimit | number | optional | 0–100 recent media; defaults to min(25, server cap). |
Use cases #
A few workflows the tool set makes natural.
📅 Draft, review, then publish
Compose a carousel and read back exactly what would post before anything goes live, then apply.
instagram_post_image → instagram_get_publishing_limit💬 Triage the morning's comments
Pull the latest comments across recent posts, reply to questions, and hide spam — with deletes kept behind the destructive gate.
instagram_list_comments → instagram_reply_to_comment → instagram_hide_comment📈 Weekly performance readout
Roll up account and per-post insights into a plain-language summary using the post-2025 views-based metrics.
instagram_get_account_insights → instagram_list_media → instagram_get_media_insights🔎 Scout a competitor
Profile another public business and skim their recent posts' engagement — subject to Meta's public-content access.
instagram_discover_business → instagram_search_hashtag → instagram_get_hashtag_mediaSecurity & write-safety #
The server is built to be handed to an autonomous model without becoming a liability. Safety is layered — no single switch is the only thing standing between the model and an irreversible action.
Preview by default
Every write returns what it would do and changes nothing until apply: true and IG_WRITE_MODE=apply.
Destructive double-gate
Deleting a comment is irreversible, so it also requires IG_ALLOW_DESTRUCTIVE=true — off by default.
Audit journal
Applied writes are appended to a local JSONL journal at ~/.local/state/instagram-mcp-ai/writes.jsonl.
Untrusted-text fencing
Captions, usernames, and bios are fenced before returning, so hostile text can't smuggle instructions to the model.
Official endpoints only
Requests go only to graph.instagram.com and graph.facebook.com — no scraping, no private APIs.
No media fetching (SSRF-safe)
Publishing accepts only public HTTPS URLs; the server never fetches or proxies your media itself.
Treat your token like a password. It authorizes real actions on a real
account. The login flow stores it with restrictive permissions and the logger
redacts it, but anyone with the token — or your env file — can act as the account.
Architecture #
The codebase is a strict four-layer stack. Dependencies point one way — a
tools spec may import api and mcp, but never reaches
into core transport directly. The direction is enforced by an ESLint
no-restricted-imports rule, so the boundary can't quietly rot.
Troubleshooting #
Run npx instagram-mcp-ai doctor first — it diagnoses most of the below in one shot.
| Symptom | Likely cause | Fix |
|---|---|---|
| "No account profile is configured" | No token has been persisted yet | Run login --path <ig|fb> to create the default profile. |
| A write "previews" but never posts | Server is in preview mode, or apply was omitted | Set IG_WRITE_MODE=apply and pass apply: true. |
Delete is refused even with apply: true | The destructive gate is closed | Set IG_ALLOW_DESTRUCTIVE=true (only when you mean it). |
| Discovery tools aren't listed | On Path A, or discovery not selected | Use Facebook Login and include discovery (e.g. IG_TOOL_PACKAGES=reader). |
| Discovery calls error with a permissions message | Missing "Instagram Public Content Access" | Request the feature for your Meta app; it may be App-Review-gated. |
| Publishing fails on your media URL | URL isn't public HTTPS, or is still processing | Host the media at a public HTTPS URL; poll get_container_status before publishing. |
| Demographics return an error | Account has fewer than 100 followers | Demographics require ≥100 followers, plus a breakdown and timeframe. |
| Token "expiring_soon" / "expired" in doctor | Long-lived token nearing its window | Run refresh before it lapses (Path A) or issue a fresh token. |
Still stuck? Set IG_LOG_LEVEL=debug for verbose (redacted) logs, then open an issue with the output on GitHub.
FAQ #
Is this an official Meta or Instagram product?
No. instagram-mcp-ai is an independent, community-built project — not affiliated with, endorsed by, or sponsored by Meta Platforms, Inc. It talks only to the official Meta Graph API; Instagram and Meta are trademarks of Meta Platforms, Inc., used nominatively to indicate compatibility.
What kind of Instagram account do I need?
An Instagram professional account — Business or Creator. The Instagram Platform / Graph API does not expose personal accounts, so a personal profile cannot be operated by this server.
Does it scrape Instagram or automate my password?
No. Every call goes to an official Meta Graph API endpoint (graph.instagram.com or graph.facebook.com). There is no scraping, no private/undocumented API, and no credential or browser automation — authentication uses standard OAuth access tokens.
Which authentication path should I choose?
Instagram Login (Path A) is the simplest setup for publishing and engagement — graph.instagram.com, granular scopes, and a 60-day long-lived token. Facebook Login (Path B) uses graph.facebook.com v25.0 with an appsecret_proof, can issue a never-expiring system-user token, and is the only path that unlocks the discovery package.
Can it post or delete things without my say-so?
No. Every write previews by default and acts only when you pass apply: true (with IG_WRITE_MODE=apply). Deleting a comment is double-gated — it additionally requires IG_ALLOW_DESTRUCTIVE=true — and applied writes are appended to a local JSONL audit journal.
Do I need to build or host anything?
No. Run it locally with npx instagram-mcp-ai on Node.js 22 or newer. It speaks MCP over stdio to your client (Claude Desktop, Cursor, and other MCP-capable apps); an optional loopback HTTP transport is available for local development.