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

FieldTypeDescription
namestringDisplay name. Shown in the store and Launchpad.
descriptionstringShort description. Shown in search results.

Optional fields

FieldTypeDescription
authorobject{ "name": string, "url?": string }
ownersstring[]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.
iconstringRelative path to icon file. Default: "icon.png".
categoriesstring[]Only the first entry is used; extras are ignored. See Categories below.
tagsstring[]Searchable tags.
uiobjectUI config. Omit for tools-only apps.
ui.entrystringEntry point. Default: "ui/index.html".
ui.widthintegerWindow width in pixels. Default: 800.
ui.heightintegerWindow height in pixels. Default: 600.
authobjectAuth config. See Authentication.
permissionsobjectPermissions shown during install. { "network": ["api.example.com"] }
toolsarrayPre-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:

MethodDescription
initializeHandshake — returns protocol version and server info
tools/listReturns all tool definitions
tools/callExecutes 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

APIDescription
new ConstructApp({ name, version })Create app instance
app.tool(name, definition)Register a tool. Chainable.
export default appCloudflare Worker entry point
requireAuth(ctx)Throw if not authenticated. Use in tool handlers.
ctx.userIdUser ID from x-construct-user header
ctx.auth.access_tokenOAuth token from x-construct-auth header
ctx.isAuthenticatedWhether 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):

APIDescription
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):

APIDescription
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 → SettingsDeveloper, 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

1

Prepare your app

Make sure your repo has all required files:

  • manifest.json — with name and description
  • server.ts (or src/index.ts or index.ts) — MCP server entry point
  • icon.png — 256×256 icon (or icon.svg, icon.jpg)
  • README.md — used as the store description
2

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
3

Get your commit SHA

git rev-parse HEAD
# → abc123def456789abc123def456789abc123def4

This pins your app to an exact, auditable version.

4

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.

5

Open a pull request

CI automatically validates your submission:

  • Clones your repo at the pinned commit
  • Validates manifest.json has name and description
  • 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.ts exists and compiles (npm run build or deno check)
  • Verifies icon.png (or .svg/.jpg) and README.md exist

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

productivity developer-tools communication finance media ai-tools data utilities integrations shopping games

Troubleshooting

CI validation failed

Common fixes:

  • Missing manifest.json — add it to your repo root
  • Missing required fields — ensure name and description are present
  • No entry point — create server.ts, src/index.ts, or index.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.