drafted 1.11.37 → 1.12.0
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 +149 -17
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -215,6 +215,9 @@ const TOOL_ANNOTATIONS = {
|
|
|
215
215
|
// Auth — initiates external browser / email flows
|
|
216
216
|
auth: { title: 'Sign in', readOnlyHint: false, destructiveHint: false, openWorldHint: true, description: 'Sign in to Drafted. `action=get_link` returns a URL immediately and starts background approval polling; after the user opens the link, later Drafted tool calls also auto-consume the approved login. `action=login` opens a browser when needed and explicitly waits/polls for approval.' },
|
|
217
217
|
|
|
218
|
+
// Identity — read-only introspection of THIS agent's session
|
|
219
|
+
whoami: { title: 'Session identity', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'Return THIS agent session\'s identity: its server-assigned human-readable name + emoji (the correlation key between an agent window and its web-app session tab), sessionId, userId, orgId, active projectId, editor label, server URL, and surfaced/alive state. Read-only. Use this — not guesses from the host environment — to report which session you are.' },
|
|
220
|
+
|
|
218
221
|
// Projects
|
|
219
222
|
project: { title: 'Projects', readOnlyHint: false, destructiveHint: false, openWorldHint: false, widgetUri: 'ui://widget/drafted-canvas-overview.html', description: 'Manage projects: list (start here), open (switch active project), create, update, move to another org.' },
|
|
220
223
|
get_org: { title: 'Organization', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Get the active organization (action="get", default), Google Drive availability, or switch to a different org (action="switch", orgId=...). Use switch when you need wiki/skill work in an org that has no projects — opening a project also switches, but is unavailable in empty orgs. When googleDrive.connected is true, strongly prefer Google Workspace frames for documents, sheets, and slides.' },
|
|
@@ -515,26 +518,89 @@ function mcpMode() {
|
|
|
515
518
|
return process.argv.includes('--http') ? 'http' : 'stdio';
|
|
516
519
|
}
|
|
517
520
|
|
|
521
|
+
// ponytail: reports that fail to send (e.g. server unreachable) queue here on disk
|
|
522
|
+
// instead of dropping silently, and get retried on the next tool call.
|
|
523
|
+
const PENDING_REPORTS_PATH = () => join(homedir(), '.drafted', 'pending-reports.jsonl');
|
|
524
|
+
const MAX_PENDING_REPORTS = 50;
|
|
525
|
+
|
|
526
|
+
function readPendingReports() {
|
|
527
|
+
try {
|
|
528
|
+
return readFileSync(PENDING_REPORTS_PATH(), 'utf8')
|
|
529
|
+
.split('\n')
|
|
530
|
+
.filter(Boolean)
|
|
531
|
+
.map((line) => { try { return JSON.parse(line); } catch { return null; } })
|
|
532
|
+
.filter(Boolean);
|
|
533
|
+
} catch {
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function writePendingReports(reports) {
|
|
539
|
+
const reportsPath = PENDING_REPORTS_PATH();
|
|
540
|
+
if (!reports.length) {
|
|
541
|
+
try { unlinkSync(reportsPath); } catch { /* nothing to remove */ }
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
mkdirSync(dirname(reportsPath), { recursive: true });
|
|
546
|
+
writeFileSync(reportsPath, reports.slice(-MAX_PENDING_REPORTS).map((r) => JSON.stringify(r)).join('\n') + '\n', { mode: 0o600 });
|
|
547
|
+
} catch { /* best effort */ }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function flushPendingReports(serverUrl) {
|
|
551
|
+
const pending = readPendingReports();
|
|
552
|
+
if (!pending.length) return;
|
|
553
|
+
const stillPending = [];
|
|
554
|
+
for (const body of pending) {
|
|
555
|
+
try {
|
|
556
|
+
await fetch(`${serverUrl}/api/installations/report`, {
|
|
557
|
+
method: 'POST',
|
|
558
|
+
headers: { 'Content-Type': 'application/json', 'User-Agent': `Drafted MCP/${PACKAGE_VERSION}` },
|
|
559
|
+
body: JSON.stringify(body),
|
|
560
|
+
});
|
|
561
|
+
} catch {
|
|
562
|
+
stillPending.push(body);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
writePendingReports(stillPending);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Serializes all report sends/flushes so rapid back-to-back tool calls (the common
|
|
569
|
+
// case during an outage, when a client retries repeatedly) don't race on a
|
|
570
|
+
// read-modify-write of the same pending-reports file and drop queued events.
|
|
571
|
+
let installReportChain = Promise.resolve();
|
|
572
|
+
|
|
573
|
+
async function sendInstallationReport(serverUrl, body) {
|
|
574
|
+
await flushPendingReports(serverUrl);
|
|
575
|
+
try {
|
|
576
|
+
await fetch(`${serverUrl}/api/installations/report`, {
|
|
577
|
+
method: 'POST',
|
|
578
|
+
headers: { 'Content-Type': 'application/json', 'User-Agent': `Drafted MCP/${PACKAGE_VERSION}` },
|
|
579
|
+
body: JSON.stringify(body),
|
|
580
|
+
});
|
|
581
|
+
} catch {
|
|
582
|
+
writePendingReports([...readPendingReports(), body]);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
518
586
|
function reportInstallationEvent(event, extra = {}) {
|
|
519
587
|
const info = getInstallInfo();
|
|
520
588
|
if (!info) return;
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}),
|
|
537
|
-
}).catch(() => {});
|
|
589
|
+
const serverUrl = getServerUrl();
|
|
590
|
+
const body = {
|
|
591
|
+
installId: info.installId,
|
|
592
|
+
event,
|
|
593
|
+
schemaVersion: 1,
|
|
594
|
+
cliVersion: PACKAGE_VERSION,
|
|
595
|
+
osFamily: osFamily(),
|
|
596
|
+
osVersion: osRelease().slice(0, 60),
|
|
597
|
+
arch: normalizedArch(),
|
|
598
|
+
nodeVersion: process.version,
|
|
599
|
+
mcpMode: mcpMode(),
|
|
600
|
+
source: 'mcp',
|
|
601
|
+
...extra,
|
|
602
|
+
};
|
|
603
|
+
installReportChain = installReportChain.then(() => sendInstallationReport(serverUrl, body)).catch(() => {});
|
|
538
604
|
}
|
|
539
605
|
|
|
540
606
|
function classifyMcpError(error) {
|
|
@@ -1001,6 +1067,10 @@ function withProjectBreadcrumb(result) {
|
|
|
1001
1067
|
|
|
1002
1068
|
let agentWs = null;
|
|
1003
1069
|
let agentWsReconnectTimer = null;
|
|
1070
|
+
// Cached agent-hello-ack: this agent's own surface identity (name/emoji/alive) as
|
|
1071
|
+
// assigned by the server. The playful name is the correlation key between an agent
|
|
1072
|
+
// window and its web-app session tab; whoami reads it from here.
|
|
1073
|
+
let agentSurface = null;
|
|
1004
1074
|
|
|
1005
1075
|
function setMcpActiveProject(projectId, meta = null) {
|
|
1006
1076
|
const s = getState();
|
|
@@ -1052,6 +1122,21 @@ async function connectAgentWs() {
|
|
|
1052
1122
|
}
|
|
1053
1123
|
});
|
|
1054
1124
|
|
|
1125
|
+
agentWs.on('message', (raw) => {
|
|
1126
|
+
// agent-hello-ack carries this agent's server-assigned surface identity back on the
|
|
1127
|
+
// owning connection. Cache it so whoami can report the name without a prod WS probe.
|
|
1128
|
+
try {
|
|
1129
|
+
const m = JSON.parse(raw.toString());
|
|
1130
|
+
if (m.type === 'agent-hello-ack') {
|
|
1131
|
+
agentSurface = {
|
|
1132
|
+
sessionId: m.sessionId, userId: m.userId, orgId: m.orgId, projectId: m.projectId,
|
|
1133
|
+
name: m.name, emoji: m.emoji, alive: m.alive, surfaced: true,
|
|
1134
|
+
capturedAt: Date.now(),
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
} catch { /* ignore non-JSON / unexpected */ }
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1055
1140
|
agentWs.on('close', () => {
|
|
1056
1141
|
console.error('[MCP-WS] Disconnected, reconnecting in 5s...');
|
|
1057
1142
|
agentWs = null;
|
|
@@ -1426,6 +1511,53 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a ver
|
|
|
1426
1511
|
} catch (error) { return err(error); }
|
|
1427
1512
|
});
|
|
1428
1513
|
|
|
1514
|
+
// Identity: report THIS agent session's own surface identity. The name/emoji come from
|
|
1515
|
+
// the agent-hello-ack cached on the WS; falls back to /auth/me for userId/org if the WS
|
|
1516
|
+
// ack hasn't landed yet. Read-only — no state changed.
|
|
1517
|
+
tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human-readable name + emoji (the correlation key between an agent window and its web-app session tab), sessionId, userId, orgId, active projectId, editor label, server URL, and surfaced/alive state. Read-only.', {}, async () => {
|
|
1518
|
+
try {
|
|
1519
|
+
const server = getServerUrl();
|
|
1520
|
+
const editor = (process.env.DRAFTED_AGENT_NAME || '').trim() || null;
|
|
1521
|
+
const base = {
|
|
1522
|
+
server,
|
|
1523
|
+
editor,
|
|
1524
|
+
agentLabel: getAgentLabel(),
|
|
1525
|
+
sessionId: agentSurface?.sessionId || getState().sessionId || null,
|
|
1526
|
+
};
|
|
1527
|
+
if (agentSurface) {
|
|
1528
|
+
return ok({
|
|
1529
|
+
...base,
|
|
1530
|
+
userId: agentSurface.userId ?? null,
|
|
1531
|
+
orgId: agentSurface.orgId ?? null,
|
|
1532
|
+
projectId: agentSurface.projectId ?? null,
|
|
1533
|
+
name: agentSurface.name,
|
|
1534
|
+
emoji: agentSurface.emoji,
|
|
1535
|
+
surfaced: true,
|
|
1536
|
+
alive: !!agentSurface.alive,
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
// No WS ack yet — best-effort identity from /auth/me so callers still get a userId/org.
|
|
1540
|
+
const sid = base.sessionId || getBootstrapSessionId();
|
|
1541
|
+
let me = null;
|
|
1542
|
+
if (sid) {
|
|
1543
|
+
try {
|
|
1544
|
+
const res = await fetch(`${server}/auth/me`, { headers: { Cookie: `gc_session=${sid}` } });
|
|
1545
|
+
if (res.ok) me = await res.json();
|
|
1546
|
+
} catch { /* not yet authenticated */ }
|
|
1547
|
+
}
|
|
1548
|
+
return ok({
|
|
1549
|
+
...base,
|
|
1550
|
+
userId: me?.userId ?? null,
|
|
1551
|
+
orgId: me?.currentOrg?.id ?? null,
|
|
1552
|
+
projectId: getState().projectId ?? null,
|
|
1553
|
+
name: null,
|
|
1554
|
+
emoji: null,
|
|
1555
|
+
surfaced: false,
|
|
1556
|
+
alive: false,
|
|
1557
|
+
});
|
|
1558
|
+
} catch (error) { return err(error); }
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1429
1561
|
// ── Project management tools (direct HTTP) ────────────────────────
|
|
1430
1562
|
|
|
1431
1563
|
tool('project', 'START HERE for project management. Dispatch by `action`: list (lists all projects across all orgs — always call first), open (switch the active project; required before reading/writing frames), create (new project, optionally from a template), update (change name/folder/description/layers), move (transfer to another org). Opening a project auto-switches the org. To change orgs WITHOUT a project (for wiki/skill work in an empty org), use get_org(action="switch", orgId=...). **Skill gate:** projects with attached skills will REJECT all mutations (write, edit, mv, rm, shape, group, connector, layout, layer, asset upload) until you have loaded each attached skill via skill(action="load"). Skills tell you HOW to do the work — they\'re not optional. Open returns the attached skill list and auto-inlines content for projects with ≤3 skills.', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
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": [
|