local-develop-server 0.11.0

Unified MCP server for the coding pipeline — consolidates git / recipe / sandbox tooling into one process with shared session state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# lds — local-develop-server

Unified MCP server for AI-driven coding agents. Bundles git read/write,
`just`-based recipe execution, and file-sandbox operations into one process
backed by a shared `Session` state — so an agent can open a repository once
with `session_start` and then run git, recipe, and sandbox tools against the
same project root without re-establishing context per tool call.

## Architecture

```
Claude Code / Agent
       │ stdio (MCP JSON-RPC)
┌─────────────────────────────────┐
│         LdsServer               │
│    Arc<RwLock<Inner>>           │
│    #[tool_router] + stdio       │
└──────────┬──────────────────────┘
     ┌─────┼──────────┐
     ▼     ▼          ▼
  GitModule  RecipeModule  SandboxModule
  git2-rs    just CLI      fs + snapshot
     │         │             │
     └─────────┴─────────────┘
        Session (core)
        root / session_id / timeout / max_output / global_recipe_dirs
```

### Session — Smart Inject Env

`session_start(root)` injects the project root into every module in one call.
Each module reads `root` / `timeout` / `max_output` from the shared `Session`.
The git module additionally tracks write scope (`owned_worktrees`) internally,
separately from `Session`.

**Auto session-start**: when the server is launched inside a ProjectRoot
(a directory containing `.git` or `justfile`), the first tool call
automatically starts a session using the startup CWD — `session_start` is
optional in that case. It remains available for switching to a different
project root explicitly. Auto-started calls include an `auto_session_start`
field in their response.

**No-session error**: when a tool is called without an active session, every
handler returns JSON-RPC error code `-32603` with the message `"no session"`.

**Session root gone error**: when the session root is removed after
`session_start` (e.g. a worktree was deleted while the session was still
active), recipe-family tools (`recipe_run` / `recipe_list` /
`recipe_list_plugins`) return
`"session root path no longer exists, please call session_start again: <path>"`.
Re-invoking `session_start` with a valid root recovers the state.

### Resolve Chain (recipe)

Dotenv-style hierarchical resolution. Justfiles are scanned in the priority
order below (low → high) and merged per recipe; later sources win on name
collision (Project has highest priority).

| Priority | Source | Notes |
|---|---|---|
| lowest | `~/.config/lds/justfile` (default global) | always scanned |
|| `config.toml` `recipes.dirs` | additional directories declared in `~/.config/lds/config.toml` |
|| `LDS_RECIPE_GLOBAL_DIRS` env var | colon-separated dirs; legacy / CI |
| highest | Project (`{root}/justfile`) | project justfile at the session root |

Each recipe carries `ResolveInfo { level, source_path }` so its source layer
is traceable. Adding a new layer (e.g. Worktree) only requires extending the
`ResolveLevel` enum with a new variant.

### Output Safety

- **timeout**: `tokio::time::timeout` is applied to recipe / sandbox execution (default 60s)
- **truncation**: when stdout / stderr exceeds `max_output` (default 100KB) it
  is truncated to a head + tail pair, respecting UTF-8 character boundaries

## Crate Structure

```
crates/
├── core/    lds-core     Session, SessionConfig, LdsState, truncate_output
├── git/     lds-git      GitModule (git2-rs, write scope tracking)
├── gh/      lds-gh       GhModule (gh CLI subprocess wrapper, read-only API, auth fail-fast)
├── recipe/  lds-recipe   RecipeModule (just CLI, resolve chain, content args)
├── sandbox/ lds-sandbox  SandboxModule (file-scoped read/append, snapshot/rollback)
├── journal/ lds-journal  JournalModule (journal-mcp-rmcp SDK consumer, `journal_*` tools forwarded)
├── outline/ lds-outline  OutlineModule (outline-mcp-rmcp SDK consumer, prefixed `outline_*` tools)
└── lds/     lds          MCP binary (rmcp v1.7, stdio transport)
```

## Tools

### Session

| Tool | Description |
|---|---|
| `session_start` | Initialize session with project root. Optional when the server was launched inside a ProjectRoot (directory containing `.git` or `justfile`) — the first tool call auto-starts the session using the startup CWD. Call `session_start` explicitly to use a different root. |

### Git (read)

| Tool | Description |
|---|---|
| `git_status` | Working tree status |
| `git_log` | Commit log (configurable max_count) |
| `git_diff` | Diff working tree vs HEAD |

### Gh (read)

GitHub CLI (`gh`) wrapper. Requires `gh auth login` before use; every tool
invocation checks `gh auth status` and returns a typed error if unauthenticated.

| Tool | Description |
|---|---|
| `gh_auth_status` | Check gh CLI authentication status |
| `gh_pr_list` | List PRs as JSON (number/title/state/author). `limit` optional (default 30). |
| `gh_pr_view` | View a single PR as JSON. Requires `number`. |
| `gh_pr_diff` | Show diff of a PR. Requires `number`. |
| `gh_issue_list` | List issues as JSON (number/title/state). `limit` optional (default 30). |
| `gh_issue_view` | View a single issue as JSON. Requires `number`. |
| `gh_repo_view` | Repository metadata as JSON (name/owner/defaultBranchRef). |
| `gh_run_list` | List Actions workflow runs as JSON. `limit` optional (default 30). |
| `gh_run_view` | View a single workflow run as JSON (status/conclusion/jobs/...). Requires `run_id`. |
| `gh_run_log_failed` | View failed-step logs of a workflow run, parsed into `{failed_steps: [{job_name, step_name, log_tail}]}`. |
| `gh_run_jobs` | List jobs of a workflow run as JSON. Requires `run_id`. |
| `gh_release_view` | View a single release (tag, assets, body) as JSON. Requires `tag`. |
| `gh_release_list` | List releases as JSON. `limit` optional (default 30). |
| `gh_workflow_list` | List workflows as JSON (name/state/id). |
| `gh_workflow_view` | View a single workflow (path/state/latest_run) as JSON. Requires `name_or_id`. |
| `gh_pr_checks` | List CI check results of a PR as JSON. Requires `number`. |

**Write operations not exposed**: `gh pr create` / `gh issue create` /
`gh release create` / `gh pr merge` are deliberately not exposed as MCP tools.
`gh run cancel` (write op) and `gh run watch` (long-polling, incompatible with
MCP request-response semantics — use `gh_run_view` + polling instead) are also
absent. `gh search repos` is out of scope for this release.
Users invoke write operations via shell directly.

### Git (write)

Session-scoped write operations: `worktree_add` registers the created worktree in
the session's `owned_worktrees` set, and subsequent write tools (`commit`,
`merge`, `worktree_remove`, `branch_delete`) refuse to operate on paths /
branches that are not session-owned. This prevents one agent from destroying
another's work.

| Tool | Description |
|---|---|
| `git_commit` | Stage and commit changes in a session-owned working directory |
| `git_worktree_add` | Create a worktree under `.worktrees/` with a new branch (session-owned) |
| `git_worktree_remove` | Remove a session-owned worktree |
| `git_worktree_list` | List worktrees with session-ownership annotation |
| `git_merge` | Merge a branch into another in a session-owned working directory |
| `git_branch_delete` | Delete a session-owned branch |

### Recipe

| Tool | Description |
|---|---|
| `recipe_list` | List allow-agent recipes (with ResolveInfo source tracking) |
| `recipe_run` | Run recipe with args + content env vars, timeout + truncation |

### Outline

Forwarded from the upstream `outline-mcp-rmcp` server. Each tool is exposed
under the `outline_` prefix (e.g. `mcp__lds__outline_shelf`). Shelf root
defaults to `$HOME/.config/outline-mcp/books`, overridable via
`LDS_OUTLINE_SHELF_DIR`. See the upstream [outline-mcp README][outline-mcp]
for full tool semantics; the surface below lists the wire names.

| Tool | Description |
|---|---|
| `outline_shelf` | List available books on the shelf |
| `outline_select_book` | Select a book (by slug or number) to operate on |
| `outline_init` | Create a new book |
| `outline_toc` | Table of contents (numbered IDs) for the selected book |
| `outline_checklist` | Export a section as a Markdown checklist |
| `outline_dump` | Full JSON dump of a book |
| `outline_node_create` | Add a node under a parent (or root) |
| `outline_node_update` | Edit a node's title/body/type/placeholder |
| `outline_node_move` | Move a node under a new parent |
| `outline_node_batch_move` | Bulk move (UUID required) |
| `outline_node_batch_update` | Bulk update (UUID required) |
| `outline_node_history` | Change history for a single node |
| `outline_book_history` | Change history for the whole book |
| `outline_snapshot_create` | Snapshot the current book state |
| `outline_snapshot_list` | List snapshots |
| `outline_snapshot_restore` | Restore a snapshot |
| `outline_snapshot_tag` | Tag a snapshot with a label |
| `outline_snapshot_diff` | Diff two snapshots |
| `outline_snapshot_dump` | Dump one snapshot |
| `outline_snapshot_dump_all` | Dump every snapshot |
| `outline_import` | Import a book from JSON |
| `outline_gen_routing` | Generate routing metadata |

Bundled guide resources are surfaced verbatim under the `outline://guides/*`
URI scheme via `list_resources` / `read_resource`.

[outline-mcp]: https://github.com/ynishi/outline-mcp

#### Migrating from legacy `[[export]] route = "outline"` (pre-v0.7.0 users)

Before v0.7.0 the recommended way to reach outline-mcp through lds was to
declare a `[[route]] name = "outline"` and re-expose its snapshot / history
tools via `[[export]]` in `~/.config/lds/config.toml` (or the project-local
equivalent). Since v0.7.0 the outline surface is provided by the in-process
`lds-outline` SDK integration, so the export block is no longer needed —
and, if left in place, it silently **shadows** the SDK path for exported
tool names.

Concretely: lds's `call_tool` dispatch checks `[[export]]` before the
direct-embed outline delegation, so a config like

```toml
[[route]]
name = "outline"
command = "outline-mcp"

[[export]]
route = "outline"
tools = ["snapshot_create", "snapshot_list", "snapshot_dump",
         "snapshot_dump_all", "snapshot_tag", "snapshot_diff",
         "book_history"]
```

routes `outline_snapshot_*` and `outline_book_history` to a subprocess
`outline-mcp` binary while `outline_shelf` / `outline_select_book` /
`outline_toc` / `outline_dump` / `outline_node_*` go through the direct
SDK. The subprocess and the in-process `OutlineMcpServer` hold separate
`selected` state, so `outline_select_book` from the SDK path never reaches
the subprocess and the exported snapshot tools return
`-32602: No book selected` even right after a successful `outline_select_book`.

**Migration**: remove the `[[export]] route = "outline"` block (and, if you
had no other reason to keep it, the `[[route]] name = "outline"` block as
well) from your lds config, then restart your MCP client. All `outline_*`
tools will flow through the in-process SDK and share a single `selected`
state.

## Roadmap

| Stage | Scope | Status |
|---|---|---|
| S1 | Git write ops (`commit` / `merge` / `worktree_{add,remove,list}` / `branch_delete`) with session-scoped write safety | ✅ done |
| S2 | Recipe schema validation (typed content-key contract for `recipe_run`) | planned |
| S3 | Sandbox extensions — optional container / subprocess isolation backends for the sandbox module | planned |

## Why one process?

Each MCP server an agent has to talk to is one more process to install, one
more `session_start` call to make, and one more reference to thread through
prompts. Folding git, recipe, and sandbox into a single binary backed by a
shared `Session` collapses the install surface to one target, the per-task
session call to one invocation, and lets every module read the same project
root / timeout / output limits without duplicate configuration.

## Usage

```sh
cargo install --path crates/lds

# .mcp.json
{
  "mcpServers": {
    "lds": { "command": "lds", "args": [] }
  }
}
```

### Plugin Recipes

Justfile recipes tagged with `[group('lds-plugin')]` are auto-registered
as MCP tools at startup. Drop a `justfile` at `~/.config/lds/justfile`
(global) or in your project root (project-scoped) and each plugin
recipe becomes `mcp__lds__<name>`.

Quick bootstrap:

```sh
cp examples/global-justfile.skeleton ~/.config/lds/justfile
# restart Claude Code so the MCP server re-reads the global plugin set
```

The skeleton ships with `complexity` / `search-excluding` /
`remote-url` / `text-stats` / `greet`. See
[docs/plugin-recipe-authoring.md](docs/plugin-recipe-authoring.md) for
the full IF contract, parameter mapping, shebang recipes, and the
macOS-awk / CWD pitfalls. The same doc carries the
[Plugin vs AllowAgent decision flowchart](docs/plugin-recipe-authoring.md#11-decision-flowchart)
and the
[naming-collision guide](docs/plugin-recipe-authoring.md#12-plugin-naming-collision-guide)
for picking the right group.

#### config.toml (Recommended)

The preferred way to configure persistent global recipe directories is
`~/.config/lds/config.toml`:

```toml
[recipes]
dirs = ["/opt/shared-recipes", "~/team-recipes"]

[paths]
global_justfile = "~/.config/lds/justfile"
```

Use the `lds recipe-dir` CLI to manage `recipes.dirs` without hand-editing:

```sh
lds recipe-dir add ~/team-recipes
lds recipe-dir list
lds recipe-dir remove ~/team-recipes
```

> **Tilde expansion**: `lds recipe-dir add ~/team-recipes` expands the path
> to an absolute path before writing it to `config.toml`. Existing comments
> and other sections in `config.toml` are preserved (patch-safe write).

**Resolution priority** (low → high):
`~/.config/lds/justfile` (default) → `config.toml` `recipes.dirs` →
`LDS_RECIPE_GLOBAL_DIRS` env → project `justfile`

**Restart required**: `config.toml` is read once at process startup.
Changes to `config.toml` require restarting the lds process to take effect.
SIGHUP-based reload is not implemented (tracked as a separate issue).

#### Additional Global Recipe Directories — Legacy (`LDS_RECIPE_GLOBAL_DIRS`)

> **Legacy**: prefer `config.toml` + `lds recipe-dir add` (above) for new
> setups. `LDS_RECIPE_GLOBAL_DIRS` continues to work and is useful for CI /
> ephemeral environments where a config file is inconvenient.

Set `LDS_RECIPE_GLOBAL_DIRS` to a colon-separated list of directories
(PATH-style) to load additional global justfiles beyond `~/.config/lds/`:

```sh
# .mcp.json
{
  "mcpServers": {
    "lds": {
      "command": "lds",
      "args": [],
      "env": {
        "LDS_RECIPE_GLOBAL_DIRS": "/opt/shared-recipes:/home/user/team-recipes"
      }
    }
  }
}
```

When both `config.toml` and `LDS_RECIPE_GLOBAL_DIRS` are set, directories
from `LDS_RECIPE_GLOBAL_DIRS` take precedence over `config.toml` on name
collision — env is loaded after config in the resolution chain, following
the standard CLI convention (cargo, git, gh: env overrides file config).
Same-name recipes in later entries override earlier ones; the project
justfile always wins.

#### Alternative: `import '<abs>/justfile'`

Add an `import` statement to `~/.config/lds/justfile` to pull in another
justfile directly:

```just
import '/opt/shared/shared-recipes.just'
```

This approach requires editing `~/.config/lds/justfile` by hand and does
not appear in `lds recipe-dir list`. It is provided for compatibility with
existing setups.

### Global Recipe Contract

Consumer-facing IF for serving recipes via lds. The five points below are
the contract; they are not optional behaviors.

1. **Discovery paths**: lds reads `~/.config/lds/justfile` (default global),
   every directory listed in `config.toml` `recipes.dirs`, and every
   directory listed in `LDS_RECIPE_GLOBAL_DIRS`. Recipes brought in by
   just's native `import '<path>'` from any of those justfiles are also
   served — there is no separate registration step for imported recipes.

2. **Group filter** (mutually exclusive routing):

   | Tag | Routing |
   |---|---|
   | `[group('lds-plugin')]` | Registered as a dedicated MCP tool at startup (`mcp__lds__<name>`) **and** listed by `recipe_list` / runnable via `recipe_run`. Intended for global utilities. |
   | `[group('allow-agent')]` | Listed by `recipe_list` and runnable via `recipe_run` only. Not exposed as an individual MCP tool. Intended for project/task recipes invoked through `recipe_run`. |
   | no group | **Excluded.** Not served at all (legacy `# [allow-agent]` doc comment is still honored for backward compatibility). |

3. **Dedup**: When the same recipe arrives through two paths (e.g. env
   injection + root `import`), `just --dump` dedupes by recipe name; lds
   does not error and serves a single entry.

4. **Restart required**: lds resolves the global justfile set at **process
   startup** using `config.toml` and `LDS_RECIPE_GLOBAL_DIRS`. `recipe_list`
   / `recipe_run` re-parse justfiles live, but changes to `config.toml`,
   env vars, or newly added global directories require a Claude Code restart
   to take effect. SIGHUP reload is not implemented.

5. **Three coexisting routes for adding global recipes**: (a) declare in
   `config.toml` `recipes.dirs` via `lds recipe-dir add` (recommended), (b)
   inject via `LDS_RECIPE_GLOBAL_DIRS` env (legacy / CI), or (c) add
   `import '<abs>/justfile'` to `~/.config/lds/justfile` (manual). All three
   are supported simultaneously.

## License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT) at your option.