@firstfluke/haejo-mcp
v0.1.0
Published
MCP server for 콘텐츠해줘 (Contents) — Threads management insights, bookmarks, and engagers over the Contents API
Maintainers
Readme
@firstfluke/haejo-mcp
An MCP (Model Context Protocol) stdio server that exposes 콘텐츠해줘 (Contents) Threads analytics to MCP hosts such as Claude Desktop, Cursor, and other LLM clients that speak the Model Context Protocol.
The server proxies six read-only tools over the Contents API: account-level insights, per-post metrics, profile-view timeseries, saved bookmarks, top engagers, and connection health — all scoped to a single Threads-connected channel at a time.
Pro plan required. Every tool is gated at the Pro tier. Calls on a Free or Basic account return
tier_requiredbefore any API request is made. See Troubleshooting.
Requirements
- Node.js >= 24
- A 콘텐츠해줘 Pro account (Pro, Modoo Pro, or Ultra tier)
- The
threads:readscope (granted byhaejo-mcp login)
Installation
npx (no install)
npx @firstfluke/haejo-mcp login # authorize once// Claude Desktop / MCP client config
{
"mcpServers": {
"haejo": {
"command": "npx",
"args": ["-y", "@firstfluke/haejo-mcp"]
}
}
}Global install
npm install -g @firstfluke/haejo-mcp
haejo-mcp login # authorize once// Claude Desktop / MCP client config
{
"mcpServers": {
"haejo": {
"command": "haejo-mcp",
"args": []
}
}
}The MCP host runs haejo-mcp (or npx …) with no arguments to start the server. You run haejo-mcp login separately in a terminal before the first tool call.
Authentication
haejo-mcp uses OAuth 2.0 Authorization Code + PKCE (RFC 7636). It is a public client — no client secret is embedded or required.
Why login is a separate step
The server speaks stdio JSON-RPC. If a tool call triggered a browser flow, the host's conversation would stall until you found your browser and completed the redirect — with no way to communicate what was happening. haejo-mcp login is a foreground terminal program that can wait five minutes for you; the MCP server cannot.
Authorize this machine
haejo-mcp loginThe command prints an authorization URL, opens it in your browser, waits on a loopback listener for the callback, and stores the resulting tokens.
Tokens persist to $XDG_CONFIG_HOME/haejo-mcp/token.json (falls back to ~/.config/haejo-mcp/token.json). The file is written with 0600 permissions (owner-only). Tokens refresh automatically when they expire; you should not need to run login again unless you explicitly log out or the refresh grant is revoked.
Revoke credentials
haejo-mcp logoutDeletes the stored credential file. The next tool call will return auth_required.
Scope
Only threads:read is requested. Write operations (adding or removing bookmarks, posting) are intentionally absent from this server.
Tools
Every tool accepts a channelId (the Contents channel UUID — not the Threads account id or handle) and an optional format parameter ("markdown" or "json", defaulting to "markdown"). JSON format returns the raw validated payload; markdown format returns a human-readable digest.
threads_insights_summary
Account-level Threads performance for one channel: followers, profile views, lifetime 7-day/30-day metric blocks, a windowed aggregate, and top-performing posts.
| Input | Type | Default | Constraints | Description |
|---|---|---|---|---|
| channelId | UUID string | — | UUID | Channel UUID |
| days | integer | 30 | 7–90 | Top-performer window; lifetime cards (7d/30d) ignore it |
| format | "markdown" | "json" | "markdown" | — | Output format |
threads_insights_posts
Post-level Threads metrics for one channel — views, likes, replies, reposts, and engagement rate per post, sortable and filterable.
| Input | Type | Default | Constraints | Description |
|---|---|---|---|---|
| channelId | UUID string | — | UUID | Channel UUID |
| days | integer | 30 | 1–90 | Lookback window |
| limit | integer | 100 | 1–200 | Maximum posts to return |
| order | enum | "engagement" | see below | Sort key |
| search | string | — | max 200 chars, optional | Substring filter on post text |
| format | "markdown" | "json" | "markdown" | — | Output format |
order values: "engagement", "views", "likes", "recent", "replies", "reposts", "engagement_rate", "engagement_amount".
threads_insights_timeseries
Daily profile-view timeseries for one channel, for trend and seasonality analysis.
| Input | Type | Default | Constraints | Description |
|---|---|---|---|---|
| channelId | UUID string | — | UUID | Channel UUID |
| days | integer | 30 | 7–90 | Window length |
| format | "markdown" | "json" | "markdown" | — | Output format |
threads_bookmarks_list
Lists the Threads posts bookmarked in a channel, newest first, as Threads media_id values. Pair with threads_insights_posts to turn ids into text and metrics. Adding or removing bookmarks is not available in this version.
| Input | Type | Default | Constraints | Description |
|---|---|---|---|---|
| channelId | UUID string | — | UUID | Channel UUID |
| format | "markdown" | "json" | "markdown" | — | Output format |
threads_engagers
Ranks the accounts that comment most on a channel's Threads posts over a recent window (Top 20, comments only). The Threads API does not expose per-user like data, so this is a comment ranking. Accounts linked to the channel itself are excluded; one-off commenters (fewer than 2 interactions) are omitted.
| Input | Type | Default | Constraints | Description |
|---|---|---|---|---|
| channelId | UUID string | — | UUID | Channel UUID |
| months | integer | 3 | 1–12 | Look-back window in months |
| format | "markdown" | "json" | "markdown" | — | Output format |
threads_connection_status
Checks whether a channel's Threads account is still connected and whether the stored token needs reconnecting. Returns one of three statuses: connected, reconnect_required, or not_connected. No token material is exposed.
| Input | Type | Default | Constraints | Description |
|---|---|---|---|---|
| channelId | UUID string | — | UUID | Channel UUID |
| format | "markdown" | "json" | "markdown" | — | Output format |
Configuration
| Variable | Default | Description |
|---|---|---|
| CONTENTS_API_BASE_URL | https://haejo-api.firstfluke.com | Contents API origin. Set to http://localhost:8020 to target a local dev server. |
| XDG_CONFIG_HOME | ~/.config | Base directory for the credential file (haejo-mcp/token.json inside). |
CONTENTS_API_BASE_URL is the only setting relevant to normal use. Trailing slashes are stripped automatically; an empty or whitespace-only value falls back to the production URL.
To pass it to the MCP host:
{
"mcpServers": {
"haejo": {
"command": "haejo-mcp",
"args": [],
"env": {
"CONTENTS_API_BASE_URL": "http://localhost:8020"
}
}
}
}Troubleshooting
Every tool returns errors as an MCP error result (visible to the model and relayed to you)
carrying a stable code, a message, and — where there is a concrete next step — a hint.
All six tools use the same vocabulary, so a client can branch on code without knowing
which tool produced it.
Codes are split by where the failure happened. Anything prefixed upstream_ came back
from the Contents API; everything else was decided locally, before or after the call.
Local (this server, before a request is sent)
| Code | Meaning | Remedy |
|---|---|---|
| auth_required | No credential is stored, or the stored token could not be refreshed. | Run haejo-mcp login. |
| insufficient_scope | The token does not carry threads:read. | Run haejo-mcp login again to re-consent. |
| invalid_token | The stored token failed signature or claim validation (clock skew, key rotation, wrong issuer). | haejo-mcp logout, then haejo-mcp login. |
| tier_required | The account is below Pro. Also emitted when the API answers 402, i.e. the server disagrees with the local gate. | Upgrade to Pro. |
Upstream (returned by the Contents API)
| Code | HTTP | Meaning | Remedy |
|---|---|---|---|
| upstream_unauthorized | 401 | The token verified locally but the server refused it — revoked grant, rotated key, clock skew. | Run haejo-mcp login. |
| upstream_forbidden | 403 | The request was denied. | Confirm the channel belongs to the authenticated workspace. |
| channel_not_found | 404 | No such channel, or the workspace on the token does not own it. | Check the channel UUID; threads_connection_status confirms it exists and is linked. |
| invalid_request | 422 | The API rejected the request parameters. | Check the tool arguments against the docs above. |
| rate_limited | 429 | Too many requests. | Retry after a short pause. |
| upstream_error | 5xx | Server-side failure. | Usually transient; retry shortly. |
| upstream_request_failed | other | A non-2xx status with no more specific mapping. | Read the message; it carries the status and the API's detail. |
| upstream_unreachable | — | No response at all: DNS, refused connection, TLS, or timeout. | Check connectivity and that CONTENTS_API_BASE_URL points at the right origin. |
Response problems
| Code | Meaning | Remedy |
|---|---|---|
| invalid_upstream_response | A 2xx whose body did not match the expected schema — the API contract moved. | Upgrade @firstfluke/haejo-mcp; report it if the newest version still fails. |
| unexpected_error | Anything not covered above. | Read the message; report it if it recurs. |
Input validation failures (a channelId that is not a UUID, an out-of-range months)
are rejected by the MCP layer before the handler runs, and surface as an
Invalid arguments error naming the offending field.
License
MIT
