@hanzo/build 0.2.6 → 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
@@ -70,6 +70,7 @@ hanzo.id token for `Host.admin`. One left column, never two.
70
70
  | `''` | New — the empty state and the composer |
71
71
  | `sess_<32 hex>` | one run, live |
72
72
  | `-/automations` | repeating work, read from `/v1/auto/flows` |
73
+ | `-/mcp` | the fleet's native MCP servers, from `POST /v1/mcp` `tools/list` |
73
74
  | `-/codebases` | the forge's repositories |
74
75
  | `-/sync` | bring granted repositories onto the forge |
75
76
  | `-/projects` | the forge's boards |
@@ -84,10 +85,13 @@ collide.
84
85
  ## What is on the screen
85
86
 
86
87
  **The rail** (hanzo.build). New, then Automations, then the forge — Codebase, Projects, Issues —
87
- then Artifacts, Customize (the platform's plugins), More (Templates, Machines,
88
- Docs), then the org's coding runs newest first with a live status dot, and the
89
- account. Collapse is an explicit toggle kept in this browser. A host with its
90
- 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.
91
95
 
92
96
  **New.** "What's up next?", and at the foot the composer: where the run runs
93
97
  (Default is the platform's sandbox; the org's machines follow), the codebase
@@ -123,6 +127,7 @@ picker and open-in-tab; Share and Publish; the console dock under it.
123
127
  | `GET /v1/projects` · `POST /v1/projects/fork` · `GET /v1/templates` | artifacts, templates |
124
128
  | `GET /v1/git/repos/{name}/tree` · `/blob` | Files and Code |
125
129
  | `POST /v1/platform/apps` · `GET /v1/platform/builds` | Add to project, Publish |
130
+ | `POST /v1/mcp` | the native MCP servers, `tools/list` |
126
131
  | `GET /v1/models` · `POST /v1/event` | the model list, a verdict |
127
132
 
128
133
  `mode`, `model` and `effort` are sent with a run as asked; the platform
@@ -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
@@ -78,7 +79,7 @@ function Shell() {
78
79
  active: screen === 'issues',
79
80
  },
80
81
  { id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
81
- { 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' },
82
83
  ];
83
84
  const more = [
84
85
  { id: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'templates' })), active: screen === 'templates' },
package/lib/forge.js CHANGED
@@ -18,6 +18,7 @@ import { boardKey, pinBoard, pinCodebase, readPending, writePending } from './ch
18
18
  import { useKept, useRead } from './data.js';
19
19
  import { useHost, useTarget } from './host.js';
20
20
  import { path } from './route.js';
21
+ import { Servers } from './mcp.js';
21
22
  import { Sync } from './sync.js';
22
23
  import { grants } from './api/github.js';
23
24
  function Head({ title, says }) {
@@ -41,6 +42,8 @@ export function Forge({ screen }) {
41
42
  return _jsx(Automations, {});
42
43
  if (screen === 'sync')
43
44
  return _jsx(Sync, {});
45
+ if (screen === 'mcp')
46
+ return _jsx(Servers, {});
44
47
  if (screen === 'codebases')
45
48
  return _jsx(Codebases, {});
46
49
  if (screen === 'projects')
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.6",
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).",