leglas-mcp 0.2.0 → 0.3.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 CHANGED
@@ -52,7 +52,7 @@ and sessions that clean up after themselves.
52
52
  - Ask for changes without leaving the comparison: describe what you
53
53
  want on the direction you're looking at, and Leglas turns it into a
54
54
  precise request for your agent, file path included. Leave
55
- `leglas watch --run "claude -p {prompt}"` running in another terminal
55
+ `npx leglas watch --run "claude -p {prompt}"` running in another terminal
56
56
  and your agent picks each request up as you send it. In Claude Code,
57
57
  the Leglas MCP server can also push each request straight into your
58
58
  open session as a channel event (channels are a research preview:
@@ -76,57 +76,81 @@ preview of your app root. Add a config file to compare more than one thing.
76
76
 
77
77
  It works with whatever you're building in. Leglas never imports or
78
78
  executes your framework, so the target can be Next, Vite, Remix,
79
- SvelteKit, Astro, or a folder of static files. Requires Node 24 or
80
- newer.
79
+ SvelteKit, Astro, or a folder of static files.
80
+
81
+ ## Install
82
+
83
+ There is nothing you have to install: `npx leglas` fetches the CLI on
84
+ first use and starts from npm's cache after that, and every instruction
85
+ Leglas writes for agents uses the same form, so a fresh clone works
86
+ with no setup at all. Requires Node 24 or newer.
87
+
88
+ Two optional upgrades:
89
+
90
+ - `npm install -D leglas` pins the version in a project. Teammates and
91
+ CI get the same Leglas from their normal install, and `npx` resolves
92
+ the local copy from then on.
93
+ - `npm install -g leglas` is for typing `leglas` without the prefix.
81
94
 
82
95
  ## Working with coding agents
83
96
 
84
- Run `leglas init` once. It writes a section into your project's
85
- `AGENTS.md`, creates a starter config, and gitignores Leglas's working
86
- directory. That section travels with the repo, so Claude Code, Cursor,
87
- Codex, or whatever you switch to next opens the project already knowing
88
- how to add design directions to it. Every command accepts `--json` and
89
- prints a single machine-readable envelope, so agents drive the same CLI
90
- you do.
97
+ The fastest way in is the agent skill:
98
+
99
+ ```sh
100
+ npx skills add FredAmartey/leglas
101
+ ```
102
+
103
+ One install, and your agent recognises "give me a few directions for the
104
+ pricing page" as a Leglas exploration in any project, including ones
105
+ that have never seen Leglas. It sets the project up itself and gets to
106
+ work.
107
+
108
+ In a project, run `npx leglas init` once. It writes a section into your
109
+ project's `AGENTS.md`, creates a starter config, and gitignores Leglas's
110
+ working directory. That section travels with the repo, so Claude Code,
111
+ Cursor, Codex, or whatever you switch to next opens the project already
112
+ knowing how to add design directions to it. Every command accepts
113
+ `--json` and prints a single machine-readable envelope, so agents drive
114
+ the same CLI you do.
91
115
 
92
116
  The instructions center on one rule: add beside what exists, never
93
117
  rewrite it. Two directions that rewrite the same file cannot render from
94
118
  one server, and asking an agent to "make the hero calmer" tempts it to
95
119
  edit the hero. The supporting commands:
96
120
 
97
- - `leglas explore hero --count 6` briefs the exploration: what the set
121
+ - `npx leglas explore hero --count 6` briefs the exploration: what the set
98
122
  is for, why it only works if the six genuinely disagree, and how each
99
123
  direction registers. Unbriefed, six requests come back as six variants
100
124
  of one idea. With `--based-on "Aurora"` the goal flips: six deliberate
101
125
  variants of a direction you already like, and drifting into a new
102
126
  direction is the failure. The designs themselves are the agent's;
103
127
  Leglas prescribes none.
104
- - `leglas new hero --from src/Hero.tsx` scaffolds a switcher under
128
+ - `npx leglas new hero --from src/Hero.tsx` scaffolds a switcher under
105
129
  `.leglas/variants/hero/`. With `--from`, the baseline re-exports your
106
130
  real component, so you never compare against a stale copy. Leglas
107
131
  prints the one line to add in your component and does not edit it,
108
132
  because rewriting a file it does not understand is how a tool breaks a
109
133
  codebase. Scaffolded branch points return the fallback in production
110
134
  builds, so a committed one cannot expose an unreleased direction.
111
- - `leglas classify --change package.json --rewrite src/theme.css` answers
135
+ - `npx leglas classify --change package.json --rewrite src/theme.css` answers
112
136
  where a direction should live before it is written. Changing
113
137
  dependencies, build configuration, or an existing file's behaviour
114
138
  cannot be additive, so those directions build on their own git branch
115
139
  and register with `leglas add --branch`. Everything else stays in-app,
116
140
  where switching is instant.
117
- - `leglas show "Aurora" --json` answers for one direction: its entry, the
141
+ - `npx leglas show "Aurora" --json` answers for one direction: its entry, the
118
142
  source file behind it, the variants based on it, what it is being compared
119
143
  against, and anything still pending on it. Copying a direction from the
120
144
  rail hands over a block that ends in this command, so an agent given the
121
145
  block can go and get the rest.
122
- - `leglas keep "Aurora" --to src/components/hero.tsx` moves the winner
146
+ - `npx leglas keep "Aurora" --to src/components/hero.tsx` moves the winner
123
147
  into real source and ends the exploration.
124
148
 
125
149
  Asking for a change works from the interface too. Type what you want
126
150
  changed into the field under the rail (or press `R`) and Leglas composes a
127
151
  prompt naming the direction and the file behind it, copies it to your
128
152
  clipboard, and queues it. The direction it means is the one highlighted
129
- directly above the field. Your agent drains the queue with `leglas requests --json` and clears
153
+ directly above the field. Your agent drains the queue with `npx leglas requests --json` and clears
130
154
  it with `--clear`. Leglas runs no model of its own; your agent already
131
155
  knows your conventions and your taste.
132
156
 
@@ -136,6 +160,9 @@ For agent hosts that cannot run shell commands, `leglas-mcp` exposes the
136
160
  same operations as MCP tools over stdio: `start`, `add`, `list`, `show`,
137
161
  `classify`, `explore`, `scaffold`, `keep`, `requests`, and `init`. Each
138
162
  tool calls exactly what the CLI calls and returns the same envelope.
163
+ `watch` is the one command with no tool behind it: it is a loop that
164
+ holds a terminal open, and on a host that speaks channels the server
165
+ already pushes each request into the session as it arrives.
139
166
 
140
167
  ```sh
141
168
  claude mcp add leglas -- npx -y leglas-mcp
@@ -147,9 +174,34 @@ Or in `.mcp.json`:
147
174
  { "mcpServers": { "leglas": { "command": "npx", "args": ["-y", "leglas-mcp"] } } }
148
175
  ```
149
176
 
150
- The host's working directory names the project. The `start` tool boots
151
- the viewer and returns its URL, and anything it started stops when the
152
- session ends.
177
+ The host's working directory names the project, the same contract as the
178
+ CLI. A host that starts the server somewhere else is asked where the
179
+ project is, over MCP roots. The `start` tool boots the viewer and returns
180
+ its URL, and anything it started stops when the session ends.
181
+
182
+ ### As an Agent Plugin
183
+
184
+ The repository is also an [Agent Plugin](https://agent-plugins.org), the
185
+ open standard for shipping Agent Skills and MCP server configuration in
186
+ one format. Clients that implement it install the skill and the server
187
+ together, instead of the two steps above. It is a layout rather than a
188
+ build: `plugin.json` and `mcp.json` at the root, the skill in
189
+ `skills/leglas/`, nothing generated.
190
+
191
+ An Agent Plugins client starts a plugin's server in the plugin's own
192
+ install directory rather than the project, so on that path the working
193
+ directory names a copy of Leglas and nothing else. The server therefore
194
+ takes the project from the workspace the host declares over MCP roots,
195
+ and the working directory only when it sits inside one. If a host offers
196
+ neither, `LEGLAS_PROJECT_DIR` names the project outright; without it the
197
+ tools report that there is no project rather than writing into a plugin
198
+ cache. `mcp.json` passes `${PLUGIN_ROOT}` for exactly that check, and
199
+ nothing else.
200
+
201
+ The plugin's version covers the skill and the configuration, not the
202
+ server it launches: `npx` fetches the current `leglas-mcp` the same way
203
+ every `npx leglas` in these instructions fetches the current CLI, which
204
+ keeps both faces of Leglas on one version in a project they share.
153
205
 
154
206
  ## Configuration
155
207
 
package/dist/bin.js CHANGED
@@ -3,10 +3,12 @@ import { createRequire } from "node:module";
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { CHANNEL_CAPABILITY, CHANNEL_INSTRUCTIONS, startChannel } from "./channel.js";
6
+ import { hostProject } from "./project.js";
6
7
  import { registerLeglasTools } from "./tools.js";
7
8
  /**
8
- * The stdio face. An agent host spawns this in the project directory, which
9
- * is the same contract as the CLI: the working directory names the project.
9
+ * The stdio face. A host that spawns this in the project directory gets the
10
+ * CLI's contract, where the working directory names the project; a host that
11
+ * spawns it somewhere else is asked where the project is. See project.ts.
10
12
  */
11
13
  function version() {
12
14
  const require = createRequire(import.meta.url);
@@ -18,7 +20,12 @@ const server = new McpServer({ name: "leglas", version: version() },
18
20
  // speak channels; on Claude Code they let change requests from the
19
21
  // interface arrive in the open session as events.
20
22
  { capabilities: { experimental: CHANNEL_CAPABILITY }, instructions: CHANNEL_INSTRUCTIONS });
21
- const tools = registerLeglasTools(server, { cwd: process.cwd() });
23
+ const project = hostProject(server.server, {
24
+ cwd: process.cwd(),
25
+ override: process.env["LEGLAS_PROJECT_DIR"],
26
+ pluginRoot: process.env["LEGLAS_PLUGIN_ROOT"],
27
+ });
28
+ const tools = registerLeglasTools(server, { project });
22
29
  let channel = null;
23
30
  let stopping = false;
24
31
  const shutdown = async () => {
@@ -39,4 +46,4 @@ transport.onclose = () => {
39
46
  };
40
47
  await server.connect(transport);
41
48
  // Only after connect: a notification with no transport throws.
42
- channel = startChannel(server, { cwd: process.cwd() });
49
+ channel = startChannel(server, { project });
package/dist/channel.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { type PendingRequest } from "leglas";
3
+ import type { Project } from "./project.js";
3
4
  /**
4
5
  * The push face, for hosts that treat an MCP server as a channel.
5
6
  *
@@ -51,7 +52,7 @@ export type Channel = {
51
52
  * because the queue file still holds the request.
52
53
  */
53
54
  export declare function startChannel(server: McpServer, options: {
54
- cwd: string;
55
+ project: Project;
55
56
  pollMs?: number;
56
57
  /** Tests gate this to force overlapping polls; production reads the file. */
57
58
  read?: (cwd: string) => Promise<PendingRequest[]>;
package/dist/channel.js CHANGED
@@ -70,7 +70,16 @@ export function startChannel(server, options) {
70
70
  }
71
71
  };
72
72
  const push = async () => {
73
- const fresh = unpushed(await read(options.cwd), pushed);
73
+ // The first poll waits here for the host to initialize, which is what
74
+ // makes the queue this reads the project's rather than whatever directory
75
+ // the process happened to start in.
76
+ const located = await options.project.locate();
77
+ if (!located.ok) {
78
+ // Settled for the life of the process, so there is no queue coming.
79
+ clearInterval(timer);
80
+ return;
81
+ }
82
+ const fresh = unpushed(await read(located.directory), pushed);
74
83
  for (const request of fresh) {
75
84
  pushed.add(request.id);
76
85
  try {
package/dist/index.d.ts CHANGED
@@ -2,3 +2,5 @@ export { registerLeglasTools } from "./tools.js";
2
2
  export type { LeglasTools } from "./tools.js";
3
3
  export { CHANNEL_CAPABILITY, CHANNEL_INSTRUCTIONS, CHANNEL_POLL_MS, channelEvent, startChannel, unpushed, } from "./channel.js";
4
4
  export type { Channel, ChannelEvent } from "./channel.js";
5
+ export { UNRESOLVED_PROJECT, fixedProject, hostProject } from "./project.js";
6
+ export type { HostProjectOptions, Located, Project, RootsHost } from "./project.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { registerLeglasTools } from "./tools.js";
2
2
  export { CHANNEL_CAPABILITY, CHANNEL_INSTRUCTIONS, CHANNEL_POLL_MS, channelEvent, startChannel, unpushed, } from "./channel.js";
3
+ export { UNRESOLVED_PROJECT, fixedProject, hostProject } from "./project.js";
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Which project the tools act on.
3
+ *
4
+ * Every other face of Leglas learns this the same easy way: the CLI runs in
5
+ * the project, so the working directory is the answer. An MCP server is not
6
+ * always so lucky. An Agent Plugins client starts a plugin's server in the
7
+ * plugin's own install directory (Agent Plugins 1.0.0 §7.2.1), which holds a
8
+ * copy of Leglas and never a project anyone is designing. Taken at face value
9
+ * there, the working directory would send `init` into a plugin cache, leave
10
+ * the rail empty, and report all of it as success.
11
+ *
12
+ * So the directory is asked for rather than assumed:
13
+ *
14
+ * 1. `LEGLAS_PROJECT_DIR`, when someone has said outright where to work.
15
+ * 2. The workspace the host declares over MCP roots, which is the project it
16
+ * has open. The working directory wins when it sits inside one of those
17
+ * roots, because a host started in `packages/app` means that, not the
18
+ * repository above it. Several roots and no match takes the first, which
19
+ * is a guess, and the reason the override above exists.
20
+ * 3. The working directory, which is the right answer for every host that
21
+ * starts the server in the project, and the reason nothing changes for
22
+ * `claude mcp add` or a hand-written `.mcp.json`.
23
+ *
24
+ * When none of those name a project, the tools say so. Refusing is the whole
25
+ * point: a wrong directory is worse than no directory, because it writes.
26
+ */
27
+ export type Located = {
28
+ ok: true;
29
+ directory: string;
30
+ } | {
31
+ ok: false;
32
+ reason: string;
33
+ };
34
+ export type Project = {
35
+ /**
36
+ * The project directory, resolved once and held for the life of the process.
37
+ * A viewer, a request queue, and a channel all bind to one project, so a
38
+ * session that changed its mind halfway would already have a server running
39
+ * against the old one.
40
+ */
41
+ locate(): Promise<Located>;
42
+ };
43
+ /** A project fixed to one directory: what an embedder or a test already knows. */
44
+ export declare function fixedProject(directory: string): Project;
45
+ /**
46
+ * The part of an MCP server this needs. The SDK's `Server` satisfies it
47
+ * structurally, which keeps the resolution testable without a transport.
48
+ */
49
+ export type RootsHost = {
50
+ getClientCapabilities(): {
51
+ roots?: unknown;
52
+ } | undefined;
53
+ listRoots(): Promise<{
54
+ roots: {
55
+ uri: string;
56
+ }[];
57
+ }>;
58
+ oninitialized?: (() => void) | undefined;
59
+ };
60
+ export type HostProjectOptions = {
61
+ /** The process working directory. */
62
+ cwd: string;
63
+ /** `LEGLAS_PROJECT_DIR`: an explicit answer that ends the search. */
64
+ override?: string | undefined;
65
+ /**
66
+ * `LEGLAS_PLUGIN_ROOT`: where an Agent Plugins client installed us, which
67
+ * the plugin's own `mcp.json` passes as `${PLUGIN_ROOT}`. Its only use is
68
+ * recognising a working directory that means nothing.
69
+ */
70
+ pluginRoot?: string | undefined;
71
+ };
72
+ /**
73
+ * Worded for every way the search comes up empty: a host with no roots, a host
74
+ * with an empty workspace, and a host that failed to answer. Naming only the
75
+ * first would describe the others wrongly.
76
+ */
77
+ export declare const UNRESOLVED_PROJECT: string;
78
+ export declare function hostProject(host: RootsHost, options: HostProjectOptions): Project;
@@ -0,0 +1,102 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { isAbsolute, resolve, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /** A project fixed to one directory: what an embedder or a test already knows. */
5
+ export function fixedProject(directory) {
6
+ const located = { ok: true, directory };
7
+ return { locate: async () => located };
8
+ }
9
+ /**
10
+ * Worded for every way the search comes up empty: a host with no roots, a host
11
+ * with an empty workspace, and a host that failed to answer. Naming only the
12
+ * first would describe the others wrongly.
13
+ */
14
+ export const UNRESOLVED_PROJECT = "Leglas could not tell which project to work in. This agent host started the " +
15
+ "Leglas MCP server in the plugin's own directory and named no workspace to " +
16
+ "work in, so there is no project here to act on. Set LEGLAS_PROJECT_DIR to " +
17
+ "the project directory, or run the leglas CLI in the project instead.";
18
+ export function hostProject(host, options) {
19
+ let pending = null;
20
+ return { locate: () => (pending ??= discover(host, options)) };
21
+ }
22
+ async function discover(host, options) {
23
+ const override = options.override?.trim();
24
+ if (override !== undefined && override !== "") {
25
+ return { ok: true, directory: resolve(override) };
26
+ }
27
+ const cwd = canonical(options.cwd);
28
+ const roots = await declaredRoots(host);
29
+ if (roots.some((root) => contains(canonical(root), cwd))) {
30
+ return { ok: true, directory: options.cwd };
31
+ }
32
+ const first = roots[0];
33
+ if (first !== undefined)
34
+ return { ok: true, directory: first };
35
+ const pluginRoot = options.pluginRoot?.trim();
36
+ // A client that does not expand ${PLUGIN_ROOT} leaves the literal behind; it
37
+ // matches no real directory, so the check simply does not fire.
38
+ if (pluginRoot !== undefined && pluginRoot !== "" && contains(canonical(pluginRoot), cwd)) {
39
+ return { ok: false, reason: UNRESOLVED_PROJECT };
40
+ }
41
+ return { ok: true, directory: options.cwd };
42
+ }
43
+ async function declaredRoots(host) {
44
+ await initialized(host);
45
+ if (host.getClientCapabilities()?.roots === undefined)
46
+ return [];
47
+ try {
48
+ const { roots } = await host.listRoots();
49
+ return roots
50
+ .map((root) => toDirectory(root.uri))
51
+ .filter((directory) => directory !== null);
52
+ }
53
+ catch {
54
+ // A host that advertises roots and then refuses to list them has told us
55
+ // nothing, which leaves the working directory as good a guess as before.
56
+ return [];
57
+ }
58
+ }
59
+ /**
60
+ * Roots can only be asked for once the client has initialized; asking earlier
61
+ * is a protocol error. A stdio server is connected well before a host gets
62
+ * around to initializing, and the channel starts polling immediately, so this
63
+ * wait is the ordinary path rather than an edge case.
64
+ */
65
+ function initialized(host) {
66
+ if (host.getClientCapabilities() !== undefined)
67
+ return Promise.resolve();
68
+ return new Promise((ready) => {
69
+ const previous = host.oninitialized;
70
+ host.oninitialized = () => {
71
+ previous?.();
72
+ ready();
73
+ };
74
+ });
75
+ }
76
+ /** Roots are file URIs by the spec; a host that sends a bare path still reads. */
77
+ function toDirectory(uri) {
78
+ if (uri.startsWith("file:")) {
79
+ try {
80
+ return fileURLToPath(uri);
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ return isAbsolute(uri) ? uri : null;
87
+ }
88
+ function contains(parent, child) {
89
+ if (child === parent)
90
+ return true;
91
+ return child.startsWith(parent.endsWith(sep) ? parent : parent + sep);
92
+ }
93
+ /** Compared paths are real paths: /tmp is a symlink on macOS, among others. */
94
+ function canonical(directory) {
95
+ const absolute = resolve(directory);
96
+ try {
97
+ return realpathSync(absolute);
98
+ }
99
+ catch {
100
+ return absolute;
101
+ }
102
+ }
package/dist/tools.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { Project } from "./project.js";
2
3
  export type LeglasTools = {
3
4
  /** Stop anything the tools started. Wired to the transport's close. */
4
5
  shutdown(): Promise<void>;
5
6
  };
6
7
  export declare function registerLeglasTools(server: McpServer, options: {
7
- cwd: string;
8
+ project: Project;
8
9
  }): LeglasTools;
package/dist/tools.js CHANGED
@@ -23,8 +23,24 @@ async function capture(invoke) {
23
23
  }
24
24
  return { content: [{ type: "text", text: lines.join("\n") }], isError: exitCode !== 0 };
25
25
  }
26
+ /**
27
+ * Run a command in the project, or report that there is no project to run it
28
+ * in. Resolution is asked for on the first call and held after that, so the
29
+ * cost lands once and every tool acts on the same directory.
30
+ */
31
+ async function inProject(project, invoke) {
32
+ const located = await project.locate();
33
+ if (!located.ok) {
34
+ // The CLI's shape for a failure, so a host parses this like any other.
35
+ return {
36
+ content: [{ type: "text", text: JSON.stringify({ ok: false, error: located.reason }) }],
37
+ isError: true,
38
+ };
39
+ }
40
+ return capture((deps) => invoke(located.directory, deps));
41
+ }
26
42
  export function registerLeglasTools(server, options) {
27
- const cwd = options.cwd;
43
+ const project = options.project;
28
44
  // One viewer per MCP process. The handle is held so a host that dies or
29
45
  // disconnects never leaves a dev server running on a port nobody remembers.
30
46
  let viewer = null;
@@ -46,7 +62,7 @@ export function registerLeglasTools(server, options) {
46
62
  ],
47
63
  };
48
64
  }
49
- return capture(async (deps) => {
65
+ return inProject(project, async (cwd, deps) => {
50
66
  const result = await run({ port, userPort: undefined, configPath: undefined, open: false, json: true, cwd }, { open: async () => { }, log: deps.log });
51
67
  viewer = result;
52
68
  return result;
@@ -71,12 +87,12 @@ export function registerLeglasTools(server, options) {
71
87
  basedOn: z.string().optional()
72
88
  .describe("Title of the direction this is a variant of; the rail groups the family."),
73
89
  },
74
- }, async ({ title, url, note, tags, branch, file, basedOn }) => capture((deps) => runAdd({ preview: { title, url, note, tags, branch, file, basedOn }, json: true, cwd }, deps)));
90
+ }, async ({ title, url, note, tags, branch, file, basedOn }) => inProject(project, (cwd, deps) => runAdd({ preview: { title, url, note, tags, branch, file, basedOn }, json: true, cwd }, deps)));
75
91
  server.registerTool("list", {
76
92
  title: "List previews",
77
93
  description: "Every preview, shared and local, with its URL and backing branch if any.",
78
94
  inputSchema: {},
79
- }, async () => capture((deps) => runList({ json: true, cwd }, deps)));
95
+ }, async () => inProject(project, (cwd, deps) => runList({ json: true, cwd }, deps)));
80
96
  server.registerTool("show", {
81
97
  title: "Inspect one direction",
82
98
  description: "Everything Leglas knows about one direction: its full entry, the source file behind " +
@@ -89,7 +105,7 @@ export function registerLeglasTools(server, options) {
89
105
  .min(1)
90
106
  .describe("The direction's title as the config spells it, not a renamed display name."),
91
107
  },
92
- }, async ({ title }) => capture((deps) => runShow({ title, json: true, cwd }, deps)));
108
+ }, async ({ title }) => inProject(project, (cwd, deps) => runShow({ title, json: true, cwd }, deps)));
93
109
  server.registerTool("classify", {
94
110
  title: "Decide where a direction should live",
95
111
  description: "Before writing a direction, declare what it will touch and learn whether it can be " +
@@ -103,7 +119,7 @@ export function registerLeglasTools(server, options) {
103
119
  }))
104
120
  .min(1),
105
121
  },
106
- }, async ({ changes }) => capture((deps) => runClassify({ changes, json: true, cwd }, deps)));
122
+ }, async ({ changes }) => inProject(project, (cwd, deps) => runClassify({ changes, json: true, cwd }, deps)));
107
123
  server.registerTool("explore", {
108
124
  title: "Brief an exploration",
109
125
  description: "What a set for a surface needs and how it registers here. Directions must genuinely " +
@@ -118,7 +134,9 @@ export function registerLeglasTools(server, options) {
118
134
  .optional()
119
135
  .describe("An existing direction's title: ask for variants of it instead of new directions."),
120
136
  },
121
- }, async ({ surface, count, basedOn }) => capture((deps) => runExplore({ surface, count: count ?? 3, basedOn: basedOn ?? null, json: true }, deps)));
137
+ }, async ({ surface, count, basedOn }) => capture((deps) =>
138
+ // The brief is the same wherever it is read from; it touches no project.
139
+ runExplore({ surface, count: count ?? 3, basedOn: basedOn ?? null, json: true }, deps)));
122
140
  server.registerTool("scaffold", {
123
141
  title: "Scaffold a branch point",
124
142
  description: "Create a switcher and a first direction for a surface under .leglas/variants/. " +
@@ -128,7 +146,7 @@ export function registerLeglasTools(server, options) {
128
146
  from: z.string().optional().describe("Path of the component rendering this surface today."),
129
147
  print: z.boolean().optional().describe("Print the scaffold instead of writing it."),
130
148
  },
131
- }, async ({ surface, from, print }) => capture((deps) => runNew({ surface, print: print ?? false, json: true, from, cwd }, deps)));
149
+ }, async ({ surface, from, print }) => inProject(project, (cwd, deps) => runNew({ surface, print: print ?? false, json: true, from, cwd }, deps)));
132
150
  server.registerTool("keep", {
133
151
  title: "Keep a winner",
134
152
  description: "Move the winning direction into real source, delete the rest of the exploration, and " +
@@ -137,24 +155,25 @@ export function registerLeglasTools(server, options) {
137
155
  title: z.string().min(1).describe("Title of the direction to keep."),
138
156
  to: z.string().min(1).describe("Path in real source where the winner should live."),
139
157
  },
140
- }, async ({ title, to }) => capture((deps) => runKeep({ title, to, json: true, cwd }, deps)));
158
+ }, async ({ title, to }) => inProject(project, (cwd, deps) => runKeep({ title, to, json: true, cwd }, deps)));
141
159
  server.registerTool("requests", {
142
160
  title: "Collect change requests",
143
161
  description: "Pending change requests made from the interface, each naming the direction and the file " +
144
162
  "behind it. Call this when starting work in a project that uses Leglas, and again before " +
145
163
  "changing any direction: the user may have described the change from the interface while " +
146
- "you worked, and collecting marks it picked up there. Pass clear once they are done.",
164
+ "you worked, and collecting marks it picked up there. Pass clear once they are done: it " +
165
+ "drops what you collected and reports anything that arrived since, which is yours to do next.",
147
166
  inputSchema: {
148
167
  clear: z.boolean().optional(),
149
168
  },
150
- }, async ({ clear }) => capture((deps) => runRequests({ json: true, clear: clear ?? false, cwd }, deps)));
169
+ }, async ({ clear }) => inProject(project, (cwd, deps) => runRequests({ json: true, clear: clear ?? false, cwd }, deps)));
151
170
  server.registerTool("init", {
152
171
  title: "Prepare a project",
153
172
  description: "Write the AGENTS.md section, a starter config, and the gitignore entry into this project.",
154
173
  inputSchema: {
155
174
  force: z.boolean().optional().describe("Rewrite the AGENTS.md section if it exists."),
156
175
  },
157
- }, async ({ force }) => capture((deps) => runInit({ cwd, force: force ?? false, json: true }, deps)));
176
+ }, async ({ force }) => inProject(project, (cwd, deps) => runInit({ cwd, force: force ?? false, json: true }, deps)));
158
177
  return {
159
178
  shutdown: async () => {
160
179
  const running = viewer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leglas-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.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.2.0"
18
+ "leglas": "^0.3.0"
19
19
  },
20
20
  "license": "MIT",
21
21
  "repository": {