leglas-mcp 0.1.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 +395 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +138 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# Leglas
|
|
2
|
+
|
|
3
|
+
Compare design directions inside your own running app.
|
|
4
|
+
|
|
5
|
+
Leglas is a local development tool. You point it at a dev server you are
|
|
6
|
+
already running, list the URLs you want to compare, and flip between them in
|
|
7
|
+
one interface. Every preview is your real application: real data, real
|
|
8
|
+
authentication, real behaviour. Nothing is mocked, nothing is cloned, and
|
|
9
|
+
your project needs no changes to work with it.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Node.js 24 or newer, developed and tested against Node 26
|
|
14
|
+
- A dev server you can run locally
|
|
15
|
+
|
|
16
|
+
TypeScript config files rely on Node stripping types natively, which is
|
|
17
|
+
available without a flag from Node 23.6. On an older runtime, use
|
|
18
|
+
`leglas.config.mjs` or `leglas.config.json` instead.
|
|
19
|
+
|
|
20
|
+
Leglas never imports or executes your framework, so the target can be Next,
|
|
21
|
+
Vite, Remix, Create React App, SvelteKit, Astro, or static output.
|
|
22
|
+
|
|
23
|
+
## Getting started
|
|
24
|
+
|
|
25
|
+
Start your dev server as usual, then run Leglas from your project directory:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npx leglas
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Leglas starts on port 4100, proxies your app, and opens the interface at
|
|
32
|
+
`http://localhost:4100/leglas`. With no configuration it shows a single
|
|
33
|
+
preview of your app root, which is enough to confirm the connection.
|
|
34
|
+
|
|
35
|
+
To compare more than one thing, add a config file.
|
|
36
|
+
|
|
37
|
+
> Leglas is not published to npm yet. To try it now, clone this repository,
|
|
38
|
+
> run `pnpm install && pnpm build`, and invoke
|
|
39
|
+
> `node <path-to-repo>/packages/cli/dist/bin.js` from your project.
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
Create `leglas.config.ts` at the root of the project you want to preview.
|
|
44
|
+
`.js`, `.mjs` and `.json` also work. Config resolution walks upward from the
|
|
45
|
+
working directory, so in a monorepo the nearest file wins.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
export default {
|
|
49
|
+
devServer: "http://localhost:3000",
|
|
50
|
+
previews: [
|
|
51
|
+
{ title: "Current", url: "/" },
|
|
52
|
+
{
|
|
53
|
+
title: "Wave",
|
|
54
|
+
url: "/?v-hero=wave",
|
|
55
|
+
note: "Client artwork, bottom anchored.",
|
|
56
|
+
tags: ["Hero"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
title: "Dot grid",
|
|
60
|
+
url: "/?v-hero=dotgrid",
|
|
61
|
+
note: "Purple lattice that wakes near the pointer.",
|
|
62
|
+
tags: ["Hero"],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Field | Required | Purpose |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| `title` | yes | Label in the rail, and the key for your saved layout |
|
|
71
|
+
| `url` | unless `file` | Root relative (`/pricing`) or absolute (`https://staging.example.com`) |
|
|
72
|
+
| `note` | no | Second line under the title |
|
|
73
|
+
| `tags` | no | The first tag renders as a pill |
|
|
74
|
+
| `branch` | no | Preview a git branch instead of the running dev server |
|
|
75
|
+
| `file` | no | A project-relative HTML file served by Leglas itself, instead of `url` |
|
|
76
|
+
| `devServer` | no | Defaults to `http://localhost:3000` |
|
|
77
|
+
| `devCommand` | with `branch` | How to start the app; must contain `{port}`. Also lets Leglas start your own app when nothing is listening |
|
|
78
|
+
| `installCommand` | no | Defaults to `npm install` |
|
|
79
|
+
|
|
80
|
+
Titles must be unique. A configuration error does not stop the server: it
|
|
81
|
+
starts anyway and the interface reports what is wrong, so you can fix the
|
|
82
|
+
file without hunting through a stack trace.
|
|
83
|
+
|
|
84
|
+
TypeScript config files are read natively by Node, so there is no compiler,
|
|
85
|
+
bundler or extra dependency involved.
|
|
86
|
+
|
|
87
|
+
## Command line
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
leglas init Prepare a project and teach its agents
|
|
91
|
+
leglas [options] Start the server and open the interface
|
|
92
|
+
leglas new <surface> Scaffold a branch point for a surface
|
|
93
|
+
leglas explore <surface> Print distinct angles for an agent to build
|
|
94
|
+
leglas classify Decide where a direction should live
|
|
95
|
+
leglas add --title T --url U Register a preview on this machine
|
|
96
|
+
leglas list Show every preview, shared and local
|
|
97
|
+
leglas requests Collect change requests made from the interface
|
|
98
|
+
leglas keep <title> Keep a winner and end the exploration
|
|
99
|
+
|
|
100
|
+
--user-port <port> Port your dev server is on
|
|
101
|
+
--port <port> Port for Leglas (default 4100, next free if taken)
|
|
102
|
+
--config <path> Use this config file instead of searching upward
|
|
103
|
+
--no-open Do not open the browser
|
|
104
|
+
--json Print one machine readable envelope
|
|
105
|
+
-h, --help
|
|
106
|
+
-v, --version
|
|
107
|
+
|
|
108
|
+
--print (new) Print the scaffold instead of writing it
|
|
109
|
+
--count <n> (explore) How many angles, default 3
|
|
110
|
+
--to <path> (keep) Where the winner should live
|
|
111
|
+
--note <text> (add) Second line under the title
|
|
112
|
+
--tag <text> (add) Repeatable
|
|
113
|
+
--branch <name> (add) Back the preview with a checkout of this branch
|
|
114
|
+
--change <path> (classify) A file the direction creates or wires up
|
|
115
|
+
--rewrite <path> (classify) An existing file whose behaviour it must change
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Shared and local previews
|
|
119
|
+
|
|
120
|
+
`leglas.config.ts` is the shared description of a project: commit it, and a
|
|
121
|
+
teammate gets the same directions on clone.
|
|
122
|
+
|
|
123
|
+
`leglas add` registers a preview on your machine only, in
|
|
124
|
+
`.leglas/previews.json`. Exploration is short-lived and its code lives in a
|
|
125
|
+
gitignored directory, so a teammate must never receive a config entry
|
|
126
|
+
pointing at something they do not have. `leglas list` shows both, marking
|
|
127
|
+
which are local. Any command that writes into `.leglas/` also makes sure the
|
|
128
|
+
directory is gitignored.
|
|
129
|
+
|
|
130
|
+
### Working with coding agents
|
|
131
|
+
|
|
132
|
+
`leglas init` writes a section into your project's `AGENTS.md`, creates a
|
|
133
|
+
starter config, and ignores the working directory. The section is read by
|
|
134
|
+
Cursor, Claude Code and most other agents, and needs no per-user setup: it
|
|
135
|
+
travels with the clone, so anyone who opens the repository gets an agent that
|
|
136
|
+
already knows how to add design directions to it.
|
|
137
|
+
|
|
138
|
+
The instruction that matters most in that section is to add directions beside
|
|
139
|
+
what exists rather than rewriting it. Asked to make a hero calmer, an agent's
|
|
140
|
+
instinct is to edit the hero, and two directions that both rewrite the same
|
|
141
|
+
file cannot render from one server. Nothing in Leglas can prevent that,
|
|
142
|
+
because it never sees your source, so the contract has to say it.
|
|
143
|
+
|
|
144
|
+
Every command accepts `--json` and prints a single envelope with a stable exit
|
|
145
|
+
code, so an agent can drive the tool without parsing prose.
|
|
146
|
+
|
|
147
|
+
### Agent hosts that cannot run a shell
|
|
148
|
+
|
|
149
|
+
`leglas-mcp` is a stdio MCP server exposing the same operations as tools:
|
|
150
|
+
`start`, `add`, `list`, `classify`, `explore`, `scaffold`, `keep`,
|
|
151
|
+
`requests`, and `init`. It holds no logic of its own; each tool calls
|
|
152
|
+
exactly what the CLI calls and returns the same JSON envelope.
|
|
153
|
+
|
|
154
|
+
Register it in your agent host from the project directory, for example in
|
|
155
|
+
Claude Code:
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
claude mcp add leglas -- npx -y leglas-mcp
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
or in a `.mcp.json`:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{ "mcpServers": { "leglas": { "command": "npx", "args": ["-y", "leglas-mcp"] } } }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The host's working directory names the project, the same contract as the
|
|
168
|
+
CLI. The `start` tool boots the viewer and returns the interface URL, and
|
|
169
|
+
the server it started stops when the session ends, so a dead host never
|
|
170
|
+
leaves a dev server running on a forgotten port.
|
|
171
|
+
|
|
172
|
+
### When a direction cannot be additive
|
|
173
|
+
|
|
174
|
+
Most directions are additive: new files beside what exists, rendering from
|
|
175
|
+
the dev server you already run. Some genuinely are not. A direction that
|
|
176
|
+
changes dependencies, changes build configuration, or only works by
|
|
177
|
+
rewriting what an existing file renders cannot share the running server
|
|
178
|
+
with its siblings.
|
|
179
|
+
|
|
180
|
+
`leglas classify` decides which kind you have, before the code is written.
|
|
181
|
+
Declare what the direction will touch and it answers with the route and the
|
|
182
|
+
reason:
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
leglas classify --change package.json --rewrite src/theme.css --json
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`--change` marks a file the direction creates or wires up; `--rewrite`
|
|
189
|
+
marks an existing file whose behaviour it must alter. The answer is either
|
|
190
|
+
`in-app`, the ordinary path under `.leglas/variants/`, or `checkout`: build
|
|
191
|
+
the direction on its own git branch and register it with
|
|
192
|
+
`leglas add --title "Calm" --url "/" --branch <branch>`. Branch-backed
|
|
193
|
+
previews need `devCommand` in the config so Leglas can start the checkout,
|
|
194
|
+
and they appear in the same rail as everything else.
|
|
195
|
+
|
|
196
|
+
The escalation is the exception, and it is never silent: isolation costs an
|
|
197
|
+
install and a boot, so a direction goes to a checkout only when it names a
|
|
198
|
+
reason it cannot sit in the running app.
|
|
199
|
+
|
|
200
|
+
### Exploring several directions at once
|
|
201
|
+
|
|
202
|
+
`leglas explore hero --count 6` prints six distinct angles to build, each with
|
|
203
|
+
a line naming the obvious approach that would collapse the difference. Left to
|
|
204
|
+
itself an agent iterates narrowly around its first idea, so six requests come
|
|
205
|
+
back as six shades of one design. The angles vary composition, medium, density,
|
|
206
|
+
motion, texture and palette rather than colour alone, and they are ordered for
|
|
207
|
+
spread so asking for three still explores widely.
|
|
208
|
+
|
|
209
|
+
The `AGENTS.md` section tells agents to run this first, so "give me a few
|
|
210
|
+
options" reaches for the angles instead of inventing variations of what is
|
|
211
|
+
already there.
|
|
212
|
+
|
|
213
|
+
### Asking for a change without leaving
|
|
214
|
+
|
|
215
|
+
The tools popover has a field for the direction you are looking at. Type what
|
|
216
|
+
you want changed and press Enter: Leglas composes a prompt naming that
|
|
217
|
+
direction and the file behind it, copies it to your clipboard, and queues it.
|
|
218
|
+
|
|
219
|
+
Leglas runs no model of its own. Your agent already knows your conventions,
|
|
220
|
+
your design system and your taste, which is context no external worker has, so
|
|
221
|
+
it does the work. `leglas requests --json` hands over anything pending and
|
|
222
|
+
`leglas requests --clear` empties the queue once it is done. If you would
|
|
223
|
+
rather not wire that up, the clipboard copy is the whole feature: paste and
|
|
224
|
+
go.
|
|
225
|
+
|
|
226
|
+
### Scaffolding a surface
|
|
227
|
+
|
|
228
|
+
`leglas new hero --from src/Hero.tsx` creates a switcher and a first direction
|
|
229
|
+
under `.leglas/variants/hero/`, and adds `.leglas/` to your `.gitignore`.
|
|
230
|
+
|
|
231
|
+
`--from` points at whatever renders that surface today. The baseline then
|
|
232
|
+
re-exports that component rather than copying it, so editing the real
|
|
233
|
+
component changes the baseline too and you are never comparing against a stale
|
|
234
|
+
duplicate of your own code. Without `--from` you get a placeholder to fill in
|
|
235
|
+
yourself. The
|
|
236
|
+
generated code is ordinary application code: it imports nothing from Leglas,
|
|
237
|
+
so removing the tool leaves it working. It also renders the fallback in
|
|
238
|
+
production regardless of the URL, so a branch point that reaches a deployed
|
|
239
|
+
build cannot expose an unreleased direction.
|
|
240
|
+
|
|
241
|
+
One step is left to you. Leglas prints the import and the element to use, but
|
|
242
|
+
does not edit the component itself, because rewriting a file it does not
|
|
243
|
+
understand is how a tool breaks a codebase. Use `--print` to see the scaffold
|
|
244
|
+
without writing anything.
|
|
245
|
+
|
|
246
|
+
`--json` exists so coding agents can drive the tool. It prints a single
|
|
247
|
+
object with the interface URL, the resolved port, whether the dev server
|
|
248
|
+
answered, and any configuration errors.
|
|
249
|
+
|
|
250
|
+
## Keyboard
|
|
251
|
+
|
|
252
|
+
| Key | Action |
|
|
253
|
+
| --- | --- |
|
|
254
|
+
| Up, Down | Move between directions |
|
|
255
|
+
| `\` | Split the stage against the direction you were last on |
|
|
256
|
+
| `/` | Focus search |
|
|
257
|
+
| `[` | Collapse or open the rail |
|
|
258
|
+
| Escape | Clear search, or close the tools popover |
|
|
259
|
+
|
|
260
|
+
Directions can be renamed, removed, restored and dragged into any order.
|
|
261
|
+
Layout is saved per project, so it survives restarts and a change of port.
|
|
262
|
+
|
|
263
|
+
### Comparing two at once
|
|
264
|
+
|
|
265
|
+
Flipping shows a difference over time. A split shows it at once, which is what
|
|
266
|
+
you want for the last two directions still in contention.
|
|
267
|
+
|
|
268
|
+
Hover any direction in the rail and press its compare button. It becomes the
|
|
269
|
+
right pane, the active direction stays on the left, and the row is marked so
|
|
270
|
+
you can see what you are comparing against without hovering. Press it again to
|
|
271
|
+
close the split. `\` splits against whichever direction you were looking at
|
|
272
|
+
before this one.
|
|
273
|
+
|
|
274
|
+
The tools widget can be dragged to any corner, since a floating control has a
|
|
275
|
+
habit of sitting exactly where you need to look. Its corner is remembered.
|
|
276
|
+
|
|
277
|
+
Frameworks paint their own dev badge over the running app, which lands on the
|
|
278
|
+
same corner and is tooling rather than design. Leglas hides those by default,
|
|
279
|
+
and the popover turns them back on. This is done by styling inside the
|
|
280
|
+
preview frame, never by altering what the proxy forwards, so what your dev
|
|
281
|
+
server sent is what your app receives.
|
|
282
|
+
|
|
283
|
+
## How it works
|
|
284
|
+
|
|
285
|
+
Leglas runs a local server that does two things. It serves the interface at
|
|
286
|
+
`/leglas`, and it forwards every other request to your dev server. Because
|
|
287
|
+
previews load through that proxy, they are same origin with the interface,
|
|
288
|
+
which means no CORS configuration and no special cases for cookies.
|
|
289
|
+
|
|
290
|
+
The proxy is designed to be invisible. Hot module replacement survives the
|
|
291
|
+
hop, so editing a file still updates every preview. Redirects that point at
|
|
292
|
+
your dev server are rewritten to keep you inside the interface, and
|
|
293
|
+
redirects to anywhere else are left alone. Responses stream rather than
|
|
294
|
+
buffer. If an app behaves differently through Leglas than it does on its own
|
|
295
|
+
port, that is a bug.
|
|
296
|
+
|
|
297
|
+
## Comparing more than design variants
|
|
298
|
+
|
|
299
|
+
A preview is a URL, so the same interface compares anything your server can
|
|
300
|
+
serve:
|
|
301
|
+
|
|
302
|
+
- Two implementations of a surface, selected by a query parameter
|
|
303
|
+
- Two routes, such as `/pricing` against `/pricing-v2`
|
|
304
|
+
- A local server against a deployed one
|
|
305
|
+
|
|
306
|
+
Absolute URLs load directly rather than through the proxy, so they are
|
|
307
|
+
subject to the target's frame policy. A site that refuses to be framed will
|
|
308
|
+
not preview, and the interface says so rather than showing an empty pane.
|
|
309
|
+
|
|
310
|
+
## Starting before you have an app
|
|
311
|
+
|
|
312
|
+
Leglas does not require a running app.
|
|
313
|
+
|
|
314
|
+
**If the project exists but nothing is listening**, set `devCommand` in the
|
|
315
|
+
config (with `{port}`) and Leglas starts your app itself on a free port,
|
|
316
|
+
proxies it, and stops it when you quit, exactly as it does for branch
|
|
317
|
+
checkouts. This is reported as status, never asked as a question. When
|
|
318
|
+
`--user-port` names a server explicitly, Leglas never starts a different
|
|
319
|
+
one behind that flag.
|
|
320
|
+
|
|
321
|
+
**If there is no app at all**, a direction can be a plain HTML file:
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
export default {
|
|
325
|
+
previews: [
|
|
326
|
+
{ title: "Aurora", file: ".leglas/pages/aurora.html" },
|
|
327
|
+
{ title: "Ember", file: ".leglas/pages/ember.html" },
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Leglas serves each file itself, from its own origin, so the full rail,
|
|
333
|
+
stage, viewports, and split comparison work with no dev server anywhere.
|
|
334
|
+
The file's directory is mounted rather than the lone file, so stylesheets
|
|
335
|
+
and images beside it resolve normally. `leglas add --title "Aurora" --file
|
|
336
|
+
.leglas/pages/aurora.html` registers one from the command line, and the
|
|
337
|
+
AGENTS.md section teaches agents the same loop, so "show me three landing
|
|
338
|
+
page directions" works in an empty repository. When the real app arrives,
|
|
339
|
+
directions graduate to app code and nothing about the interface changes.
|
|
340
|
+
|
|
341
|
+
## When two directions render the same page
|
|
342
|
+
|
|
343
|
+
Leglas compares what each preview actually draws and warns when two of them are
|
|
344
|
+
identical. This catches the failure that is otherwise invisible: a typo like
|
|
345
|
+
`?v-hero=wavee` that your app ignores, serving its default page while the rail
|
|
346
|
+
implies you are comparing something.
|
|
347
|
+
|
|
348
|
+
The comparison reads the rendered page rather than the server's response, so it
|
|
349
|
+
works whether your app renders on the server or in the browser. A single-page
|
|
350
|
+
app returns the same HTML for every URL, which makes any server-side comparison
|
|
351
|
+
useless there.
|
|
352
|
+
|
|
353
|
+
Previews are compared once they have been opened, and a preview served from
|
|
354
|
+
another origin is never compared, because the browser will not let one page read
|
|
355
|
+
another origin's content. Nothing here blocks the interface, and it stays quiet
|
|
356
|
+
rather than guessing.
|
|
357
|
+
|
|
358
|
+
## Limitations
|
|
359
|
+
|
|
360
|
+
Leglas shows design directions. It does not create them. Comparing routes
|
|
361
|
+
that already exist costs nothing, but a new direction is still code you or
|
|
362
|
+
your agent writes in the app.
|
|
363
|
+
|
|
364
|
+
Only the rendered markup is compared for duplicates, and only when the server
|
|
365
|
+
renders one. Two previews that differ solely in a script are reported as the
|
|
366
|
+
same, and in a client-rendered app the check says nothing at all.
|
|
367
|
+
|
|
368
|
+
The interface is built for desktop widths. It is a development tool and is
|
|
369
|
+
not intended to ship in a production runtime.
|
|
370
|
+
|
|
371
|
+
## Development
|
|
372
|
+
|
|
373
|
+
This repository is a pnpm workspace.
|
|
374
|
+
|
|
375
|
+
```sh
|
|
376
|
+
pnpm install
|
|
377
|
+
pnpm build # build every package
|
|
378
|
+
pnpm test # run the test suite
|
|
379
|
+
pnpm typecheck # type check every package
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
| Package | Contents |
|
|
383
|
+
| --- | --- |
|
|
384
|
+
| `packages/server` | Config loading, the proxy, and the local server |
|
|
385
|
+
| `packages/shell` | The interface, a React application built with Vite |
|
|
386
|
+
| `packages/cli` | The `leglas` binary |
|
|
387
|
+
| `packages/mcp` | The `leglas-mcp` stdio server for agent hosts |
|
|
388
|
+
|
|
389
|
+
To work on the interface with live reload, run a Leglas server in one
|
|
390
|
+
terminal and `pnpm --filter @leglas/shell dev` in another. The dev server
|
|
391
|
+
proxies the API through to port 4100.
|
|
392
|
+
|
|
393
|
+
## License
|
|
394
|
+
|
|
395
|
+
MIT
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { registerLeglasTools } from "./tools.js";
|
|
6
|
+
/**
|
|
7
|
+
* The stdio face. An agent host spawns this in the project directory, which
|
|
8
|
+
* is the same contract as the CLI: the working directory names the project.
|
|
9
|
+
*/
|
|
10
|
+
function version() {
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const pkg = require("../package.json");
|
|
13
|
+
return pkg.version;
|
|
14
|
+
}
|
|
15
|
+
const server = new McpServer({ name: "leglas", version: version() });
|
|
16
|
+
const tools = registerLeglasTools(server, { cwd: process.cwd() });
|
|
17
|
+
let stopping = false;
|
|
18
|
+
const shutdown = async () => {
|
|
19
|
+
if (stopping)
|
|
20
|
+
return;
|
|
21
|
+
stopping = true;
|
|
22
|
+
await tools.shutdown();
|
|
23
|
+
process.exit(0);
|
|
24
|
+
};
|
|
25
|
+
// The host closing stdin is the ordinary way a stdio server ends; signals
|
|
26
|
+
// cover a host that kills instead. Either way the viewer stops with us.
|
|
27
|
+
process.on("SIGINT", shutdown);
|
|
28
|
+
process.on("SIGTERM", shutdown);
|
|
29
|
+
const transport = new StdioServerTransport();
|
|
30
|
+
transport.onclose = () => {
|
|
31
|
+
void shutdown();
|
|
32
|
+
};
|
|
33
|
+
await server.connect(transport);
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerLeglasTools } from "./tools.js";
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export type LeglasTools = {
|
|
3
|
+
/** Stop anything the tools started. Wired to the transport's close. */
|
|
4
|
+
shutdown(): Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
export declare function registerLeglasTools(server: McpServer, options: {
|
|
7
|
+
cwd: string;
|
|
8
|
+
}): LeglasTools;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { run, runAdd, runClassify, runExplore, runInit, runKeep, runList, runNew, runRequests, } from "leglas";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
async function capture(invoke) {
|
|
4
|
+
const lines = [];
|
|
5
|
+
const deps = {
|
|
6
|
+
log: (line) => lines.push(line),
|
|
7
|
+
error: (line) => lines.push(line),
|
|
8
|
+
};
|
|
9
|
+
const { exitCode } = await invoke(deps);
|
|
10
|
+
// The envelope is the last JSON line. Anything else captured (there should
|
|
11
|
+
// be nothing under --json) rides along so a surprise is visible, not lost.
|
|
12
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
13
|
+
const line = lines[index];
|
|
14
|
+
if (line === undefined || !line.startsWith("{"))
|
|
15
|
+
continue;
|
|
16
|
+
try {
|
|
17
|
+
JSON.parse(line);
|
|
18
|
+
return { content: [{ type: "text", text: line }], isError: exitCode !== 0 };
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Not the envelope; keep looking.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { content: [{ type: "text", text: lines.join("\n") }], isError: exitCode !== 0 };
|
|
25
|
+
}
|
|
26
|
+
export function registerLeglasTools(server, options) {
|
|
27
|
+
const cwd = options.cwd;
|
|
28
|
+
// One viewer per MCP process. The handle is held so a host that dies or
|
|
29
|
+
// disconnects never leaves a dev server running on a port nobody remembers.
|
|
30
|
+
let viewer = null;
|
|
31
|
+
server.registerTool("start", {
|
|
32
|
+
title: "Start the Leglas viewer",
|
|
33
|
+
description: "Boot the Leglas server for this project and return the interface URL. " +
|
|
34
|
+
"Idempotent per session: calling it again returns the running viewer.",
|
|
35
|
+
inputSchema: {
|
|
36
|
+
port: z.number().int().min(0).max(65535).optional()
|
|
37
|
+
.describe("Port for Leglas itself; defaults to 4100, next free if taken."),
|
|
38
|
+
},
|
|
39
|
+
}, async ({ port }) => {
|
|
40
|
+
if (viewer !== null) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{ type: "text", text: JSON.stringify({ ok: true, url: viewer.url, alreadyRunning: true }) },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return capture(async (deps) => {
|
|
48
|
+
const result = await run({ port, userPort: undefined, configPath: undefined, open: false, json: true, cwd }, { open: async () => { }, log: deps.log });
|
|
49
|
+
viewer = result;
|
|
50
|
+
return result;
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
server.registerTool("add", {
|
|
54
|
+
title: "Register a preview",
|
|
55
|
+
description: "Register a design direction on this machine so it appears in the rail. " +
|
|
56
|
+
"Use branch for a direction that lives on its own git branch.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
title: z.string().min(1).describe("Unique title; identifies the preview."),
|
|
59
|
+
url: z.string().min(1).optional()
|
|
60
|
+
.describe('Root-relative ("/?v-hero=aurora") or absolute URL. Omit for a file preview.'),
|
|
61
|
+
note: z.string().optional().describe("One line on the idea, shown under the title."),
|
|
62
|
+
tags: z.array(z.string()).optional(),
|
|
63
|
+
branch: z.string().optional()
|
|
64
|
+
.describe("Back the preview with a checkout of this git branch."),
|
|
65
|
+
file: z.string().optional()
|
|
66
|
+
.describe("Project-relative HTML file for Leglas to serve itself; no dev server needed."),
|
|
67
|
+
},
|
|
68
|
+
}, async ({ title, url, note, tags, branch, file }) => capture((deps) => runAdd({ preview: { title, url, note, tags, branch, file }, json: true, cwd }, deps)));
|
|
69
|
+
server.registerTool("list", {
|
|
70
|
+
title: "List previews",
|
|
71
|
+
description: "Every preview, shared and local, with its URL and backing branch if any.",
|
|
72
|
+
inputSchema: {},
|
|
73
|
+
}, async () => capture((deps) => runList({ json: true, cwd }, deps)));
|
|
74
|
+
server.registerTool("classify", {
|
|
75
|
+
title: "Decide where a direction should live",
|
|
76
|
+
description: "Before writing a direction, declare what it will touch and learn whether it can be " +
|
|
77
|
+
"additive in the running app or needs its own checkout, with the reason and the steps.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
changes: z
|
|
80
|
+
.array(z.object({
|
|
81
|
+
path: z.string().min(1).describe("Project-relative path."),
|
|
82
|
+
kind: z.enum(["change", "rewrite"])
|
|
83
|
+
.describe('"change": create or wire up. "rewrite": alter what an existing file renders.'),
|
|
84
|
+
}))
|
|
85
|
+
.min(1),
|
|
86
|
+
},
|
|
87
|
+
}, async ({ changes }) => capture((deps) => runClassify({ changes, json: true, cwd }, deps)));
|
|
88
|
+
server.registerTool("explore", {
|
|
89
|
+
title: "Get distinct angles for a surface",
|
|
90
|
+
description: "Distinct design angles to build for a surface, each naming what to avoid, ordered for " +
|
|
91
|
+
"spread. Use these instead of inventing variations of the current design.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
surface: z.string().min(1),
|
|
94
|
+
count: z.number().int().min(1).max(24).optional().describe("How many angles; default 3."),
|
|
95
|
+
},
|
|
96
|
+
}, async ({ surface, count }) => capture((deps) => runExplore({ surface, count: count ?? 3, json: true }, deps)));
|
|
97
|
+
server.registerTool("scaffold", {
|
|
98
|
+
title: "Scaffold a branch point",
|
|
99
|
+
description: "Create a switcher and a first direction for a surface under .leglas/variants/. " +
|
|
100
|
+
"With from, the baseline re-exports the component that renders the surface today.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
surface: z.string().min(1),
|
|
103
|
+
from: z.string().optional().describe("Path of the component rendering this surface today."),
|
|
104
|
+
print: z.boolean().optional().describe("Print the scaffold instead of writing it."),
|
|
105
|
+
},
|
|
106
|
+
}, async ({ surface, from, print }) => capture((deps) => runNew({ surface, print: print ?? false, json: true, from, cwd }, deps)));
|
|
107
|
+
server.registerTool("keep", {
|
|
108
|
+
title: "Keep a winner",
|
|
109
|
+
description: "Move the winning direction into real source, delete the rest of the exploration, and " +
|
|
110
|
+
"drop them from the rail.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
title: z.string().min(1).describe("Title of the direction to keep."),
|
|
113
|
+
to: z.string().min(1).describe("Path in real source where the winner should live."),
|
|
114
|
+
},
|
|
115
|
+
}, async ({ title, to }) => capture((deps) => runKeep({ title, to, json: true, cwd }, deps)));
|
|
116
|
+
server.registerTool("requests", {
|
|
117
|
+
title: "Collect change requests",
|
|
118
|
+
description: "Pending change requests made from the interface, each naming the direction and the file " +
|
|
119
|
+
"behind it. Pass clear once they are done.",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
clear: z.boolean().optional(),
|
|
122
|
+
},
|
|
123
|
+
}, async ({ clear }) => capture((deps) => runRequests({ json: true, clear: clear ?? false, cwd }, deps)));
|
|
124
|
+
server.registerTool("init", {
|
|
125
|
+
title: "Prepare a project",
|
|
126
|
+
description: "Write the AGENTS.md section, a starter config, and the gitignore entry into this project.",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
force: z.boolean().optional().describe("Rewrite the AGENTS.md section if it exists."),
|
|
129
|
+
},
|
|
130
|
+
}, async ({ force }) => capture((deps) => runInit({ cwd, force: force ?? false, json: true }, deps)));
|
|
131
|
+
return {
|
|
132
|
+
shutdown: async () => {
|
|
133
|
+
const running = viewer;
|
|
134
|
+
viewer = null;
|
|
135
|
+
await running?.stop().catch(() => { });
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "leglas-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stdio MCP server exposing Leglas to agent hosts that cannot run a shell.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"leglas-mcp": "./dist/bin.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
17
|
+
"zod": "^3.25.76",
|
|
18
|
+
"leglas": "^0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
27
|
+
}
|
|
28
|
+
}
|