drafted 1.7.24 → 1.7.26

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 +83 -17
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -108,6 +108,25 @@ export function runWithRequestState(initial, fn) {
108
108
  return requestState.run(merged, fn);
109
109
  }
110
110
 
111
+ // Params that only work when the MCP runs on the user's machine (stdio).
112
+ // Stripped from the advertised schema on remote transports.
113
+ const LOCAL_ONLY_PARAMS = ['file_path'];
114
+
115
+ // Remove sentences that reference local-file params from a tool description,
116
+ // so remote-transport descriptions don't mention options that were stripped.
117
+ function scrubLocalPathMentions(description) {
118
+ if (!description) return description;
119
+ return description
120
+ .split('\n')
121
+ .map((para) =>
122
+ para
123
+ .split(/(?<=\.)\s+/)
124
+ .filter((sentence) => !/\bfile_path\b/.test(sentence))
125
+ .join(' ')
126
+ )
127
+ .join('\n');
128
+ }
129
+
111
130
  // ── MCP server factory ────────────────────────────────────────────
112
131
  // Streamable-HTTP requires a fresh McpServer per request — the SDK only
113
132
  // permits one transport connection per server instance, so a singleton
@@ -115,7 +134,17 @@ export function runWithRequestState(initial, fn) {
115
134
  // inside the factory so each HTTP request gets its own isolated server.
116
135
  // Stdio mode uses the `mcpServer` singleton (built once at module load).
117
136
 
118
- export function createMcpServer() {
137
+ export function createMcpServer(transport) {
138
+ // Remote transports (hosted HTTP MCP for claude.ai / ChatGPT) run on the
139
+ // server, not the user's machine, so local-filesystem params like `file_path`
140
+ // can't reach the user's files. Hide them from the advertised schema to avoid
141
+ // confusing web users. The in-server route passes 'http' explicitly because
142
+ // the server process has no --http argv to auto-detect. When called with no
143
+ // argument (stdio singleton / standalone bootstrap), detect from argv — this
144
+ // mirrors mcpMode(), which is defined later inside this factory and so is not
145
+ // available as a default-parameter expression here.
146
+ const mode = transport || (process.argv.includes('--http') ? 'http' : 'stdio');
147
+ const isRemote = mode !== 'stdio';
119
148
  trackUmamiEvent(UMAMI_EVENTS.MCP_CONNECTED, { source: 'mcp' });
120
149
  const server = new McpServer({
121
150
  name: 'drafted',
@@ -249,6 +278,20 @@ function tool(name, descOrSchema, schemaOrHandler, handler) {
249
278
  inputSchema = descOrSchema;
250
279
  cb = schemaOrHandler;
251
280
  }
281
+ // On remote transports, drop local-filesystem params so web clients don't see
282
+ // options that can't work off their machine. Handlers fall back to
283
+ // base64/content/url when file_path is absent. Also scrub references to those
284
+ // params from the tool description and any sibling param descriptions, so the
285
+ // advertised schema has zero mentions of options the client can't use.
286
+ if (isRemote && inputSchema && typeof inputSchema === 'object') {
287
+ for (const k of LOCAL_ONLY_PARAMS) delete inputSchema[k];
288
+ for (const [k, field] of Object.entries(inputSchema)) {
289
+ if (field?.description && /\bfile_path\b/.test(field.description)) {
290
+ inputSchema[k] = field.describe(scrubLocalPathMentions(field.description));
291
+ }
292
+ }
293
+ description = scrubLocalPathMentions(description);
294
+ }
252
295
  const config = {
253
296
  title: ann.title,
254
297
  description,
@@ -571,34 +614,50 @@ function schedulePendingAuthPoll(delayMs = 2000) {
571
614
  if (typeof pendingAuthPollTimer.unref === 'function') pendingAuthPollTimer.unref();
572
615
  }
573
616
 
617
+ // Cookie for normal tool operations. Deliberately uses ONLY this instance's
618
+ // per-process session (getState().sessionId) and never the shared root login in
619
+ // ~/.drafted/auth.json. The root is a *credential for minting child sessions*
620
+ // (see cloneSession): every agent process on the machine reads the same root, so
621
+ // operating on it directly is exactly what let one agent's get_org switch move
622
+ // every other agent's active org. ensureSession() guarantees a per-instance
623
+ // session before any operation runs.
574
624
  function getAuthHeaders() {
575
- const sid = getState().sessionId || getBootstrapSessionId();
625
+ const sid = getState().sessionId;
576
626
  if (sid) return { Cookie: `gc_session=${sid}` };
577
627
  return {};
578
628
  }
579
629
 
630
+ // Mint a fresh per-instance child session from the shared root login and bind it
631
+ // to this process. The root session id (getBootstrapSessionId) is shared by every
632
+ // agent on this machine via ~/.drafted/auth.json; the clone gives THIS process its
633
+ // own server-side session so its active org/project stay isolated from other
634
+ // agents. Returns true once a per-instance session is set.
580
635
  async function cloneSession() {
636
+ if (getState().sessionId) return true;
581
637
  const bootstrapId = getBootstrapSessionId();
582
- if (!bootstrapId) return;
638
+ if (!bootstrapId) return false;
583
639
 
584
640
  try {
585
- const url = `${getServerUrl()}/auth/session/clone`;
586
- const res = await fetch(url, {
641
+ const res = await fetch(`${getServerUrl()}/auth/session/clone`, {
587
642
  method: 'POST',
588
643
  headers: { Cookie: `gc_session=${bootstrapId}` },
589
644
  });
590
- if (!res.ok) return;
591
- const data = await res.json();
592
- if (data.sessionId) {
593
- getState().sessionId = data.sessionId;
645
+ if (res.ok) {
646
+ const data = await res.json();
647
+ if (data.sessionId) {
648
+ getState().sessionId = data.sessionId;
649
+ return true;
650
+ }
594
651
  }
595
- } catch { /* server may not be ready yet, will retry on first API call */ }
652
+ } catch { /* server may not be ready yet; caller retries on next API call */ }
653
+ return false;
596
654
  }
597
655
 
598
656
  async function ensureSession() {
599
657
  if (getState().sessionId) return;
600
- await consumePendingDeviceCode();
601
- if (getState().sessionId) return;
658
+ // A pending device-code login (from `auth get_link`) takes priority; consuming
659
+ // it mints this instance's own session. Otherwise clone the saved root login.
660
+ if (await consumePendingDeviceCode()) return;
602
661
  await cloneSession();
603
662
  }
604
663
 
@@ -1087,10 +1146,14 @@ async function consumePendingDeviceCode() {
1087
1146
  if (!res.ok) return false;
1088
1147
  const data = await res.json();
1089
1148
  if (data.status === 'approved' && data.sessionId) {
1149
+ // data.sessionId is the shared ROOT login. Persist it as the machine-level
1150
+ // credential, then mint our OWN per-instance child session from it (mirrors
1151
+ // the `auth login` tool) so this process never operates on the shared root.
1090
1152
  persistAuthSession(data);
1091
- getState().sessionId = data.sessionId;
1092
1153
  clearPendingDeviceCode();
1093
- return true;
1154
+ getState().sessionId = null;
1155
+ await cloneSession();
1156
+ return !!getState().sessionId;
1094
1157
  }
1095
1158
  if (data.status === 'expired') {
1096
1159
  clearPendingDeviceCode();
@@ -1425,6 +1488,7 @@ tool('screenshot', 'Render a PNG via headless browser. `scope=frame` captures a
1425
1488
  frameId = frame.id;
1426
1489
  }
1427
1490
  const url = `${getServerUrl()}/api/screenshot/${frameId}?width=${width}&height=${height}&fullPage=${fullPage}`;
1491
+ await ensureSession();
1428
1492
  const res = await fetch(url, { headers: getAuthHeaders() });
1429
1493
  if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
1430
1494
  const buffer = await res.arrayBuffer();
@@ -1443,6 +1507,7 @@ tool('screenshot', 'Render a PNG via headless browser. `scope=frame` captures a
1443
1507
  targetSlug = active.slug;
1444
1508
  }
1445
1509
  const url = `${getServerUrl()}/api/canvas-screenshot/${encodeURIComponent(targetSlug)}?layer=${encodeURIComponent(layer)}&width=${width}&height=${height}`;
1510
+ await ensureSession();
1446
1511
  const res = await fetch(url, { headers: getAuthHeaders() });
1447
1512
  if (!res.ok) throw new Error(`Canvas screenshot failed: ${res.status} ${await res.text()}`);
1448
1513
  const buffer = await res.arrayBuffer();
@@ -1763,7 +1828,7 @@ tool('get_org', {
1763
1828
 
1764
1829
  // ── Filesystem tools (direct HTTP to /api/fs) ─────────────────────
1765
1830
 
1766
- 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:** 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`. 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`.', {
1831
+ 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`.', {
1767
1832
  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', '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.'),
1768
1833
  path: z.string().optional().describe('[read] /{layer}/{lane}/{filename}, frame URL, or UUID. [write|edit|anchor] /{layer}/{lane}/{filename}.'),
1769
1834
  lines: z.string().optional().describe('[read] line range (e.g. "1-50"). Omit to read all.'),
@@ -2248,8 +2313,9 @@ tool('batch', 'Batch operations on the ACTIVE PROJECT. Response includes "projec
2248
2313
  operations: z.array(z.object({
2249
2314
  tool: z.enum(['write', 'rm', 'mv', 'edit', 'upload_asset']).describe('Tool to execute'),
2250
2315
  path: z.string().optional().describe('Path (for write, rm, edit)'),
2251
- content: z.string().optional().describe('Content (for write). Mutually exclusive with file_path.'),
2252
- file_path: z.string().optional().describe('Absolute path to a local file to upload (for write, upload_asset). Mutually exclusive with content.'),
2316
+ content: z.string().optional().describe(`Content (for write).${isRemote ? '' : ' Mutually exclusive with file_path.'}`),
2317
+ // file_path is local-only omitted on remote transports (web MCP).
2318
+ ...(isRemote ? {} : { file_path: z.string().optional().describe('Absolute path to a local file to upload (for write, upload_asset). Mutually exclusive with content.') }),
2253
2319
  color: z.string().optional().describe('CSS color for frame border (for write)'),
2254
2320
  from: z.string().optional().describe('Source path (for mv)'),
2255
2321
  to: z.string().optional().describe('Destination path (for mv)'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.7.24",
3
+ "version": "1.7.26",
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": [