MCP server

MCP server

Upload, attach, and search files from any MCP client.

Connect #

The hosted server gives an agent upload tools without a local shell or checkout. With it an agent can:

The server works with any MCP client that supports remote servers over HTTP. Add it to yours:

claude mcp add --transport http uploads https://agents.uploads.sh/mcp

The client opens a browser to sign in on first use.

Have the CLI? uploads install registers the server with Claude Code, Codex, and Grok if they are installed.

Using claude.ai or Cowork? Add uploads from the Claude directory instead.

Authentication #

The server accepts two kinds of bearer token.

  • OAuth (recommended). Clients that support MCP authorization find the authorization server on their own. You sign in at uploads.sh and choose one workspace for the grant.
  • Workspace token. Send the up_<workspace>_… token that uploads login creates, or a ups_<workspace>_… service token that a workspace admin creates for CI. The server reads the workspace from the token.

Send the token in the Authorization header:

Authorization: Bearer up_<workspace>_…

Most tools need one scope. The tool reference shows it next to each tool.

Scope Grants
files:read List, search, and read files, metadata, usage
files:write Upload, promote, set metadata, galleries, feeds
files:delete Delete files and purge expired ones

Client developers: discovery, client registration, and token details are in auth.md.

Stage, then promote #

An agent usually takes screenshots before the pull request exists. The hosted server has no git checkout, so pass repo and branch to put to stage each file for that branch:

put arguments
{
"contentUrl": "https://cdn.example/settings-after.png",
"repo": "acme/web",
"branch": "feat/settings",
"state": "after"
}

After the agent opens the PR, call promote with the same branch. It copies the staged files into the PR and updates the attachments comment:

promote arguments
{ "repo": "acme/web", "pr": 12, "branch": "feat/settings" }

PR already open? Pass pr and repo to put to attach in one call.

If the GitHub App is installed, it promotes staged files when the PR opens, and promote is safe to call again.

Hosted vs. local #

The CLI also runs an MCP server on your machine with uploads mcp. It reads local files and fills in repo and branch from git. Most tools are the same on both servers.

Server Only this server has
Hosted promote, list_activity, list_repo_files, repo_link_status
Local attach, staged, screenshot, doctor, report

To register the local server instead, see Skill vs. MCP.

Tools #

Every tool on the hosted server, as tools/list returns it. Tools marked “Hosted only” are not on the local server.

Upload and attach

putUpload file

Can overwrite or deletefiles:write

Upload a file (or files) and get a public URL plus GitHub-ready markdown. Prefer the returned embedUrl in GitHub markdown. All uploads are public. Pass pr+repo to attach to a PR (stable key + managed comment). Pass branch+repo to stage for a PR that does not exist yet. When the bytes are already at a public HTTPS URL, pass contentUrl instead of base64 — the server fetches them. Accepts images (PNG, JPEG, GIF, WebP, AVIF), video (MP4, WebM, MOV), PDF, zip, gzip, and text (plain, markdown, CSV, JSON). SVG and XML are accepted only on storage lanes verified to serve them sandboxed (see the workspace's storage settings); HTML is rejected.

Returns workspacekeyurlembedUrlsizecontentTypereplacedmarkdownprovenancemetadatavisibilityfileuploadsfailurescommentcommentErrorpromotionpromoteErroroptimizeframedryRunhint

ParameterTypeDescription
contentBase64stringBase64-encoded file content to upload (must be non-empty). Exactly one of contentBase64, contentUrl, or files is required.
contentUrlstringPublic HTTPS URL of an already-hosted file to fetch and re-host. Filename is optional when the URL path has a leaf. No auth headers are forwarded; private/internal hosts are rejected. Exactly one of contentBase64, contentUrl, or files is required.
filenamestringFilename for the content (drives the key and content type). Required with contentBase64; optional with contentUrl when the URL path has a leaf.
filesobject[]Multiple files in one call (max 20). Each item needs contentBase64 or contentUrl. Filename is optional with contentUrl when the URL path has a leaf. Cannot combine with contentBase64, contentUrl, filename, or key.
keystringOverride the object key. Cannot combine with pr/issue/branch.
prefixstringKey prefix for the default key layout (default: screenshots).
repostringGitHub owner/name. Required with pr/issue/branch. Optional otherwise (default: misc).
refstringOptional key segment (default: today). Cannot combine with pr/issue/branch.
prnumberAttach to this pull request (stable gh/ key). Mutually exclusive with issue. Requires repo. With branch, also promotes that branch's staged files.
issuenumberAttach to this issue (stable gh/ key). Mutually exclusive with pr and branch. Requires repo.
branchstringGit branch to stage under (no comment yet). With pr, also promote this branch's staged files into the PR. Requires repo.
commentbooleanWith pr or issue: create or update the managed attachments comment after upload. Default true. Pass false to skip. A comment failure never fails the upload.
altstringAlt text for the markdown (default: filename).
widthnumberEmit <img width=…> markdown instead of a plain image embed.
metadataobjectQueryable tags for later search (key→value). Use path for the route (e.g. /settings), not route/page/screen. Omit to leave existing tags; pass an object (even {}) to replace them. state and app have their own fields. On pr/issue/branch, gh.* is set for you.
state"before" | "after" | "empty" | "error" | "loading"The UI state this image shows. Set it whenever the image is one side of a comparison — before/after is the most useful pair in a PR, and is what makes find_files with state=after work later.
appstringWhich surface is shown: web, ios, android, cli. Worth setting only when the same route exists on more than one surface.
replacebooleanOverwrite an existing object on a non-gh/ key. Default false. No effect on gh/ keys, which always overwrite.

promotePromote staged attachments

Can overwrite or deletefiles:writeHosted only

Copy staged or existing files into a PR's stable gh/…/pull/… prefix, then refresh the managed comment. Pass branch and/or keys. Requires repo and pr.

Returns promotionattachedattachFailurescommentcommentError

ParameterTypeDescription
reporequiredstringGitHub owner/name. Required — this server cannot infer it from git.
prrequirednumberPull request number to promote/attach into. Never applies to issues.
branchstringStaged branch to copy into the PR.
keysstring[]Already-uploaded object keys or uploads.sh URLs to copy into the PR. One bad source does not abort the rest.
movebooleanFor keys only: delete each source after a successful copy. Default false. Ignored for the branch sweep.
commentbooleanRefresh the managed attachments comment after a successful copy. Default true. A comment failure never fails the copy.

commentSync attachments comment

Can overwrite or deletefiles:read

Refresh the managed attachments comment on a GitHub PR or issue without re-uploading. Use after deleting media. Requires repo and exactly one of pr/issue.

Returns postedreasonmessagefixUrlrequiredactioncountcommentUrl

ParameterTypeDescription
reporequiredstringGitHub owner/name. Required — this server cannot infer it from git.
prnumberPull request number. Mutually exclusive with issue.
issuenumberIssue number. Mutually exclusive with pr.

Files and metadata

listList files

Read-onlyfiles:read

List uploaded objects in the workspace, optionally filtered by key prefix. Paginate with cursor; each item includes its public URL when the workspace has one.

Returns itemscursorprefixes

ParameterTypeDescription
prefixstringKey prefix filter.
limitnumberPage size (default 100, max 1000).
cursorstringPagination cursor from a previous call.

find_filesFind files

Read-onlyfiles:read

Search files by metadata (filters) and/or filename substring (name). At least one is required.

Returns itemscursortruncated

ParameterTypeDescription
filtersobjectEquality filters; all must match. Use path for the route (e.g. /settings), not route/page/screen. Optional when name is set.
namestringCase-insensitive substring match on object keys (1–128 chars). Optional when filters is non-empty.
prefixstringKey prefix filter, combinable with filters/name.
limitnumberPage size (default 50, max 500).
cursorstringOpaque continuation from a previous call's cursor. Pass it back unchanged with the same filters/name to get the next page; a null cursor means there are no more pages.

get_metadataGet metadata

Read-onlyfiles:read

Read the queryable tags on one file. Returns { metadata } (empty when none).

Returns metadata

ParameterTypeDescription
keyrequiredstringObject key to inspect.

set_metadataSet metadata

Can overwrite or deletefiles:write

Set or delete queryable tags on an existing file. set wins over delete for the same key. Tags appear on the public file page — do not store secrets. Requires set and/or delete.

Returns metadata

ParameterTypeDescription
keyrequiredstringObject key to update.
setobjectKeys to set or overwrite. Use path for the route (e.g. /settings), not route/page/screen.
deletestring[]Keys to remove.

list_metadata_keysList metadata keys

Read-onlyfiles:read

List metadata keys in the workspace (with counts). Pass key to list that key's values instead. Use before find_files.

Returns keystruncatedkeyvalues

ParameterTypeDescription
keystringWhen set, return distinct values for this metadata key (with counts) instead of the key list.

deleteDelete file

Can overwrite or deletefiles:delete

Permanently delete one uploaded file by key, along with its metadata.

Returns keydeleteddryRun

ParameterTypeDescription
keyrequiredstringObject key to delete.

Repo activity

list_activityList recent activity

Read-onlyfiles:readHosted only

Recent GitHub-tagged activity in this workspace, grouped by pull request (default) or by repo. Pull rows are pull requests that received media tagged gh.kind=pull, newest last_media_at first. The default window is 90 days. Set all to true to lift it. Optional repo and state (open, closed, or merged) narrow those rows. A title is included only when the repo is linked to this workspace. Repo rows are distinct gh.repo values, newest metadata updated_at first. These times are not the object's uploaded-at. Untagged uploads are absent. This does not publish a feed. Pass cursor back unchanged with the same arguments for the next page. Use list_repo_files to open one repo's files, and find_files for tag or filename search in key order.

Returns byrepospullscursor

ParameterTypeDescription
by"pull" | "repo"Group the page by pull request ("pull", the default) or by repository ("repo").
repostringWhen by is pull, keep rows for this owner/repo only. Ignored values are rejected when by is repo.
state"open" | "closed" | "merged"When by is pull, keep rows in this state. Rows with no stored state drop out. Rejected when by is repo.
allbooleanWhen by is pull, include pull requests whose last media is older than 90 days. Default false. Rejected when by is repo.
limitnumberPage size. Pulls default to 20 (max 100). Repos default to 20 (max 50).
cursorstringOpaque cursor from the previous page. Send it back with the same by, repo, state, and all.

list_repo_filesList files in a repo

Read-onlyfiles:readHosted only

Files tagged with gh.repo for one repository, newest metadata updated_at first. That time is not the object's uploaded-at. repo is owner/name. Optional pr matches gh.number. Optional path matches the path tag exactly. type is a filename suffix class: screenshot, video, or other. PDFs are other. Promoted copies (gh.status=promoted) are omitted. This does not publish a feed. Pass cursor back unchanged with the same arguments for the next page.

Returns itemscursor

ParameterTypeDescription
reporequiredstringGitHub repository as owner/name. Matching is case-insensitive.
prnumberWhen set, keep files tagged with this pull-request number (gh.number).
pathstringWhen set, keep files whose path tag equals this string.
type"screenshot" | "video" | "other"Filename suffix class. screenshot is png/jpg/jpeg/webp/gif/avif, video is mp4/webm/mov, and other is everything else (including PDF).
limitnumberPage size (default 50, max 100).
cursorstringOpaque cursor from the previous page. Send it back with the same repo, pr, path, and type.

Galleries

Live links

feed_createCreate change feed

Writesfiles:write

Create a public newest-first screenshot feed (same product as CLI uploads feed create). Pass repo alone for the whole GitHub repo. Pass pr plus repo — or github as owner/repo#123 / a GitHub PR URL — to scope that same feed to one pull request. issue does the same for an issue. Optional path filters by page-path metadata. Creating the same scope again returns the existing URL. This is a live query, not a curated gallery. Anyone who knows the URL can view it. See https://uploads.sh/docs/feeds.

Returns idurlworkspacerepopathnumberkindtitlecreatedAtupdatedAtitems

ParameterTypeDescription
repostringGitHub owner/repo, for example acme/app. Required unless github is set.
printegerScope the feed to this pull request. Same product as a repo feed — one extra filter. Mutually exclusive with issue and github.
issueintegerScope the feed to this issue. Mutually exclusive with pr and github.
githubstringScope via owner/repo#number or a GitHub issue/PR URL. Supplies repo when omitted. Mutually exclusive with pr and issue.
pathstringOptional exact page-path metadata filter, for example /settings.

feed_getGet change feed

Read-onlyfiles:read

Get a workspace-owned change feed by ID, including its current newest-first screenshots and the canonical public /c/<id> URL. Each item includes pageUrl (/c/<id>/<item>) for the pager. The feed may be repo-wide or scoped to one pull request / issue (see number and kind on the result). Anyone with the URL can view the media.

Returns idurlworkspacerepopathnumberkindtitlecreatedAtupdatedAtitems

ParameterTypeDescription
feedIdrequiredstringOpaque feed ID.

Workspace

whoamiWho am I

Read-onlyAny token

Show the current uploads.sh identity: the workspace this token acts in and its scopes. A successful result also means the server is up.

Returns okworkspacescopesuserIdserviceTokensignedInapiUrl

usageShow usage

Read-onlyfiles:read

Workspace storage and monthly upload counters (and remaining headroom when budgets are configured).

Returns workspacebytesobjectssharedBytessharedObjectsuploadsInPeriodperiodStartupdatedAtstorageBudgetBasismaxStorageBytesstorageRemainingBytesmaxUploadsPerPerioduploadsRemainingscopesplanstorage

reconcileReconcile usage

Writesfiles:write

Rebuild usage ledger bytes/objects from storage (source of truth). Preserves the monthly upload counter. Requires files:write.

Returns workspacebytesobjectspreviouschangedusageunprefixedBucket

purge_expiredPurge expired files

Can overwrite or deletefiles:delete

Delete objects older than the workspace retentionDays setting, then reconcile. Skips if retention is unset. Requires files:delete.

Returns skippedreasonworkspaceretentionDayscutoffdeletedfreedByteskeyskeysTruncatedreconcile

Docs and changelog

search_docsSearch uploads.sh docs

Read-onlyAny token

Search the public uploads.sh documentation. Use this to answer questions about uploads.sh product workflows, including: attaching screenshots and video to GitHub PRs and issues; staging files before a PR exists; galleries; the GitHub App; comment config (.uploads.yml); hosted MCP and agent setup; screenshot capture and annotate; plans and limits; bring-your-own bucket. Same as uploads docs. Returns titles, URLs, and snippets. Pass page to fetch the full markdown of one page (slug, path, or URL). Omit query to list the catalog.

Returns urlqueryresultstotal

ParameterTypeDescription
querystringSearch query (e.g. 'stage before a PR'). A single slug like 'attach' fetches that page.
pagestringFetch one page as markdown. Accepts a slug (attach), path (/docs/agents), or https://uploads.sh URL.
limitnumberHow many search hits to return (default 5, max 50). Ignored when fetching one page.

changelogProduct changelog

Read-onlyAny token

Read recent uploads.sh product updates (platform and CLI). Returns the latest entries with titles, dates, summaries, and a link to the full changelog at https://uploads.sh/changelog. Same as uploads changelog.

Returns urlfeedentries

ParameterTypeDescription
limitnumberHow many entries to return (default 5, max 50).

Protocol details #

These matter only if you write your own client.

  • Transport. Stateless Streamable HTTP. Each POST carries one JSON-RPC message and gets a plain JSON reply. There are no sessions and no SSE stream, so GET and DELETE return 405.
  • Headers. Every POST must send Content-Type: application/json and Accept: application/json, text/event-stream. A missing content type returns 415, and a missing Accept returns 406.
  • Spec versions. The server speaks the 2026-07-28 revision and the earlier initialize-based flow on the same endpoint.
  • Limits. The REST API’s upload limits apply, and writes are rate limited per workspace. See Plans & limits.
  • Discovery. The server card is at /.well-known/mcp/server-card.json.