Skip to main content

Agents (MCP)

joe-links exposes a Model Context Protocol server at /mcp, so AI agents can create, look up, and share go-links conversationally. It runs inside the joe-links binary (Streamable HTTP transport, stateless), authenticates with the same personal access tokens as the REST API, and enforces the same authorization rules — tools delegate to the same code paths as /api/v1.

Governing design artifacts: ADR-0018 and SPEC-0018 (Agent Access via MCP).

Connect

Claude Code (one command):

claude mcp add --transport http joe-links https://go.example.com/mcp \
--header "Authorization: Bearer jl_your_token_here"

Claude Desktop / other clients: add a remote MCP server pointing at https://go.example.com/mcp with an Authorization: Bearer jl_… header. Clients that only speak stdio can bridge with mcp-remote.

Tokens are minted in the web UI under API Tokens (/dashboard/settings/tokens) and can be revoked there at any time — revocation takes effect on the agent's next call.

Agents work best with their own identity: sign an agent account in once via your OIDC provider, mint it a PAT, and give that token to your agents. Links the agent creates are honestly owned by the agent, and handing one to a human is an explicit share_with — which is exactly the flow the tools are optimized for. Running agents on your own PAT also works; sharing to yourself is then unnecessary.

The share-with-me workflow

The headline use case is one tool call:

{
"name": "create_link",
"arguments": {
"slug": "retro-notes",
"url": "https://docs.example.com/retro-2026-07",
"title": "July retro notes",
"share_with": ["joe@example.com"]
}
}

This creates the link with secure visibility, grants joe@example.com access, and returns the working short URL (https://go.example.com/retro-notes) — all atomically. If any share_with email has no joe-links account yet, nothing is created and the error names the emails.

Defaults that differ from the REST API

Agent-created links default to private (REST defaults to public): agent output should never land in the public Browse page unless asked. Providing share_with without an explicit visibility upgrades the default to secure, because share grants only gate secure links. An explicit visibility always wins.

Tools

ToolPurposeKey inputs
create_linkCreate a link (returns short_url)slug, url, title, description, tags[], visibility, share_with[]
get_linkOne link incl. owners and (for owners) shareslink* (slug or id)
list_linksLinks visible to youq, tag, filter (mine|shared|public), limit
update_linkModify an owned link (omitted fields unchanged)link* + url/title/description/tags[]/visibility
delete_linkDelete an owned linklink*
share_linkGrant a user access to a secure linklink, email
unshare_linkRevoke a grantlink, email
add_co_ownerAdd a co-owner (can edit/delete)link, email
get_link_statsClick totals + recent clickslink*, limit
suggest_link_metadataLLM-suggested slug/title/description/tagsurl*
list_keywordsKeyword templates on this server

suggest_link_metadata only appears in tools/list when the server has an LLM provider configured. Admin operations are deliberately not exposed over MCP.

Error codes

Tool failures come back as MCP error results whose text content is machine-readable JSON: {"code": "...", "message": "..."}. Codes are stable:

CodeMeaning
validation_failedBad slug format, reserved slug, invalid URL/visibility, over-length text
duplicate_slugSlug already exists — pick another
not_foundNo link with that slug or id
forbiddenYou lack access (not an owner/recipient/admin for this operation)
unknown_userA share_with/email has no joe-links account (users must sign in once first)
duplicate_share / duplicate_ownerGrant or ownership already exists
llm_errorThe configured LLM provider failed
internal_errorServer-side failure — check server logs

Authentication failures are HTTP-level: 401 with a WWW-Authenticate: Bearer challenge (missing, invalid, expired, or revoked token).

Authorization model

Identical to the REST API, verified by a cross-surface test matrix:

OperationOwner / co-ownerShare recipientAnyone elseAdmin
get_link
update_link / delete_link
get_link_stats
share_link / unshare_link / add_co_owner
list_links filter:publicpublic links only — never other users' private/secure

Operational notes

  • The endpoint is stateless: no session affinity, safe behind any proxy, survives server restarts mid-conversation.
  • Request bodies are capped at 1 MB; responses carry strict security headers.
  • Every tool call increments joelinks_mcp_tool_calls_total{tool,outcome} in the Prometheus registry (/metrics).
  • mcp is a reserved slug — no go-link can shadow the endpoint.

Troubleshooting

  • 401 on every call — token missing/revoked/expired, or the header isn't reaching the server (some proxies strip Authorization; Caddy passes it by default).
  • suggest_link_metadata missing from tools/list — the server has no JOE_LLM_* configuration; that's by design.
  • unknown_user when sharing — the recipient has never signed in to joe-links; have them log in once, then share.