Skip to content
Conduit

mcp server

go-hooppy

Go client library, CLI and MCP server for the Hooppy (hooppy.ru) social media auto-posting API — publish to VK, Telegram, Instagram, YouTube and 20+ other networks

anatolykoptev/go-hooppycommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

This project does not publish an install method we can turn into a config. Follow the README instead.

README

synced Oct 21, 2026

go-hooppy

Go client library, CLI, and MCP server for the Hooppy social media auto-posting API.

Hooppy publishes posts to 20+ social networks (VK, OK, Telegram, Instagram, Twitter/X, TikTok, YouTube, LinkedIn, Dzen, Max, Threads, and more) from a single API. This package provides:

  • Client library — pure Go API client with bearer JWT auth, zero heavy deps
  • CLI — hooppy command for scripts and manual use
  • MCP server — hooppy-mcp for LLM agents (Claude, etc.) to publish via Model Context Protocol

Install

go get github.com/anatolykoptev/go-hooppy@latest

Or install the CLI tools:

go install github.com/anatolykoptev/go-hooppy/cmd/hooppy@latest
go install github.com/anatolykoptev/go-hooppy/cmd/hooppy-mcp@latest

Authentication

Get your API token from Hooppy → Settings → API-token (https://hooppy.ru/settings/api).

Set it via environment variable:

export HOOPPY_TOKEN="eyJ0eXAiOiJKV1Qi..."

Or save to a file (the client checks both):

mkdir -p ~/.config/hooppy
echo -n "eyJ0eXAiOiJKV1Qi..." > ~/.config/hooppy/token
chmod 600 ~/.config/hooppy/token

Client library usage

package main

import (
    "context"
    "fmt"
    "log"
    
    "github.com/anatolykoptev/go-hooppy"
)

func main() {
    client, err := hooppy.NewClientFromEnv()
    if err != nil {
        log.Fatal(err)
    }

    // List connected accounts
    resp, err := client.ListAccounts(context.Background(), hooppy.ListAccountsFilter{})
    if err != nil {
        log.Fatal(err)
    }
    for _, acc := range resp.List {
        fmt.Printf("ID=%d  %s (source=%d)\n", acc.ID, acc.SocialAccountName, acc.SourceID)
    }

    // Publish a post immediately to selected pages
    result, err := client.CreatePost(context.Background(), hooppy.PostPublishNowPayload{
        PublicationWhenType: 1,
        PublicationHowType:  1,
        SelectedPagesIDs:    []int{123456, 789012}, // your page IDs from hooppy pages list
        Texts:               []hooppy.PostText{{Text: "Hello from go-hooppy!", SourceID: 0}},
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Post created: ID=%d\n", result.ID)
}

Retry and HTTP client configuration

By default, the client does NOT retry failed requests. Enable retry for transient failures (429/5xx) on safe methods (GET, DELETE) via Config.RetryOptions:

client, err := hooppy.NewClient(hooppy.Config{
    Token: "your-jwt-token",
    RetryOptions: &retry.Options{
        MaxAttempts:  3,                   // default: 3
        InitialDelay: 500 * time.Millisecond, // default: 500ms
        MaxDelay:     5 * time.Second,     // default: 5s
        // MaxElapsedTime defaults to 30s if unset.
        // OnRetry: func(attempt int, err error) { /* observe retries */ },
    },
})

Retry behavior:

  • GET and DELETE are retried on 429/500/502/503/504 with exponential backoff.
  • POST and streaming uploads (CreatePost, BatchDeletePosts, UploadMedia, UploadDocument) NEVER retry — they are non-idempotent.
  • The Retry-After header (RFC 7231: seconds or HTTP-date) overrides the backoff delay.
  • Context is the sole deadline authority — retry stops when the context is cancelled.
  • APIError.RetryAfter exposes the parsed header value (0 if absent).

To customize the HTTP transport (connection pool, TLS, proxies), inject your own *http.Client:

client, err := hooppy.NewClient(hooppy.Config{
    Token:      "your-jwt-token",
    HTTPClient: &http.Client{Transport: &http.Transport{
        MaxIdleConnsPerHost: 30, // tune for high-concurrency (e.g. MCP server)
        // ... other transport fields
    }},
})

CLI usage

# List accounts
hooppy accounts list
hooppy accounts list --source 6   # Pinterest only

# List pages (groups)
hooppy pages list --source 1      # VK pages

# Create and publish a post
hooppy posts create --text "Hello world" --to <page-id>,<page-id>

# List unpublished posts
hooppy posts list --unpublished

# Delete a post
hooppy posts delete <post-id>

# Upload media
hooppy files upload-media ./photo.jpg

# List projects and schedules
hooppy projects list
hooppy schedules list

# Create / update / delete projects (undocumented)
hooppy projects create --name "My Project" --page <page-id>
hooppy projects update <project-id> --name "Renamed Project"
hooppy projects delete <project-id>

# Create / update / delete schedules (undocumented)
hooppy schedules create --name "Daily 09:00"
hooppy schedules update <schedule-id> --name "Renamed" --state 0
hooppy schedules delete <schedule-id>

# Update an existing post (undocumented)
hooppy posts update <post-id> --text "Updated text" --to <page-id>

# Create a post via an alternative mode (undocumented)
# Modes: search, copy, sources, import, crosspost, rewrite, translate,
#         queue, drafts, templates, rss, feeds, tags, watermarks, batch
hooppy posts crosspost --mode search --text "Found via search" --to <page-id>
hooppy posts crosspost --mode copy --text "Copied post" --to <page-id>
hooppy posts crosspost --mode rss --text "From RSS feed" --to <page-id>

# Disconnect a page (undocumented)
hooppy pages disconnect <page-id>

# User profile / watermarks / proxies / notifications (undocumented)
hooppy user
hooppy watermarks list
hooppy watermarks create --name "WM1" --file /path/to/wm.png --opacity 50
hooppy watermarks update <id> --name "Renamed" --opacity 80
hooppy watermarks delete <id>
hooppy proxies list
hooppy proxies create --ip 1.2.3.4 --port 8080 --login user --password pass
hooppy proxies update <id> --name "Renamed proxy" --ip 5.6.7.8
hooppy proxies delete <id>
hooppy notifications

# Search and scrape posts from external social media pages (UNDOCUMENTED)
hooppy search sources                          # list configured source resources
hooppy search posts --source-type 1  # list scraped posts (1=social, 2=RSS)
hooppy search posts --source-type 1 --text "search query" --date-from 01.01.2026
hooppy search status                           # check if parsing is in progress
hooppy search parse --source-resource-id <id> --account-id <id>  # start scraping
hooppy search stop                             # stop in-progress scraping
hooppy search copy --post-id <id> --to <page-id>  # DEPRECATED: alias for 'search import' (resolve+publish); use 'search import' instead
hooppy search rewrite --post-id <id> --text "..." --to <page-id>  # rewrite with custom text (attachments preserved)
hooppy search rewrite --post-id <id> --text "..." --to <page-id> --no-attachments  # rewrite, strip attachments
hooppy search rewrite --post-ids "id1,id2,id3" --to <page-id>  # batch rewrite (each post resolved+published independently; attachments preserved per post; no text override in batch)
hooppy search import --post-id <id> --schedules <sched-id>  # copy a scraped post (resolve+publish: text + photos/videos preserved from the resolve step)
hooppy search import --post-ids "id1,id2,id3" --schedules <sched-id>  # batch import (keeps each post's original text; each post resolved+published independently)

# Diagnose broken connections (read-only). GET /accounts reports status: 1
# even when an account's OAuth token is dead — the notification log is the
# only endpoint that reports publication failures. doctor walks it, groups
# errors by (page, error message), and classifies them so you can tell at a
# glance which connections need a human to reconnect them.
hooppy doctor                              # last 7 days, exit 1 if any error in window
hooppy doctor --since 30                   # widen the window to 30 days
hooppy doctor --exit-code=false            # print the report without the non-zero exit

# Print MCP setup instructions
hooppy mcp-config

hooppy doctor

doctor is a read-only diagnostic that answers one question: which connections are broken right now? It exists because GET /accounts reports status: 1 for an account whose OAuth token is dead — measured on a live account where five networks had been failing every publish for weeks while every account still read status: 1. The notification log (GET /notifications) is the only endpoint that reports publication failures, and doctor makes it legible.

It walks the full notification log, filters to error rows whose operation_date falls inside the --since window (default 7 days), groups them by (page, error message), and prints a JSON report on stdout. Each group carries the page id, page name, network, the vendor's error string verbatim, an occurrence count, the first and last operation_date in the window, and a classification:

  • expired_credential — the vendor's text asks the user to reconnect the account. The tool cannot fix this; it must be named loudly.
  • missing_media — a text-only post routed to a network that requires an image. A targeting mistake in a schedule; user-fixable.
  • upstream_error — a 5xx from the social network. Transient; must not carry the same weight as the first two.
  • unknown — anything unmatched, carrying the raw vendor string. Never forced into a known bucket.

A row whose operation_date fails to parse is reported in unparseable_rows — never silently dropped, since dropping a row hides exactly the failure the command exists to surface. Unparseable-date rows are reported regardless of --since — they cannot be dated, so the window check cannot be applied to them. One permanently malformed historical row therefore wedges exit 1 until the row ages out of the log; narrowing --since cannot clear it.

Exit code: --exit-code (default true) — exit 1 if any error signal is present: grouped errors inside the --since window, unparseable-date rows (reported regardless of --since because they cannot be dated), or a truncated walk (walk_incomplete). Exit 0 otherwise. This lets doctor run in cron or as a pre-flight before a bulk import. Pass --exit-code=false to get the report without the non-zero exit.

--since semantics: 0 means "no window" (every dated row is included); a negative value is rejected with an error (never silently clamped — clamping a bad value into the quietest configuration hides exactly the failures the command exists to surface).

MCP server setup

Claude Code (stdio)

claude mcp add hooppy --transport stdio -- hooppy-mcp

Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "hooppy": {
      "command": "hooppy-mcp",
      "args": ["--stdio"]
    }
  }
}

HTTP mode (for remote agents)

hooppy-mcp   # starts on :8080 with /mcp endpoint

Available MCP tools

ToolDescription
hooppy_list_accountsList connected social network accounts
hooppy_list_pagesList groups/pages for publishing
hooppy_list_postsList posts with filters (status, date, source, etc.)
hooppy_create_postCreate and publish a post (immediate, scheduled, by project, or by schedule)
hooppy_delete_postDelete a single post
hooppy_batch_delete_postsDelete multiple posts
hooppy_upload_mediaUpload a photo or video file
hooppy_upload_documentUpload a document file
hooppy_list_projectsList post projects
hooppy_list_schedulesList publication schedules
hooppy_create_projectCreate a post project
hooppy_update_projectUpdate a project name
hooppy_delete_projectDelete a project
hooppy_create_scheduleCreate a publication schedule
hooppy_update_scheduleUpdate a publication schedule
hooppy_delete_scheduleDelete a publication schedule
hooppy_update_postUpdate an existing post
hooppy_get_userGet current user profile
hooppy_list_watermarksList watermarks
hooppy_create_watermarkCreate a watermark
hooppy_update_watermarkUpdate a watermark
hooppy_delete_watermarkDelete a watermark
hooppy_list_proxiesList proxy servers
hooppy_create_proxyCreate a proxy server
hooppy_update_proxyUpdate a proxy server
hooppy_delete_proxyDelete a proxy server
hooppy_list_notificationsList publication notifications
hooppy_disconnect_pageDisconnect a social media page
hooppy_list_search_postsList posts scraped from external pages (UNDOCUMENTED)
hooppy_list_source_resourcesList configured source resources to scrape from (UNDOCUMENTED)
hooppy_parsing_statusCheck scraping status + available parsers (UNDOCUMENTED)
hooppy_start_parsingStart scraping posts from an external source (UNDOCUMENTED)
hooppy_stop_parsingStop in-progress scraping job (UNDOCUMENTED)
hooppy_copy_search_postDeprecated: copy a scraped post to your pages (now delegates to resolve+publish; use hooppy_import_search_post instead) (UNDOCUMENTED)
hooppy_rewrite_search_postRewrite one or more scraped posts with custom text (single) or batch keeping original text (UNDOCUMENTED)
hooppy_import_search_postImport one or more scraped posts (single or batch; keeps original text + attachments) (UNDOCUMENTED)

Note — hooppy_copy_search_post is deprecated. The historical CopySearchPost (PUT /posts/copy) created empty posts; the restored shim now delegates to resolve+publish (the same path hooppy_import_search_post uses), preserving text + attachments. Use hooppy_import_search_post for the non-deprecated name, or hooppy_rewrite_search_post for a text override. Batch import is fully exposed via MCP (hooppy_import_search_post with search_post_ids).

Social network source IDs

IDNetworkIDNetwork
1VKontakte14TikTok
2Odnoklassniki17YouTube
3Facebook18LinkedIn
4Twitter/X22WhatsApp
5My World23Rutube
6Pinterest29Instagram
8Tumblr32Yappy
9Telegram (channels)33Max
10Instagram FB34Threads
11Telegram (accounts)35VK Chats
13Dzen

Use hooppy.SourceVK, hooppy.SourcePinterest, etc. in Go code.

API reference

The full OpenAPI 3.0 specification is in openapi.yaml (copied from hooppy.ru/openapi.yaml).

Base URL: https://api.hooppy.ru/api
Auth: Authorization: Bearer <JWT token>

Undocumented endpoints

The following endpoints are NOT in the public OpenAPI spec (v0.1.0) but were discovered via live API probing. They may change without notice. Use with caution.

MethodEndpointGo methodNotes
GET/users/meGetUserCurrent user profile (sensitive fields excluded)
GET/watermarksListWatermarksPaginated
POST/watermarksCreateWatermark6 fields: name, file, space, position, opacity, size
PUT/watermarks/{id}UpdateWatermarkSame 6 fields
DELETE/watermarks/{id}DeleteWatermarkReturns {"success":true}
GET/proxiesListProxiesAll proxies
POST/proxiesCreateProxy5 fields: name, ip, port, login, password
PUT/proxies/{id}UpdateProxySame 5 fields
DELETE/proxies/{id}DeleteProxyReturns {"success":true}
GET/notificationsListNotificationsPublication status notifications
POST/posts/schedulesCreateSchedule34 required fields; use NewSchedulePayload(name)
PUT/posts/schedules/{id}UpdateScheduleSame 34 fields
DELETE/posts/schedules/{id}DeleteScheduleReturns {"success":true,"schedules":[...]}
POST/posts/projectsCreateProject56 required fields; use NewProjectPayload(name, pageID)
PUT/posts/projects/{id}UpdateProjectBody: {"name":"..."}
DELETE/posts/projects/{id}DeleteProjectReturns {"success":true}
PUT/posts/{id}UpdatePostSame payload as CreatePost
DELETE/accounts/pages/{id}DisconnectPageIdempotent
PUT/posts/searchSearchPostsCross-posting mode
PUT/posts/copyCopyPostCross-posting mode
PUT/posts/sourcesSourcesPostCross-posting mode
PUT/posts/importImportPostCross-posting mode
PUT/posts/crosspostCrossPostCross-posting mode
PUT/posts/rewriteRewritePostCross-posting mode
PUT/posts/translateTranslatePostCross-posting mode
PUT/posts/queueQueuePostCross-posting mode
PUT/posts/draftsDraftPostCross-posting mode
PUT/posts/templatesTemplatePostCross-posting mode
PUT/posts/rssRSSPostCross-posting mode
PUT/posts/feedsFeedPostCross-posting mode
PUT/posts/tagsTagPostCross-posting mode
PUT/posts/watermarksWatermarkPostCross-posting mode
PUT/posts/batchBatchPostCross-posting mode

All cross-posting modes (PUT /posts/{mode}) accept the same payload as CreatePost and return {"id":...}.

// Create a schedule with defaults
payload := hooppy.NewSchedulePayload("My Daily Schedule")
payload.PublishAsStory = 1   // enable stories
payload.IsCommentsDisabled = 1
resp, err := client.CreateSchedule(context.Background(), payload)

// Create a project
payload := hooppy.NewProjectPayload("My Project", pageID)
resp, err := client.CreateProject(context.Background(), payload)

// Cross-post via search mode
postPayload := hooppy.PostPublishNowPayload{
    PublicationWhenType: 1,
    PublicationHowType:  1,
    SelectedPagesIDs:    []int{pageID},
    Texts:               []hooppy.PostText{{Text: "hello", SourceID: 0}},
    Attachments:         []hooppy.Attachment{},
}
resp, err := client.SearchPosts(context.Background(), postPayload)

License

Apache 2.0 — same as the Hooppy OpenAPI specification.

↑↓ navigate openesc close