Skip to content

MCP server: OAuth discovery, tool annotations, file params and result widget - #529

Draft
kvz wants to merge 7 commits into
mainfrom
mcp-oauth-discovery
Draft

kvz wants to merge 7 commits into
mainfrom
mcp-oauth-discovery

Conversation

@kvz

@kvz kvz commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Why

The hosted https://api2.transloadit.com/mcp endpoint must point clients at API2's authorization server (401 + WWW-Authenticate: Bearer resource_metadata=…) and meet the ChatGPT plugin and Anthropic connector directory requirements (per-tool securitySchemes, title, readOnlyHint/destructiveHint, Origin validation). Phase 1 of the plan adds openai/fileParams on transloadit_create_assembly and an MCP Apps result widget.

Plan: ChatGPT plugin + agent OAuth plan (Content). Task note for this branch: docs/prompts/2026-09-30-mcp-oauth-discovery.md.

What landed

Transport (hosted mode, TRANSLOADIT_MCP_RESOURCE_METADATA_URL)

  • MCP requests without a bearer token: 401, WWW-Authenticate: Bearer resource_metadata="<url>", JSON body { error: "unauthorized", error_description }. Bare GET without Accept: text/event-stream still returns the friendly 200. Self-hosted TRANSLOADIT_MCP_TOKEN behavior unchanged; with neither variable set nothing changes.
  • Origin policy when hosted and no explicit allowedOrigins: https://chatgpt.com, https://chat.openai.com, https://claude.ai, https://claude.com, https://transloadit.com, https://*.transloadit.com, https://transloadit.dev:*, https://*.transloadit.dev:*, loopback (localhost, 127.0.0.1, [::1], any port). Other browser Origins get 403; requests without Origin pass. allowedOrigins entries now accept *. and :* wildcards.
  • WWW-Authenticate is exposed through CORS; the server card advertises schemes: ["oauth2", "bearer"] plus resourceMetadataUrl.
  • CLI: TRANSLOADIT_MCP_RESOURCE_METADATA_URL, TRANSLOADIT_MCP_CONSOLE_URL; hosted mode may bind to non-localhost without TRANSLOADIT_MCP_TOKEN.

Tools

  • title + readOnlyHint/destructiveHint (always false)/idempotentHint/openWorldHint (true only for transloadit_create_assembly) on every tool.
  • Top-level securitySchemes (via a tools/list override, since registerTool() cannot emit it) mirrored in _meta.securitySchemes: noauth for list_robots/get_robot_help/lint; oauth2 assemblies:write (create), assemblies:read (status, wait), templates:read (list_templates), oauth2 with no scopes for the profile tool.
  • Auth failures are isError results with _meta["mcp/www_authenticate"]: ["Bearer resource_metadata=\"…\", error=\"…\", error_description=\"…\""] (array of header strings, per OpenAI's documented shape) plus readable text. insufficient_scope when no credentials, invalid_token when API2 answers 401.
  • New transloadit_get_profile (_meta["openai/profile"]: true) returning { id, name?, nickname? }, derived from the Workspace's latest Assembly or an owned Template.
  • transloadit_create_assembly gains attachments (_meta["openai/fileParams"]: ["attachments"]), exactly the OpenAI file object; each entry maps onto the existing URL-input path. files (url/base64) unchanged.
  • MCP Apps widget ui://transloadit/assembly-result (text/html;profile=mcp-app, inline HTML/JS, CSP connectDomains/resourceDomains = https://*.transloadit.com, https://*.transloadit.net, plus openai/widgetCSP/openai/widgetDescription aliases), linked from create/wait via _meta.ui.resourceUri and _meta["openai/outputTemplate"]. Results carry _meta["transloadit/widget"] with authenticated, assembly_console_url and new_template_url for the Open in Console / Save as Template buttons.
  • openai/toolInvocation/invoking / invoked strings (≤ 64 chars) on the Assembly and Template tools.

Upstream identification (follow-up from the devdock QA run)

  • API2 rejects relayed aud=mcp bearers on ordinary endpoints (TOKEN_INVALID_AUDIENCE) unless the request comes from the hosted MCP service. New env var TRANSLOADIT_MCP_UPSTREAM_SECRET (option / JSON config key upstreamSecret) is sent as the header Transloadit-Mcp-Upstream: <secret> on every API2 call that relays a forwarded bearer token; never in self-hosted key/secret mode, never logged (redactor scrubs the header and the listed value), never in errors or the server card.
  • @transloadit/node gains a narrow extraHeaders client option (fixed headers merged into every _remoteJson request) so the MCP server can carry that header; changeset patches @transloadit/node and transloadit.
  • Tests: test/unit/upstream-secret.test.ts (nock asserts the header is on the wire with a forwarded bearer, absent without a secret and in key/secret mode; secret absent from tool errors and the server card; log redaction) and a node SDK test for extraHeaders.

Docs and packaging

  • README: connect by URL first (Claude Code, Claude.ai, ChatGPT, Codex, Cursor, Inspector), minted bearer tokens under CI/headless, roadmap TODO removed.
  • packages/mcp-server/plugin.json, mcp.json, .codex-plugin/plugin.json (skills referenced by catalog URL, not bundled). Changeset: minor for @transloadit/mcp-server.

Deviations from the cross-repo contract

  • _meta["mcp/www_authenticate"] is an array of WWW-Authenticate header strings rather than an object; that is the shape OpenAI documents for the linking UI. Missing credentials use error="insufficient_scope" (OpenAI's own example) instead of the non-RFC unauthorized.
  • File params live under attachments, not files: the OpenAI validator wants items to be exactly the file object, and files must keep accepting kind: "url" | "base64" inputs for existing clients.
  • _meta.ui.domain is not set yet (needs a dedicated, verifiable origin decision before plugin submission).

Related PRs

Test plan

  • corepack yarn --cwd packages/mcp-server check (115 unit tests: 401/metadata, origins, security schemes and annotations for every tool, file-param mapping, widget resource and CSP, auth errors, profile tool) and corepack yarn check at the root.
  • Local build copied into the api2 devdock worktree: both packages/mcp-server/dist → api2/node_modules/@transloadit/mcp-server/dist and packages/node/dist → api2/node_modules/@transloadit/node/dist (the published @transloadit/node 4.14.0 there has no extraHeaders, so without the second copy the Transloadit-Mcp-Upstream header is silently dropped). mcp-server service restarted with TRANSLOADIT_MCP_RESOURCE_METADATA_URL, TRANSLOADIT_MCP_UPSTREAM_SECRET and TRANSLOADIT_ENDPOINT set; endpoint probed with curl.
  • Result: Claude Code connected through the OAuth flow against devdock and transloadit_list_templates succeeded end to end (2 templates returned). The Opus QA scenario mcp-oauth in Content covers the remaining clients.

Release coupling for API2

API2 must bump both @transloadit/node (patch, adds extraHeaders) and @transloadit/mcp-server (minor) together once this branch publishes. Bumping only the MCP server keeps the old SDK in its node_modules and the upstream header is never sent, so relayed aud=mcp tokens are rejected again.

Status: draft, iterating against local devdock until all three clients connect by URL alone.

🤖 Generated with Claude Code

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kvz kvz self-assigned this Sep 30, 2026
kvz and others added 6 commits September 30, 2026 21:47
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…to the MCP server

Hosted mode (`TRANSLOADIT_MCP_RESOURCE_METADATA_URL`) answers unauthenticated MCP requests with
`401` and `WWW-Authenticate: Bearer resource_metadata="…"`, keeps the bare-GET health probe, and
limits browser Origins to ChatGPT, Claude, Transloadit and loopback unless `allowedOrigins` is
set. Self-hosted `TRANSLOADIT_MCP_TOKEN` behavior is unchanged.

Every tool now carries a title, annotations and per-tool `securitySchemes` (top-level via a
`tools/list` override, mirrored in `_meta`). Auth failures return `isError` results with
`_meta["mcp/www_authenticate"]`. New `transloadit_get_profile` tool for multi-account hosts,
`attachments` on `transloadit_create_assembly` for `_meta["openai/fileParams"]`, and the MCP Apps
widget `ui://transloadit/assembly-result` linked from the Assembly tools.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
README leads with the hosted endpoint and OAuth clients, moves minted bearer tokens to the CI
section and drops the roadmap TODO. `plugin.json`, `mcp.json` and `.codex-plugin/plugin.json`
describe the ChatGPT and Codex plugin and reference the skills catalog by URL. The task note
checklist and devdock paths are updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
API2 only accepts relayed `aud=mcp` bearer tokens on ordinary endpoints when the request comes
from the Transloadit-hosted MCP service. `TRANSLOADIT_MCP_UPSTREAM_SECRET` (option and JSON
config key `upstreamSecret`) is sent as `Transloadit-Mcp-Upstream` next to a forwarded bearer
token, never in key/secret mode, and is redacted from logs. `@transloadit/node` gains an
`extraHeaders` client option to carry the fixed header.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant