drafted 1.11.16 → 1.11.18

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 (2) hide show
  1. package/mcp/server.mjs +48 -4
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -135,7 +135,7 @@ const LOCAL_ONLY_PARAMS = ['file_path'];
135
135
  // tool wrapper before the handler runs — so the stdio contract, the Zod runtime
136
136
  // validation, and every handler stay byte-for-byte unchanged.
137
137
  const REMOTE_JSON_STRING_PARAMS = {
138
- frame: ['excalidraw_data', 'requests'],
138
+ frame: ['excalidraw_data', 'requests', 'state'],
139
139
  wiki: ['frontmatter', 'pages'],
140
140
  project: ['layers'],
141
141
  template: ['layers'],
@@ -367,7 +367,7 @@ function tool(name, descOrSchema, schemaOrHandler, handler) {
367
367
  const v = args[0][k];
368
368
  if (typeof v === 'string' && v.trim()) {
369
369
  try { args[0][k] = JSON.parse(v); }
370
- catch { return err(new Error(`Param "${k}" must be a JSON-encoded ${['excalidraw_data', 'frontmatter', 'target', 'output'].includes(k) ? 'object' : 'array'} string; JSON.parse failed.`)); }
370
+ catch { return err(new Error(`Param "${k}" must be a JSON-encoded ${['excalidraw_data', 'frontmatter', 'target', 'output', 'state'].includes(k) ? 'object' : 'array'} string; JSON.parse failed.`)); }
371
371
  }
372
372
  }
373
373
  }
@@ -766,6 +766,23 @@ async function api(method, path, body, extraHeaders = {}, _retried = false) {
766
766
  const scopedPath = pid ? `${path}${sep}projectId=${pid}` : path;
767
767
  const url = `${getServerUrl()}${scopedPath}`;
768
768
  const headers = { ...getAuthHeaders(), ...extraHeaders };
769
+ // Bind every data request to the org this MCP session is actually working in
770
+ // (the active project's org, or the org chosen via get_org switch). Without
771
+ // this, project-scoped routes that resolve org from the SHARED server-side
772
+ // session row (notably /api/fs ls + frame read, via effectiveOrgId) break when
773
+ // a concurrent agent session for the same user flips that row to another org:
774
+ // the active project then "isn't in the current org", the 404 handler below
775
+ // clears it, and the agent sees the "active project no longer in the current
776
+ // org" churn. X-Drafted-Org is validated against the caller's memberships
777
+ // server-side and never mutates the session, so it scopes resolution
778
+ // per-request — exactly like the org-less /project/:slug link that already
779
+ // works. An explicit override in extraHeaders (e.g. the wiki/skill `org` arg)
780
+ // always wins, and /auth/* is left untouched so it reports true session state.
781
+ const boundOrg = getSessionState().boundOrgId;
782
+ const hasExplicitOrg = Object.keys(headers).some((k) => k.toLowerCase() === 'x-drafted-org');
783
+ if (boundOrg && !hasExplicitOrg && !path.startsWith('/auth/')) {
784
+ headers['X-Drafted-Org'] = boundOrg;
785
+ }
769
786
  const opts = { method, headers };
770
787
 
771
788
  if (body !== undefined) {
@@ -1935,12 +1952,13 @@ tool('get_org', {
1935
1952
 
1936
1953
  // ── Filesystem tools (direct HTTP to /api/fs) ─────────────────────
1937
1954
 
1938
- tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by path, frame URL, or UUID), write (new frame or overwrite), Google Sheet actions (`get_sheet`, `read_sheet_values`, `write_sheet_values`, `append_sheet_rows`, `clear_sheet_range`, `update_sheet`), Google Doc actions (`get_doc`, `read_doc_content`, `write_doc_content`, `append_doc_content`, `clear_doc_content`, `update_doc`), Google Slide actions (`get_slide`, `read_slide_content`, `write_slide_content`, `append_slides`, `clear_slides`, `update_slide`), write_excalidraw (native editable Excalidraw diagram), edit (hashline ops), mv (rename/move), anchor (mark as required-read for the layer), search (match frame names). Use project(action="open") first. For listing use `ls`, for deletion use `rm`.\n\n**Google Workspace native content:** Create or attach Google Docs/Sheets/Slides with `frame(action="write", googleType=...)`. After creating, immediately populate the native file using the matching write action in the same tool — do NOT leave it empty and do NOT tell the user you cannot write to it. For Sheets: `write_sheet_values` or `append_sheet_rows` (pass `path` or `googleId` from the create response). For Docs: `write_doc_content`/`append_doc_content`. For Slides: `write_slide_content`/`append_slides`. Read with `read_sheet_values`/`read_doc_content`/`read_slide_content`. Do NOT use inline `frame.write(content)` or hashline `frame.edit` to populate Google Workspace frames.\n\n**Write — content, binary, or Google Workspace frame:** ' + (isRemote ? 'Provide exactly one of `content` (HTML/markdown/text), `base64` (base64-encoded binary with optional `content_type`), or `googleType` (`google-doc`, `google-sheet`, `google-slide`).' : 'Provide exactly one of `content` (HTML/markdown/text), `file_path` (absolute local file), `base64` (base64-encoded binary with optional `content_type`), or `googleType` (`google-doc`, `google-sheet`, `google-slide`).') + ' Call get_org first; when `googleDrive.connected` is true, strongly prefer Google Workspace frames for docs, sheets, and slides in that org. For inline content, filename extension matters: use `.html` for complete HTML documents and `.md` for Markdown. Never place a full HTML document in a `.md` or extensionless frame. For a new Google file, pass `googleType` and optional `title`; for an existing Google file, pass `googleType` plus `url` or `googleId`. ' + (isRemote ? 'For binary frames (images, PDFs, videos), pass `base64` with the binary bytes.' : 'For binary frames (images, PDFs, videos), use `file_path` when the file is local to the MCP host, or `base64` when the caller already has binary bytes.') + '\n\n**Write — dimensions:** By default, frames use the layer\'s default size (e.g. 1440×900 for designs, 1440×3000 for wireframes). Often too large for small content. Use `autoSize: true` to measure HTML content and size to fit, or pass explicit `width`/`height`.', {
1939
- action: z.enum(['read', 'write', 'write_sheet_values', 'read_sheet_values', 'append_sheet_rows', 'clear_sheet_range', 'get_sheet', 'update_sheet', 'get_doc', 'read_doc_content', 'write_doc_content', 'append_doc_content', 'clear_doc_content', 'update_doc', 'get_slide', 'read_slide_content', 'write_slide_content', 'append_slides', 'clear_slides', 'update_slide', 'create_office', 'read_office', 'edit_office', 'write_excalidraw', 'edit', 'mv', 'anchor', 'search', 'versions', 'read_version', 'restore_version']).describe('Operation to perform. Use native Doc/Slide actions for Google Docs/Slides; do not use inline write/edit for native Workspace content.'),
1955
+ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by path, frame URL, or UUID), write (new frame or overwrite), set_state / get_state (persist or read a deployed windowType:"app" frame\'s hydration state — push e.g. {specText} to drive a generic app frame with data after deploy; the canvas hydrates the app from it on load), Google Sheet actions (`get_sheet`, `read_sheet_values`, `write_sheet_values`, `append_sheet_rows`, `clear_sheet_range`, `update_sheet`), Google Doc actions (`get_doc`, `read_doc_content`, `write_doc_content`, `append_doc_content`, `clear_doc_content`, `update_doc`), Google Slide actions (`get_slide`, `read_slide_content`, `write_slide_content`, `append_slides`, `clear_slides`, `update_slide`), write_excalidraw (native editable Excalidraw diagram), edit (hashline ops), mv (rename/move), anchor (mark as required-read for the layer), search (match frame names). Use project(action="open") first. For listing use `ls`, for deletion use `rm`.\n\n**Google Workspace native content:** Create or attach Google Docs/Sheets/Slides with `frame(action="write", googleType=...)`. After creating, immediately populate the native file using the matching write action in the same tool — do NOT leave it empty and do NOT tell the user you cannot write to it. For Sheets: `write_sheet_values` or `append_sheet_rows` (pass `path` or `googleId` from the create response). For Docs: `write_doc_content`/`append_doc_content`. For Slides: `write_slide_content`/`append_slides`. Read with `read_sheet_values`/`read_doc_content`/`read_slide_content`. Do NOT use inline `frame.write(content)` or hashline `frame.edit` to populate Google Workspace frames.\n\n**Write — content, binary, or Google Workspace frame:** ' + (isRemote ? 'Provide exactly one of `content` (HTML/markdown/text), `base64` (base64-encoded binary with optional `content_type`), or `googleType` (`google-doc`, `google-sheet`, `google-slide`).' : 'Provide exactly one of `content` (HTML/markdown/text), `file_path` (absolute local file), `base64` (base64-encoded binary with optional `content_type`), or `googleType` (`google-doc`, `google-sheet`, `google-slide`).') + ' Call get_org first; when `googleDrive.connected` is true, strongly prefer Google Workspace frames for docs, sheets, and slides in that org. For inline content, filename extension matters: use `.html` for complete HTML documents and `.md` for Markdown. Never place a full HTML document in a `.md` or extensionless frame. For a new Google file, pass `googleType` and optional `title`; for an existing Google file, pass `googleType` plus `url` or `googleId`. ' + (isRemote ? 'For binary frames (images, PDFs, videos), pass `base64` with the binary bytes.' : 'For binary frames (images, PDFs, videos), use `file_path` when the file is local to the MCP host, or `base64` when the caller already has binary bytes.') + '\n\n**Write — dimensions:** By default, frames use the layer\'s default size (e.g. 1440×900 for designs, 1440×3000 for wireframes). Often too large for small content. Use `autoSize: true` to measure HTML content and size to fit, or pass explicit `width`/`height`.', {
1956
+ action: z.enum(['read', 'write', 'set_state', 'get_state', 'write_sheet_values', 'read_sheet_values', 'append_sheet_rows', 'clear_sheet_range', 'get_sheet', 'update_sheet', 'get_doc', 'read_doc_content', 'write_doc_content', 'append_doc_content', 'clear_doc_content', 'update_doc', 'get_slide', 'read_slide_content', 'write_slide_content', 'append_slides', 'clear_slides', 'update_slide', 'create_office', 'read_office', 'edit_office', 'write_excalidraw', 'edit', 'mv', 'anchor', 'search', 'versions', 'read_version', 'restore_version']).describe('Operation to perform. Use native Doc/Slide actions for Google Docs/Slides; do not use inline write/edit for native Workspace content.'),
1940
1957
  path: z.string().optional().describe('[read] /{layer}/{lane}/{filename}, frame URL, or UUID. [write|edit|anchor] /{layer}/{lane}/{filename}.'),
1941
1958
  lines: z.string().optional().describe('[read] line range (e.g. "1-50"). Omit to read all.'),
1942
1959
  content: z.string().optional().describe('[write] HTML/markdown/text for Drafted inline frames. [write_doc_content|append_doc_content] native Google Doc body text. Do not use action=write content to populate Google Doc/Slide frames.'),
1943
1960
  excalidraw_data: z.any().optional().describe('[write_excalidraw] Excalidraw scene JSON object or JSON string. Defaults to an empty scene.'),
1961
+ state: z.any().optional().describe('[set_state] App-frame state object (JSON) to persist for a deployed windowType:"app" frame — e.g. {specText:"..."} for the AS/NZS electrical app. The canvas hydrates the app from this on load (the host posts a "hydrate" message with it when the frame mounts), so you can deploy a generic app frame once and drive it with data afterwards. Max 64KB. Frame must be an app frame.'),
1944
1962
  mermaid: z.string().optional().describe('[write_excalidraw] Mermaid source to convert into an editable Excalidraw scene.'),
1945
1963
  file_path: z.string().optional().describe('[write] absolute path to a local file to upload. Mutually exclusive with content/base64/googleType.'),
1946
1964
  base64: z.string().optional().describe('[write] base64-encoded binary content. Mutually exclusive with content/file_path/googleType. Use with content_type when known.'),
@@ -2112,6 +2130,32 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
2112
2130
  _meta: { frameHtml: result.content },
2113
2131
  });
2114
2132
  }
2133
+ case 'set_state':
2134
+ case 'get_state': {
2135
+ // Read/write the persisted appState of a deployed windowType:'app' frame
2136
+ // (designs.metadata.appState via /api/file/:id/state). The canvas hydrates
2137
+ // the app from this on load — so a generic app frame can be driven with
2138
+ // data after deploy (e.g. push {specText} to the AS/NZS electrical app).
2139
+ const { path } = args;
2140
+ if (!path) throw new Error(`path required for action=${action}`);
2141
+ const urlMatch = path.match(/\/f\/([a-f0-9-]{36})/);
2142
+ const uuidMatch = path.match(/^[a-f0-9-]{36}$/);
2143
+ let frameId = urlMatch?.[1] || (uuidMatch ? path : null);
2144
+ if (!frameId) {
2145
+ const parts = path.replace(/^\/+/, '').split('/');
2146
+ if (parts.length !== 3) throw new Error('Path must be /{layer}/{lane}/{filename}, a frame URL, or a frame ID');
2147
+ const f = await api('GET', `/api/fs/${parts[0]}/${parts[1]}/${parts[2]}`);
2148
+ frameId = f.id;
2149
+ if (!frameId) throw new Error(`Could not resolve a frame id for ${path}`);
2150
+ }
2151
+ if (action === 'get_state') {
2152
+ const r = await api('GET', `/api/file/${frameId}/state`);
2153
+ return ok(JSON.stringify({ frameId, state: r.state ?? null }, null, 2));
2154
+ }
2155
+ if (args.state === undefined) throw new Error('state required for action=set_state — the app-frame state object to persist; the app hydrates from it on load');
2156
+ await api('POST', `/api/file/${frameId}/state`, { state: args.state });
2157
+ return ok(`App state persisted to frame ${frameId}. The app hydrates from it on next load — open or refresh the frame to see it render with the new state.`);
2158
+ }
2115
2159
  case 'get_sheet':
2116
2160
  case 'read_sheet_values':
2117
2161
  case 'write_sheet_values':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.11.16",
3
+ "version": "1.11.18",
4
4
  "description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
5
5
  "type": "module",
6
6
  "files": [