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.
- package/mcp/server.mjs +83 -17
- 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
|
|
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
|
|
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 (
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
|
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
|
-
|
|
601
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
2252
|
-
file_path
|
|
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.
|
|
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": [
|