Docs
MCP server
Connect Claude Code, Claude Desktop, Cursor or any MCP client to anymd.cc/mcp so your agents can read the public web, remember it and search it.
anymd runs a remote Model Context Protocol server. It is the main way agents use anymd as their web context layer: connect it once and your agent can read public web content as structured Markdown, recall everything it has read from your private library, and (with the right scopes) manage pages.
| Endpoint | https://anymd.cc/mcp |
| Transport | Streamable HTTP, stateless JSON responses |
| Protocol | 2026-07-28 (stateless) and 2025-11-25 and earlier |
| Auth | Authorization: Bearer amd_…, or OAuth 2.1 |
Authentication
API key. Simplest option. Create a key in Dashboard → API keys. The Convert + library (agents) preset is a good default for an assistant. Details: API keys & roles.
OAuth 2.1. For clients that support it, just add the URL and sign in when prompted. anymd supports Dynamic Client Registration and PKCE:
| Metadata | /.well-known/oauth-authorization-server |
| Authorize | /oauth/authorize |
| Token | /oauth/token |
| Registration | /oauth/register |
Either way, your agent only sees the tools its scopes allow. An OAuth client that requests no scopes gets convert, library:read and library:write (read the web, save to and search your own library); anything more, admin scopes above all, must be requested by name and approved on the consent screen. When a read is not saved, the result says so: saved: false with not_saved_reason (missing_scope means the connection lacks library:write). See API keys & roles.
Client setup
Run anymd mcp from the CLI to print these snippets with your key filled in.
Claude Code
claude mcp add --transport http anymd https://anymd.cc/mcp \
--header "Authorization: Bearer amd_…"
Claude Desktop
Add anymd as a custom connector: Settings → Connectors → Add custom connector, URL https://anymd.cc/mcp, then sign in through OAuth.
If you'd rather use an API key, bridge the remote server through mcp-remote in claude_desktop_config.json:
{
"mcpServers": {
"anymd": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://anymd.cc/mcp", "--header", "Authorization: Bearer amd_…"]
}
}
}
Cursor
~/.cursor/mcp.json (global) or .cursor/mcp.json (per project):
{
"mcpServers": {
"anymd": {
"url": "https://anymd.cc/mcp",
"headers": { "Authorization": "Bearer amd_…" }
}
}
}
Any other client
Any client that speaks Streamable HTTP can use the same shape: URL https://anymd.cc/mcp plus an Authorization header, or OAuth discovery from the metadata URL above.
Tools
Tools are listed only when the caller holds the scope they need.
Conversion and library
| Tool | Input | Needs |
|---|---|---|
read_url |
{ url, save?, fresh?, removeImages?, expandThread?, maxThreadPosts?, includeComments?, analyzeImages?, maxComments?, maxImages?, maxCredits? } |
convert (saving also needs library:write) |
convert_url |
Same input as read_url |
Same as read_url, kept under its original name for existing clients |
get_video_download |
{ id } |
convert |
search_library |
{ query, mode?, limit? } |
library:read |
search_social |
{ platform, query, cursor? } (platform may be all) |
convert (account required) |
get_document |
{ id } |
library:read |
list_documents |
{ limit?, domain?, tags?, before? } |
library:read |
list_tags |
{ limit? } |
library:read |
tag_document |
{ id, add?, remove?, set? } |
library:write |
delete_document |
{ id } |
library:write |
usage_summary |
{} |
usage:read |
search_social searches public posts on X, Facebook, Instagram, Threads, LinkedIn or all of them and returns normalized results with a per-platform status and a next_cursor; see Social search. read_url and convert_url are the same tool with the same input and output; new integrations should use read_url. mode is hybrid (default), bm25, fulltext or semantic. See Library & search.
tag_document adds and/or removes tags, or replaces them all with set (not combinable with add/remove; [] clears), and returns { id, tags }. Tags are lowercased and keep only letters, digits, - and _; a document holds at most 20, and an edit past that fails with too_many_tags instead of dropping tags. list_tags returns { items: [{ tag, count }] }, most used first. list_documents with tags keeps documents carrying every listed tag (whole-tag match, up to 10).
expandThread, includeComments and analyzeImages are opt-in deep reading flags that cost extra credits. Options the agent leaves out follow the account's saved deep reading defaults, otherwise they are off: connecting an agent never turns enrichment on by itself. maxThreadPosts defaults to 20 and is capped at 100; maxCredits defaults to 100 and is capped at 1,000; maxComments defaults to 100 and is capped at 1,000; maxImages defaults to 10 and is capped at 20. removeImages strips image/media URLs at no credit cost. X reads return the single requested post unless thread expansion is enabled. The response's reading_options shows each effective value and its source (request, preference or default). Social post adapters for Facebook, Instagram, Threads and LinkedIn require an account. The response includes credit_breakdown and enrichment coverage so partial provider, limit or timeout results are explicit; cached reads are free.
With downloadVideo: true (or the saved setting), a YouTube read_url also starts a background download of the lowest-quality video to the anymd CDN and returns video_download.id; the Markdown says the same. Call get_video_download with that id every 15–30 seconds until status is ready (cdn_url is the MP4) or failed. 20 credits are charged only when ready. See Sources → YouTube. With analyzeVideo: true (or the saved setting; it turns on the download too), the same job then gets an AI analysis of the video by google/gemini-3.8-flash (10 + 20 credits per started minute, charged when ready): keep calling get_video_download until analysis.status is ready (analysis.markdown) or failed.
Pages and posts
| Tool | Input | Needs |
|---|---|---|
list_blocks |
{} |
pages:read |
list_page_templates |
{} |
pages:read |
list_pages |
{} |
pages:read |
get_page |
{ id } |
pages:read |
create_page |
{ slug, title, description?, template? } |
pages:write |
apply_page_ops |
{ pageId, baseRevision, ops, idempotencyKey? } |
pages:write |
publish_page |
{ pageId, revision? } |
pages:publish |
unpublish_page |
{ pageId } |
pages:publish |
list_posts |
{} |
content:read |
upsert_post |
{ id?, slug?, title, markdown, excerpt?, tags? } |
content:write |
publish_post |
{ id, publish } |
content:publish |
| get_page_markdown | { pageId } | pages:read |
| list_page_revisions | { pageId } | pages:read |
| rotate_page_preview_token | { pageId } | pages:write |
| archive_page | { pageId } | pages:write |
| get_post | { id } (id or slug) | content:read |
| fork_post | { slug } (copy a bundled post into the CMS) | content:write |
| delete_post | { id } | content:write |
The ops format, revision rules and the safe editing loop are in the admin builder guide.
Your account
| Tool | Input | Needs |
|---|---|---|
list_traces |
{ cursor?, limit? } |
usage:read |
get_trace |
{ traceId } |
usage:read |
list_api_keys |
{} |
keys:manage |
create_api_key |
{ name, preset?, scopes?, expiresInDays? } |
keys:manage |
revoke_api_key |
{ keyId } |
keys:manage |
list_oauth_grants |
{} |
keys:manage |
revoke_my_oauth_grant |
{ grantId } |
keys:manage |
create_api_key returns the secret once; a new key never gets scopes the calling credential lacks. OAuth connections cannot create keys at all (oauth_key_creation_forbidden), because a key would outlive the grant: create keys with an API key, a session, or in the dashboard.
System administration
Owners and admins can run the deployment from an agent: users, credentials, credits, settings, opt-outs, billing health, audit history and system usage. Every admin tool is a thin adapter over the same services the REST API uses, so the rules are identical everywhere:
- Scoped. Each tool needs one granular scope and is hidden without it. Owner-only tools (
update_user_role,grant_credits,revoke_credit_grant) never appear for admins. - Ranked. Nobody can act on their own account; admins only act on accounts ranked below them.
- Bounded. Every list is paginated: pass
next_cursorback ascursor. There is no SQL or shell tool. - Safe to retry. Mutations accept an
idempotencyKey(required forgrant_credits); a retry returns the first result withreplayed: true, and a retry that arrives while the first attempt is still running getsidempotency_in_progress(retry shortly) instead of running the change twice. Changes guarded byexpectedRoleorexpectedVersionfail with a conflict instead of overwriting someone else's change. - Audited. Every change writes an audit row with the acting user, the API key or OAuth client, the tool (
via: mcp:<tool>), the request id and a minimal diff. Secrets are never recorded or returned. - Billing stays with the provider. Plans and subscriptions are read-only here. Use credit grants for support allowances: an active grant raises their allowance. By default a grant expires at the end of the current month (UTC), or at the end of next month when fewer than 7 days of this month remain; a grant without
recurringis a one-time pool: its credits are spent once over its whole lifetime, so whatever one month uses is gone the next (5,000 granted on the 26th with 3,000 used that month leaves at most 2,000 for next month). Passrecurring: true(with or withoutexpiresAt) for credits that renew in full every month; anexpiresAtlater than the default withoutrecurring: trueis422. On Pro and Scale, credits are used in this order: the plan's included credits, then grants, then paid overage. A grant covers usage from the moment it is granted onward and is not a refund: overage reported before it stays billed. Example: on a plan with 10,000 included credits, a 5,000-credit grant made when the account had used 12,000 this period keeps the next 5,000 credits (12,000 to 17,000) off the overage bill; the 2,000 used before the grant stay billed. Positions are counted within the current period of a monthly subscription, or the calendar month (UTC) otherwise.
Tool annotations tell the client what to expect: read tools are readOnlyHint, and tools that remove access or data (set_user_status, revoke_*, remove_site_optout, update_settings, delete_post, archive_page) are destructiveHint, so a well-behaved client asks before running them.
Read tools
| Tool | Input | Needs |
|---|---|---|
system_overview |
{} |
system:read |
list_system_usage |
{ days? (1–30, default 7), channel?, kind? } |
system:read |
list_system_traces |
{ sort?, status?, kind?, userId?, since? (at most 30 days ago), cursor?, limit? } |
system:read |
get_system_trace |
{ traceId } |
system:read |
list_users |
{ search?, role?, plan?, status?, createdAfter?, createdBefore?, lastLoginAfter?, lastLoginBefore?, cursor?, limit? } |
users:read |
get_user |
{ userId } |
users:read |
list_user_credentials |
{ userId } |
users:read |
list_roles |
{} |
users:read |
list_audit_events |
{ action?, actorUserId?, target?, targetType?, since?, until?, cursor?, limit? } |
audit:read |
list_subscriptions |
{ status?, plan?, userId?, cursor?, limit? } |
billing:read |
get_subscription |
{ subscriptionId } |
billing:read |
get_billing_diagnostics |
{} |
billing:read |
list_credit_grants |
{ userId?, state?, source?, cursor?, limit? } |
credits:read |
list_site_optouts |
{ search?, cursor?, limit? } |
optouts:read |
get_settings |
{} |
settings:read |
Timestamps accept epoch milliseconds or ISO 8601. action in list_audit_events takes an exact action or a prefix such as user.*.
Change tools
| Tool | Input | Needs |
|---|---|---|
update_user_role |
{ userId, role, expectedRole?, idempotencyKey? } |
users:roles:write (owner) |
set_user_status |
{ userId, status, reason, expectedStatus?, idempotencyKey? } |
users:sessions:write |
revoke_user_sessions |
{ userId, sessionHandle?, idempotencyKey? } |
users:sessions:write |
revoke_user_api_key |
{ userId, keyId, idempotencyKey? } |
users:credentials:write |
revoke_oauth_grant |
{ userId, grantId } |
users:credentials:write |
grant_credits |
{ userId, credits, reason, source?, expiresAt?, recurring?, idempotencyKey } |
credits:write (owner) |
revoke_credit_grant |
{ grantId, reason } |
credits:write (owner) |
add_site_optout |
{ domain, reason?, idempotencyKey? } |
optouts:write |
remove_site_optout |
{ domain, idempotencyKey? } |
optouts:write |
update_settings |
{ patch, expectedVersion?, idempotencyKey? } |
settings:write |
status is active or suspended. Suspending an account signs it out everywhere and stops its keys and OAuth grants until it is reactivated. Errors come back as tool results with structuredContent.error.code, for example role_conflict, settings_conflict, forbidden_rank, forbidden_self, idempotency_mismatch or billing_owned.
Set up an admin agent
Give the agent only what its job needs. Create a key from a preset (Dashboard → API keys, or create_api_key):
| Job | Preset |
|---|---|
| Monitoring and reports | admin-read-only |
| Support: sign users out, suspend, revoke credentials, grant credits (owner) | support |
| Site operations: settings and opt-outs | site-ops |
claude mcp add --transport http anymd-admin https://anymd.cc/mcp \
--header "Authorization: Bearer amd_…"
Over OAuth, the client asks for the scopes it needs. When it calls a tool whose scope the grant lacks but your role allows, anymd answers 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="…" listing the current scopes plus the missing one, so the client can re-authorize (step up). With an API key the same call returns a tool error with code insufficient_scope; create a key that includes the scope. When your role does not allow the scope at all, the code is forbidden.
Things to ask an admin agent:
- "How is the system doing this week? Show error rate, the slowest traces and the top errors."
- "Find the account for jane@example.com, show its keys and sessions, and sign it out everywhere."
- "Grant 500 credits to that user for the outage, reason 'October incident', for this month only, and tell me the grant id."
- "Is billing healthy? Check webhook failures and any users whose plan doesn't match their subscription."
- "Show every settings change in the last 30 days and who made it."
Things to ask your agent
- "Read https://stephango.com/saw and give me the three key ideas."
- "Before you read anything new, check my library for what we already know about this vendor."
- "Search my library for what I saved about vector databases last month."
- "Read this YouTube video's transcript and list the timestamps where pricing comes up."
- "Create a landing page from the
ads-landingtemplate for our research audience, show me the preview link, and don't publish yet."
Credits
read_url (and convert_url) costs the same credits as any other conversion; cached reads are free. search_social costs 10 credits for each page that returns results (100 on LinkedIn; all costs the sum for the platforms that returned results); empty pages are free. Reads such as search_library, get_document and list_documents are free. See Billing & credits.
Protocol versions
One endpoint serves both protocol eras, and existing clients need no change.
- 2026-07-28 (stateless). No
initialize. Each request names its version inparams._meta["io.modelcontextprotocol/protocolVersion"]and mirrors it in theMCP-Protocol-Versionheader, withMcp-Method(andMcp-Namefortools/call; non-ASCII values as=?base64?…?=). A header that disagrees with the body gets400with error-32020; an unknown version gets400with-32022and the supported list.server/discoverdescribes the server; results carryresultType, and lists carryttlMsandcacheScope: "private". One message per request: no batches, noping. - 2025-11-25 and earlier.
initializeandpingwork as before. JSON-RPC batches (up to 20 messages; each message counts toward the rate limits) work for clients on 2025-03-26 or earlier, or that send noMCP-Protocol-Versionheader; a batch sent with2025-06-18or later gets400with-32600, since those versions dropped batching. A message that is not a JSON object gets-32600without counting toward the limits.
Rate limits
MCP has its own limits, separate from the REST API, counted per user and per credential (each API key or OAuth client has its own budget): one for every request and a stricter one for tools that change data. Changes also count against one budget for the whole account, so more keys or clients do not multiply it. Over the limit you get HTTP 429 with Retry-After and JSON-RPC error -32005 (data.bucket is request or mutation; a batch gets an array with one error per request). Wait and retry.
Troubleshooting
| Symptom | Fix |
|---|---|
| A tool is missing | Your key or OAuth grant lacks its scope. Check with GET /api/v1/me or anymd whoami. |
401 invalid_api_key |
The key was revoked, expired or mistyped. Create a new one. |
402 quota_exceeded |
Free plan credits are used up for the month. |
revision_conflict from apply_page_ops |
Someone changed the page. Call get_page, then retry with the new revision. |
| Admin tools are missing over OAuth | Grants approved before least-privilege consent lose admin scopes. Reconnect and approve the admin scopes. |
403 with insufficient_scope |
The OAuth grant lacks the tool's scope. Re-authorize with the scopes in the WWW-Authenticate header. |
429 / -32005 |
MCP rate limit. Wait for Retry-After seconds. |
-32020 or -32022 |
A 2026-07-28 client sent headers that don't match the body, or an unsupported version. |
Updated 2026-10-10 · Edit on GitHub