leglas-mcp 0.3.0 → 0.4.0
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 +55 -6
- package/dist/engagement.d.ts +19 -0
- package/dist/engagement.js +68 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +13 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
<a href="LICENSE"><img src="https://img.shields.io/npm/l/leglas" alt="license"></a>
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
|
+
<p align="center">
|
|
18
|
+
<img src="https://raw.githubusercontent.com/FredAmartey/leglas/pr-assets/rail-single.jpg" width="900" alt="The Leglas interface: a rail of three design directions on the left, and the selected one running as the real app filling the rest of the window" />
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center"><i>Every direction in the rail, the selected one running as your actual app. Arrow keys flip between them.</i></p>
|
|
22
|
+
|
|
17
23
|
Code is becoming the source of truth. Features go from prompt to
|
|
18
24
|
working code in minutes, and mockups/design files eventually lag behind the product and drift out of sync. The
|
|
19
25
|
fastest teams already design in the medium they ship. Leglas is built
|
|
@@ -39,6 +45,12 @@ and sessions that clean up after themselves.
|
|
|
39
45
|
|
|
40
46
|
## What you can do with Leglas
|
|
41
47
|
|
|
48
|
+
<p align="center">
|
|
49
|
+
<img src="https://raw.githubusercontent.com/FredAmartey/leglas/pr-assets/compare-artboards.jpg" width="900" alt="The Leglas interface: a rail of three design directions on the left, and two of them running side by side as the real app, each labelled with its name." />
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<p align="center"><i>Two directions for the same page, running side by side as the actual app.</i></p>
|
|
53
|
+
|
|
42
54
|
- Pick any two variations for side-by-side comparison when it gets hard to choose.
|
|
43
55
|
- Name each direction, drag to reorder and organise your variants, set aside the ones that
|
|
44
56
|
don't feel right. Your actions on every idea survive a long exploration.
|
|
@@ -51,12 +63,23 @@ and sessions that clean up after themselves.
|
|
|
51
63
|
the spread, your agent supplies the taste.
|
|
52
64
|
- Ask for changes without leaving the comparison: describe what you
|
|
53
65
|
want on the direction you're looking at, and Leglas turns it into a
|
|
54
|
-
precise request for your agent, file path included.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
precise request for your agent, file path included. The composer
|
|
67
|
+
carries its own agent picker, the way every chat you already use
|
|
68
|
+
carries a model picker: the CLIs found on your machine (Claude Code,
|
|
69
|
+
Codex, Cursor) are one click away next to the send button, each asked
|
|
70
|
+
for its login status so a signed-out CLI says so before a run instead
|
|
71
|
+
of failing one. Not on that list? Type the command that runs your
|
|
72
|
+
agent once, in the same menu, and Leglas hands it each request; or
|
|
73
|
+
connect an agent Leglas can't spawn (an IDE panel, an MCP host) with
|
|
74
|
+
the copy-paste MCP wiring behind "Connect another agent". Each run
|
|
75
|
+
reports in a card above the field: who is working, what file they
|
|
76
|
+
are touching, how long it has been, a stop button while it runs and
|
|
77
|
+
retry when it fails. Your agent, your subscription, no keys.
|
|
78
|
+
Prefer a terminal? `npx leglas watch` is the same loop with the
|
|
79
|
+
agent's own output scrolling by. In Claude Code, the Leglas MCP
|
|
80
|
+
server can also push each request straight into your open session as
|
|
81
|
+
a channel event (channels are a research preview: start Claude Code
|
|
82
|
+
with `--dangerously-load-development-channels
|
|
60
83
|
server:<your leglas server name>`).
|
|
61
84
|
- Keep the winner with one command. Leglas moves it into your source
|
|
62
85
|
tree and clears the exploration away.
|
|
@@ -154,6 +177,23 @@ directly above the field. Your agent drains the queue with `npx leglas requests
|
|
|
154
177
|
it with `--clear`. Leglas runs no model of its own; your agent already
|
|
155
178
|
knows your conventions and your taste.
|
|
156
179
|
|
|
180
|
+
The line under the field is the whole status: whether anything is
|
|
181
|
+
listening, what you have queued, and when it has been picked up.
|
|
182
|
+
|
|
183
|
+
<p align="center">
|
|
184
|
+
<img src="https://raw.githubusercontent.com/FredAmartey/leglas/pr-assets/field-idle.png" width="290" alt="The change field reading: Enter queues it for npx leglas requests" />
|
|
185
|
+
<img src="https://raw.githubusercontent.com/FredAmartey/leglas/pr-assets/field-queued.png" width="290" alt="The change field reading: one change queued for your agent" />
|
|
186
|
+
<img src="https://raw.githubusercontent.com/FredAmartey/leglas/pr-assets/field-pickedup.png" width="290" alt="The change field reading: Your agent is on it" />
|
|
187
|
+
</p>
|
|
188
|
+
|
|
189
|
+
<p align="center"><i>Nothing waiting, then a request queued, then an agent that has taken it.</i></p>
|
|
190
|
+
|
|
191
|
+
Pick an agent once and the same line shows it working: which file it is
|
|
192
|
+
editing, a cancel if you change your mind, a retry when a run goes
|
|
193
|
+
wrong. `npx leglas watch` in another terminal is the same loop with the
|
|
194
|
+
agent's own output in view, and it needs no flag once an agent has been
|
|
195
|
+
picked in the interface.
|
|
196
|
+
|
|
157
197
|
### MCP server
|
|
158
198
|
|
|
159
199
|
For agent hosts that cannot run shell commands, `leglas-mcp` exposes the
|
|
@@ -255,6 +295,15 @@ what you want for the last two directions in contention: press `C`, or
|
|
|
255
295
|
hover a direction and press its compare button, and it becomes the right
|
|
256
296
|
pane while the active direction holds the left.
|
|
257
297
|
|
|
298
|
+
A split does not hand each side half the room. An app given half the room
|
|
299
|
+
crosses its own breakpoints and draws a different design, so you would be
|
|
300
|
+
choosing between two narrow renderings of directions meant for the wide
|
|
301
|
+
one. Instead each side is drawn at the width it had on its own and scaled
|
|
302
|
+
to fit, keeping the same proportions, so nothing reflows and flipping and
|
|
303
|
+
splitting agree about what the design is. Each pane says the width it is
|
|
304
|
+
drawn at and the scale it is shown at. If you want the narrow rendering,
|
|
305
|
+
that is what the tools popover's "Scale each side to fit" switch is for.
|
|
306
|
+
|
|
258
307
|
Arrows move between directions, `1` to `9` jump straight to one, `R` asks
|
|
259
308
|
for a change to the one you are on, `Cmd K` (`Ctrl K` elsewhere) searches
|
|
260
309
|
and `B` collapses the rail. Press `?` for the whole keymap.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type EngagementDeps = {
|
|
2
|
+
post?: (watching: boolean) => Promise<void>;
|
|
3
|
+
setInterval?: (callback: () => void, milliseconds: number) => unknown;
|
|
4
|
+
clearInterval?: (handle: unknown) => void;
|
|
5
|
+
now?: () => number;
|
|
6
|
+
};
|
|
7
|
+
export type Engagement = {
|
|
8
|
+
/**
|
|
9
|
+
* Note queue activity: starts the beat, or extends it. The returned
|
|
10
|
+
* promise settles once the server has had its chance to register the
|
|
11
|
+
* engagement; the caller awaits it before reading the queue, because the
|
|
12
|
+
* embedded runner backs off only after that registration lands. Watch
|
|
13
|
+
* closes the same handoff race the same way with its first heartbeat.
|
|
14
|
+
*/
|
|
15
|
+
touch(): Promise<void>;
|
|
16
|
+
/** End the engagement and say so, as far as one best-effort post goes. */
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
export declare function createEngagement(deps?: EngagementDeps): Engagement;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DEFAULT_PORT, LEGLAS_PREFIX } from "leglas";
|
|
2
|
+
/**
|
|
3
|
+
* Tell the Leglas server an agent is working the queue over MCP.
|
|
4
|
+
*
|
|
5
|
+
* The embedded runner yields to an attached watcher, and an MCP host driving
|
|
6
|
+
* the requests tool deserves the same right of way: without this, the runner
|
|
7
|
+
* could grab a request that arrives while the host's agent is mid-change in
|
|
8
|
+
* the same tree. Attachment is engagement, not connection: an MCP server sits
|
|
9
|
+
* connected to its host for hours doing nothing, so merely being alive proves
|
|
10
|
+
* nothing. Touch marks real queue activity, the beat keeps the server's
|
|
11
|
+
* window fresh, and a quiet spell lets it lapse.
|
|
12
|
+
*/
|
|
13
|
+
/** How often the server hears from an engaged session; matches watch. */
|
|
14
|
+
const BEAT_MS = 2000;
|
|
15
|
+
/** Queue work older than this no longer counts as engagement. */
|
|
16
|
+
const ENGAGEMENT_MS = 120_000;
|
|
17
|
+
/** A beat is worth a moment, never a stall. */
|
|
18
|
+
const POST_TIMEOUT_MS = 1000;
|
|
19
|
+
function defaultPost(watching) {
|
|
20
|
+
// Best effort at the default port: the queue is a file and every tool works
|
|
21
|
+
// without the server, so a beat that lands nowhere costs nothing. LEGLAS_PORT
|
|
22
|
+
// covers the server that had to bind elsewhere.
|
|
23
|
+
const port = Number(process.env.LEGLAS_PORT ?? "") || DEFAULT_PORT;
|
|
24
|
+
return fetch(`http://localhost:${port}${LEGLAS_PREFIX}/api/watch`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "content-type": "application/json" },
|
|
27
|
+
body: JSON.stringify({ watching }),
|
|
28
|
+
signal: AbortSignal.timeout(POST_TIMEOUT_MS),
|
|
29
|
+
}).then(() => { }, () => { });
|
|
30
|
+
}
|
|
31
|
+
export function createEngagement(deps = {}) {
|
|
32
|
+
const post = deps.post ?? defaultPost;
|
|
33
|
+
const setEvery = deps.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
|
|
34
|
+
const clearEvery = deps.clearInterval ?? ((handle) => clearInterval(handle));
|
|
35
|
+
const now = deps.now ?? (() => Date.now());
|
|
36
|
+
let timer = null;
|
|
37
|
+
let lastTouch = 0;
|
|
38
|
+
const quiet = () => {
|
|
39
|
+
if (timer === null)
|
|
40
|
+
return;
|
|
41
|
+
clearEvery(timer);
|
|
42
|
+
timer = null;
|
|
43
|
+
};
|
|
44
|
+
const beat = () => {
|
|
45
|
+
if (now() - lastTouch > ENGAGEMENT_MS) {
|
|
46
|
+
quiet();
|
|
47
|
+
void post(false);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
void post(true);
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
touch() {
|
|
54
|
+
lastTouch = now();
|
|
55
|
+
// Mid-cycle the server already knows: nothing to wait for.
|
|
56
|
+
if (timer !== null)
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
timer = setEvery(beat, BEAT_MS);
|
|
59
|
+
return post(true).catch(() => { });
|
|
60
|
+
},
|
|
61
|
+
async stop() {
|
|
62
|
+
const wasBeating = timer !== null;
|
|
63
|
+
quiet();
|
|
64
|
+
if (wasBeating)
|
|
65
|
+
await post(false);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type Engagement } from "./engagement.js";
|
|
2
3
|
import type { Project } from "./project.js";
|
|
3
4
|
export type LeglasTools = {
|
|
4
5
|
/** Stop anything the tools started. Wired to the transport's close. */
|
|
@@ -6,4 +7,5 @@ export type LeglasTools = {
|
|
|
6
7
|
};
|
|
7
8
|
export declare function registerLeglasTools(server: McpServer, options: {
|
|
8
9
|
project: Project;
|
|
10
|
+
engagement?: Engagement;
|
|
9
11
|
}): LeglasTools;
|
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { run, runAdd, runClassify, runExplore, runInit, runKeep, runList, runNew, runRequests, runShow, } from "leglas";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { createEngagement } from "./engagement.js";
|
|
3
4
|
async function capture(invoke) {
|
|
4
5
|
const lines = [];
|
|
5
6
|
const deps = {
|
|
@@ -44,6 +45,9 @@ export function registerLeglasTools(server, options) {
|
|
|
44
45
|
// One viewer per MCP process. The handle is held so a host that dies or
|
|
45
46
|
// disconnects never leaves a dev server running on a port nobody remembers.
|
|
46
47
|
let viewer = null;
|
|
48
|
+
// Working the queue over MCP counts as attachment, exactly like watch: the
|
|
49
|
+
// embedded runner must not race a host's agent for the same tree.
|
|
50
|
+
const engagement = options.engagement ?? createEngagement();
|
|
47
51
|
server.registerTool("start", {
|
|
48
52
|
title: "Start the Leglas viewer",
|
|
49
53
|
description: "Boot the Leglas server for this project and return the interface URL. " +
|
|
@@ -166,7 +170,14 @@ export function registerLeglasTools(server, options) {
|
|
|
166
170
|
inputSchema: {
|
|
167
171
|
clear: z.boolean().optional(),
|
|
168
172
|
},
|
|
169
|
-
}, async ({ clear }) =>
|
|
173
|
+
}, async ({ clear }) => {
|
|
174
|
+
// Touched on every call, empty queue included: an agent that just asked
|
|
175
|
+
// is an agent about to act, and the beat lapses on its own once the
|
|
176
|
+
// asking stops. Awaited before the queue is read so the runner has
|
|
177
|
+
// backed off by the time this session could collect anything.
|
|
178
|
+
await engagement.touch();
|
|
179
|
+
return inProject(project, (cwd, deps) => runRequests({ json: true, clear: clear ?? false, cwd }, deps));
|
|
180
|
+
});
|
|
170
181
|
server.registerTool("init", {
|
|
171
182
|
title: "Prepare a project",
|
|
172
183
|
description: "Write the AGENTS.md section, a starter config, and the gitignore entry into this project.",
|
|
@@ -178,6 +189,7 @@ export function registerLeglasTools(server, options) {
|
|
|
178
189
|
shutdown: async () => {
|
|
179
190
|
const running = viewer;
|
|
180
191
|
viewer = null;
|
|
192
|
+
await engagement.stop().catch(() => { });
|
|
181
193
|
await running?.stop().catch(() => { });
|
|
182
194
|
},
|
|
183
195
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leglas-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Stdio MCP server exposing Leglas to agent hosts that cannot run a shell.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
17
17
|
"zod": "^3.25.76",
|
|
18
|
-
"leglas": "^0.
|
|
18
|
+
"leglas": "^0.4.0"
|
|
19
19
|
},
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"repository": {
|