Build & Publish an App
Create an app for Construct and share it with every user. Apps are just MCP servers — you write tools, the AI agent calls them. Optionally add a visual UI. The registry is fully open; every listing is a reviewable pull request. Full developer docs →
Quick Start
Scaffold a complete project in seconds:
git clone https://github.com/construct-computer/construct-app-sample.git my-app
cd my-app
pnpm install
pnpm dev
Your app is running at http://localhost:8787. Test it:
# Health check
curl http://localhost:8787/health
# List tools
curl -X POST http://localhost:8787/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# Call a tool
curl -X POST http://localhost:8787/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"hello","arguments":{"name":"World"}},"id":2}'
Use --with-ui to include a visual interface, or --no-ui for a tools-only app.
Project Structure
Every Construct app follows this layout:
my-app/
├── manifest.json # App metadata (required)
├── server.ts # MCP server — registers tools (required)
├── icon.png # 256×256 icon (required; .svg and .jpg also work)
├── README.md # Used as the store description (required)
├── package.json # Dependencies and scripts
├── wrangler.toml # Cloudflare Workers config for local dev
├── .gitignore
└── ui/ # OPTIONAL — visual interface
├── index.html # UI entry point, loads the Construct SDK
└── construct.d.ts # TypeScript types for construct.* globals
Tools-only apps (no UI) skip the ui/ directory and the ui field in their manifest.
Entry points: The registry looks for server.ts, src/index.ts, or index.ts (in that order).
manifest.json
Defines your app's metadata. The shape is described by the JSON Schema — set $schema to get editor autocomplete + validation. CI re-checks required fields at PR time.
{
"$schema": "https://raw.githubusercontent.com/construct-computer/app-sdk/main/schemas/manifest.schema.json",
"name": "My App",
"description": "A short one-line description of what your app does.",
"author": { "name": "Your Name", "url": "https://github.com/you" },
"owners": ["your-github-login"],
"icon": "icon.png",
"categories": ["utilities"],
"tags": ["example", "demo"],
"ui": {
"entry": "ui/index.html",
"width": 800,
"height": 600
}
}
Required fields
| Field | Type | Description |
|---|---|---|
name | string | Display name. Shown in the store and Launchpad. |
description | string | Short description. Shown in search results. |
Optional fields
| Field | Type | Description |
|---|---|---|
author | object | { "name": string, "url?": string } |
owners | string[] | GitHub logins (lowercase, ^[a-z0-9][a-z0-9-]{0,38}$). Gates who can submit registry PRs bumping this app's pinned commit, and who can manage env vars via the developer dashboard. |
icon | string | Relative path to icon file. Default: "icon.png". |
categories | string[] | Only the first entry is used; extras are ignored. See Categories below. |
tags | string[] | Searchable tags. |
ui | object | UI config. Omit for tools-only apps. |
ui.entry | string | Entry point. Default: "ui/index.html". |
ui.width | integer | Window width in pixels. Default: 800. |
ui.height | integer | Window height in pixels. Default: 600. |
auth | object | Auth config. See Authentication. |
permissions | object | Permissions shown during install. { "network": ["api.example.com"] } |
tools | array | Pre-declared tool list. Auto-discovered on deploy if omitted. |
MCP Server
Your server.ts is a Cloudflare Worker that handles JSON-RPC 2.0 requests on POST /mcp. Three methods:
| Method | Description |
|---|---|
initialize | Handshake — returns protocol version and server info |
tools/list | Returns all tool definitions |
tools/call | Executes a tool and returns the result |
Using the App SDK (recommended)
The @construct-computer/app-sdk handles all the JSON-RPC boilerplate for you:
import { ConstructApp } from '@construct-computer/app-sdk';
const app = new ConstructApp({ name: 'my-app', version: '1.0.0' });
app.tool('hello', {
description: 'Say hello to someone',
parameters: {
name: { type: 'string', description: 'Who to greet' },
},
handler: async (args) => {
return `Hello, ${args.name}!`;
},
});
export default app;
For production, the SDK is inlined into your server.ts (the scaffolder does this automatically). You can also npm install @construct-computer/app-sdk for local development with TypeScript types.
Writing from scratch
If you prefer, handle the JSON-RPC protocol yourself. You must handle initialize, tools/list, and tools/call. Your handler must also respond to GET /health with "ok" and handle CORS preflight (OPTIONS). The x-construct-user and x-construct-auth headers may be present on any request.
Handler return values
Tool handlers can return:
- A string — automatically wrapped in a text content block:
return "Hello!" - A ToolResult — for errors or multiple blocks:
return { content: [{ type: 'text', text: 'Query is required' }], isError: true }
App SDK Reference
Install for local development: npm install @construct-computer/app-sdk
| API | Description |
|---|---|
new ConstructApp({ name, version }) | Create app instance |
app.tool(name, definition) | Register a tool. Chainable. |
export default app | Cloudflare Worker entry point |
requireAuth(ctx) | Throw if not authenticated. Use in tool handlers. |
ctx.userId | User ID from x-construct-user header |
ctx.auth.access_token | OAuth token from x-construct-auth header |
ctx.isAuthenticated | Whether valid auth credentials are present |
Adding a Visual UI
If you want users to interact with your app directly (not just through the AI), add a ui/ directory:
1. Add the ui field to your manifest:
"ui": {
"entry": "ui/index.html",
"width": 800,
"height": 600
}
2. Create ui/index.html
Include the Construct SDK to communicate with the platform:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My App</title>
<link rel="stylesheet" href="/sdk/construct.css">
<script src="/sdk/construct.js"></script>
</head>
<body>
<div class="app">
<input id="name" type="text" placeholder="Enter name" />
<button class="btn" onclick="runTool()">Greet</button>
<div id="output"></div>
</div>
<script>
construct.ready(() => {
construct.ui.setTitle('My App');
});
async function runTool() {
const result = await construct.tools.callText('hello', {
name: document.getElementById('name').value
});
document.getElementById('output').textContent = result;
}
</script>
</body>
</html>
3. Local development
Add a [assets] section to wrangler.toml:
[assets]
directory = "./ui"
binding = "ASSETS"
not_found_handling = "none"
run_worker_first = ["/*"]
Then serve static files through the ASSETS binding in your server. In production, UI files are served from GitHub's CDN automatically.
Browser SDK
The SDK is a postMessage bridge that lets your UI communicate with Construct. Add to your ui/index.html:
<link rel="stylesheet" href="/sdk/construct.css">
<script src="/sdk/construct.js"></script>
Core methods (always available):
| API | Description |
|---|---|
construct.ready(cb) | Run code when the SDK bridge is ready. Always wrap init code in this. |
construct.tools.call(name, args) | Call an MCP tool. Returns { content, isError }. |
construct.tools.callText(name, args) | Call a tool, get just the text result. Most common. |
construct.ui.setTitle(title) | Update the window title bar. |
construct.ui.getTheme() | Get { mode: 'light'|'dark', accent }. |
construct.ui.close() | Close this app window. |
Extended methods (only available inside the Construct desktop):
| API | Description |
|---|---|
construct.state.get() | Read persistent app state (max 1MB). |
construct.state.set(state) | Write state. Triggers onUpdate on all clients. |
construct.state.onUpdate(cb) | Subscribe to state changes from the agent or other tabs. |
construct.agent.notify(message) | Send a message to the AI agent. |
TypeScript declarations are available at ui/construct.d.ts (included in the template repo). The full API reference is in the developer docs.
Authentication
Construct supports four auth schemes: oauth2, api_key, bearer, and basic. Declare any combination in auth.schemes[]; the user picks one when connecting.
1. Declare auth schemes in your manifest:
"auth": {
"schemes": [
{
"type": "oauth2",
"label": "Sign in with Example",
"authorization_url": "https://api.example.com/oauth/authorize",
"token_url": "https://api.example.com/oauth/token",
"scopes": ["read", "write"],
"scope_separator": " "
},
{
"type": "api_key",
"label": "Use API Key",
"instructions": "Get your key at https://api.example.com/settings/keys",
"fields": [
{ "name": "api_key", "displayName": "API Key", "type": "password", "required": true }
]
}
]
}
OAuth client_id / client_secret are not put in the manifest. They are stored as platform secrets (APP_OAUTH_<APP_ID>_CLIENT_ID / _CLIENT_SECRET); open an issue on the registry repo to have them added.
2. Guard authenticated tools:
import { requireAuth } from '@construct-computer/app-sdk';
app.tool('get_my_account', {
description: 'Get the authenticated user account',
handler: async (args, ctx) => {
requireAuth(ctx); // throws if not authenticated
// ctx.auth.type is 'oauth2' | 'api_key' | 'bearer' | 'basic'
// OAuth: ctx.auth.access_token / refresh_token / expires_at
// api_key/bearer: whichever field name you declared
// basic: ctx.auth.username, ctx.auth.password
const token = ctx.auth.access_token || ctx.auth.api_key;
const res = await fetch('https://api.example.com/me', {
headers: { Authorization: `Bearer ${token}` },
});
return await res.text();
},
});
Mix public and authenticated tools in the same app. The platform injects auth credentials via the x-construct-auth header when the user has connected their account. OAuth tokens are refreshed automatically before dispatch.
Testing Locally
# Start dev server
npm run dev # runs on http://localhost:8787
# Health check
curl http://localhost:8787/health
# Test MCP endpoint
curl -X POST http://localhost:8787/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# Test with auth headers
curl -X POST http://localhost:8787/mcp \
-H 'Content-Type: application/json' \
-H 'x-construct-auth: {"access_token":"test-token","user_id":"user-123"}' \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"hello","arguments":{"name":"World"}},"id":2}'
Test in Construct: Open Construct → Settings → Developer, toggle Developer Mode on, paste http://localhost:8787 into Connect Dev Server, and click Connect. Construct validates /health + /mcp, registers your tools with the agent, and opens your UI in a sandboxed window.
Remote testing: Use cloudflared tunnel to expose your local server, then paste the tunnel URL into Connect Dev Server:
cloudflared tunnel --url http://localhost:8787
Publishing to the Registry
Prepare your app
Make sure your repo has all required files:
manifest.json— withnameanddescriptionserver.ts(orsrc/index.tsorindex.ts) — MCP server entry pointicon.png— 256×256 icon (oricon.svg,icon.jpg)README.md— used as the store description
Push to GitHub
Create a public repo. The naming convention is construct-app-{name}:
git init && git add -A
git commit -m "Initial release"
git remote add origin [email protected]:you/construct-app-myapp.git
git push -u origin main
Get your commit SHA
git rev-parse HEAD
# → abc123def456789abc123def456789abc123def4
This pins your app to an exact, auditable version.
Add a pointer file
Fork construct-computer/app-registry and add apps/{your-app-id}.json:
{
"repo": "https://github.com/you/construct-app-myapp",
"versions": [
{
"version": "1.0.0",
"commit": "abc123def456789abc123def456789abc123def4",
"date": "2026-04-10"
}
]
}
The pointer only needs repo and versions. The listing (name, description, icon, etc.) is read from your repo's manifest.json at the pinned commit.
The app ID (filename without .json) must match ^[a-z0-9][a-z0-9-]{0,40}[a-z0-9]?$ and not be a reserved name (registry, apps, api, www, auth, admin, etc.). The registry appends a random suffix, so your app lives at {your-app-id}-{nanoid}.apps.construct.computer.
Open a pull request
CI automatically validates your submission:
- Clones your repo at the pinned commit
- Validates
manifest.jsonhasnameanddescription - Enforces the ownership gate: if
manifest.owners[]is set, the PR author's GitHub login must be in it - Checks that
server.ts/src/index.ts/index.tsexists and compiles (npm run buildordeno check) - Verifies
icon.png(or.svg/.jpg) andREADME.mdexist
Once a maintainer approves and merges, your app goes live within minutes!
Updating Your App
Push the update to your repo, then open a PR to the registry adding a new version:
{
"repo": "https://github.com/you/construct-app-myapp",
"versions": [
{ "version": "1.0.0", "commit": "abc123...", "date": "2026-04-01" },
{ "version": "1.1.0", "commit": "def456...", "date": "2026-04-10" }
]
}
The last entry in the versions array becomes the current version. Previous versions remain accessible in the version history.
Bumps require the PR author to be in manifest.owners[] (when set), so add co-maintainers there in your app repo before they try to publish.
Categories
Troubleshooting
CI validation failed
Common fixes:
- Missing manifest.json — add it to your repo root
- Missing required fields — ensure
nameanddescriptionare present - No entry point — create
server.ts,src/index.ts, orindex.ts - No icon — add
icon.png(or.svg/.jpg) - Missing README.md — add one to your repo root
App not appearing in the store
Make sure the PR was merged (not just opened), the commit SHA is correct, and wait a few minutes for the sync pipeline.
Auth header not received
The x-construct-auth header is only present when the user has connected their account. Test locally by adding headers manually with curl.
UI not loading
Make sure manifest.json has the ui field, ui/index.html exists, and you're loading the SDK from /sdk/construct.js and /sdk/construct.css.