@hanzo/build 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,9 @@ the database, the sign-in and the storage already there.
5
5
 
6
6
  The builder is one component, `<Builder host={…} />` from `@hanzo/build`, with
7
7
  three hosts: this page (hanzo.build), and the Dev section of the Hanzo app at
8
- hanzo.ai/dev and hanzo.app/dev. None of them forks it.
8
+ hanzo.ai/dev and hanzo.app/dev. None of them forks it. hanzo.build is this
9
+ frontend. A click stays on this origin — sign-in returns to `/auth/callback`
10
+ here — and github.com is opened only for the repository grant.
9
11
 
10
12
  ```
11
13
  pnpm install
@@ -68,6 +70,7 @@ hanzo.id token for `Host.admin`. One left column, never two.
68
70
  | `''` | New — the empty state and the composer |
69
71
  | `sess_<32 hex>` | one run, live |
70
72
  | `-/automations` | repeating work, read from `/v1/auto/flows` |
73
+ | `-/mcp` | the fleet's native MCP servers, from `POST /v1/mcp` `tools/list` |
71
74
  | `-/codebases` | the forge's repositories |
72
75
  | `-/sync` | bring granted repositories onto the forge |
73
76
  | `-/projects` | the forge's boards |
@@ -82,10 +85,13 @@ collide.
82
85
  ## What is on the screen
83
86
 
84
87
  **The rail** (hanzo.build). New, then Automations, then the forge — Codebase, Projects, Issues —
85
- then Artifacts, Customize (the platform's plugins), More (Templates, Machines,
86
- Docs), then the org's coding runs newest first with a live status dot, and the
87
- account. Collapse is an explicit toggle kept in this browser. A host with its
88
- own rail draws `DevSection` there instead.
88
+ then MCP, Artifacts, More (Templates, Machines, Docs), then the org's coding
89
+ runs newest first with a live status dot, and the account. Collapse is an
90
+ explicit toggle kept in this browser. A host with its own rail draws
91
+ `DevSection` there instead.
92
+
93
+ **MCP.** The native servers `POST /v1/mcp` lists, each with the operations it
94
+ names. A run starts a server when it calls it, and the server stays up afterwards.
89
95
 
90
96
  **New.** "What's up next?", and at the foot the composer: where the run runs
91
97
  (Default is the platform's sandbox; the org's machines follow), the codebase
@@ -121,6 +127,7 @@ picker and open-in-tab; Share and Publish; the console dock under it.
121
127
  | `GET /v1/projects` · `POST /v1/projects/fork` · `GET /v1/templates` | artifacts, templates |
122
128
  | `GET /v1/git/repos/{name}/tree` · `/blob` | Files and Code |
123
129
  | `POST /v1/platform/apps` · `GET /v1/platform/builds` | Add to project, Publish |
130
+ | `POST /v1/mcp` | the native MCP servers, `tools/list` |
124
131
  | `GET /v1/models` · `POST /v1/event` | the model list, a verdict |
125
132
 
126
133
  `mode`, `model` and `effort` are sent with a run as asked; the platform
package/lib/account.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type { RailSession } from '@hanzo/ui/chat';
2
+ /** The builder listens, so a settings control on this page opens the account dialog. */
3
+ export declare function onAccount(fn: () => void): () => void;
4
+ export declare function revealAccount(): void;
2
5
  export declare function Account({ open, onOpenChange }: {
3
6
  open: boolean;
4
7
  onOpenChange: (o: boolean) => void;
package/lib/account.js CHANGED
@@ -8,10 +8,33 @@ import { LogOut, Settings } from '@hanzogui/lucide-icons-2';
8
8
  import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
9
9
  import { useMemo, useState } from 'react';
10
10
  import { useHost } from './host.js';
11
+ let reveal = null;
12
+ /** The builder listens, so a settings control on this page opens the account dialog. */
13
+ export function onAccount(fn) {
14
+ reveal = fn;
15
+ return () => {
16
+ if (reveal === fn)
17
+ reveal = null;
18
+ };
19
+ }
20
+ export function revealAccount() {
21
+ reveal?.();
22
+ }
23
+ /** A settings address on another host. This page's own account is the dialog itself. */
24
+ function accountSettings(href) {
25
+ if (typeof window === 'undefined')
26
+ return false;
27
+ try {
28
+ return new URL(href, window.location.origin).origin !== window.location.origin;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
11
34
  export function Account({ open, onOpenChange }) {
12
35
  const host = useHost();
13
36
  const who = host.person;
14
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 360, children: [_jsx(DialogTitle, { children: "Account" }), _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: who?.name || who?.email || 'Signed out' }), who?.email && who.name ? (_jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email })) : null, host.org ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Organization: ", host.org] })) : null] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", flexWrap: "wrap", children: [_jsxs(Button, { variant: "outline", size: "sm", onPress: () => host.open(host.links.settings), children: [_jsx(Settings, { size: 14 }), " Settings"] }), host.signOut ? (_jsxs(Button, { variant: "outline", size: "sm", onPress: () => host.signOut?.(), children: [_jsx(LogOut, { size: 14 }), " Sign out"] })) : null] })] }) }));
37
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 360, children: [_jsx(DialogTitle, { children: "Account" }), _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: who?.name || who?.email || 'Signed out' }), who?.email && who.name ? (_jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email })) : null, host.org ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Organization: ", host.org] })) : null] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", flexWrap: "wrap", children: [accountSettings(host.links.settings) ? (_jsxs(Button, { variant: "outline", size: "sm", onPress: () => host.open(host.links.settings), children: [_jsx(Settings, { size: 14 }), " Settings"] })) : null, host.signOut ? (_jsxs(Button, { variant: "outline", size: "sm", onPress: () => host.signOut?.(), children: [_jsx(LogOut, { size: 14 }), " Sign out"] })) : null] })] }) }));
15
38
  }
16
39
  export function Find({ open, onOpenChange, recents, onOpen, }) {
17
40
  const [q, setQ] = useState('');
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The fleet's native MCP servers, as the MCP server itself lists them.
3
+ *
4
+ * POST /v1/mcp {"method":"tools/list"} → {result:{tools:[…]}}
5
+ *
6
+ * One tool is one native server. Its operations are the names in `op`. A run
7
+ * starts a server the first time it calls it; the server stays up afterwards.
8
+ * `describe` is how a caller reads one operation's schema, so it is not a server.
9
+ */
10
+ import { type Target } from './call.ts';
11
+ export interface Native {
12
+ name: string;
13
+ description: string;
14
+ ops: string[];
15
+ }
16
+ /** The servers in one tools/list answer. A missing tool array is a refusal. */
17
+ export declare function nativesOf(body: unknown): Native[];
18
+ /** Every native server the fleet MCP server is serving. */
19
+ export declare function natives(t: Target): Promise<Native[]>;
package/lib/api/mcp.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The fleet's native MCP servers, as the MCP server itself lists them.
3
+ *
4
+ * POST /v1/mcp {"method":"tools/list"} → {result:{tools:[…]}}
5
+ *
6
+ * One tool is one native server. Its operations are the names in `op`. A run
7
+ * starts a server the first time it calls it; the server stays up afterwards.
8
+ * `describe` is how a caller reads one operation's schema, so it is not a server.
9
+ */
10
+ import { call, Refusal } from './call.js';
11
+ const words = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string' && x !== '') : []);
12
+ /** The servers in one tools/list answer. A missing tool array is a refusal. */
13
+ export function nativesOf(body) {
14
+ const root = body && typeof body === 'object' ? body : {};
15
+ const err = root.error;
16
+ if (err && typeof err === 'object') {
17
+ const message = err.message;
18
+ throw new Refusal(200, typeof message === 'string' && message ? message : 'The MCP server refused the list');
19
+ }
20
+ const result = root.result && typeof root.result === 'object' ? root.result : null;
21
+ const tools = result && Array.isArray(result.tools) ? result.tools : null;
22
+ if (!tools)
23
+ throw new Refusal(502, 'The MCP server did not list its tools');
24
+ const out = [];
25
+ for (const raw of tools) {
26
+ if (!raw || typeof raw !== 'object')
27
+ continue;
28
+ const t = raw;
29
+ const name = typeof t.name === 'string' ? t.name : '';
30
+ if (!name || name === 'describe')
31
+ continue;
32
+ const schema = t.inputSchema && typeof t.inputSchema === 'object' ? t.inputSchema : {};
33
+ const props = schema.properties && typeof schema.properties === 'object' ? schema.properties : {};
34
+ const op = props.op && typeof props.op === 'object' ? props.op : {};
35
+ out.push({
36
+ name,
37
+ description: typeof t.description === 'string' ? t.description : '',
38
+ ops: words(op.enum),
39
+ });
40
+ }
41
+ return out;
42
+ }
43
+ /** Every native server the fleet MCP server is serving. */
44
+ export async function natives(t) {
45
+ return nativesOf(await call(t, 'POST', '/v1/mcp', { jsonrpc: '2.0', id: 1, method: 'tools/list' }));
46
+ }
package/lib/builder.js CHANGED
@@ -11,6 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  * /-/issues the forge's issues
12
12
  * /-/artifacts what this org has built
13
13
  * /-/templates the public starters
14
+ * /-/mcp the fleet's native MCP servers
14
15
  * /<slug> a deployed project's workspace, in this same window
15
16
  *
16
17
  * The rail is the sessions rail every Hanzo surface shares: New, the builder's
@@ -29,7 +30,7 @@ import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, Kanban, LayoutTemplate, M
29
30
  import { Button } from '@hanzo/ui';
30
31
  import { SessionRail } from '@hanzo/ui/chat';
31
32
  import { HanzoMark } from '@hanzo/ui/product';
32
- import { useMemo, useState } from 'react';
33
+ import { useEffect, useMemo, useState } from 'react';
33
34
  import { pinBoard } from './choice.js';
34
35
  import { useKept, useRecents } from './data.js';
35
36
  import { Forge } from './forge.js';
@@ -40,7 +41,7 @@ import { path, route } from './route.js';
40
41
  import { Run } from './run.js';
41
42
  import { DOTS } from './section.js';
42
43
  import { Artifacts, Templates } from './shelf.js';
43
- import { Account, Find } from './account.js';
44
+ import { Account, Find, onAccount } from './account.js';
44
45
  function Shell() {
45
46
  const host = useHost();
46
47
  const t = useTarget();
@@ -51,6 +52,7 @@ function Shell() {
51
52
  const [order, setOrder] = useState('newest');
52
53
  const [account, setAccount] = useState(false);
53
54
  const [finding, setFinding] = useState(false);
55
+ useEffect(() => onAccount(() => setAccount(true)), []);
54
56
  const go = (p) => {
55
57
  setDrawer(false);
56
58
  host.go(p);
@@ -77,16 +79,45 @@ function Shell() {
77
79
  active: screen === 'issues',
78
80
  },
79
81
  { id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
80
- { id: 'customize', label: 'Customize', icon: _jsx(Puzzle, { size: 16 }), onPress: () => host.open(host.links.customize) },
82
+ { id: 'mcp', label: 'MCP', icon: _jsx(Puzzle, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'mcp' })), active: screen === 'mcp' },
81
83
  ];
82
84
  const more = [
83
85
  { id: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'templates' })), active: screen === 'templates' },
84
- { id: 'machines', label: 'Machines', icon: _jsx(Cpu, { size: 16 }), onPress: () => host.open(`${host.links.home.replace(/\/+$/, '')}/platform/computers`) },
86
+ {
87
+ id: 'machines',
88
+ label: 'Machines',
89
+ icon: _jsx(Cpu, { size: 16 }),
90
+ onPress: () => {
91
+ const home = host.links.home.replace(/\/+$/, '');
92
+ const computers = `${home}/platform/computers`;
93
+ try {
94
+ if (new URL(computers, window.location.origin).origin === window.location.origin) {
95
+ host.go('');
96
+ return;
97
+ }
98
+ }
99
+ catch {
100
+ return;
101
+ }
102
+ host.open(computers);
103
+ },
104
+ },
85
105
  { id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open('https://docs.hanzo.ai/docs/dev', '_blank', 'noopener,noreferrer') },
86
106
  ];
87
107
  return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsxs(YStack, { position: "relative", shrink: 0, children: [_jsx(SessionRail, { onNew: () => go(''), fresh: r.kind === 'new', links: links, more: more, recents: rows, active: r.kind === 'run' ? r.id : null, onOpen: (id) => go(id), onSort: () => setOrder(order === 'newest' ? 'running' : 'newest'), sortLabel: order === 'newest' ? 'Show running first' : 'Show newest first', empty: _jsx(SizableText, { size: "$1", color: "$soft", px: "$2", children: !host.person ? 'Sign in to see your runs.' : recents.error ? recents.error.message : recents.loading ? 'Reading…' : 'No runs yet.' }), account: host.person
88
108
  ? { name: host.person.email || host.person.name, onPress: () => setAccount(true) }
89
- : { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => host.open(host.links.settings), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : 44, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsxs(XStack, { position: "absolute", t: 8, l: 8, r: 8, z: 2, items: "center", gap: "$2", render: "button", "aria-label": host.org ? `Organization ${host.org}` : 'Hanzo', onPress: () => go(''), children: [_jsx(Brand, { org: host.org }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: host.org || 'Hanzo' })] }))] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsx(XStack, { height: 44, px: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Account, { open: account, onOpenChange: setAccount }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
109
+ : { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => {
110
+ try {
111
+ if (new URL(host.links.settings, window.location.origin).origin === window.location.origin) {
112
+ setAccount(true);
113
+ return;
114
+ }
115
+ }
116
+ catch {
117
+ return;
118
+ }
119
+ host.open(host.links.settings);
120
+ }, onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : 44, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsxs(XStack, { position: "absolute", t: 8, l: 8, r: 8, z: 2, items: "center", gap: "$2", render: "button", "aria-label": host.org ? `Organization ${host.org}` : 'Hanzo', onPress: () => go(''), children: [_jsx(Brand, { org: host.org }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: host.org || 'Hanzo' })] }))] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsx(XStack, { height: 44, px: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Account, { open: account, onOpenChange: setAccount }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
90
121
  }
91
122
  /** Whichever pane the address names, beside a rail — the builder's or the host's. */
92
123
  function Pane({ onStarted }) {
package/lib/forge.js CHANGED
@@ -10,6 +10,7 @@ import { SizableText, XStack, YStack } from '@hanzo/gui';
10
10
  import { ChevronDown, GitBranch, Plus, Settings, Workflow } from '@hanzogui/lucide-icons-2';
11
11
  import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
12
12
  import { useEffect, useState } from 'react';
13
+ import { revealAccount } from './account.js';
13
14
  import { add, arm, flows } from './api/auto.js';
14
15
  import { codebases, create } from './api/codebases.js';
15
16
  import { boards, issues } from './api/work.js';
@@ -17,6 +18,7 @@ import { boardKey, pinBoard, pinCodebase, readPending, writePending } from './ch
17
18
  import { useKept, useRead } from './data.js';
18
19
  import { useHost, useTarget } from './host.js';
19
20
  import { path } from './route.js';
21
+ import { Servers } from './mcp.js';
20
22
  import { Sync } from './sync.js';
21
23
  import { grants } from './api/github.js';
22
24
  function Head({ title, says }) {
@@ -40,6 +42,8 @@ export function Forge({ screen }) {
40
42
  return _jsx(Automations, {});
41
43
  if (screen === 'sync')
42
44
  return _jsx(Sync, {});
45
+ if (screen === 'mcp')
46
+ return _jsx(Servers, {});
43
47
  if (screen === 'codebases')
44
48
  return _jsx(Codebases, {});
45
49
  if (screen === 'projects')
@@ -169,7 +173,18 @@ function Codebases() {
169
173
  pinCodebase(host.org, c);
170
174
  host.go('');
171
175
  };
172
- return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => host.open(host.links.settings), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1 }), _jsx(YStack, { width: 240, minW: 0, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : when(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
176
+ return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => {
177
+ try {
178
+ if (new URL(host.links.settings, window.location.origin).origin === window.location.origin) {
179
+ revealAccount();
180
+ return;
181
+ }
182
+ }
183
+ catch {
184
+ return;
185
+ }
186
+ host.open(host.links.settings);
187
+ }, children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1 }), _jsx(YStack, { width: 240, minW: 0, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : when(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
173
188
  const row = await create(t, name, description);
174
189
  pinCodebase(host.org, row);
175
190
  setMaking(false);
package/lib/host.d.ts CHANGED
@@ -32,18 +32,18 @@ export interface Host {
32
32
  go: (path: string, how?: {
33
33
  replace?: boolean;
34
34
  }) => void;
35
- /** Where the builder links out to. Same-window addresses on the host, or https. */
35
+ /** Where the builder links out. On hanzo.build these are addresses of this page. */
36
36
  links: {
37
- /** Troubleshoot GitHub connection: the GitHub pane of the platform's plugins. */
37
+ /** GitHub for this page: bringing repositories onto the forge. */
38
38
  github: string;
39
- /** Customize: the platform's plugins (skills, MCP, connectors). */
39
+ /** Customize, an address of this host. */
40
40
  customize: string;
41
- /** Account settings. */
41
+ /** Account settings, an address of this host. */
42
42
  settings: string;
43
- /** The platform itself, where the mark leads. */
43
+ /** This host's home. On hanzo.build it is the page itself. */
44
44
  home: string;
45
45
  };
46
- /** Leave the builder for a host address, same window. */
46
+ /** Open a host address in this window. hanzo.build keeps the window on its own origin. */
47
47
  open: (href: string) => void;
48
48
  signIn?: () => void;
49
49
  signOut?: () => void;
package/lib/mcp.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function Servers(): import("react").JSX.Element;
package/lib/mcp.js ADDED
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The fleet's native MCP servers.
4
+ *
5
+ * The list is the MCP server's own tools/list. A server starts when a run
6
+ * calls it and stays up afterwards, so this pane lists them rather than
7
+ * starting them.
8
+ */
9
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
10
+ import { ChevronDown } from '@hanzogui/lucide-icons-2';
11
+ import { Input } from '@hanzo/ui';
12
+ import { useState } from 'react';
13
+ import { natives } from './api/mcp.js';
14
+ import { useRead } from './data.js';
15
+ import { useTarget } from './host.js';
16
+ export function Servers() {
17
+ const t = useTarget();
18
+ const list = useRead(() => natives(t), [], [t]);
19
+ const [q, setQ] = useState('');
20
+ const [open, setOpen] = useState(null);
21
+ const needle = q.trim().toLowerCase();
22
+ const shown = list.value.filter((s) => !needle || s.name.toLowerCase().includes(needle) || s.ops.some((op) => op.toLowerCase().includes(needle)));
23
+ const ops = shown.reduce((n, s) => n + s.ops.length, 0);
24
+ return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: _jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: "MCP" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Native servers. A run starts one when it calls it, and the server stays up afterwards." })] }), _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find a server or an operation\u2026", "aria-label": "Find a server" }), list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading the servers\u2026" })) : list.error && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : shown.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.value.length === 0 ? 'The MCP server listed no native servers.' : 'No server matches.' })) : (_jsxs(YStack, { gap: "$1", children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: [shown.length, " servers \u00B7 ", ops, " operations"] }), shown.map((s) => {
25
+ const on = open === s.name;
26
+ return (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { render: "button", "aria-expanded": on, "aria-label": s.name, items: "center", gap: "$3", px: "$3", py: "$2.5", hoverStyle: { bg: '$hover' }, onPress: () => setOpen(on ? null : s.name), children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: s.name }), s.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: on ? undefined : 2, children: s.description })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", children: s.ops.length }), _jsx(ChevronDown, { size: 14, style: { transform: on ? 'rotate(180deg)' : undefined } })] }), on ? (_jsx(YStack, { px: "$3", py: "$2", gap: "$1", borderTopWidth: 1, borderColor: "$borderColor", children: s.ops.length === 0 ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "This server named no operations." })) : (s.ops.map((op) => (_jsx(SizableText, { size: "$1", color: "$ink", children: op }, op)))) })) : null] }, s.name));
27
+ })] }))] }) }));
28
+ }
package/lib/route.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * so it can hold neither the `_` a session id carries nor start with `-`.
11
11
  * Anything else is not an address here and reads as the empty state.
12
12
  */
13
- export type Screen = 'artifacts' | 'templates' | 'codebases' | 'projects' | 'issues' | 'automations' | 'sync';
13
+ export type Screen = 'artifacts' | 'templates' | 'codebases' | 'projects' | 'issues' | 'automations' | 'sync' | 'mcp';
14
14
  export type Route = {
15
15
  kind: 'new';
16
16
  } | {
package/lib/route.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export const SESSION = /^sess_[0-9a-f]{32}$/;
2
2
  export const SLUG = /^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/;
3
- const SCREENS = ['artifacts', 'templates', 'codebases', 'projects', 'issues', 'automations', 'sync'];
3
+ const SCREENS = ['artifacts', 'templates', 'codebases', 'projects', 'issues', 'automations', 'sync', 'mcp'];
4
4
  export function route(path) {
5
5
  const p = path.replace(/^\/+|\/+$/g, '');
6
6
  if (SESSION.test(p))
package/lib/section.js CHANGED
@@ -11,7 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
11
11
  * host's own `go`.
12
12
  */
13
13
  import { SizableText } from '@hanzo/gui';
14
- import { Blocks, CircleDot, FolderGit2, Kanban, LayoutTemplate, SquarePen, Workflow } from '@hanzogui/lucide-icons-2';
14
+ import { Blocks, CircleDot, FolderGit2, Kanban, LayoutTemplate, Puzzle, SquarePen, Workflow } from '@hanzogui/lucide-icons-2';
15
15
  import { SidebarItem, SidebarSection, StatusDot } from '@hanzo/ui/chat';
16
16
  import { useMemo } from 'react';
17
17
  import { pinBoard } from './choice.js';
@@ -48,5 +48,5 @@ export function DevSection({ host, onPick, label = 'Runs', children, }) {
48
48
  return (_jsxs(_Fragment, { children: [_jsx(SidebarItem, { icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), active: r.kind === 'new', onPress: () => go(''), children: "New run" }), _jsx(SidebarItem, { icon: _jsx(Workflow, { size: 16, "aria-hidden": true }), active: screen === 'automations', onPress: () => go(path({ kind: 'screen', screen: 'automations' })), children: "Automations" }), _jsx(SidebarItem, { icon: _jsx(FolderGit2, { size: 16, "aria-hidden": true }), active: screen === 'codebases', onPress: () => go(path({ kind: 'screen', screen: 'codebases' })), children: "Codebase" }), _jsx(SidebarItem, { icon: _jsx(Kanban, { size: 16, "aria-hidden": true }), active: screen === 'projects', onPress: () => go(path({ kind: 'screen', screen: 'projects' })), children: "Projects" }), _jsx(SidebarItem, { icon: _jsx(CircleDot, { size: 16, "aria-hidden": true }), active: screen === 'issues', onPress: () => {
49
49
  pinBoard(host.org, '');
50
50
  go(path({ kind: 'screen', screen: 'issues' }));
51
- }, children: "Issues" }), _jsx(SidebarItem, { icon: _jsx(Blocks, { size: 16, "aria-hidden": true }), active: screen === 'artifacts', onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), children: "Artifacts" }), _jsx(SidebarItem, { icon: _jsx(LayoutTemplate, { size: 16, "aria-hidden": true }), active: screen === 'templates', onPress: () => go(path({ kind: 'screen', screen: 'templates' })), children: "Templates" }), children, _jsx(SidebarSection, { label: label, children: !host.person ? (_jsx(Note, { children: "Sign in to see your runs." })) : sessions.value.length ? (sessions.value.map((s) => (_jsx(SidebarItem, { icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }), active: r.kind === 'run' && r.id === s.id, onPress: () => go(s.id), children: s.title || 'Untitled run' }, s.id)))) : (_jsx(Note, { children: sessions.error ? sessions.error.message : sessions.loading ? 'Reading…' : 'No runs yet. Say what to build and it lands here.' })) })] }));
51
+ }, children: "Issues" }), _jsx(SidebarItem, { icon: _jsx(Puzzle, { size: 16, "aria-hidden": true }), active: screen === 'mcp', onPress: () => go(path({ kind: 'screen', screen: 'mcp' })), children: "MCP" }), _jsx(SidebarItem, { icon: _jsx(Blocks, { size: 16, "aria-hidden": true }), active: screen === 'artifacts', onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), children: "Artifacts" }), _jsx(SidebarItem, { icon: _jsx(LayoutTemplate, { size: 16, "aria-hidden": true }), active: screen === 'templates', onPress: () => go(path({ kind: 'screen', screen: 'templates' })), children: "Templates" }), children, _jsx(SidebarSection, { label: label, children: !host.person ? (_jsx(Note, { children: "Sign in to see your runs." })) : sessions.value.length ? (sessions.value.map((s) => (_jsx(SidebarItem, { icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }), active: r.kind === 'run' && r.id === s.id, onPress: () => go(s.id), children: s.title || 'Untitled run' }, s.id)))) : (_jsx(Note, { children: sessions.error ? sessions.error.message : sessions.loading ? 'Reading…' : 'No runs yet. Say what to build and it lands here.' })) })] }));
52
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a page (hanzo.build) and as a component (hanzo.ai/dev).",