vee.kr

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_accountnoneWho you are, which resource types you can create, storage used and available, and which features are on. Worth calling before a large upload.
list_resourcesq?, 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_resourceslug, content?The full editable state of one resource and its etag. Markdown comes back as a short snippet unless you pass content: "full".
get_analyticsslug, 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_linkslug? or target_url?, plus platform?, user_agent?, referrer?, variant_draw?, and moreWhich destination a visitor would be sent to, given the link's redirect rules and A/B variants. Nothing is recorded.
list_markdown_versionsslugEarlier versions of a Markdown page, newest first.
get_markdown_versionslug, version_id, content?The full source of one version.
get_jobjob_idA background job's status, result or error, and how many seconds to wait before checking again.
list_upload_requestsnoneYour upload requests, without their secret tokens.
get_upload_requestupload_request_idOne upload request and its status, without its token.
get_notion_pageslug, page_idOne cached Notion page and its rendered HTML.
  • type is one of link, markdown, file, image, collection, notion, bio, or artifact.
  • sort is one of newest, pinned, title, expiring, most_viewed, or recently_updated. archived is false (the default), true, or all.
  • Pass next_cursor back exactly as you got it.

bidama_create

Needs the write scope. type decides which fields apply:

type Required Optional
linktarget_urlmax_views, redirect_rules, test_variants, fallback_url, webhook_clicks
markdownraw_markdownmax_views
filefile_name, file_contentcontent_type, max_views
imagefile_name, file_contentcontent_type
collectionnonenone
notionsource_urlroot_page_id
biononedisplay_name, headline, bio, theme
artifactfiles or archive_base64visibility
  • Any type can also take slug, title, description, starts_at, expires_at, password, and labels. Leave out slug to get a random one. Everything except artifacts is created as public; artifacts take an optional visibility.
  • file_content is base64. Bidama works out the file type from the bytes, the same way the REST upload does, and ignores content_type. An image must really be an image, a file must not be one, and file types that REST wouldn't accept are refused with unsupported_media.
  • For an artifact, send either files or archive_base64, never both. files must be a real JSON array of {path, content, encoding?} objects, not a string containing JSON. archive_base64 is a base64 ZIP of the whole site and is often easier for larger sites. The server also offers a short guide at bidama://guides/artifacts.md.
  • Inline content (a file, a files array, or a ZIP) can be up to 8 MiB once decoded, and a files array can have up to 500 entries. For anything bigger, use the REST API upload routes.
  • The artifact's entry page is index.html at the root. Without one, the first .html or .htm file in the order you sent them is used. Either way it can't be empty.
  • The result has the new slug, url, and etag. Artifacts also return deployment_id, origin_url, and entry_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_resourceslug, 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_secondsChanges only the fields you send. An empty starts_at, expires_at, fallback_url, or password removes it.
restore_markdown_versionslug, version_id, if_match?Makes an earlier version current. The text it replaces is kept as a version, so you can undo it.
redeploy_artifactslug, if_match?, and files or archive_base64Publishes a new version of an artifact. Same content rules and limits as creating one.
optimize_originalslugQueues 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_bioslug, 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_itemsslug, action (append, update, or delete), if_match?, then source_resource_id, items, or item_id depending on the actionAdds, 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_notionslug, mode? (full or page), page_id?Queues a refresh of the whole Notion mirror or one page.
retry_jobjob_idRuns a failed or skipped job again as a new job.
create_upload_requesttitle, max_files, max_file_bytes, description?, password?, expires_at?, labels?Creates an upload request and returns its secret token and upload link.
update_upload_requestupload_request_id and at least one of title, description, password, expires_at, max_files, max_file_bytes, labelsChanges the fields you send. An empty password removes the password.
set_upload_request_statusupload_request_id, status (open, paused, or completed)Opens, pauses, or closes an upload request.
rotate_upload_request_tokenupload_request_idReplaces the token. The old link stops working.
revoke_upload_requestupload_request_idShuts the upload request down for good. Needs delete.
  • slug always names the resource as it is now. To rename it, put the new name in new_slug.
  • Only create_upload_request and rotate_upload_request_token return the secret token and upload link. Save them right away; no other call shows them again.
  • optimize_original, refresh_notion, and retry_job return a job with its job_id and next_poll_seconds. Wait that long, then check with bidama_read and get_job. If you get job_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 etag you last saw as if_match when you change something. If the resource changed in the meantime you get etag_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 429 with Retry-After.
  • The server is stateless and answers with plain JSON. It doesn't keep MCP sessions, so GET and DELETE on /mcp return 405, 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_takenChoose a different slug or omit it to auto-generate one.
etag_mismatchRe-read the resource and retry with the fresh etag.
insufficient_scopeThe OAuth token lacks the required read, write, or delete scope.
rate_limitedWait before retrying.
quota_exceededStorage quota is full; inspect the account to check current usage.
job_already_activeReuse the job_id from the earlier accepted call; if unavailable, wait before retrying.
validation_failedCorrect the named field and retry.
artifact_index_missingInclude a non-empty root index.html or at least one non-empty .html file.
artifacts_disabled, feature_disabledThis feature is disabled for the deployment or account; do not retry.
not_foundCheck 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