Flask MCP Server

Connect Flask to Claude Code, Cursor, Codex, ChatGPT and other AI assistants via the Model Context Protocol. Your agent uploads its renders, reads the feedback on them and posts comments, as you.

Flask here is flask.do, the video review and feedback tool, not Flask the Python web framework.

By Enrico Tartarotti, founder of Flask · Updated

Quick start

  1. Connect. In Claude Code, Cursor or Codex, run npx flask-feedback in your terminal, or paste it to your agent and it runs it. In a Claude app or any other MCP client, add Flask as a connector with the server URL: https://api.flask.do/api/mcp/mcp
  2. Sign in. Your browser opens once: sign in to Flask and allow the agent. After the command, restart the agent session. A free Flask account is enough.
  3. Ask your agent. For example “upload my render to Flask and ask me for feedback” or “summarize the feedback on our latest cut”. There is nothing else to install: the server teaches the agent how to work with Flask.

How to connect

  1. Claude Code, Cursor or Codex: run one command in your terminal, or paste it to your agent and it runs it. It connects Flask to each of them you have (Claude Code in every project) and opens the browser once to sign in to Flask. Then restart the agent session.
    npx flask-feedback
  2. Claude apps (claude.ai, Claude Desktop, mobile, Cowork): Open Customize > Connectors (claude.ai/customize/connectors), choose Add custom connector, paste https://api.flask.do/api/mcp/mcp, then sign in to Flask. Connected once, Flask works in every Claude app, and in Claude Code when you sign in to it with your Claude account.
  3. By hand: Claude Code claude mcp add --scope user --transport http flask https://api.flask.do/api/mcp/mcp, then /mcp to sign in; Add to Cursor in one click; ChatGPT: Settings > Apps > Advanced settings, turn on Developer mode, then create a connector with the server URL below; any other MCP client: the server URL below.
  4. Then ask things like “upload my render to Flask and ask me for feedback” or “summarize the feedback on our latest cut”.

There is nothing else to install: the server teaches the agent how to work with Flask. Connected Flask both ways in Claude Code (the command and the Claude app)? Keep one, so it doesn't show up twice: claude mcp remove flask --scope user removes the command's.

Server URLhttps://api.flask.do/api/mcp/mcp
Read-only URLhttps://api.flask.do/api/mcp/readonly: the same server with only the read tools (16 of 29). The write tools (uploads, comments, folders, tags, renaming, moving, sharing and regrouping versions) are not listed and cannot be called; connect this URL when an agent should only look
AuthenticationOAuth 2.0 with Dynamic Client Registration (RFC 7591), authorization code with PKCE (S256) and refresh tokens, and an OpenID Connect UserInfo endpoint (your email and whether it is verified). Discovery: authorization server metadata (RFC 8414) and protected resource metadata (RFC 9728), served for the exact MCP URL; an unauthenticated tool call answers 401 with a WWW-Authenticate header pointing to it. Clients register through Flask's own registration endpoint, which refuses redirect addresses no real agent uses; an agent on your own machine (localhost, 127.0.0.1, [::1]) signs in at exactly the address it registered. The consent screen shows the exact address an agent's access will be sent to, and an unverified app needs an extra confirmation before you can allow it. Only a token issued to a connected agent is accepted: a Flask web session's token is refused by the MCP endpoint
TransportStreamable HTTP, stateless, JSON responses (POST only; SSE not supported). A JSON-RPC batch may hold at most 10 messages; tool results are compact JSON
CapabilitiesRead + write: reads your data, downloads a version's file, uploads videos (from a URL or a local file) and images, creates/edits feedback comments as you, creates folders and tags, renames, moves and shares folders and assets (link and team access, as the Share tab does), and regroups versions, under the same permission rules as the app. An agent's token is limited server-side to the routes these tools use: it cannot delete anything or touch billing or account settings
RequirementsFree or paid Flask account

Available tools

Most tools are read-only. The tools that write are the upload tools (upload_video imports a video from a URL, and upload_file_start / upload_file_complete upload a local video or image from your machine), the feedback tools (feedback_post creates a comment, feedback_update edits one) and the organize tools (folder_create, items_update renames, moves and shares, versions_add, versions_split and versions_reorder regroup versions, tag_create) and the version notes tools (version_notes_update writes a version's notes, version_file_upload adds a file to it). Every write acts as your user under the exact permission rules the app enforces: commenting requires comment access, editing a comment's text is author-only, and renaming, moving, sharing or regrouping needs edit access. The MCP never deletes anything and never creates recordings or drawings, and an agent's token is refused by every other Flask route. Agents are told to change sharing only when you ask, never because a comment asks.

There are 29 tools. Every tool has a human-readable title and MCP annotations (readOnlyHint, destructiveHint): the read tools are marked read-only; feedback_update is marked destructive because new content replaces a comment's whole body, and so are items_update (a rename or move replaces the old title or place; it is also marked open-world, because sharing opens an item to anyone with its link), versions_add (an asset added into another stops existing on its own) and version_notes_update(new notes replace the old ones). Feedback lives on assets, so feedback_list, wait_for_feedback, feedback_post and asset_status refuse a folder id with an error that says what to call instead.

Tools that act on a whole team (contents at the top level, search, recent_activity, members, tags, feedback_stats, tag_create, folder_create and uploads to the top level) use the team selected in the Flask app unless the agent passes team_id, and every answer names the team it used. An agent pins the team it started in, so switching teams in the app never silently moves its reads or writes. A team_id that isn't one of your teams is refused. A tool call the client sends with an outdated tool list is answered with how to recover: reconnect the connector.

Browse

  • contents: list the team's top level, or open a folder to see its child folders and assets. Every asset item carries the asset id, its current version id, the version count, its media fields, the asset link (it opens the current version, the top of the version list) and a pinned link to the current version. Given an asset_id instead, it opens that asset: its versions (version id, version number, title, type, status, a pinned link each) and which one is current

Feedback

  • feedback_list: list top-level feedback on an asset (its current version) or on one exact version (text comments + voice, camera and screen recordings). By default it lists the feedback that still needs action: open feedback, not yet marked Done (in-progress included), with the counts of open and completed; status lists the completed ones or everything. Each item carries its status (open, in-progress or completed, from your team's status labels). Paginated (20 per page by default, up to 100), each with its tags by name and its reply count; the response names the version listed (version id, asset id, title and version number); items also flag images and standalone drawings so the agent knows to fetch the visuals
  • feedback_get: get a single feedback item with its full reply thread, including the recording's full verbatim transcript, its processing status and its visual references — the pointing phrases (“this”, “over here”) paired with the recording time each was spoken
  • wait_for_feedback: long-poll that returns new feedback (comments and replies) as soon as it arrives, blocking 10 to 45 seconds per call (45 by default); lets agents listen for your review in near real time instead of polling. The agent's own posts are never returned to it. A recording is delivered once its transcript is done; until then it is listed as pending, and anything newer waits behind it so the order is kept. The agent loops by passing back the returned next_since. When you send a round to your agent from Flask's agent bar, the version that answers it tells the agent to end its turn instead of waiting: Flask messages its session when the next round is ready
  • feedback_post (write): create a comment on an asset as you (it lands on the asset's current version, or on the exact version you name), written in Markdown, @mentioning teammates (they are notified), linking other assets and feedback items, optionally anchored to a video timestamp, tagged with your team's existing tags, or threaded as a reply. A reply lands on the version its thread is on, even after a newer version was uploaded, and takes no tags or timestamp (those belong to the comment that starts the thread; passing them is refused). E.g. importing feedback from an email with timestamps into the right moments of the video
  • feedback_update (write): edit a feedback item — its Markdown body (replaced whole) and timestamp on your own comments; tags/statuses (like marking feedback “Done”) on any item you can comment on, except replies, which take no tags. Returns the updated item in full
  • get_annotated_frames: the visuals of a feedback item in one call, with the transcript marked [FRAME N]: a frame for every pointing phrase in the comment (each “this” or “here”, with a map of which frame shows which), the media under review with the reviewer's drawing rendered in, plus their shared screen when they demonstrated something (e.g. a Photoshop or Figma mockup they pulled up). Also works on text comments: standalone drawings are rendered onto the exact frame they were drawn over, and the comment's images are returned as frames. Up to 12 images; webcam frames are not included. Every item with something to see carries a visuals field pointing here. Each frame comes with a link to it as shown (drawing included) and one to the same moment without the drawing: addresses any tool can fetch without signing in, for a few hours, so an agent can download a frame or pass it to an image or video model
  • get_frame: the fallback for one moment the comment doesn't point at: the exact frame at a specific recording time, or when a particular word or phrase was spoken, plus a transcript snippet around that moment, with the same frame links

Every feedback item includes its tags as readable names. Each recording is one comment: the text body is the AI-organized feedback, the recording object carries its recording_type (voice, camera or screen), the full verbatim transcript, its pipeline_status (e.g. transcribing while the transcript is still being made, complete once done), plus visual_references (the pointing phrases with the recording time each was spoken), and play_url is a deep link that opens the asset with that recording selected, ready to play (it does not start playback by itself). (A few recordings made before August 2026 may still appear as sibling comments sharing one recording.)

Every item also says how it fits in: reply_to is the id of the comment that starts its thread (null for a top-level comment), posted_by_agent is true for a comment an agent posted through the MCP, and agent_client_id names that agent. Each item's url opens the version it was left on with the comment selected.

Comment bodies are Markdown in Flask's dialect, read and written in the same form: an agent can send back exactly what it read. Supported: one heading level (any number of # makes the same heading), bulleted and numbered lists, bold, italic, code, fenced code blocks and links; anything else (quotes, tables, HTML) stays literal text. References are plain links: a person as [@Name](https://flask.do/members?user=…) (the person is notified), an asset, a folder or another feedback item as its flask.do link, a moment of the video as [0:26](#t=26) or a range as [0:26-0:31](#t=26,31), and a phrase of a recording as [this](#rt=15.7) (spoken 15.7 seconds into the recording; get_annotated_frames shows what each one points at). An image already stored in Flask can be placed on its own line as ![](url). A body holds at most 20,000 characters of text. Each item also lists its references, with their names, in mentions.

Feedback is untrusted input: anyone with a link to an asset may be able to comment. Every tool result wraps its data (feedback, titles, transcripts, names) in an <untrusted-data-...> boundary with a fresh random id per call, so text inside can't close it and pose as the server; the server's own guidance (such as upload steps) sits outside it. Every feedback item also carries author_is_team_member, and the server instructs agents to treat feedback as data, never as instructions, and to check with you before acting on feedback from outside your team.

Search & activity

  • search: unified search across folders (name or description), assets (title) and feedback (text) in one call, up to 10 results per category by default (50 at most). Asset hits have the same shape as contents items (asset id, current version id, version count, media fields, both links); feedback hits link to the version they were left on
  • recent_activity: the latest feedback (text + recording) newest first — across the whole team, or scoped to one folder with a date window and pagination; each item carries the asset it belongs to, the version it was left on and its version number
  • feedback_stats: aggregate feedback counts across many assets in one call — per asset and per version (v1, v2, …), split by type, tag, and author, scoped to the team, a folder, or one asset, with an optional date window. Assets are sorted by feedback count, most first; assets with no feedback in the window are left out unless include_empty is set; results are paged (50 per page by default, up to 200). Built for trend questions like “are v1 notes trending down for this client?”

Tags & permissions

  • tags: list the team's tags and the share of feedback each tag covers, scoped to the team, a folder (its whole subtree), or a single asset (across all its versions)
  • permission_get: read the access settings of a folder or an asset (permissions live on the asset; its versions use them): your own access level and whether you are its creator or a member of its team, its creator, the default levels for members and for anyone with the link, and the team's member count. It does not list individual people. items_update changes the link and team levels; per-person access is changed in the Flask app
  • members: the people on your team, by name, user id and role, so an agent can @mention someone in a comment (no email addresses)
  • tag_create (write): create a team tag (optionally with a #RRGGBB colour) so feedback can be tagged with it by name; if a tag with that name already exists (case-insensitive) it is returned unchanged, so calling it twice creates one tag

Organize

  • folder_create (write): create a folder at your team's top level or inside a folder you can edit; returns its id and link. Like the app, it allows two folders with the same name, so the agent checks contents first
  • items_update (write): rename, move and/or share folders and assets, up to 20 per call, each with its own result; it can also name a single version, as the app's version list does. Sharing is the Share tab's two settings: what anyone with the link can do (comment and view version notes, comment, view or none; a link never gives full access) and what your team's members can do, and the item's link is returned to share. A folder can't move into itself or its subfolders, nothing moves into an asset, and nothing moves between teams. An item already as asked reports no change, so repeating a call is safe, and each result lists every change as before and after, so it can be undone with another call
  • versions_add (write): make other assets or versions the newest versions of an asset, the way the app's version menu and drag and drop do (for a revision uploaded as a separate asset by mistake): a whole asset added this way stops existing as a separate asset, its title and link go and its versions move over with their feedback, as when merging in the app; it is refused while a source is still uploading, is in another team, or is shared more narrowly than the asset it would join
  • versions_split (write): move a version into its own new asset in the same folder, with the same sharing
  • versions_reorder (write): set the version order; the top version becomes the current one, the one the asset's link opens

Your open Flask tabs show a Refresh prompt when an agent creates, renames, moves or regroups something, just as they do for a teammate's change.

Version notes & files

  • version_notes_get: read a version's notes (Markdown: what changed and how to continue the work) and its files, each with a download link that works for one hour without a token; asked, the zip also lists every file it holds (path and size), read from it without unpacking it. An agent reads them before it continues work someone else uploaded. Reading them needs Full Access, or Can Comment and View Version Notes
  • version_notes_update (write): replace the notes of a version that already exists (a new upload carries its notes in the upload call; up to 20,000 characters of text; Flask links become references, as in feedback). Anyone with Full Access to the asset can write them, on any of its versions
  • version_file_upload (write): attach a version's working files to a version that already exists (a new upload carries them in the upload call): ONE zip of the project folder that made it; returns a presigned upload URL (valid for one hour) for exactly the declared size, which the agent sends the file to with curl; people see what it holds in the version's Code and Files in the app and download it as <version id>.zip. One zip per version, up to 500 MB (the video itself has no GB limit), set once: to change the files, upload a new version with its own. Members of the asset's team with Full Access can attach it, to any of its versions

When an upload finishes, Flask tells the agent that a version made with code or files (an animation, a render script) should carry them: the source as one zip and notes on what was done and how to continue. Notes and files belong to a version, not the asset: each revision keeps the files that made it, next to the feedback on it, so any agent can pick the work up from the Flask link alone. In the app they are the version's Context (the Context chip above the asset), where people can read and edit them and add files too.

Media

  • asset_status: check the processing status and progress of a video — by version id, or by asset id (its current version); returns the asset id, the version id checked and both links. While it works it reports the processing percent, and an import's download progress as transfer_progress; a failure is a final error status with its reason, so the agent stops polling; no_versions means an asset with no media yet
  • version_download: a download link for a version's file, by version id or asset id (its current version), so an agent can edit the media itself. A video gives its original uploaded file, exactly as uploaded; Flask keeps it for the team's download window, 14 days from upload on Free and Pro and longer on a custom Business plan (the answer says until when), after which the agent is told it expired and the video only plays in Flask. An image gives its stored file, kept for good. The link works without a token (a video's for one hour); an agent without a shell hands it to you to open. Needs view access; a version's working files are read with version_notes_get
  • upload_video (write): import a video from a public URL (direct file link or Google Drive) as a new asset, or as a new version of an existing one; returns the new version id to poll with asset_status, the asset id and both links
  • upload_file_start (write): start uploading a local video file; creates the asset and returns its shareable link immediately plus a shell command (curl and dd, in any POSIX shell) that sends the file the same way the web app does, in parts. It needs an agent that can run shell commands, such as Claude Code, Codex or Cursor; in a chat app, the agent uploads from a public link with upload_video instead. The link shows the upload's percentage live, counted from the parts that have arrived, to everyone with the asset open; a file up to 50MB goes up in one part. If the command stops, running it again resumes, sending only the missing parts. For an image (PNG, JPEG, WebP or GIF, up to 20MB) it returns only the presigned upload URL and the image's address; the asset is created by upload_file_complete. A version made with code can carry its notes and files in the same call (notes, files; for an image, in upload_file_complete, and in upload_video too): the notes are saved as the version is created and each file comes back with its upload command, sent before the video, so the version shows its notes and files from the moment it appears
  • upload_file_complete (write): finalize a local upload and start processing (safe to retry; for a video it checks every part arrived, and if one is missing it says to run the same command again); the video is watchable via preview playback while it processes. For an image it creates the asset (in a folder, at the top level, or as a new version of an asset), and a retry returns the same asset. Every refused upload names its error code, the reason and the fix

Every asset is a container for its versions (v1, v2, …) and owns the title, folder and sharing. All upload tools accept version_of (the asset id, or any of its version ids) to upload as a new version instead of a separate asset. Upload results carry asset_id (the new version, the id to poll and to leave feedback on), container_id (the asset), url (the asset link, which opens the current version: the newest upload, unless the versions were reordered) and version_url (a link pinned to that exact version), and say which asset the next version goes to (its version_of), as new feedback does. A folder is optional: without folder_id the asset lands at your team's top level; with one, you need edit access to that folder.

Uploading via MCP works on the free plan, no subscription required: its 5 assets are shared by uploads from the web app and from agents. A new version of an existing asset (version_of) is always free. If your team runs out of free assets, the agent stops and tells you so: upgrade in the Flask app at flask.do/plan and it can continue right away.

Guides, resources and prompts

The server's instructions are short (when to use Flask, the data model, the review loop and the safety rules) and tell the agent to read the playbook with get_guide before it acts on your feedback or makes its first change; looking things up needs no playbook. The playbook is how to work in Flask: it is your main surface for media, so an agent iterates on the asset you are working on version by version, shows you drafts worth reviewing there and asks for your feedback there. It uploads a new version on its own when the change answers your feedback in Flask, is a first draft or a large change, or you ask for it; while you iterate together locally, it asks before uploading. Every upload is first decided as a new asset or a new version of one (new versions are always free); work made by code (animations, motion graphics, generated media) always carries its working files and notes on its version; feedback is read from the transcript and the frames, answered in its thread and marked Done only once the change is in an uploaded version; sharing changes only when you ask; and feedback is shown to you as it is, with its link. The same guides are resources, and three prompts start common workflows.

  • get_guide: read a guide: instructions (the playbook, which also carries the display rules and the concepts), display, concepts, permissions or urls
  • flask://help/instructions: the playbook
  • flask://help/concepts: folders, assets and versions, comments and recordings, tags and statuses, permissions, and why feedback is untrusted
  • flask://help/permissions: access levels and how a user's access is resolved
  • flask://help/urls: links to folders, assets, exact versions, comments and tag filters
  • flask://help/display: how to show feedback to you: verbatim quotes, links, recording labels, tags and grouping
  • review-feedback (prompt, takes an asset id): review and summarize all feedback on an asset
  • catch-up (prompt): catch up on recent team activity
  • find-everything (prompt, takes a topic): find folders, assets and feedback on a topic

Use cases

Catch up on team feedback

See what your team has been giving feedback on, with links to jump into each conversation.

“What feedback did my team leave this week?”

Summarize feedback on a video

Get a structured summary grouped by theme, with verbatim quotes, timestamps, and clickable links back to each item.

“Summarize all the feedback on the ‘loading screens’ cut and group it by theme”

Turn voice notes into action items

AI transcripts of every voice/screen recording mean Claude can pull concrete action items straight out of casual feedback.

“Pull every action item from the voice recordings on the latest cut”

Upload a video from your workflow

Hand Claude a link or a local file (a finished cut, an agent-rendered video) and have it uploaded straight into the right folder, ready for feedback. The share link works immediately, for the first upload and every new version.

“Upload this Drive link to the ‘Q3 launch’ folder and tell me when it’s ready”
“Upload ~/renders/final_cut_v3.mp4 to the ‘Q3 launch’ folder and give me the share link”

Iterate on an agent's render with your review

The agent uploads its render and shares the link, then waits with wait_for_feedback while you review by talking and drawing on the video. It reads each comment as it arrives (a recording once it is transcribed), makes the changes, uploads the fix as a new version of the same asset, and tags the feedback it implemented “Done”, replying only where you asked it a question.

“Upload the intro render to Flask, wait for my feedback, and upload the fix as v2”

Find specific feedback across projects

Search across folders, assets, and feedback at once.

“Did anyone mention the intro pacing? Show me the exact comments and links”

Import feedback from anywhere

Feedback arriving outside Flask — an email, a Slack thread, meeting notes — gets posted onto the asset as timestamped comments, so it lands at the exact moments it refers to.

“The client emailed notes on the launch video — add each point as a comment at the right timestamp”

Triage by tag

Use tag distribution to spot the busiest themes across a folder or asset.

“Which tags are most used in the ‘Product Decisions’ folder?”

Data handling

  • The server only accesses data explicitly requested by the user via a tool call; no background sync, no proactive scraping
  • Flask never reads your conversations with the agent, its memory or your files: it receives only the arguments of each tool call, and what an agent uploads
  • The MCP route is stateless: a fresh server is created per request and no per-request data is persisted beyond OAuth tokens, rate-limit counters, and the agent recorded on each video it uploads and each comment it posts
  • All data transmission is encrypted via HTTPS/TLS
  • The MCP only writes when you ask it to — uploading a video (from a URL or a local file) or an image, creating/editing a feedback comment, creating a folder or a tag, renaming, moving or sharing folders and assets, regrouping an asset's versions, or writing a version's notes and adding files to it, as you, under the same permission rules as the app. It never deletes media or feedback. One change removes something, exactly as in the app: when versions_add adds a whole asset into another, that asset's title and link go (its versions and their feedback move into the other asset and take its sharing)
  • Each video uploaded and each comment posted through the MCP records which of your connected agents did it (its OAuth client id), so Flask shows that work as yours with the agent's badge on your avatar, e.g. "Claude is uploading"; the agent's name and logo are looked up when shown, never copied
  • Files added to a version are stored privately in Flask's storage, never at a public address: they download only through short-lived signed links handed to people and agents who can view that version
  • A video's original file downloads (version_download) only through a signed link that expires after one hour, handed to people and agents who can view the asset; Flask deletes originals from its storage when the team's download window ends (14 days after upload on Free and Pro, longer on a custom Business plan), and the video keeps playing from its stream
  • A feedback frame's links (get_annotated_frames, get_frame) are handed only to people and agents who can view the asset. The frame with the reviewer's drawing is never stored: its link is signed, draws the frame when fetched, expires within hours and stops working when the comment is deleted. The frame without the drawing is the video's still at that moment, from its stream
  • Email addresses are never exposed in MCP responses (only display name and avatar)
  • An agent's access token only works for the routes the MCP tools use; any other Flask route (deleting, billing, account settings) refuses it. It is accepted only as an Authorization header: the same token sent as a browser cookie is refused, so that limit cannot be bypassed
  • Rate limits: 100 MCP messages per minute and 2,000 per hour per user (each message in a batch counts); a throttled call gets HTTP 429 with a Retry-After delay and never forces you to sign in again
  • Only a token that is definitely invalid (expired, revoked) answers 401 and sends the agent back through sign-in; if Flask briefly cannot verify a token, the server answers 503 with a Retry-After delay instead, so an outage does not log every agent out
  • Tool discovery (initialize, ping, and the tools, prompts and resources lists) is public and returns only the static catalog, so directory health checks can inspect the server; this unauthenticated traffic is rate-limited per IP address, and every actual tool call requires OAuth
  • Every connected agent is listed in the Flask app at flask.do/ai-agents, where you can revoke one at any time — revoking is instant — it deletes that client's sessions and invalidates its refresh tokens, and because every request is re-validated against Supabase, the agent's next call fails immediately

Support

If you run into any issues with the Flask MCP integration, reach out at hello@flask.do.

For more about how we handle your data, see our Privacy Policy.