Illustrated 1926 magazine page explaining how photographs were scanned, transmitted across the Atlantic by cable, and reconstructed at the receiving station.

AI Agents Can Build Apps. Why Can’t They Use Them?

“How Photos Are Cabled Across Atlantic,” Science and Invention, April 1926. Public domain scan via Wikimedia Commons.

A few weeks ago I rage-coded a fax app (promptfax.app) after I was forced to send a fax again for the first time in a decade. I did not want to deal with sending a one-off fax, so like I often do with stasks I don’t want to deal with — I asked chatGPT and Claude to do it for me. Sadly, neither of them could send my fax on their own. The advice they gave me for how to do it was far less than ideal.

Agents can code complex apps in minutes, yet they can’t send a simple fax? How could this be? This set me off down a rabbit hole exploring how AI agents should implement a simple use case like this. There are so many ways to let an LLM interact with your app and do stuff—MCP, connectors, AI browsers, AI browser extensions, computer use, WebMCP… What is a web app supposed to do to help here? Are all of these needed? Which is best?

To answer these questions, I ended up making a simple web app aimed at humans. I then started looking into every option reasonably available to let an agent interact with my app. The landscape of options was way larger than I had previously thought about. There are big choices in how much control you want some external LLM agent to have vs. your own app. Some approaches are standardized, or on their way to be standardized. Others are only applicable to a single LLM vendor (i.e., OpenAI). The matrix below gives a starting summary.

Two-axis matrix positioning Computer Use, Browser Control, MCP Server, MCP App, WebMCP, Custom GPT, and an in-app LLM by LLM-led versus app-led interaction and availability across products versus a single AI product.
I explored 7 different ways to integrate AI Agents with my app.

I tested and/or implemented all of these. My learnings are outlined below. Given the limited scope of faxing, my focus was on general AI interaction with web apps. These mechanisms and interactions are not at all specific to my app, so I hope you find it helpful for your app.

Contents

Agent to App interaction mechanisms

Add an LLM to the app

Shoving an LLM chatbox onto the page is a very common approach and is where I started. Most of the time embedded website chat agents are hooked into some Retrieval-Augmented Generation (RAG) system to answer questions. Answering questions is easy. Having them execute actions is more complex. If your app is relatively simple and built with the right abstractions, it shouldn’t be too hard to map your internal API functions to LLM tools. Then the LLM can invoke those tools as needed.

My first thought was, why not just expose a chatbox and skip the tedious work of building a GUI altogether? I really like how apps like lovable.dev just give you a prompt and let you build things. It turns out that a text prompt is a horrible user interface for doing most simple things. There is a reason personal computers stopped booting to a prompt decades ago. In my case, the simple steps of adding a document, a phone number, and providing a quote via chat were way more tedious than just making a couple of clicks.

Animated PromptFax interface showing a user asking whether chat is better than the existing controls, followed by the assistant recommending the buttons for uploading, entering a fax number, paying, and sending.
The in-app chat could drive the same fax workflow, but typing through it was slower than using the controls.

If your UX is good, the user shouldn’t need to ask questions or type what they need to do. I kept the in-app LLM for anyone who wants to use it (one so far), but demoted its prominence.

In-app LLM — an app-owned chat and tool interface

Primary user interface: the app

Pros

Cons

  • Gives the product control over the model, tools, guardrails, UI, and data flow
  • Keeps the user in the existing product and can use app-specific context
  • Useful for open-ended help or capabilities that do not fit the normal UI
  • Chat is slower than a purpose-built UI for simple, known workflows
  • Usually not an efficient way for the user to discover features and capabilities
  • Adds model cost, latency, safety, and testing burden to the app

Computer Use

Computer use is a broad capability that lets the LLM see and interact with the computer using screen grabs and OS-level accessibility features. Both Claude and ChatGPT have forms of Computer Use in their desktop apps.

In theory these can do anything the user can do. It is rarely faster than operating the desktop yourself. The agent needs to do continual screen grabs, interpret those screens, then move and click. Those iterations take a lot of time—and tokens.

Generating training data must be tough here vs. scraping the Internet. I am sure the frontier model vendors have farms of people clicking through apps to train these models, but there is a massive amount of visual variety to consider. This will certainly get better, but it does not seem like an efficient way for a machine to control another machine.

There are required OS-level permissions to consider which could easily scare or confuse customers:

macOS permission dialog warning that Claude wants to bypass the system private window picker and directly access the screen and audio, with Allow and Open System Settings buttons.
Claude’s computer-use flow requires broad macOS screen-recording permission. 😱 Scary for the privacy concerned.

Not much we can do to help AI Agents here

There isn’t much an app can do to specifically help the LLM in this situation. Perhaps other than running a CAPTCHA or using Cloudflare’s bot tools, there is no great way to detect if the user is an LLM. If the app is simple enough for users, hopefully it is simple enough for the LLM to interact with at a reasonable speed.

Computer use — the agent operates the user's desktop visually

Primary user interface: the LLM agent, acting through the normal desktop UI

Pros

Cons

  • Works across browsers and native apps without an app-specific integration
  • Can bridge several applications in one task
  • Provides a fallback when no API or agent interface exists
  • Slow, compute-intensive, and less reliable than structured tools
  • Broad OS and accessibility permissions create privacy and security concerns
  • UI changes, pop-ups, and ambiguous screen states can derail execution
  • The app developer has little control/influence beyond providing a clear, accessible UI

AI Browser Control

Computer Use works at the OS level. There are also a number of approaches that let the LLM interact with the browser directly:

  • Dedicated browser - OpenAI has had its own Chrome-fork called Atlas. This is now built into its Desktop App like Claude does.
  • Chrome extension — Codex and Claude have Chrome extensions.
  • Hosted virtual browser — ChatGPT’s Agent mode renders a restricted Chrome browser in the cloud with an added extension
ChatGPT Agent mode showing a hosted Chromium window where chrome://system is blocked by organization policy and the ChatGPT Agent extension reports no access.
ChatGPT Agent’s hosted Chromium browser uses a ChatGPT Agent extension.

The main advantage of this over Computer Use is that it can see and execute web-page code directly. Consuming a relatively small amount of text tokens is very efficient compared to interpreting images, especially when most of what is on the screen often is not immediately relevant to the task at hand. Unlike computer use, this is strictly limited to the browser, so it can’t help you with native desktop or mobile apps.

I have had decent success having AI browser control fill out long forms in the background. However, I often see it get confused on more complex UI interactions. Parsing the website’s DOM and code and iterating through many interactions can still consume a lot of tokens.

Detecting and adapting to AI Agents isn’t easy

I spent a little bit of time exploring if I could detect if an LLM was controlling the browser. If I could tell if it is a robot visiting, then I could render a simpler—if uglier—agent-optimized app. Unfortunately there is no great universal way to detect robot visitors. Extensions remain hidden from apps unless they want to be exposed. I did find ChatGPT’s Agent remote browser does expose a Signature-Agent header, so that could possibly be used if you wanted to go down the route of an agent-specific page.

One advantage of exposing the DOM is that it can contain more information than appears visually on the page. Some of the machine-readable context I added for Generative Engine Optimization (GEO) could, in theory, provide additional guidance to an agent that inspects the page or follows the linked resources. Good accessibility practices should also help: descriptive ARIA labels and semantic fields such as type="tel" and aria-label="Fax destination" make the purpose of controls clearer to both assistive technologies and DOM-reading agents. My app is simple enough that I did not see much room for further optimization.

AI browser control — the agent operates a website through the browser

Primary user interface: the browser plus an agent window or side panel

Pros

Cons

  • Works with most web apps without an app-specific integration
  • More efficient and usually more reliable than full-screen computer use
  • Useful for long forms and tasks that span multiple websites
  • Limited to the browser; access to profiles, logins, and local files varies
  • Still slower and more brittle than structured API or tool calls
  • Complex widgets, pop-ups, CAPTCHAs, and DOM changes can derail the agent
  • The app has little control, and reliably detecting an agent is brittle

MCP Server

Model Context Protocol (MCP) is a standard way for an AI application to connect to external tools and data. The official MCP specification describes a client-server model where the host is the LLM application, the client manages the connection, and the server provides capabilities such as tools, resources, and prompts.

From the app developer’s perspective, this means your app can expose a machine-readable interface for the LLM to use. Instead of asking an agent to visually click through your website, the LLM can discover a set of explicit tools and call them with structured parameters. For example, my fax app exposes functions like start_session, attach_document, get_quote, etc.

This is much more efficient than Computer Use or browser control. The model does not need to interpret screenshots, inspect the DOM, or guess which button to click. The app tells the model what actions are possible and what parameters are required.

The downside is that a traditional MCP server is an interface for machines with no user interface. It works well when the model needs to retrieve information or execute a clear action. If the required flow is less clear and requires several MCP tools, then the risk of the LLM messing something up increases. Fortunately this is fine for my simple fax application.

There is also a discovery problem for most users. Developers and power users can add a custom MCP server URL, but that is too much friction for many casual users. Directories, app stores, and host LLM app recommendations may help, but a new or niche app should not assume users will find and configure its MCP server on their own.

Claude Code terminal locating the PromptFax MCP server at promptfax.app/mcp and preparing to add its Streamable HTTP endpoint to the Claude settings.
Claude Code easily discovered PromptFax’s MCP endpoint and added it to the client configuration.

MCP is easy for developers

Adding an MCP server was easy for me. If your app already has a clean API, building an MCP server is often straightforward. The main work is deciding which capabilities should become tools, how those tools should be described, what authentication is needed, and how much state the server should expose back to the model.

The most important design choice is tool shape. Do not just expose every internal API endpoint. Think in terms of the typical workflows the LLM agent should execute and bundle those into MCP tools. The model should not have to assemble a dozen low-level API calls if the app already understands the workflow.

MCP Server — structured tools and data for MCP-capable hosts

Primary user interface: the LLM host's chat interface

Pros

Cons

  • Explicit, machine-readable tools are faster and more reliable than visual automation
  • Developer defines supported workflows, schemas, authentication, and guardrails
  • Reusable across MCP-capable hosts
  • Widely supported - ChatGPT, Claude, coding tools, and many other MCP-capable clients
  • Strong fit for clear backend actions, data access, and state transitions
  • Setup can be too technical for many casual users
  • Discovery depends on directories, recommendations, or the user already knowing the app exists
  • Tool-only chat is awkward for files, visual review, forms, approvals, and payment
  • The model still decides when and how to call tools, so tool design matters
  • Remote authentication, file handling, and approval behavior remain inconsistent

MCP Apps

I ran into the same issue with the MCP server that I originally had with my LLM prompt-driven web application. For my use case, it is much easier for a user to click through a few controls than engage in a multi-turn chat interaction. The MCP server just replicates that prompt-only interface inside the LLM where the app has no real control beyond the tools it exposes.

Fortunately a solution for this exists: MCP Apps. The MCP project introduced this as the MCP Apps Extension. MCP Apps start with an MCP server, but add user interface elements implemented as web widgets that are rendered inside the LLM host (Claude, ChatGPT, etc.).

The motivation for this is simple: not every app interaction should be reduced to text. As I discovered with my in-app Agent attempt, some tasks are just better executed with simple GUI controls like buttons, previews, forms, tables, etc. Without a UI layer, the model either has to describe everything in chat or the host has to invent its own way to render each tool result. That does not scale well.

MCP’s SEP-1865 standardizes this. A server can declare UI resources and link them to tools. The host can render those resources in a sandboxed iframe and let the UI communicate with the host using structured messages. In plain English: the LLM can still reason and call tools, but the user can interact with an actual app surface when that is better than typing back and forth.

The important caveat is that MCP App support is not as universal as MCP server support. ChatGPT has its Apps SDK and supports MCP Apps-style embedded UIs. Claude has Connectors and also supports interactive connector experiences in some contexts. Other MCP clients may only support text and tool calls. That means an app should treat MCP Apps as progressive enhancement, not as a replacement for an MCP server.

ChatGPT conversation showing a PDF attachment, a fax request, and the embedded PromptFax widget with document upload, destination number, and Pay & send steps.
The PromptFax MCP App embeds the main workflow as a web widget inside ChatGPT.

Implementing the MCP App ✕ 3

My app evolved exactly as I outlined above, so I already had a basic UI for users and a remote MCP server. So to build this, I had to adapt my UI to a widget that fit ChatGPT’s App specs. Then I had to adapt it again to work for Claude’s MCP connector behavior. Finally, I adapted it again to fit the generic MCP Apps model as described by the SEP-1865 spec. It sounds like a lot of work, but it was fast to vibe code the first iteration after I better abstracted my front end.

Handling MCP App failures did introduce some architectural challenges. For example, if a user has a file they want to use in their context history, the LLM should attempt to attach it. Unfortunately the LLM hosts do not have a reliable mechanism for uploading files to external services (ChatGPT is currently better than Claude here). If the LLM is unable to attach a file, I still need a fallback mechanism—which for me is the normal web app. To prevent users from having to completely start over, I had to share state between the MCP server, widget, and my web app. This made everything much more complex, which necessitated more testing. Then, setting up automated tests to install and use the MCP App inside ChatGPT and Claude chewed up a lot of time.

MCP Apps — an interactive app surface backed by MCP

Primary user interface: LLM chat plus an app-owned embedded widget

Pros

Cons

  • Combines model reasoning and tool calls with familiar controls, previews, and confirmations
  • Lets the app guide workflows in which the user and agent both participate
  • Better suited than chat alone to files, forms, document review, and checkout
  • Builds on the MCP server and can fall back to tool-only clients
  • Host support and UI capabilities differ, so portability is incomplete
  • Requires widget UI plus shared state and fallback paths across the host, MCP server, and web app
  • More implementation and automated testing than a tool-only MCP server
  • App-directory discovery is not guaranteed

WebMCP

MCP and MCP Apps assume the user starts inside an MCP client like ChatGPT, Claude, Cursor, etc. WebMCP flips that around.

MCP offers server-side APIs. MCP Apps need server-hosted web widgets. WebMCP is a different approach that doesn’t require a server at all. WebMCP exposes an agent-readable interface to the browser or browser-side agent. Everything stays in the browser environment. WebMCP is currently in origin trials in both Chrome and Edge (versions ≥ 149).

With normal browser automation, the agent has to inspect the page, infer which buttons matter, and operate the UI like a human. With WebMCP, the page can explicitly tell the agent what actions are available. In theory, the agent no longer has to guess what controls like “Submit,” “Continue,” or “Pay” do. The site can expose structured tools, prompts, and resources for interacting with the web app. The web app could use those tools to interact with some server-side component, but that is not required.

Who and what exactly will invoke WebMCP is not totally clear. Chrome has Ask Gemini built into the browser and Edge has the equivalent in Copilot, which loads in a side panel. These assistants, browser extensions, and other AI browsers could potentially offer to use these tools when a user visits a site that exposes them. However, it is also possible that an LLM could load a browser and use WebMCP as discussed in the AI Browser Control use case. The exact trigger, permission prompt, UI placement, and relationship to existing ChatGPT/Claude/Gemini/Copilot experiences have not been fully described.

At a minimum, I would guess that this capability will be added to the built-in Gemini in Chrome and Copilot in Edge, but who knows when.

Implementation

The first step was deciding what tools to expose. My exposed WebMCP tools are similar to my MCP ones, but not all the same. Items like start_session aren’t needed because the page load handles that. I bundled things like the destination number as a parameter in the MCP server, but I needed a dedicated tool to drive the UI element in WebMCP.

Then the Web App needs to register its tools, which is as simple as this:

document.modelContext.registerTool({  // note: navigator.modelContext deprecated
  name: "set_destination_number",
  inputSchema: { /* ... */ },

  execute: async (args) => {
    // code to validate the number and update the UI
  }
});

The hardest part is actually finding an LLM interface that uses WebMCP. Gemini in Chrome and Copilot in Edge don’t use it (at least, not yet), so you need a way to trigger the tool calls. I used the WebMCP - Model Context Tool Inspector extension by a Google Chrome Evangelist. This lets you invoke the WebMCP tools manually and with Gemini.

PromptFax open beside the WebMCP Model Context Tool Inspector, which lists browser tools and shows an agent calling set_destination_number to update the fax number on the page.
The WebMCP inspector extension shows WebMCP tools and lets you invoke them with Gemini or manually.

I noticed the LLM would sometimes hallucinate a number. For now, I will continue requiring human verification of fields before sending.

WebMCP — structured website actions for a browser-side agent

Primary user interface: the website plus a browser-associated agent (still TBD)

Pros

Cons

  • Starts from the normal website, which remains visible and authoritative
  • Replaces DOM/screenshot guessing with explicit, app-defined actions
  • Could let browser agents discover capabilities without a separate host-specific integration
  • Lets the developer reserve sensitive steps for explicit human confirmation
  • No mainstream end-user flow yet; invocation, permissions, and agent UI remain unsettled
  • Support and tooling are experimental and fragmented
  • Discovery benefit is still speculative and begins only after the site is reached
  • Dynamic tool registration and third-party scripts introduce new security concerns
  • Does not replace remote MCP when the task begins in a non-browser host

Direct API Exposure → GPTs

There is one other mechanism - exposing an API directly and hoping the LLM can figure out how to use it. Most leading LLMs have some sort of code generation capability that could navigate an OpenAPI definition and then use curl or some Python code to interact with the app via an API. However, authentication makes the interaction more complicated since some back-and-forth is needed. The LLM would need some guidance on how to deal with the various interactions. That could be defined in a SKILL.md, but that is one more thing that needs to be installed.

None of this is a big deal for developers who do this sort of thing all the time, but it is not generally consumer-friendly—particularly in my case where use is designed to be infrequent. In addition, one of the drivers behind MCP was to solve the challenges of direct API exposure in an agent-friendly way. This problem is already addressed there.

I initially dismissed the idea of exposing another API. Then I discovered OpenAI’s “no digital services commerce” terms would not let me publicly publish my plugin. So then I looked into Custom GPTs as an alternative. Custom GPTs are basically a direct API exposure approach. Unlike Plugins, custom GPTs are easy to self-publish in ChatGPT’s public GPT directory. ChatGPT’s custom GPTs just use an OpenAPI definition, a static API key, and LLM instructions.

ChatGPT Custom GPT editor for PromptFax showing API-key authentication, an OpenAPI actions schema and endpoints on the left, with the published GPT preview and starter prompts on the right.
A Custom GPT maps PromptFax’s OpenAPI schema to ChatGPT Actions and prompt starters.

Custom GPT Implementation

This was another case of adapting the endpoints I made for the MCP server and swapping out OAuth for a static API key. You make an OpenAPI JSON file (example here) to describe the endpoints and set up a basic key mechanism for authentication. Custom GPTs also come with Instructions that essentially act like your system prompt. I had to do some prompt engineering to improve the flow, but nothing major.

The MCP App / Plugin is definitely a better user experience, but this custom GPT can be used by anyone with one click: https://chatgpt.com/g/g-6a4ee7fc2f988191a9e13903e53958a0-promptfax

Custom GPT — LLM calls an OpenAPI-described API

Primary user interface: ChatGPT prompts

Pros

Cons

  • Reuses an existing API and OpenAPI schema
  • Easy to implement when the application already has structured endpoints
  • Custom GPTs can be published in ChatGPT’s public GPT directory and opened with one click
  • Instructions provide lightweight guidance without building a full MCP integration
  • General API use is not consumer friendly, especially when authentication requires back-and-forth
  • Static API keys are a poor fit for per-user authorization
  • Custom GPTs are specific to ChatGPT and do not carry over to other LLM hosts
  • Text-only interaction is awkward for files, previews, confirmations, and payment
  • The model must interpret the API schema and instructions, so prompt engineering and guardrails still matter

Conclusions

Let’s recap:

  • In-app LLM – trying to skip making a UI was a bad idea; with a good UI this isn’t really needed
  • Computer use – there is nothing special to be done here, so don’t worry about it if your UI is easy
  • Browser use – beef up your DOM with more info, but you should do that anyway for accessibility and GEO
  • MCP server – this is a critical starting point and worth doing even if it is really more developer-oriented
  • MCP apps – immature, but the right concept for an app like PromptFax
  • WebMCP – I like the concept, but it looks like we need to wait months to see exactly how this will be implemented
  • Custom GPT – only for ChatGPT, but this is the quickest way to publish something that works with an LLM, even if it has limited capabilities

I started this project because I was frustrated that I couldn’t find an LLM app to quickly send a fax for me. I assumed this would be easy. It was not. I did make progress on making this easier to do, but not to the level I would like yet. Unfortunately, none of these offer a seamless user experience in my situation today.

I did not discuss challenges related to file uploads and payments, which make my LLM-directed flows challenging. I look forward to experimenting with agentic commerce solutions such as ACP and MPP when those are more widely implemented. I’ll save this topic for a follow-up post.

I was also focused more on the “Work” persona. There are more options for the “Developer” persona. I need to publish a developer skill even if this is not at all designed for development.

So, none of this is great, but certainly there must be imminent improvements coming quickly? Then again, businesses are still requiring people to use a 30-year-old image transmission technology designed for landlines, so maybe I should adjust my expectations.