Nihmbus Extensions — Author Guide

A Nihmbus Extension is an installable package that adds capabilities to the agent. It's the equivalent of an OpenClaw plugin, and it maps onto three concepts Nihmbus already has:

Concept What it is Where it lives
Skill A SKILL.md markdown playbook that orchestrates tools. ClawHub / Anthropic-Agent-Skills compatible. a skills row
Tool An executable capability the agent can call. the agent tool registry
Extension A package bundling skills and/or tools (via a remote MCP server). extension_installations

A plain ClawHub skill (just a SKILL.md) is the degenerate case: an extension with skills and no MCP server. Conversely, a full extension can ship both a playbook and the executable tools that playbook calls.

As of extension API 1.1.0, an extension can also contribute product surface — a UI panel + nav, named callable actions, its own HTTP endpoints, and its own workspace-scoped data:

Block Adds See
ui A client-side React panel + sidebar nav (/x/<ext>) contribution-points §2
actions Named actions ext.<ext>.<name> (via /api/invoke, optionally agent tools) §3
api An HTTP namespace /api/ext/<ext>/* + the built-in /data store §3

This guide covers skills + the MCP tool tier; /help/extensions/contribution-points is the deep reference for ui/actions/api/data and the security model.

Executable tools are MCP-only. Nihmbus runs serverless, so extensions contribute executable agent tools by declaring a remote MCP server (Streamable-HTTP or SSE). Nihmbus connects as an MCP client per agent request and merges the server's tools into the registry, namespaced ext__<name>__<tool>. (UI runs client-side via Module Federation; actions/api run server-side under the calling user's context — see the contribution-points doc.)

Package layout

my-extension/
  nihmbus.extension.json            # the manifest (this file's format below)
  skills/
    my-skill/
      SKILL.md                    # one or more bundled skills

The manifest — nihmbus.extension.json

The authoritative schema is extensionManifestSchema in lib/extensions/manifest.ts (re-exported from @/lib/extensions). Validate your manifest against it:

import { extensionManifestSchema } from '@/lib/extensions'
extensionManifestSchema.parse(JSON.parse(fs.readFileSync('nihmbus.extension.json', 'utf8')))
{
  "manifestVersion": 1,
  "name": "vercel",                 // lowercase slug, unique per workspace
  "displayName": "Vercel / v0",
  "version": "0.1.0",               // semver of YOUR extension
  "publisher": "nihmbus",
  "description": "Generate a landing page with v0, buy a domain, deploy …",
  "homepage": "https://vercel.com",
  "icon": "▲",                      // emoji or URL
  "engines": { "nihmbus": ">=1.0.0" },// checked against the extension API version
 
  "permissions": ["spend", "domains.buy", "deploy"],   // ENFORCED (see "Permissions" below)
 
  "secrets": [
    {
      "key": "VERCEL_TOKEN",        // referenced by mcp.authSecret
      "label": "Vercel access token",
      "description": "Token with v0, Domains, and Deployments scope.",
      "required": false,            // false ⇒ tools may run in a simulated/limited mode
      "source": "vault",            // 'vault' (encrypted, default) | 'env'
      "category": "integrations"
    }
  ],
 
  "mcp": {
    "transport": "streamable-http", // 'streamable-http' | 'sse'
    "url": "https://mcp.example.com/mcp",   // may use {BASE_URL} for in-repo servers
    "authSecret": "VERCEL_TOKEN",   // which declared secret provides auth
    "headers": { "Authorization": "Bearer {secret}" },  // {secret} / {secret.FIELD} interpolated
    "toolAllowlist": ["generate_landing_page", "buy_domain", "deploy"],  // optional
    "mutatingTools": ["buy_domain", "deploy"]   // see "Spend & approval" below
  },
 
  // Optional product-surface blocks (extension API 1.1.0). Full reference:
  // docs//help/extensions/contribution-points
  "ui": {
    "remoteName": "ext_vercel",
    "remoteEntry": "/ext-remotes/vercel/remoteEntry.js",  // https URL, version-pinned
    "exposedModule": "./Panel",
    "requestedIsolation": "federated",                    // a request; the HOST decides the tier
    "nav": [{ "id": "home", "label": "Vercel / v0", "icon": "▲", "route": "" }]
  },
  "actions": [{
    "name": "deploy",                                     // → ext.vercel.deploy
    "description": "Deploy and attach a domain.",
    "method": "POST", "mutating": true,
    "inputSchema": { "type": "object", "properties": { "projectName": { "type": "string" } }, "required": ["projectName"] },
    "handler": { "kind": "mcp", "tool": "deploy" },        // or { "kind": "http", "path": "/deploy" }
    "requiredPermissions": ["deploy"]
  }],
  "api": {
    "routes": [{ "method": "GET", "path": "/summary", "requiredPermissions": [] }]
  },
 
  "skills": [{ "path": "skills/launch-event-landing-page" }]
}

A skills-only manifest omits mcp and secrets entirely — that's a valid extension and the same shape a bare ClawHub skill synthesizes into on import. The ui/actions/api blocks are likewise optional and independent.

SKILL.md — the playbook

Standard Agent Skills / ClawHub format: YAML frontmatter + markdown body. Only the description (and triggers) are injected into the agent's prompt; the full body loads on demand via skill_load.

---
name: launch-event-landing-page
description: Spin up a promotable landing page … Use when the user says "make a landing page and market it".
license: MIT
triggers: [landing page, v0, launch site]
---
 
# Launch event landing page
 
## Procedure
1. Resolve the subject …
2. Call `ext__vercel__generate_landing_page`
3. **Checkpoint (spend):** confirm, then call `ext__vercel__buy_domain`
4. Deploy and market with `publish_to_channels`.

Recognized frontmatter: name, description (required); license, homepage, triggers, user-invocable, disable-model-invocation, command-dispatch, command-tool. Unknown keys are preserved verbatim in the skill's frontmatter.

The MCP server contract

Your server is a standard MCP server reachable over Streamable-HTTP. Nihmbus:

  1. Resolves mcp.authSecret and interpolates it into mcp.headers (so your server authenticates the request however you like — typically a bearer token).
  2. Calls listTools() and registers each tool as ext__<name>__<tool>.
  3. Calls callTool({ name, arguments }) when the agent invokes one. Return the standard MCP result ({ content: [{ type: 'text', text }] }, or structuredContent); set isError: true to surface a failure to the agent.

See app/api/ext/vercel-mcp/[transport]/route.ts for a working reference server built with mcp-handler. It runs in simulated mode when no token is configured — a good pattern so your extension demos on a fresh install.

In-repo vs vendor-hosted MCP servers. The bundled Vercel and Commerce extensions point mcp.url at our own in-repo servers ({BASE_URL}/api/ext/…), which make REST calls to the vendor and add guardrails (pricing band, idempotency, simulated fallback, a curated tool surface). To point an extension at a vendor's own hosted MCP server instead, you only change the mcp block — see /help/extensions/swapping-mcp-servers for the how + the trade-offs.

Installing

From the Extensions page (/extensions) or extensions.install:

  • catalog — bundled, one-click (e.g. the Vercel reference extension).
  • git / clawhubowner/repo[/subdir][@ref]. A repo with a nihmbus.extension.json installs as a full extension; a repo with only a SKILL.md synthesizes a skill-only extension (full ClawHub interop).
  • url — a JSON manifest, or { manifest, skills }.
  • local — a package dir under extensions/.

Installing creates the skills rows (published, so they're discoverable) and registers the MCP server. Uninstalling removes the row and cascade-deletes its skills.

Permissions (enforced)

permissions is no longer consent-only. The subset granted at install is stored on the install (granted_permissions), and an action, api route, or data op that declares requiredPermissions is rejected (403) unless the install holds them. data.read / data.write gate the built-in store. See contribution-points §4.

Spend & approval (important)

MCP tools are opaque — Nihmbus can't tell that buy_domain spends money. Guard spend with all three:

  1. mcp.mutatingTools — tags the tool with a "mutates state" hint in its description (the same signal native mutating actions carry).
  2. permissions — granted at install and enforced for actions/api.
  3. The skill body — make spend steps an explicit confirmation checkpoint ("only after the user confirms, call buy_domain").

Limitation: requiresRole and the supervised-mission mutation queue apply to native + extension actions — they do not gate raw MCP tool calls invoked inside the model loop. Hard spend caps must live in your MCP server.

Security notes

  • MCP / api.baseUrl URLs must be https:// (localhost allowed for dev); private/link-local and cloud-metadata hosts are blocked (SSRF guard).
  • Secrets are encrypted at rest (AES-256-GCM) in the vault, flow only into outbound headers (never logged), and each resolution is audited.
  • UI is a trust decision, not a sandbox. The in-DOM federated tier runs with the user's ambient authority and is reserved for publisher: 'nihmbus' + allowlisted publishers (EXT_TRUSTED_PUBLISHERS); everyone else is forced to a sandboxed iframe. See contribution-points §4.

Roadmap

  • Declarative-REST and bundled-JS executable-tool tiers (MCP is the only tier today).
  • A postMessage host-SDK bridge for the iframe UI tier.
  • A hosted Nihmbus extension marketplace (today: bundled catalog + git/ClawHub/local).