Skip to main content
Glama

Solar

A runtime UI framework built for AI-generated code.

When an AI agent builds UI with React or Vue, it guesses. It guesses prop names, types, and valid values. When it gets something wrong, it gets a stack trace. Stack traces are not machine-readable feedback.

Solar gives agents a clear path. Call registry.manifest() and you get a JSON schema of every registered component, including what props it takes and what values are valid. Mount a component with wrong props and you get a ContractError, a structured object with expected, received, and fix fields. The agent reads fix and retries. No human required.


Core principles

  • Explicit contracts over conventions: props are typed and validated at the component boundary, not inferred

  • Component registry: models read a machine-readable manifest before generating any composition code

  • Structured errors: validation failures return parseable JSON an agent can act on directly

  • Declared side effects: effects declare what they depend on and what they touch; no implicit subscriptions

  • Typed composition: slot props enforce which component can fill them, with runtime validation

  • Runtime-based: no compiler required; debuggable in the browser as-is

  • Small surface area: fewer primitives means fewer ways to generate something wrong


Related MCP server: bunge-ds-mcp

How it works

Defining a component

Every component declares its shape explicitly. Register it so other components and models can discover it.

import { createElement, defineComponent, registry } from './framework/index.js'

const Button = defineComponent({
  name: 'Button',
  props: {
    label: { type: 'string', required: true },
    onClick: { type: 'function', required: true },
    variant: { type: 'string', enum: ['primary', 'secondary'], default: 'primary' },
  },
  render({ label, onClick, variant }) {
    return createElement('button', { class: variant, onclick: onClick }, label)
  }
})

registry.register(Button)

Structured errors

Pass the wrong type and you get a structured error, not a string. An agent can parse and self-correct without regex.

Button({ label: 42, onClick: () => {} })
// throws ContractError:
{
  "error": "ContractError",
  "component": "Button",
  "prop": "label",
  "expected": "string",
  "received": "number",
  "fix": "Pass a string value for \"label\""
}

Component registry

Before generating composition code, a model reads the full catalog:

registry.manifest()
// →
[
  {
    "name": "Button",
    "props": {
      "label": { "type": "string", "required": true },
      "onClick": { "type": "function", "required": true },
      "variant": { "type": "string", "enum": ["primary", "secondary"], "default": "primary" }
    }
  },
  ...
]

Typed slots (component composition)

A parent component declares which component type can fill a slot. Passing anything else throws a ContractError.

const Card = defineComponent({
  name: 'Card',
  props: {
    title: { type: 'string', required: true },
    action: { type: 'slot', accepts: 'Button', required: true },
  },
  render({ title, action }) {
    return createElement('div', { class: 'card' },
      createElement('h3', {}, title),
      action,
    )
  }
})

// valid
Card({ title: 'Hello', action: Button({ label: 'Go', onClick: () => {} }) })

// throws: Card: prop "action": expected slot(Button), got vnode with no _source
Card({ title: 'Hello', action: createElement('button', {}, 'Go') })

Compact h() notation

h() parses a dense array format into a vnode tree. It's registry-aware, so component names resolve automatically.

import { h } from './framework/index.js'

// plain DOM nodes
h(['div', { class: 'row' },
  ['p', {}, 'Hello'],
  ['button', { class: 'btn' }, 'Click'],
])

// registered component by name, dispatches to Button()
h(['Button', { label: 'Save', onClick: handleSave, variant: 'primary' }])

Hooks and effects

State is useState. Side effects use three explicit primitives instead of a general useEffect:

  • useResource: async data fetching with automatic AbortController cancellation on key change

  • useSubscription: event listener that attaches and detaches when source/event/handler changes

  • onMount / onUnmount: lifecycle callbacks that run once, not on every render

const UserCard = defineComponent({
  name: 'UserCard',
  props: { userId: { type: 'number', required: true } },
  render({ userId }) {
    const [width, setWidth] = useState(window.innerWidth)

    // cancels the previous fetch automatically when userId changes
    const { data, loading, error } = useResource({
      key: userId,
      fetch: async (signal) => {
        const res = await fetch(`/api/user/${userId}`, { signal })
        return res.json()
      },
    })

    // attaches once, re-attaches only if source/event/handler changes
    useSubscription({ source: window, event: 'resize', handler: () => setWidth(window.innerWidth) })

    onMount(() => analytics.track('UserCard mounted'))
    onUnmount(() => analytics.track('UserCard unmounted'))

    if (loading) return createElement('p', {}, 'Loading...')
    return createElement('p', {}, `${data.name} - viewport: ${width}px`)
  }
})

Architecture

framework/
├── core/
│   ├── createElement.js     # vnode factory
│   ├── render.js            # mount vnode tree to DOM
│   ├── diff.js              # reconcile old and new vnode trees
│   ├── hooks.js             # useState, useMemo, useResource, useSubscription, onMount, onUnmount
│   └── scheduler.js         # batch and defer re-renders
├── contract/
│   ├── defineComponent.js   # strict component definition with prop schema
│   ├── ContractError.js     # structured error class with toJSON()
│   ├── validate.js          # runtime prop validation
│   └── types.js             # primitive type definitions + slot type
├── runtime/
│   ├── reconciler.js        # component lifecycle and mount/unmount
│   └── events.js            # delegated event handling
├── registry.js              # component catalog and manifest
├── h.js                     # compact array notation parser
└── index.js                 # public API

Components live in components/, one per file, default export is the defineComponent call, self-registered on import. Rules a model can follow without inference.


Live demo

Open in StackBlitz — runs a 3-step agent loop in the browser: discover components via registry.manifest(), catch a ContractError on a bad mount, self-correct using the fix field.


Getting started

npm create solarbuild@latest my-app
cd my-app
npm run dev

Cursor plugin: cursor.directory/plugins/framework-solar — adds Solar rules to Cursor so your AI assistant knows the API out of the box.

Or use the CDN directly. No install, no build step:

<!DOCTYPE html>
<html>
<body>
  <div id="app"></div>
  <script type="module">
    import {
      defineComponent, mountComponent, registry,
      useState, createElement
    } from 'https://cdn.jsdelivr.net/npm/solarbuild/framework/index.js'

    const Counter = defineComponent({
      name: 'Counter',
      props: {},
      render() {
        const [count, setCount] = useState(0)
        return createElement('button', { onclick: () => setCount(n => n + 1) }, `Clicked ${count} times`)
      }
    })

    registry.register(Counter)
    mountComponent(Counter, {}, document.getElementById('app'))
  </script>
</body>
</html>

Running the demo

npm run dev

Open http://localhost:3456/demo/ (trailing slash required; it's a static server quirk).

The demo covers all framework features: static rendering, diffing, hooks, contract validation, declared effects, batched updates, registry manifest, compact h() notation, and typed slots.


MCP Server

solarbuild-mcp is an MCP server that exposes your Solar component registry as callable tools. AI agents can discover components, validate props, and self-correct at runtime — without reading static docs.

npm install solarbuild-mcp
{
  "mcpServers": {
    "solarbuild": {
      "command": "npx",
      "args": ["solarbuild-mcp", "--components", "./components"]
    }
  }
}

Tools: manifest (full registry schema), component (single component schema), validate (props validation with fix instructions).

MCP Server docs · npm


Docs

Full documentation at docs.solarbuild.dev


What this is not

  • Not a meta-framework. No SSR, no routing, no build pipeline.

  • Not a React replacement. Narrower scope, different constraints.

  • Not optimized for humans writing components by hand, though it works fine for that too.

The thesis: the next wave of frameworks won't be designed for developers. They'll be designed for the models developers use to write code. This is an early attempt at that.

A
license - permissive license
-
quality - not tested
A
maintenance

Maintenance

Maintainers
Response time
0dRelease cycle
4Releases (12mo)
Commit activity

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Stahlwalker/framework-solar'

If you have feedback or need assistance with the MCP directory API, please join our Discord server