duet-mcp 0.6.1

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +367 -0
  3. package/doc/README.jp.md +347 -0
  4. package/lib/blob.d.ts +16 -0
  5. package/lib/blob.js +57 -0
  6. package/lib/boot.d.ts +2 -0
  7. package/lib/boot.js +134 -0
  8. package/lib/client-store.d.ts +28 -0
  9. package/lib/client-store.js +147 -0
  10. package/lib/client.d.ts +25 -0
  11. package/lib/client.js +58 -0
  12. package/lib/diff.d.ts +27 -0
  13. package/lib/diff.js +103 -0
  14. package/lib/doc.d.ts +30 -0
  15. package/lib/doc.js +221 -0
  16. package/lib/edit.d.ts +25 -0
  17. package/lib/edit.js +63 -0
  18. package/lib/http.d.ts +9 -0
  19. package/lib/http.js +151 -0
  20. package/lib/index.d.ts +3 -0
  21. package/lib/index.js +2 -0
  22. package/lib/mcp.d.ts +5 -0
  23. package/lib/mcp.js +109 -0
  24. package/lib/op.d.ts +10 -0
  25. package/lib/op.js +19 -0
  26. package/lib/paths.d.ts +4 -0
  27. package/lib/paths.js +9 -0
  28. package/lib/protocol.d.ts +36 -0
  29. package/lib/protocol.js +10 -0
  30. package/lib/server.d.ts +1 -0
  31. package/lib/server.js +1 -0
  32. package/lib/shot.d.ts +8 -0
  33. package/lib/shot.js +85 -0
  34. package/lib/transport.d.ts +7 -0
  35. package/lib/transport.js +22 -0
  36. package/lib/types.d.ts +66 -0
  37. package/lib/types.js +1 -0
  38. package/lib/wire.d.ts +14 -0
  39. package/lib/wire.js +42 -0
  40. package/package.json +97 -0
  41. package/template/app.ts +17 -0
  42. package/template/doc.ts +18 -0
  43. package/template/main.ts +8 -0
  44. package/template/ops.ts +42 -0
  45. package/template/start.ts +4 -0
  46. package/template/ui/canvas.tsx +91 -0
  47. package/template/ui/card-editing.tsx +48 -0
  48. package/template/ui/edit-actions.tsx +30 -0
  49. package/template/ui/index.html +15 -0
  50. package/template/ui/main.tsx +53 -0
  51. package/template/ui/style.css +15 -0
  52. package/template/ui/tsconfig.json +15 -0
  53. package/template/ui/vite.config.ts +24 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Taniguchi Ryoga (SabaCan0141)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,367 @@
1
+ # duet-mcp
2
+
3
+ English | [日本語](doc/README.jp.md)
4
+
5
+ **Build apps where humans edit through a GUI and LLMs edit the same JSON document through MCP.**
6
+
7
+ Define each operation once and expose it through both HTTP and MCP. One daemon owns the document,
8
+ and callers can wait for changes since their last observation. Designed for discrete operations on
9
+ small to medium documents: game boards, task boards, diagrams, and slide outlines.
10
+
11
+ ## Define an operation once
12
+
13
+ ```ts
14
+ import { opFactory } from "duet-mcp";
15
+ import { z } from "zod";
16
+
17
+ type Doc = { text: string };
18
+ const op = opFactory<Doc>();
19
+
20
+ op({
21
+ name: "set_text",
22
+ description: "Replace the text.",
23
+ input: { text: z.string() },
24
+ handler: ({ doc, reject }, { text }) => {
25
+ if (text.length > 100) return reject("Use no more than 100 characters.");
26
+ doc.text = text;
27
+ },
28
+ });
29
+ ```
30
+
31
+ This defines the MCP tool `set_text({ text, baseRevision })` and the HTTP endpoint
32
+ `POST /api/op/set_text`. The GUI calls `snap.run("set_text", { text })` from a subscribed snapshot.
33
+
34
+ **An operation applies to the document revision the caller observed when forming its intent.**
35
+ If the document has changed, duet returns `conflict` and the latest document without running the
36
+ handler. Even unrelated changes conflict. Read the current document and reconsider the operation;
37
+ do not automatically resend the same arguments with a newer revision.
38
+
39
+ ## Use the npm package
40
+
41
+ Requires Node.js 22 or later and ESM. The React adapter targets React 18.3; input schemas use Zod 3.
42
+ CI covers Node.js 22 / 24 on Ubuntu and macOS. Windows startup and build instructions are untested.
43
+
44
+ The package name is `duet-mcp`. Registry installation becomes available after the first release.
45
+ Before publication, use an absolute path to a tarball produced by `npm pack` in place of
46
+ `duet-mcp` in the installation command.
47
+
48
+ ```bash
49
+ mkdir my-duet-app
50
+ cd my-duet-app
51
+ npm init -y
52
+ npm pkg set type=module
53
+ npm install duet-mcp react@^18.3.1 react-dom@^18.3.1 zod@^3.23.8
54
+ npm install -D typescript@^5.7.2 vite@^6.0.5 @vitejs/plugin-react@^4.3.4 tailwindcss@^4.3.3 @tailwindcss/vite@^4.3.3 @types/node@^22.10.2 @types/react@^18.3.17 @types/react-dom@^18.3.5
55
+ npx playwright install chromium
56
+ cp -R node_modules/duet-mcp/template ./template
57
+ ```
58
+
59
+ On Linux, use `npx playwright install --with-deps chromium` if system libraries are also needed.
60
+ Browser downloads are explicit, not an installation hook. Run the command again after updating Playwright.
61
+
62
+ Create `tsconfig.json` at the project root:
63
+
64
+ ```json
65
+ {
66
+ "compilerOptions": {
67
+ "target": "ES2022",
68
+ "module": "NodeNext",
69
+ "moduleResolution": "NodeNext",
70
+ "rootDir": ".",
71
+ "outDir": "dist",
72
+ "strict": true,
73
+ "skipLibCheck": true
74
+ },
75
+ "include": ["template/*.ts"]
76
+ }
77
+ ```
78
+
79
+ ```bash
80
+ npx tsc
81
+ npx vite build --config template/ui/vite.config.ts
82
+ node dist/template/main.js
83
+ ```
84
+
85
+ For GUI development, run `npx vite --config template/ui/vite.config.ts` in another terminal.
86
+ Register `node` and the absolute path to `<project>/dist/template/main.js` with your MCP client.
87
+ When renaming the app, update its directory name, `app.id`, `webDist`, build inputs, and startup path together.
88
+
89
+ The package exposes four entry points. Direct imports from `lib/` are not public API.
90
+
91
+ ```ts
92
+ import { defineApp, opFactory, type AppDef, type Op } from "duet-mcp";
93
+ import { runApp } from "duet-mcp/server";
94
+ import { useDoc, useEdit, EditSession, refreshDoc } from "duet-mcp/react";
95
+ import { portFor, baseUrlFor } from "duet-mcp/wire";
96
+ ```
97
+
98
+ `rootDir` is an absolute path to the application root. Documents and blobs are stored under its
99
+ `data/` directory. `webDist` is either relative to `rootDir` or an absolute path.
100
+ Without `rootDir`, duet uses the process working directory. MCP clients may launch from any directory,
101
+ so explicitly derive the root from `import.meta.url`, as the template does.
102
+ Keeping the same `rootDir` and `app.id` preserves the storage location across package updates.
103
+ When migrating from a copied repository, point `rootDir` at the previous project root.
104
+
105
+ Update the library with `npm install duet-mcp@<version>`. Your app owns the copied template files.
106
+
107
+ ## Run from the repository
108
+
109
+ ```bash
110
+ npm ci
111
+ npx playwright install chromium
112
+ npm run build
113
+ npm start
114
+ ```
115
+
116
+ The GUI URL is printed to stderr. `template/` is a starter app built with React and Tailwind CSS.
117
+ It includes text fields, notes, checkboxes, radio buttons, a select, an opacity slider, and a real
118
+ canvas with a draggable box and four corner resize handles.
119
+
120
+ The form commits its settings together with **Apply**. Canvas gestures commit on pointer release;
121
+ position and size are also editable through number inputs. Escape or pointer cancellation aborts
122
+ an active drag. The canvas uses a 720×480 coordinate system with a minimum box size of 64×64.
123
+ The operation handler also validates its bounds.
124
+
125
+ MCP tools `set_settings`, `set_box`, and `set_text` edit the same document. Conflicts preserve the
126
+ draft and show the current values for explicit review. Drawing and gestures live in `ui/canvas.tsx`;
127
+ commit controls and conflict review live in `ui/edit-actions.tsx`. Edit `ui/style.css` and component
128
+ Tailwind classes to customize the appearance. Text-only snapshots from the earlier template display
129
+ default values for the additional fields.
130
+
131
+ ```json
132
+ {
133
+ "mcpServers": {
134
+ "duet": {
135
+ "command": "node",
136
+ "args": ["<repo>/dist/template/main.js"]
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ Replace `<repo>` with an absolute path and adapt the configuration format to your MCP client.
143
+ After connecting, call `gui_url` for the GUI URL and `await_change` for the current document.
144
+
145
+ ```bash
146
+ DUET_APP=myapp npm start
147
+ DUET_APP=myapp npm run dev:web
148
+ # Capture the development GUI:
149
+ DUET_SHOT_ORIGIN=http://127.0.0.1:5173 DUET_APP=myapp npm start
150
+ ```
151
+
152
+ ## Documents, operations, and observations
153
+
154
+ | Name | Contract |
155
+ |---|---|
156
+ | doc | Application-defined JSON, changed through operations |
157
+ | op | A name, description, Zod input schema, and synchronous handler defined once |
158
+ | ctx | `{ doc, actor, reject }`; doc is a writable copy |
159
+ | revision | An opaque string; pass it back without parsing or arithmetic |
160
+ | snapshot | An observation containing `{ doc, revision, actor, activity }` together |
161
+ | await_change | Read the current state or wait for commits since an observed revision |
162
+
163
+ Calling `reject` discards the working copy. An operation that leaves doc unchanged does not advance
164
+ the revision. **Query operations with stale revisions also conflict.** The handler is not executed
165
+ first to determine whether it is read-only.
166
+
167
+ Handlers must be short, synchronous functions returning `Json | void`. Promises are rejected by
168
+ both the types and runtime. External APIs, file writes, timers, and deferred draft mutations are
169
+ outside the handler contract: cloning a document cannot roll back external side effects.
170
+ When applying a result computed elsewhere, use the revision of the snapshot that computation used.
171
+
172
+ Documents and results must be JSON. Properties containing `undefined`, NaN, Map, Date, and cycles
173
+ are rejected. Use `delete` to remove properties and `null` for empty values. An `undefined` return
174
+ value means there is no result. Assigning `doc = next` only reassigns a local variable; modify the
175
+ copy's properties instead.
176
+
177
+ Invalid input and application rejections return explanations. Unexpected handler or infrastructure
178
+ exceptions and persistence failures propagate as transport errors. The MCP SDK validates input
179
+ schemas in addition to HTTP-side validation.
180
+
181
+ ## GUI operations and drafts
182
+
183
+ ```tsx
184
+ const snap = useDoc<Doc>(); // null before the first response
185
+ if (!snap) return <p>Connecting…</p>;
186
+
187
+ // run uses this snapshot's revision.
188
+ const result = await snap.run("move_card", { cardId, beforeCardId });
189
+ ```
190
+
191
+ Multiple `useDoc` calls share one subscription. Documents and revisions are applied together;
192
+ late responses cannot rewind the state. A saved reference to an older `snap.run` still sends its
193
+ original revision.
194
+
195
+ Use `useEdit` for edits that span time, such as typing or dragging:
196
+
197
+ ```tsx
198
+ const snap = useDoc<Doc>();
199
+ const edit = useEdit<string>();
200
+
201
+ // When editing first begins, after a snapshot is available:
202
+ edit.begin(snap, snap.doc.text);
203
+ edit.setValue(nextText);
204
+
205
+ // Commit against the revision captured by begin.
206
+ const result = await edit.run("set_text", { text: edit.value });
207
+ ```
208
+
209
+ - `active`, `value`, `pending`, `result`, and `error` describe the edit state.
210
+ - `begin` rejects an already active edit. Subscription updates do not change the draft or its base revision.
211
+ - Duplicate `run` calls while pending share one Promise. Disable inputs while `pending`.
212
+ - Success ends the edit. Conflicts, rejections, and network failures preserve the draft.
213
+ - `cancel()` discards the draft.
214
+ - `restart(latestSnapshot, revisedValue)` explicitly replaces the base and reviewed draft.
215
+ - `refreshDoc()` requests a refresh and resolves once the subscription confirms the current state.
216
+
217
+ The template displays the draft and current values with cancel and review controls. A network
218
+ failure can leave the outcome unknown; refresh the document before deciding whether to apply again.
219
+ Drafts stored only inside a list row can disappear when another participant moves or deletes that row.
220
+
221
+ The [movable list example](template/ui/card-editing.tsx) keeps an `EditSession` per card ID in a Map
222
+ outside the columns. Rows subscribe with `useSyncExternalStore`, so drafts survive remounting after
223
+ a move between columns. This example is intended for card-based apps and is not rendered in the
224
+ studio template. `EditSession` is the state management class used by `useEdit`.
225
+
226
+ ## Responses and waiting
227
+
228
+ Operations return one of three normal responses. Each includes the document and revision:
229
+
230
+ ```jsonc
231
+ { "revision": "epoch-a:12", "actor": "llm", "doc": { "text": "hello" },
232
+ "activity": {}, "ok": true }
233
+ { "revision": "epoch-a:12", "actor": "llm", "doc": { "text": "hello" },
234
+ "activity": {}, "rejected": "Use no more than 100 characters." }
235
+ { "revision": "epoch-a:14", "actor": "llm", "doc": { "text": "new" },
236
+ "activity": {}, "conflict": true, "changes": [], "truncated": true }
237
+ ```
238
+
239
+ The revision format above is illustrative, not a public contract. A daemon restart changes the
240
+ revision even if the document is unchanged. Old operations conflict; waits using old revisions
241
+ immediately return the latest document with `truncated: true`.
242
+
243
+ - `await_change()` immediately reads the current document and revision.
244
+ - `await_change({ sinceRevision })` waits for commits after that revision, or returns immediately if they already exist.
245
+ - `until: ["move_card"]` filters the operation names to wait for and describe. The returned document is still the full current state.
246
+ - `timeoutMs` defaults to 25 seconds, with a minimum of 1 second and maximum of 120 seconds.
247
+ - A timeout may still include changes outside the filter. Adopt the returned document and revision together.
248
+
249
+ `changes` describes operations, actors, counts, and touched JSON Pointers. Consecutive commits by the
250
+ same participant using the same operation are grouped. These descriptions do not determine whether
251
+ a write is allowed and are not instructions to retry. Descriptions exceeding 100 entries or 32 KiB,
252
+ and unavailable history, return an empty array with `truncated: true`. Memory retains 1,000 commits
253
+ from the current daemon. The document itself has a separate size cost; refer to large assets by blob ID.
254
+
255
+ `activity` reports milliseconds since each participant's last activity. `useDoc` batches reports
256
+ from pointerdown and keydown events and updates the elapsed time shown by the GUI. `touch()` reports
257
+ activity explicitly. This is advisory information: it does not indicate edit completion or guarantee
258
+ priority or fairness. Activity does not advance the revision or wake waiters.
259
+
260
+ ## Application structure
261
+
262
+ Copy `template/` to `<app>/`, keeping the directory name consistent with `app.id`.
263
+ The repository's build and typecheck scripts cover all apps. The npm setup example only builds
264
+ `template/`; update its configuration when adding apps.
265
+
266
+ ```text
267
+ <app>/
268
+ doc.ts JSON types, initial values, and shared pure functions
269
+ ops.ts Operation definitions
270
+ app.ts defineApp({ id, version, rootDir?, initialDoc, ops, webDist, shot? })
271
+ start.ts runApp(app)
272
+ main.ts Protect stdout, then start the app
273
+ ui/ index.html / main.tsx / vite.config.ts / tsconfig.json
274
+ rules.ts Optional domain logic
275
+ ```
276
+
277
+ Use `.js` extensions for relative imports loaded by the server. UI-only files use bundler resolution.
278
+ Keep server dependencies out of files shared with the UI so they are not pulled into the browser.
279
+ Derived values do not need to be stored in doc: for example, share a pure board-to-FEN function
280
+ between the UI and a query operation.
281
+
282
+ ## Participants and turns
283
+
284
+ Set `env: { "DUET_ACTOR": "gpt" }` in the MCP configuration to change the participant name.
285
+ Defaults are `llm` for MCP and `human` for the browser. Subagents sharing an MCP connection also
286
+ share an actor. Authentication and multi-user account management are not provided.
287
+
288
+ Store seats and turns in doc, with a shared validation function for the UI and handlers.
289
+ Apps with seats should provide operations such as `sit` for additional participants. Rejections
290
+ should explain enough to reconsider the request, such as whose seat it is and who is calling.
291
+
292
+ ## Screenshots and blobs
293
+
294
+ `render_screenshot({ path? })` renders the same GUI in a separate headless Chromium session.
295
+ Human drafts, hover state, selection, and scroll positions are not shared. The page is opened on
296
+ first use and reused afterward.
297
+
298
+ ```ts
299
+ shot: { selector: "#board", viewport: { w: 1024, h: 768 } }
300
+ ```
301
+
302
+ `useDoc` updates `data-duet-revision` after the DOM commit. Capture waits for the requested revision
303
+ or a later revision from the same daemon; the image is not guaranteed to represent the exact snapshot
304
+ at request time. The attribute alone does not guarantee that asynchronous images or app-specific
305
+ rendering have finished. Set `DUET_SHOT_ORIGIN` to the development server when needed; otherwise,
306
+ capture uses the built GUI served from `webDist`.
307
+
308
+ `uploadBlob(file)` stores an asset. Put its returned ID in doc through an operation. The GUI uses
309
+ `blobUrl(id)` and the LLM uses `read_blob({ id })`. Immutable blobs are read directly by each MCP process.
310
+
311
+ The built-in tool names `gui_url`, `await_change`, `render_screenshot`, and `read_blob` are reserved.
312
+
313
+ ## Persistence and reconnection
314
+
315
+ ```text
316
+ data/<id>.json Document, app version, and internal commit sequence
317
+ data/<id>.log Auxiliary diagnostic log; not a complete audit or recovery log
318
+ data/<id>-blobs/ Immutable assets
319
+ ```
320
+
321
+ A snapshot commits when its temporary file is successfully renamed. A subsequent log write failure
322
+ does not undo the operation. Logs do not determine conflicts or restore change history after restart.
323
+ Full durability against power loss is not guaranteed. Unreadable snapshots are moved aside; app version
324
+ mismatches produce a uniquely named backup and a warning. Document schema validation and migrations
325
+ remain application responsibilities.
326
+
327
+ Ports are derived from `app.id` in the range 8000–8999. Override collisions with `DUET_PORT`, using
328
+ the same value for the daemon and Vite. Delegation checks the destination app ID and rejects other
329
+ apps; version mismatches generate warnings. The process owning the port owns the state, and other
330
+ MCP processes delegate through HTTP.
331
+
332
+ If the owner exits, a surviving MCP process attempts to reconnect or become the owner on its next
333
+ call. The GUI alone cannot start a daemon; keep the app process running to continue using the GUI.
334
+ **Operation POSTs are never automatically resent.** If a response is lost after persistence, refresh
335
+ the document and decide what to do. Exactly-once execution, locking, and human priority are not guaranteed.
336
+
337
+ ## Compatibility and limits
338
+
339
+ The old API using numeric revisions, standalone `runOp`, or async handlers is incompatible.
340
+ Update the daemon, MCP process, and GUI together. Old snapshot integer revisions can be imported
341
+ as internal commit sequence numbers.
342
+
343
+ Whole-document revision checks mean unrelated changes can conflict. The library is not suitable for
344
+ character-by-character simultaneous editing or guaranteed LLM progress while humans keep committing
345
+ changes. Each operation clones, saves, and sends the full document. Undo/redo, CRDT/OT, and
346
+ transactions for external side effects are not provided.
347
+
348
+ ## Development and validation
349
+
350
+ ```bash
351
+ npm run typecheck
352
+ npm test
353
+ npm run test:package
354
+ ```
355
+
356
+ Tests use temporary directories rather than modifying saved app documents. Browser tests require
357
+ localhost and Chromium. `test:package` installs a real tarball into a temporary project and checks
358
+ types, the GUI, stdio MCP, persistence, blobs, screenshots, and restart behavior. It also downloads
359
+ dependencies and Chromium and removes the temporary project when finished.
360
+
361
+ GitHub Actions runs installation, typechecking, builds, the test suite, and the installed-package test
362
+ on pushes and pull requests, using Ubuntu / macOS and Node.js 22 / 24. Use `npm ci` to install the
363
+ versions recorded in the shared `package-lock.json`.
364
+
365
+ ## License
366
+
367
+ [MIT License](LICENSE) — Copyright (c) 2026 Taniguchi Ryoga (SabaCan0141).