Install
$ agentstack add mcp-mudrii-golink ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
golink
LinkedIn CLI for humans and LLM agents.
> Status: alpha / requires LinkedIn app credentials. The command tree, OAuth login, JSON envelope contract, retryable HTTP client, and LinkedIn REST adapter are implemented and tested. Live use requires a LinkedIn developer app with the documented scopes; without credentials you can still exercise every --dry-run and schema-validated envelope.
Overview
golink talks to LinkedIn through a transport-pluggable architecture. The default official transport wraps LinkedIn's Posts API and Community Management API with retry + rate-limit awareness. An unofficial transport slot exists behind --transport=unofficial but ships as a typed ErrFeatureUnavailable no-op until wired; --transport=auto currently resolves through the official adapter and preserves unsupported responses until fallback behavior is implemented.
Capability matrix
| Command | Implemented | Live-tested | Notes | |---|---|---|---| | auth login | ✅ | via httptest | Native PKCE S256 with loopback callback | | auth status | ✅ | ✅ | Reports unauthenticated when token is missing or expired; shows refresh expiry when available | | auth logout | ✅ | ✅ | Clears the keyring entry | | auth refresh | ✅ | via httptest | Exchanges refresh token for new access token; auto-runs within 5 min of expiry under a single-flight sidecar lock | | profile me | ✅ | ✅ | Reads cached OIDC claims from the session | | post create | ✅ | httptest | --dry-run previews the exact payload | | post create --dry-run | ✅ | ✅ | Schema-validated envelope, no network | | post list | ✅ | httptest | Defaults author to the session member URN | | post get | ✅ | httptest | Entitlement-gated by LinkedIn | | post delete | ✅ | httptest | --dry-run supported | | comment add | ✅ | httptest | --dry-run supported | | comment list | ✅ | httptest | | | react add | ✅ | httptest | --dry-run supported | | react list | ✅ | httptest | | | search people | Official transport: unsupported | ✅ | Returns ErrFeatureUnavailable by design | | doctor | ✅ | ✅ | Env vars, session state, userinfo probe, feature map, audit log state | | version | ✅ | ✅ | Reports build metadata | | batch | ✅ | httptest | Reads JSONL ops file, dispatches each op, streams JSONL results; supports --fail-fast, --strict, --concurrency, --resume | | approval list | ✅ | unit | List staged approval entries | | approval show | ✅ | unit | Show a single staged entry | | approval grant | ✅ | unit | Approve a pending entry | | approval deny | ✅ | unit | Deny a pending entry | | approval run | ✅ | unit | Execute an approved entry via transport | | approval cancel | ✅ | unit | Remove a pending entry | | --require-approval | ✅ | unit | Stage supported mutating commands for review (exits 3) | | social metadata ... | ✅ | httptest | Batch engagement read: comment count, reaction totals per type, comments-state for up to 100 post URNs in one call | | post schedule --at ... | ✅ | unit | Queue a post for later publication (client-side queue) | | schedule list / show / run / cancel / next | ✅ | unit | Manage the scheduled-post queue | | post edit | ✅ | httptest | Update commentary or visibility of an existing post | | post reshare | ✅ | httptest | Reshare an existing share with optional added commentary (requires Linkedin-Version >= 202209) | | post create --image | ✅ | httptest | Upload a local image and attach it to a new post | | plan post create / delete / edit / reshare / schedule | ✅ | unit | Generate a plan document without calling LinkedIn | | plan comment add | ✅ | unit | Generate a comment-add plan document | | plan react add | ✅ | unit | Generate a react-add plan document | | execute | ✅ | unit | Execute a golink.plan/v1 document via the normal Transport path | | org list | ✅ | httptest | List organizations where the session member is ADMINISTRATOR; accepts w_organization_social_feed and legacy w_organization_social | | post create --as-org | ✅ | httptest | Post as an organization; accepts w_organization_social_feed and legacy w_organization_social |
"httptest" means the code path is covered by an integration test against a local HTTP server that mimics the LinkedIn endpoint; a real request to api.linkedin.com requires your own developer app.
Organization posting
To post as a LinkedIn organization page you should request w_organization_social_feed on your LinkedIn app. golink also accepts the legacy w_organization_social scope for older apps. The session member must be an ADMINISTRATOR of the organization.
# List organizations you administer
golink --json org list
# Post as an org page
golink --json post create \
--text "Exciting news from Acme Corp" \
--as-org urn:li:organization:12345678
# Preview without sending
golink --json --dry-run post create \
--text "Draft post" \
--as-org urn:li:organization:12345678
# Plan for review, then execute
golink --json plan post create \
--text "Queued org post" \
--as-org urn:li:organization:12345678 > plan.json
golink execute plan.json
golink doctor reflects org list and post create --as-org availability based on whether an organization social write scope is in the session scopes.
Scheduling
LinkedIn has no native scheduled-post API. golink implements a client-side queue — scheduled posts are stored on disk and executed on demand:
# Queue a post for tomorrow 9am UTC
golink post schedule --at 2026-04-18T09:00:00Z --text "Good morning"
# Inspect the queue
golink schedule list
golink schedule next # prints the earliest pending scheduled_at
# Execute past-due entries (typically invoked from cron)
golink schedule run --limit 20
golink does NOT run a daemon. Operators invoke schedule run via cron, launchd, systemd-timer, or an agent loop. Queue location: $GOLINK_SCHEDULE_DIR (override), else $XDG_STATE_HOME/golink/schedule/. --image paths must be absolute because they resolve at run time. --require-approval is not supported on scheduled posts in this release.
Installation
go install github.com/mudrii/golink@v26.05.13
Requires Go 1.26.3+.
Homebrew (tap lives in this repo's Formula/ directory):
brew tap mudrii/golink https://github.com/mudrii/golink
brew install mudrii/golink/golink
For full product, dependency, and LinkedIn Developer app setup instructions, see [LINKEDIN_SETUP.md](./LINKEDIN_SETUP.md).
Quick start
# Set your LinkedIn app client ID
export GOLINK_CLIENT_ID=your_client_id
# Authenticate (opens the system browser, waits on loopback)
golink auth login
# Everything works in JSON mode for scripts and agents
golink --json auth status
golink --json auth refresh # manually exchange a refresh token for a new access token
golink --json profile me
golink --json post create --text "Hello from golink" --visibility PUBLIC
golink --json post list --count 5
Non-interactive / agent mode
Without a TTY, all interactive prompts are disabled and required inputs come from flags:
golink --json post create --text "Hello" --visibility PUBLIC
golink --json comment add urn:li:share:123 --text "nice"
golink --json react add urn:li:share:123 --type LIKE
golink --json post list --count 10
Use --dry-run to preview the exact request payload without sending it. Supported on every mutating command (post create/delete/edit/reshare/schedule, comment add, react add, execute).
Output modes
golink supports five output modes via --output:
| Flag | Mode | Description | |---|---|---| | (default) | text | Human-readable plain text | | --json or --output=json | json | Full JSON envelope, schema-validated | | --output=jsonl | jsonl | One JSON object per line; list commands emit one item per line, scalar commands emit a single envelope line unless the command is intentionally multi-event (for example auth login) | | --output=compact or --compact | compact | Stripped envelope (no command_id, generated_at, rate_limit); useful for LLM context budgets | | --output=table | table | Tabwriter-based columnar output for list commands; scalar commands fall back to text |
# Schema-validated JSON (agent contract — byte-for-byte stable)
golink --output=json post list --count 5
golink --json post list --count 5 # identical to above
# Compact — lower token cost for LLM pipelines
golink --compact auth status
golink --output=compact post list --count 5
# JSONL — one object per line, pipeable to jq / stream processors
golink --output=jsonl post list --count 10 | jq '.text'
# Table — human-readable columns
golink --output=table post list --count 10
golink --output=table react list urn:li:share:123
Schema validation: only --output=json (and legacy --json) is validated against schemas/golink-output.schema.json. Compact, JSONL, and table modes are lossy renderings of the same data.
Precedence: --compact > --output > --json > text default. Using both --compact and --output= is a validation error.
Transport modes
| Mode | Flag | Behavior | |---|---|---| | Official | --transport=official (default) | LinkedIn REST APIs with OAuth bearer token, retry on 429/5xx, rate-limit header parsing | | Unofficial | --transport=unofficial | Stub until a concrete adapter lands; requires --accept-unofficial-risk | | Auto | --transport=auto | Official first, best-effort fallback |
Configuration
| Source | Priority | |---|---| | CLI flags | Highest | | GOLINK_* env vars | | | ~/.config/golink/config.yaml | | | Defaults | Lowest |
Environment variables
| Variable | Required | Description | |---|---|---| | GOLINK_CLIENT_ID | Yes | LinkedIn app client ID (used for auth login and auth refresh) | | GOLINK_CLIENT_SECRET | Only when GOLINK_AUTH_FLOW=oauth2 | LinkedIn app client secret for standard OAuth 2.0 token exchange. Do not store it in the repo. | | GOLINK_AUTH_FLOW | No | pkce (default) uses LinkedIn native PKCE; oauth2 uses the standard confidential-client OAuth endpoint with GOLINK_CLIENT_SECRET and a fixed GOLINK_REDIRECT_PORT. | | GOLINK_AUTH_SCOPES | No | Override login scopes (space- or comma-separated). Use when LinkedIn rejects OpenID scopes for native PKCE. | | GOLINK_MEMBER_URN | No | Manual member URN fallback, e.g. urn:li:person:abc123, used when OIDC userinfo is unavailable. | | GOLINK_API_VERSION | No | Linkedin-Version header, e.g. 202604 | | GOLINK_REDIRECT_PORT | No | Preferred OAuth loopback port; 0 picks any free port | | GOLINK_JSON, GOLINK_TRANSPORT, GOLINK_OUTPUT | No | Preflight overrides for --json / --transport / --output | | GOLINK_IDEMPOTENCY_PATH | No | Override idempotency store path | | GOLINK_AUDIT, GOLINK_AUDIT_PATH | No | Disable audit or override path | | GOLINK_APPROVAL_DIR, GOLINK_SCHEDULE_DIR | No | Override approval / schedule store dirs | | GOLINK_REFRESH_LOCK_PATH | No | Override sidecar flock path for auto-refresh single-flight serialisation (default: $XDG_STATE_HOME/golink/session.refresh.lock) | | GOLINK_RECORD, GOLINK_REPLAY | No | Record/replay HTTP exchanges to a JSONL cassette |
Tokens are stored in the OS keyring — never on disk or in logs.
> Auto-refresh hard-fail. When a session is within 5 minutes of access-token > expiry, golink runs auth refresh inline before dispatching the user's > command. The refresh is serialised across processes via a sidecar flock > (GOLINK_REFRESH_LOCK_PATH, default $XDG_STATE_HOME/golink/session.refresh.lock). > If the lock cannot be acquired, or if persisting the refreshed session fails, > the command exits 4 with an auth error — prior versions warned and proceeded. > Because LinkedIn rotates refresh tokens, silent half-state is worse than an > upfront failure: re-run the command (or run golink auth refresh explicitly) > after resolving the underlying issue.
> Redirect URI gotcha. LinkedIn matches OAuth redirects on the exact > registered URL including the port. Register http://127.0.0.1:/callback > for whatever port GOLINK_REDIRECT_PORT resolves to; 0 only works if the > app is configured with every port you might fall through to (rare). Native > PKCE needs no client_secret; standard OAuth 2.0 requires > GOLINK_AUTH_FLOW=oauth2 and GOLINK_CLIENT_SECRET.
> Native PKCE scope gotcha. Some LinkedIn apps reject OpenID scopes > (openid profile email) on the native PKCE endpoint. For those apps, set > GOLINK_AUTH_SCOPES="w_member_social r_profile_basicinfo". golink will > try GET /v2/me?fields=id as a non-OIDC profile fallback. If that endpoint > is unavailable for your app, also provide > GOLINK_MEMBER_URN=urn:li:person: so golink can still build post author > URNs.
> Standard OAuth 2.0 fallback. If your LinkedIn app shows > Not enough permissions to access Native PKCE protocol, use the confidential > OAuth flow instead: > > ``sh > export GOLINK_AUTH_FLOW=oauth2 > export GOLINK_CLIENT_SECRET= > export GOLINK_AUTH_SCOPES="w_member_social r_profile_basicinfo" > export GOLINK_REDIRECT_PORT=8080 > ` > > Add http://127.0.0.1:8080/callback to the app's Authorized redirect URLs. > LinkedIn requires an exact match: same scheme (http), host (127.0.0.1), > port (8080), and path (/callback). localhost and 127.0.0.1` are not > interchangeable for this check.
Idempotency keys
Mutating commands that dispatch a single operation (post create, post delete, post edit, post reshare, post schedule, comment add, react add, and execute) accept --idempotency-key . On the first successful call the result is cached locally; subsequent calls with the same key within 24 hours replay the cached result and set from_cache: true in the envelope — the transport is never called again.
golink --json post create --text "Hello" --idempotency-key my-post-1
# Second call returns from_cache:true, no network request
golink --json post create --text "Hello" --idempotency-key my-post-1
Store location (first match wins):
GOLINK_IDEMPOTENCY_PATH=/custom/path.jsonl$XDG_STATE_HOME/golink/idempotency.jsonl~/.local/state/golink/idempotency.jsonl
The store is an append-only JSONL file created with mode 0600 in a 0700 directory. Keys expire after 24 hours. Using a key with a different command than it was first recorded against returns a validation_error.
Batch operations
golink batch reads a JSONL file where each line is an operation:
{"command":"post create","args":{"text":"Hello batch","visibility":"PUBLIC"},"idempotency_key":"b-1"}
{"command":"post schedule","args":{"at":"2026-05-06T09:00:00Z","text":"Queued post","visibility":"PUBLIC"}}
{"command":"post delete","args":{"post_urn":"urn:li:share:123"}}
{"command":"comment add","args":{"post_urn":"urn:li:share:456","text":"nice"},"dry_run":true}
{"command":"react add","args":{"post_urn":"urn:li:share:789","type":"LIKE"},"idempotency_key":"r-1"}
Results stream to stdout as JSONL — one BatchOpResultOutput envelope per input line.
Supported commands: post create, post schedule, post delete, comment add, react add.
# Basic run
golink --json batch ops.jsonl
# Stop on first error
golink --json batch --fail-fast ops.jsonl
# Exit 2 if any op is non-ok (CI gate)
golink --json batch --strict ops.jsonl
# Up to 4 parallel workers
golink --json batch --concurrency 4 ops.jsonl
# Skip ops already recorded in ops.jsonl.progress
golink --json batch --resume ops.jsonl
# Read from stdin
cat ops.jsonl | golink --json batch -
Flags:
| Flag | Default | Description | |---|---|---| | --fail-fast | false | Stop on the first op error (exit 5) | | --continue-on-error | true | Continue after op errors | | --concurrency | 1 | Parallel workers (max 4) | | --strict | false | Exit 2 if any op is non-ok | | `-
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: mudrii
- Source: mudrii/golink
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.