drafted 1.11.36 → 1.11.38
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 +85 -25
- package/package.json +1 -3
- package/src/shared/excalidraw.mjs +0 -61
package/mcp/server.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { z } from 'zod';
|
|
|
19
19
|
import { registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
|
|
20
20
|
import WebSocket from 'ws';
|
|
21
21
|
import { LAYERS } from '../src/shared/constants.mjs';
|
|
22
|
-
import { emptyExcalidrawScene,
|
|
22
|
+
import { emptyExcalidrawScene, stringifyExcalidrawScene } from '../src/shared/excalidraw.mjs';
|
|
23
23
|
import { createGateState, markSearched, g1Block, g2Block, g3Block, selectWithinBudget, wouldExceedBudget, budgetError, formatWikiIndex, PROJECT_CONTEXT_BUDGET_CHARS } from './gates.mjs';
|
|
24
24
|
import { loadPersistedProject, savePersistedProject } from './active-project-store.mjs';
|
|
25
25
|
|
|
@@ -515,26 +515,89 @@ function mcpMode() {
|
|
|
515
515
|
return process.argv.includes('--http') ? 'http' : 'stdio';
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
+
// ponytail: reports that fail to send (e.g. server unreachable) queue here on disk
|
|
519
|
+
// instead of dropping silently, and get retried on the next tool call.
|
|
520
|
+
const PENDING_REPORTS_PATH = () => join(homedir(), '.drafted', 'pending-reports.jsonl');
|
|
521
|
+
const MAX_PENDING_REPORTS = 50;
|
|
522
|
+
|
|
523
|
+
function readPendingReports() {
|
|
524
|
+
try {
|
|
525
|
+
return readFileSync(PENDING_REPORTS_PATH(), 'utf8')
|
|
526
|
+
.split('\n')
|
|
527
|
+
.filter(Boolean)
|
|
528
|
+
.map((line) => { try { return JSON.parse(line); } catch { return null; } })
|
|
529
|
+
.filter(Boolean);
|
|
530
|
+
} catch {
|
|
531
|
+
return [];
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function writePendingReports(reports) {
|
|
536
|
+
const reportsPath = PENDING_REPORTS_PATH();
|
|
537
|
+
if (!reports.length) {
|
|
538
|
+
try { unlinkSync(reportsPath); } catch { /* nothing to remove */ }
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
try {
|
|
542
|
+
mkdirSync(dirname(reportsPath), { recursive: true });
|
|
543
|
+
writeFileSync(reportsPath, reports.slice(-MAX_PENDING_REPORTS).map((r) => JSON.stringify(r)).join('\n') + '\n', { mode: 0o600 });
|
|
544
|
+
} catch { /* best effort */ }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function flushPendingReports(serverUrl) {
|
|
548
|
+
const pending = readPendingReports();
|
|
549
|
+
if (!pending.length) return;
|
|
550
|
+
const stillPending = [];
|
|
551
|
+
for (const body of pending) {
|
|
552
|
+
try {
|
|
553
|
+
await fetch(`${serverUrl}/api/installations/report`, {
|
|
554
|
+
method: 'POST',
|
|
555
|
+
headers: { 'Content-Type': 'application/json', 'User-Agent': `Drafted MCP/${PACKAGE_VERSION}` },
|
|
556
|
+
body: JSON.stringify(body),
|
|
557
|
+
});
|
|
558
|
+
} catch {
|
|
559
|
+
stillPending.push(body);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
writePendingReports(stillPending);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Serializes all report sends/flushes so rapid back-to-back tool calls (the common
|
|
566
|
+
// case during an outage, when a client retries repeatedly) don't race on a
|
|
567
|
+
// read-modify-write of the same pending-reports file and drop queued events.
|
|
568
|
+
let installReportChain = Promise.resolve();
|
|
569
|
+
|
|
570
|
+
async function sendInstallationReport(serverUrl, body) {
|
|
571
|
+
await flushPendingReports(serverUrl);
|
|
572
|
+
try {
|
|
573
|
+
await fetch(`${serverUrl}/api/installations/report`, {
|
|
574
|
+
method: 'POST',
|
|
575
|
+
headers: { 'Content-Type': 'application/json', 'User-Agent': `Drafted MCP/${PACKAGE_VERSION}` },
|
|
576
|
+
body: JSON.stringify(body),
|
|
577
|
+
});
|
|
578
|
+
} catch {
|
|
579
|
+
writePendingReports([...readPendingReports(), body]);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
518
583
|
function reportInstallationEvent(event, extra = {}) {
|
|
519
584
|
const info = getInstallInfo();
|
|
520
585
|
if (!info) return;
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}),
|
|
537
|
-
}).catch(() => {});
|
|
586
|
+
const serverUrl = getServerUrl();
|
|
587
|
+
const body = {
|
|
588
|
+
installId: info.installId,
|
|
589
|
+
event,
|
|
590
|
+
schemaVersion: 1,
|
|
591
|
+
cliVersion: PACKAGE_VERSION,
|
|
592
|
+
osFamily: osFamily(),
|
|
593
|
+
osVersion: osRelease().slice(0, 60),
|
|
594
|
+
arch: normalizedArch(),
|
|
595
|
+
nodeVersion: process.version,
|
|
596
|
+
mcpMode: mcpMode(),
|
|
597
|
+
source: 'mcp',
|
|
598
|
+
...extra,
|
|
599
|
+
};
|
|
600
|
+
installReportChain = installReportChain.then(() => sendInstallationReport(serverUrl, body)).catch(() => {});
|
|
538
601
|
}
|
|
539
602
|
|
|
540
603
|
function classifyMcpError(error) {
|
|
@@ -2046,7 +2109,6 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
2046
2109
|
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.'),
|
|
2047
2110
|
excalidraw_data: z.any().optional().describe('[write_excalidraw] Excalidraw scene JSON object or JSON string. Defaults to an empty scene.'),
|
|
2048
2111
|
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.'),
|
|
2049
|
-
mermaid: z.string().optional().describe('[write_excalidraw] Mermaid source to convert into an editable Excalidraw scene.'),
|
|
2050
2112
|
file_path: z.string().optional().describe('[write] absolute path to a local file to upload. Mutually exclusive with content/base64/googleType.'),
|
|
2051
2113
|
base64: z.string().optional().describe('[write] base64-encoded binary content. Mutually exclusive with content/file_path/googleType. Use with content_type when known.'),
|
|
2052
2114
|
content_type: z.string().optional().describe('[write + base64] MIME type for base64 binary content, e.g. image/png, application/pdf. Defaults from the path extension or application/octet-stream.'),
|
|
@@ -2438,13 +2500,12 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
2438
2500
|
});
|
|
2439
2501
|
}
|
|
2440
2502
|
case 'write_excalidraw': {
|
|
2441
|
-
const { path, excalidraw_data,
|
|
2503
|
+
const { path, excalidraw_data, width, height, color } = args;
|
|
2442
2504
|
if (!path) throw new Error('path required for action=write_excalidraw');
|
|
2443
|
-
if (excalidraw_data != null && mermaid) throw new Error('Provide only one of excalidraw_data or mermaid');
|
|
2444
2505
|
const parts = path.replace(/^\/+/, '').split('/');
|
|
2445
2506
|
if (parts.length !== 3) throw new Error('Path must be /{layer}/{lane}/{filename}');
|
|
2446
2507
|
const filename = parts[2].toLowerCase().endsWith('.excalidraw') ? parts[2] : parts[2] + '.excalidraw';
|
|
2447
|
-
const scene =
|
|
2508
|
+
const scene = excalidraw_data ?? emptyExcalidrawScene();
|
|
2448
2509
|
const body = { content: stringifyExcalidrawScene(scene) };
|
|
2449
2510
|
if (width) body.width = width;
|
|
2450
2511
|
if (height) body.height = height;
|
|
@@ -3462,13 +3523,12 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3462
3523
|
// `LINE+ID|content`). The server applies the ops via the same
|
|
3463
3524
|
// hashline algorithm — no client-side re-hashing, no algorithm drift.
|
|
3464
3525
|
case 'write_excalidraw': {
|
|
3465
|
-
const { path, excalidraw_data,
|
|
3526
|
+
const { path, excalidraw_data, width, height, color } = args;
|
|
3466
3527
|
if (!path) throw new Error('path required for action=write_excalidraw');
|
|
3467
|
-
if (excalidraw_data != null && mermaid) throw new Error('Provide only one of excalidraw_data or mermaid');
|
|
3468
3528
|
const parts = path.replace(/^\/+/, '').split('/');
|
|
3469
3529
|
if (parts.length !== 3) throw new Error('Path must be /{layer}/{lane}/{filename}');
|
|
3470
3530
|
const filename = parts[2].toLowerCase().endsWith('.excalidraw') ? parts[2] : parts[2] + '.excalidraw';
|
|
3471
|
-
const scene =
|
|
3531
|
+
const scene = excalidraw_data ?? emptyExcalidrawScene();
|
|
3472
3532
|
const body = { content: stringifyExcalidrawScene(scene) };
|
|
3473
3533
|
if (width) body.width = width;
|
|
3474
3534
|
if (height) body.height = height;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.38",
|
|
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": [
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
"@clerk/express": "^2.1.4",
|
|
46
46
|
"@clerk/mcp-tools": "^0.3.1",
|
|
47
47
|
"@excalidraw/excalidraw": "^0.18.1",
|
|
48
|
-
"@excalidraw/mermaid-to-excalidraw": "^2.2.2",
|
|
49
48
|
"@modelcontextprotocol/ext-apps": "^1.6.0",
|
|
50
49
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
51
50
|
"@sentry/browser": "^10.53.1",
|
|
@@ -59,7 +58,6 @@
|
|
|
59
58
|
"esbuild": "^0.28.0",
|
|
60
59
|
"exceljs": "^4.4.0",
|
|
61
60
|
"express": "^4.18.2",
|
|
62
|
-
"jsdom": "^27.0.1",
|
|
63
61
|
"jszip": "^3.10.1",
|
|
64
62
|
"lucide": "^1.16.0",
|
|
65
63
|
"mdast-util-from-markdown": "^2.0.3",
|
|
@@ -38,64 +38,3 @@ export function normalizeExcalidrawScene(input) {
|
|
|
38
38
|
export function stringifyExcalidrawScene(input) {
|
|
39
39
|
return JSON.stringify(normalizeExcalidrawScene(input), null, 2);
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
function restoreMermaidDom(previous, dom) {
|
|
43
|
-
for (const [key, value] of Object.entries(previous)) {
|
|
44
|
-
if (value === undefined) {
|
|
45
|
-
try { delete globalThis[key]; } catch {}
|
|
46
|
-
} else if (key === 'navigator') {
|
|
47
|
-
Object.defineProperty(globalThis, 'navigator', { value, configurable: true });
|
|
48
|
-
} else {
|
|
49
|
-
globalThis[key] = value;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (dom) dom.window.close();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function excalidrawSceneFromMermaid(mermaid) {
|
|
56
|
-
let dom;
|
|
57
|
-
const previous = {
|
|
58
|
-
window: globalThis.window,
|
|
59
|
-
document: globalThis.document,
|
|
60
|
-
DOMParser: globalThis.DOMParser,
|
|
61
|
-
XMLSerializer: globalThis.XMLSerializer,
|
|
62
|
-
Element: globalThis.Element,
|
|
63
|
-
SVGElement: globalThis.SVGElement,
|
|
64
|
-
navigator: globalThis.navigator,
|
|
65
|
-
CSSStyleSheet: globalThis.CSSStyleSheet,
|
|
66
|
-
};
|
|
67
|
-
try {
|
|
68
|
-
if (typeof document === 'undefined') {
|
|
69
|
-
const { JSDOM } = await import('jsdom');
|
|
70
|
-
dom = new JSDOM('<!doctype html><html><body></body></html>');
|
|
71
|
-
globalThis.window = dom.window;
|
|
72
|
-
globalThis.document = dom.window.document;
|
|
73
|
-
globalThis.DOMParser = dom.window.DOMParser;
|
|
74
|
-
globalThis.XMLSerializer = dom.window.XMLSerializer;
|
|
75
|
-
globalThis.Element = dom.window.Element;
|
|
76
|
-
globalThis.SVGElement = dom.window.SVGElement;
|
|
77
|
-
if (globalThis.SVGElement && !globalThis.SVGElement.prototype.getBBox) {
|
|
78
|
-
globalThis.SVGElement.prototype.getBBox = function () {
|
|
79
|
-
const text = this.textContent || '';
|
|
80
|
-
return { x: 0, y: 0, width: Math.max(24, text.length * 8), height: 20 };
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator, configurable: true });
|
|
84
|
-
globalThis.CSSStyleSheet = class {
|
|
85
|
-
constructor() { this.cssRules = []; }
|
|
86
|
-
replaceSync() { this.cssRules = []; }
|
|
87
|
-
insertRule(rule) { this.cssRules.push({ cssText: rule }); }
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
const { parseMermaidToExcalidraw } = await import('@excalidraw/mermaid-to-excalidraw');
|
|
91
|
-
const result = await parseMermaidToExcalidraw(mermaid);
|
|
92
|
-
return {
|
|
93
|
-
...emptyExcalidrawScene(),
|
|
94
|
-
elements: result.elements || [],
|
|
95
|
-
files: result.files || {},
|
|
96
|
-
appState: { viewBackgroundColor: '#ffffff' },
|
|
97
|
-
};
|
|
98
|
-
} finally {
|
|
99
|
-
if (dom) restoreMermaidDom(previous, dom);
|
|
100
|
-
}
|
|
101
|
-
}
|