The Model Context Protocol (MCP) headless API lets an application expose itself to a large language model agent. An agent such as Claude Desktop, Claude Code, Codex, or opencode can read the current screen, drive the user interface, and call tools the application publishes.

The API reuses the accessibility semantics tree. The same immutable tree that describes the screen to VoiceOver and TalkBack also describes it to the agent, so any application is drivable without extra code. Application specific data and actions are exposed through the existing com.codename1.ai.Tool contract.

The socket transport runs anywhere the port can bind the loopback interface, which includes an application running on a device, so an agent can attach to a build on a phone as well as to the simulator. It’s blocked on a release build; see Development builds only. The stdio transport needs process standard input and so is supported by the JavaSE port, which powers the simulator and the desktop tooling.

The MCP menu

Every desktop Codename One tool, including the simulator and Codename One Settings, gets a native MCP pull-down menu. The menu is added by the JavaSE port, so a tool gets it without any code of its own. It’s the main way a user turns a tool into something an agent can drive.

The menu has these items:

  • Expose This Tool To Agents starts and stops the loopback MCP server for the running tool.

  • Install in MCP Hosts registers this tool with the MCP hosts detected on the machine: Claude Desktop, Claude Code, and Codex. After a host restarts, the tool appears as an MCP server the agent can use. A detected host whose configuration format isn’t supported yet is reported rather than written to.

  • Remove From MCP Hosts removes that registration.

  • Detect MCP Hosts lists the hosts found on the machine and where their configuration lives.

  • Debug Logging controls how much of the MCP conversation is echoed to the log.

Install writes a command that launches MCPStdioLauncher, a small bridge that relays the host’s standard input and output to the running tool’s socket. Hosts speak stdio and the tool serves a socket, so the bridge lets a stdio host such as Claude Desktop drive the already-running, human-visible tool.

Starting the server

Invoking the API is the switch. There is no build hint or property to set. The socket server lets an agent attach to a session a person is watching, which is the usual choice in the simulator:

MCP.startSocketServer(8765);

A tool that a host launches as a subprocess can serve over standard input and output instead:

MCP.startStdioServer();

The stdio transport is the standard MCP local transport, exchanging newline delimited JSON-RPC messages. While it runs, application logging is redirected away from standard output so it can’t corrupt the protocol stream. The stdio transport lives in the JavaSE port because it needs process standard input, which isn’t available on every target.

Development builds only

The socket server refuses to bind on a release build and throws an IllegalStateException instead.

The reason is what an attached agent can do. It reads the screen and drives the user interface, and the loopback interface is shared by everything on the device rather than being private to one application. On a phone that means any other installed application can connect to the port and drive yours. That’s a reasonable trade while you’re developing, because it’s how an agent attaches to a running build, and a poor one in an application a user installs.

A development build is a debuggable Android package or a development provisioned iOS build. Any other target counts as a release build, so a port that can’t tell the difference withholds the server rather than exposing it.

The JavaSE port is the exception, and it’s worth understanding before you rely on this. That port runs the simulator, the designer and the desktop tooling, and it can’t distinguish those from a desktop application packaged for distribution: there’s no debuggable flag and no provisioning profile to read. It reports a development build in every case, because reporting otherwise would take the MCP menu away from every desktop tool. A packaged desktop application is therefore not gated by this, and one that needs to withhold the server from its own release build has to supply that signal itself.

Nothing binds a port unless the application calls a starter, and an application that never calls one has no server at all. The gate matters for the case where a starter is left in the code by accident, which is the way a debugging facility usually reaches production.

The same distinction is available to application code, so a starter can be left in place and simply not fire in a shipped build:

if (Display.getInstance().isDebuggableBuild()) {
    MCP.startSocketServer(8765);
}

A build that ships to devices you control, such as a kiosk fleet, a test lab, or a managed enterprise deployment, can lift the block:

MCP.setAllowOnReleaseBuilds(true);
MCP.startSocketServer(8765);

Treat that call the way you’d treat any other decision to expose a control surface, because the device itself becomes the boundary.

Built-in user interface tools

Every server registers a small set of tools that read and drive the screen through the accessibility tree. An agent calls ui_snapshot to get the semantics tree of the current form as JSON, including the identifier, role, label, value, and state of each node, and the identifiers of the actions each node supports.

The agent drives the screen with ui_perform_action, which performs an action such as activate or setText on a node from the snapshot. The convenience tools ui_activate and ui_set_text wrap the common cases, and ui_find locates nodes by identifier, by label, or by screen coordinate.

Actions run on the Codename One EDT, so an agent never touches the live component tree directly. Each action returns whether it succeeded together with a fresh snapshot.

Publishing application tools

An application publishes its own data and actions as MCP tools. A Tool has a name, a description, a JSON schema for its parameters, and a handler:

MCP.addTool(new Tool(
        "current_user",
        "Returns the signed in user",
        "{\"type\":\"object\",\"properties\":{}}",
        new ToolHandler() {
            @Override
            public String invoke(String argumentsJson) {
                return "{\"name\":\"" + signedInUser + "\"}";
            }
        }));

The server merges these tools with the built-in tools when a host lists the available tools, and routes each call to the matching handler. The same Tool type is used by the Codename One AI client, so a tool defined once serves both a hosted agent and an in-application model.

Screenshots and the simulator

The server exposes a screenshot of the current form as an MCP image resource, so a vision capable model can see the screen alongside the semantic tree. The resource is enabled by default and can be disabled per server.

The JavaSE simulator drives the same MCP menu described above, so an application can be exposed and driven from the running simulator during development.

Debug logging

Because an agent drives the tool on its own, it helps to watch what it does. The server echoes the MCP conversation to the Codename One log at a level chosen through the menu’s Debug Logging item or in code:

MCP.setVerbosity(MCPVerbosity.FULL);

The levels of MCPVerbosity are, from quietest to loudest: OFF, ERRORS (only failed calls), SUMMARY (one line per call), and FULL (every request and response). Each level includes the one below it.

Registering with installed hosts

The Install and Remove items in the MCP menu call MCPClientRegistrar, which detects the MCP hosts installed on the machine and writes a server entry into each host configuration, so an end user doesn’t edit configuration by hand. Detection and registration are available to any Codename One tool, and they run inside the runtime because they use the portable FileSystemStorage.

A caller describes the server with an MCPClientDescriptor (a name and the command that launches it) and registers it with the detected hosts. Hosts whose configuration format isn’t yet supported are reported so the user can add the entry manually. opencode is the one that’s left: it nests its servers in an mcp block whose entries have a shape of their own.

Claude Desktop and Claude Code keep their servers in a JSON mcpServers object. Codex keeps its own in ~/.codex/config.toml, as one [mcp_servers.<name>] table per server, and that file is shared by the ChatGPT desktop app, the Codex CLI, and the Codex IDE extension, so registering once reaches all three.

The default path is the one that’s used. If you’ve moved your Codex configuration with CODEX_HOME, add the entry there by hand: the registrar runs inside the Codename One runtime, which has no System.getenv, so it can’t follow the variable.

The TOML file is edited as text rather than parsed and written back. Only the lines belonging to the one server are replaced, so every other server, setting, comment, and choice of formatting in the file survives the edit exactly as the user left it, and the file keeps its own line endings. Registering again replaces the entry instead of adding a second one, and removing takes the server’s env sub-table with it.

A configuration the editor can’t make sense of is left untouched, and the reason is logged. That covers a file that isn’t valid TOML, and the shapes it won’t rewrite: the server declared through a dotted key or an inline table, mcp_servers declared as an array of tables, or the same server declared twice. Losing a Codex configuration is a worse outcome than not registering.

The host reads its configuration at startup, so a host that’s already running needs a restart before the new server appears.