Agents
MCP server
Connect ChatGPT, Claude, or any remote MCP client over OAuth. Four tools cover reading, creating, changing, and deleting your Bidama resources.
Connecting a client
Bidama runs a remote Model Context Protocol server on the same host as the website. Add it to your client as a custom connector with this URL:
https://<your-host>/mcp
Your client finds the sign-in details on its own, opens a Bidama page in your browser, and asks you to approve access. After that it calls the server with an OAuth access token. API keys from the API keys page don't work here, and MCP tokens don't work on the REST API.
| What | Where | Notes |
|---|---|---|
| MCP endpoint | POST /mcp |
Send Authorization: Bearer bdmo_at_... |
| OAuth discovery | /.well-known/oauth-protected-resource/mcp |
Authorization code flow with PKCE |
The approval page lists what the client asked for: read, write, and delete. You approve all of them or none. If the client also asked for offline_access, you can choose whether it gets a refresh token, which lets it stay connected after its one-hour access token expires. To cut a client off, revoke it on the Connected apps page.
If you're building a client that registers itself, note that a client registered without a scope may only ask for read. Register read write delete (and offline_access with the refresh_token grant, if you want refresh tokens) to be able to request them later.
Tools
The server has four tools. Each one takes a field that picks what you want to do (operation or type), and accepts only the fields that make sense for that choice.
| Tool | Picks with | Scope | readOnly |
destructive |
idempotent |
What it does |
|---|---|---|---|---|---|---|
bidama_read |
operation (11) |
read |
true | false | true | Look at your account, resources, analytics, Markdown history, jobs, upload requests, and cached Notion pages. |
bidama_create |
type (8) |
write |
false | false | false | Create any kind of resource, including file uploads and HTML artifacts. |
bidama_change |
operation (13) |
write, some delete |
false | true | false | Edit, restore, redeploy, optimize, and refresh existing resources, and manage upload requests. |
bidama_delete |
none | delete |
false | true | true | Delete one of your resources by slug. |
The readOnly, destructive, and idempotent columns are the hints your client sees. They help it decide when to ask you before running a tool. They don't grant anything; the server checks the token's scopes on every call. bidama_create and bidama_change are also marked as reaching outside Bidama, because creating or refreshing a Notion mirror fetches from Notion.
bidama_read
Needs the read scope.
operation |
Fields | Returns |
|---|---|---|
get_account | none | Who you are, which resource types you can create, storage used and available, and which features are on. Worth calling before a large upload. |
list_resources | q?, type?, label?, sort?, archived?, pinned?, limit?, cursor? | A page of short rows, 20 by default and up to 100, with next_cursor when there are more. |
get_resource | slug, content? | The full editable state of one resource and its etag. Markdown comes back as a short snippet unless you pass content: "full". |
get_analytics | slug, days?, top? | Views, visitors, a daily series, and the top entries per breakdown. days defaults to 30 (0 means all time, up to 365); top defaults to 5 (up to 10). |
simulate_link | slug? or target_url?, plus platform?, user_agent?, referrer?, variant_draw?, and more | Which destination a visitor would be sent to, given the link's redirect rules and A/B variants. Nothing is recorded. |
list_markdown_versions | slug | Earlier versions of a Markdown page, newest first. |
get_markdown_version | slug, version_id, content? | The full source of one version. |
get_job | job_id | A background job's status, result or error, and how many seconds to wait before checking again. |
list_upload_requests | none | Your upload requests, without their secret tokens. |
get_upload_request | upload_request_id | One upload request and its status, without its token. |
get_notion_page | slug, page_id | One cached Notion page and its rendered HTML. |
typeis one oflink,markdown,file,image,collection,notion,bio, orartifact.sortis one ofnewest,pinned,title,expiring,most_viewed, orrecently_updated.archivedisfalse(the default),true, orall.- Pass
next_cursorback exactly as you got it.
bidama_create
Needs the write scope. type decides which fields apply:
type |
Required | Optional |
|---|---|---|
link | target_url | max_views, redirect_rules, test_variants, fallback_url, webhook_clicks |
markdown | raw_markdown | max_views |
file | file_name, file_content | content_type, max_views |
image | file_name, file_content | content_type |
collection | none | none |
notion | source_url | root_page_id |
bio | none | display_name, headline, bio, theme |
artifact | files or archive_base64 | visibility |
- Any type can also take
slug,title,description,starts_at,expires_at,password, andlabels. Leave outslugto get a random one. Everything except artifacts is created as public; artifacts take an optionalvisibility. file_contentis base64. Bidama works out the file type from the bytes, the same way the REST upload does, and ignorescontent_type. Animagemust really be an image, afilemust not be one, and file types that REST wouldn't accept are refused withunsupported_media.- For an artifact, send either
filesorarchive_base64, never both.filesmust be a real JSON array of{path, content, encoding?}objects, not a string containing JSON.archive_base64is a base64 ZIP of the whole site and is often easier for larger sites. The server also offers a short guide atbidama://guides/artifacts.md. - Inline content (a file, a
filesarray, or a ZIP) can be up to 8 MiB once decoded, and afilesarray can have up to 500 entries. For anything bigger, use the REST API upload routes. - The artifact's entry page is
index.htmlat the root. Without one, the first.htmlor.htmfile in the order you sent them is used. Either way it can't be empty. - The result has the new
slug,url, andetag. Artifacts also returndeployment_id,origin_url, andentry_path.
bidama_change
Most operations need the write scope. Deleting a collection item and revoking an upload request need delete.
operation |
Fields | What it does |
|---|---|---|
update_resource | slug, if_match?, and at least one of title, description, new_slug, target_url, redirect_rules, test_variants, fallback_url, webhook_clicks, raw_markdown, starts_at, expires_at, max_views, password, labels, archived, pinned, original_filename, notion_auto_refresh, notion_refresh_ttl_seconds | Changes only the fields you send. An empty starts_at, expires_at, fallback_url, or password removes it. |
restore_markdown_version | slug, version_id, if_match? | Makes an earlier version current. The text it replaces is kept as a version, so you can undo it. |
redeploy_artifact | slug, if_match?, and files or archive_base64 | Publishes a new version of an artifact. Same content rules and limits as creating one. |
optimize_original | slug | Queues a job that shrinks a large JPEG or converts HEIC or HEIF to JPEG. The stored original is replaced and can't be recovered. |
update_bio | slug, pages, if_match?, display_name?, headline?, bio?, theme? | Replaces the whole bio page. Profile fields you leave out are cleared. Existing image and file blocks keep their files. |
manage_collection_items | slug, action (append, update, or delete), if_match?, then source_resource_id, items, or item_id depending on the action | Adds, edits, or removes collection items. source_resource_id is the ID of one of your files or images. For if_match, use collection.etag from get_resource. |
refresh_notion | slug, mode? (full or page), page_id? | Queues a refresh of the whole Notion mirror or one page. |
retry_job | job_id | Runs a failed or skipped job again as a new job. |
create_upload_request | title, max_files, max_file_bytes, description?, password?, expires_at?, labels? | Creates an upload request and returns its secret token and upload link. |
update_upload_request | upload_request_id and at least one of title, description, password, expires_at, max_files, max_file_bytes, labels | Changes the fields you send. An empty password removes the password. |
set_upload_request_status | upload_request_id, status (open, paused, or completed) | Opens, pauses, or closes an upload request. |
rotate_upload_request_token | upload_request_id | Replaces the token. The old link stops working. |
revoke_upload_request | upload_request_id | Shuts the upload request down for good. Needs delete. |
slugalways names the resource as it is now. To rename it, put the new name innew_slug.- Only
create_upload_requestandrotate_upload_request_tokenreturn the secret token and upload link. Save them right away; no other call shows them again. optimize_original,refresh_notion, andretry_jobreturn ajobwith itsjob_idandnext_poll_seconds. Wait that long, then check withbidama_readandget_job. If you getjob_already_active, keep polling the job you already have instead of starting another.
bidama_delete
Needs the delete scope and takes a slug. The resource disappears from your lists and its public link stops working. A successful call returns {"slug": "...", "deleted": true}. Calling it again for the same slug returns not_found, which is why the tool counts as idempotent.
Examples
Create a Markdown page:
bidama_create
{
"type": "markdown",
"title": "Release notes",
"raw_markdown": "# Hello\n\nShipped today."
}
Publish a one-page site. Note that files is an array, not a string:
bidama_create
{
"type": "artifact",
"title": "Demo",
"files": [
{"path": "index.html", "content": "<h1>Hi</h1>"}
]
}
Rename the page, using the etag from get_resource so you don't overwrite someone else's edit:
bidama_change
{
"operation": "update_resource",
"slug": "release-notes",
"if_match": "<etag from get_resource>",
"title": "Release notes (final)"
}
Check on a job that optimize_original started:
bidama_read
{
"operation": "get_job",
"job_id": "<job_id from optimize_original>"
}
Good to know
- Slugs are 3 to 64 characters long and use lowercase letters, digits, hyphens, and underscores.
- Pass the
etagyou last saw asif_matchwhen you change something. If the resource changed in the meantime you getetag_mismatch, and nothing is overwritten. - Every tool returns structured output, plus the same data as compact JSON text for clients that don't read structured output.
- Each server accepts up to four MCP requests at a time, and two per access token. A request body has 60 seconds to arrive. Past those limits you get
429withRetry-After. - The server is stateless and answers with plain JSON. It doesn't keep MCP sessions, so
GETandDELETEon/mcpreturn405, and batched JSON-RPC requests are rejected. - MCP requests count toward the same per-minute rate limit as the REST API, per access token.
- The full REST API contract is available to agents as an MCP resource (
text/yaml):bidama://openapi.yaml
Errors
A tool call that fails comes back as an error result whose text carries the same details as the REST API: the HTTP status, error_code, the message, the field at fault where there is one, and a retry delay when relevant. For example:
HTTP 409 slug_taken: Slug is already taken. Hint: Choose a different slug or omit it to auto-generate one.
Problems with the request itself are reported before any tool runs. A token without the scope a tool needs gets 403 insufficient_scope with a WWW-Authenticate header naming the scope. The exception is bidama_change, which accepts any token with write or delete and then reports a missing scope for the specific operation as an error result. Rate limits return 429 with Retry-After.
These codes come with a hint:
error_code |
What to do |
|---|---|
slug_taken | Choose a different slug or omit it to auto-generate one. |
etag_mismatch | Re-read the resource and retry with the fresh etag. |
insufficient_scope | The OAuth token lacks the required read, write, or delete scope. |
rate_limited | Wait before retrying. |
quota_exceeded | Storage quota is full; inspect the account to check current usage. |
job_already_active | Reuse the job_id from the earlier accepted call; if unavailable, wait before retrying. |
validation_failed | Correct the named field and retry. |
artifact_index_missing | Include a non-empty root index.html or at least one non-empty .html file. |
artifacts_disabled, feature_disabled | This feature is disabled for the deployment or account; do not retry. |
not_found | Check the slug and ownership; deleted or expired resources also appear not found. |
What MCP doesn't do
- There's no way to list all jobs. Poll the job IDs your own calls return.
- QR codes aren't returned as images. Use the resource's QR URL instead.
- Bio avatar, cover, and block uploads, link preview (OpenGraph) overrides, bulk link import, webhooks, analytics exports, and the artifact file browser are only in the REST API.
- There's no generic tool that forwards arbitrary REST calls.
See also: REST API · OpenAPI document