drafted 1.1.4 → 1.2.1
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 +103 -7
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -478,6 +478,43 @@ async function checkAnchors(layer) {
|
|
|
478
478
|
`Read all anchored frames first, then retry your operation.`;
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
+
// ── Project skill enforcement ─────────────────────────────────────
|
|
482
|
+
// Skills attached to a project must be loaded by the agent before any
|
|
483
|
+
// mutating operation in that project. Same shape as anchor enforcement.
|
|
484
|
+
const loadedSkillIds = new Set();
|
|
485
|
+
const projectSkillsCache = new Map(); // projectId -> { data, time }
|
|
486
|
+
|
|
487
|
+
async function getProjectSkills(projectId) {
|
|
488
|
+
if (!projectId) return [];
|
|
489
|
+
const cached = projectSkillsCache.get(projectId);
|
|
490
|
+
if (cached && Date.now() - cached.time < 10000) return cached.data;
|
|
491
|
+
try {
|
|
492
|
+
const result = await api('GET', `/api/projects/${projectId}/skills`);
|
|
493
|
+
const skills = Array.isArray(result?.skills) ? result.skills : (Array.isArray(result) ? result : []);
|
|
494
|
+
projectSkillsCache.set(projectId, { data: skills, time: Date.now() });
|
|
495
|
+
return skills;
|
|
496
|
+
} catch {
|
|
497
|
+
return [];
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function invalidateProjectSkillsCache(projectId) {
|
|
502
|
+
if (projectId) projectSkillsCache.delete(projectId);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function checkProjectSkills(projectId) {
|
|
506
|
+
if (!projectId) return null;
|
|
507
|
+
const skills = await getProjectSkills(projectId);
|
|
508
|
+
if (skills.length === 0) return null;
|
|
509
|
+
const unloaded = skills.filter(s => !loadedSkillIds.has(s.id));
|
|
510
|
+
if (unloaded.length === 0) return null;
|
|
511
|
+
|
|
512
|
+
const lines = unloaded.map(s => ` skill(action="load", skill="${s.slug || s.id}") -- ${s.name}`);
|
|
513
|
+
return `This project has ${skills.length} attached skill(s) that must be loaded before making changes. ` +
|
|
514
|
+
`Unloaded skills:\n${lines.join('\n')}\n\n` +
|
|
515
|
+
`Load all attached skills first, then retry your operation. Skills tell you HOW to do the work — they're not optional.`;
|
|
516
|
+
}
|
|
517
|
+
|
|
481
518
|
// ── CLI passthrough (for login/start/stop only) ───────────────────
|
|
482
519
|
|
|
483
520
|
function runCLI(command, args = [], options = {}) {
|
|
@@ -602,7 +639,7 @@ tool('auth', 'Sign in to Drafted. `action=get_link` returns a verification URL i
|
|
|
602
639
|
|
|
603
640
|
// ── Project management tools (direct HTTP) ────────────────────────
|
|
604
641
|
|
|
605
|
-
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). The org switches automatically when you open a project.', {
|
|
642
|
+
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). The org switches automatically when you open a project. **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.', {
|
|
606
643
|
action: z.enum(['list', 'open', 'create', 'update', 'move']).describe('Operation to perform.'),
|
|
607
644
|
projectId: z.string().optional().describe('[open|update|move] project ID. Get IDs from action=list.'),
|
|
608
645
|
name: z.string().optional().describe('[create|update] project name'),
|
|
@@ -683,11 +720,27 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
|
|
|
683
720
|
const full = await api('GET', `/api/skills/${s.id}`);
|
|
684
721
|
s.content = full.content;
|
|
685
722
|
s.files = full.files || [];
|
|
723
|
+
// Skill content was inlined in this response — count it as loaded
|
|
724
|
+
// so the project-skill gate doesn't immediately demand a re-load.
|
|
725
|
+
loadedSkillIds.add(s.id);
|
|
686
726
|
} catch { /* skip */ }
|
|
687
727
|
}
|
|
688
728
|
}
|
|
689
729
|
|
|
690
|
-
|
|
730
|
+
// Refresh project-skill cache so the gate uses fresh data after open
|
|
731
|
+
invalidateProjectSkillsCache(projectId);
|
|
732
|
+
|
|
733
|
+
const responseExtras = { url, opened: true, navigated, skills: projectSkillsList };
|
|
734
|
+
if (projectSkillsList.length > 3) {
|
|
735
|
+
const unloaded = projectSkillsList.filter(s => !loadedSkillIds.has(s.id));
|
|
736
|
+
if (unloaded.length > 0) {
|
|
737
|
+
responseExtras.skillGateNotice =
|
|
738
|
+
`This project has ${projectSkillsList.length} attached skills (too many to auto-inline). ` +
|
|
739
|
+
`You MUST call skill(action="load") for each before any mutation. Unloaded: ` +
|
|
740
|
+
unloaded.map(s => s.slug || s.id).join(', ');
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return ok({ ...result, ...responseExtras });
|
|
691
744
|
}
|
|
692
745
|
case 'create': {
|
|
693
746
|
const { name, description, templateSlug } = args;
|
|
@@ -862,6 +915,8 @@ tool('layer', 'Manage layers in a project. Dispatch by `action`: add/update/remo
|
|
|
862
915
|
try {
|
|
863
916
|
const { action, projectId } = args;
|
|
864
917
|
if (!projectId) throw new Error('projectId is required');
|
|
918
|
+
const skillErr = await checkProjectSkills(projectId);
|
|
919
|
+
if (skillErr) return err(new Error(skillErr));
|
|
865
920
|
switch (action) {
|
|
866
921
|
case 'add': {
|
|
867
922
|
const { key, label, type, width, height, description, prompt } = args;
|
|
@@ -1016,8 +1071,23 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1016
1071
|
if (!lines && result.ok && result.id) {
|
|
1017
1072
|
readFrameIds.add(result.id);
|
|
1018
1073
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1074
|
+
// Surface content as the visible text. Some Claude clients prefer
|
|
1075
|
+
// structuredContent over text when both are present and structured looks
|
|
1076
|
+
// "complete" — so include content in BOTH places to ensure the agent
|
|
1077
|
+
// can always see the frame's actual content, not just metadata.
|
|
1078
|
+
const structured = frameStructuredContent(result);
|
|
1079
|
+
structured.content = result.content ?? '';
|
|
1080
|
+
structured.size = result.size;
|
|
1081
|
+
structured.totalLines = result.totalLines;
|
|
1082
|
+
const visibleText = JSON.stringify({
|
|
1083
|
+
path: result.path,
|
|
1084
|
+
contentType: result.contentType,
|
|
1085
|
+
size: result.size,
|
|
1086
|
+
totalLines: result.totalLines,
|
|
1087
|
+
content: result.content ?? '',
|
|
1088
|
+
}, null, 2);
|
|
1089
|
+
return ok(visibleText, {
|
|
1090
|
+
structuredContent: structured,
|
|
1021
1091
|
_meta: { frameHtml: result.content },
|
|
1022
1092
|
});
|
|
1023
1093
|
}
|
|
@@ -1026,6 +1096,8 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1026
1096
|
if (!path) throw new Error('path required for action=write');
|
|
1027
1097
|
if (content != null && file_path) throw new Error('Provide content OR file_path, not both');
|
|
1028
1098
|
if (content == null && !file_path) throw new Error('Provide content or file_path');
|
|
1099
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1100
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1029
1101
|
const anchorErr = await checkAnchors(parseLayer(path));
|
|
1030
1102
|
if (anchorErr) return err(new Error(anchorErr));
|
|
1031
1103
|
const parts = path.replace(/^\/+/, '').split('/');
|
|
@@ -1061,6 +1133,8 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1061
1133
|
const { path, operations } = args;
|
|
1062
1134
|
if (!path) throw new Error('path required for action=edit');
|
|
1063
1135
|
if (!Array.isArray(operations) || operations.length === 0) throw new Error('operations (array) required for action=edit');
|
|
1136
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1137
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1064
1138
|
const anchorErr = await checkAnchors(parseLayer(path));
|
|
1065
1139
|
if (anchorErr) return err(new Error(anchorErr));
|
|
1066
1140
|
const result = await api('POST', '/api/fs/edit', { path, operations });
|
|
@@ -1072,6 +1146,8 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1072
1146
|
case 'mv': {
|
|
1073
1147
|
const { from, to } = args;
|
|
1074
1148
|
if (!from || !to) throw new Error('from and to required for action=mv');
|
|
1149
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1150
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1075
1151
|
const fromErr = await checkAnchors(parseLayer(from));
|
|
1076
1152
|
if (fromErr) return err(new Error(fromErr));
|
|
1077
1153
|
const toErr = await checkAnchors(parseLayer(to));
|
|
@@ -1082,6 +1158,8 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1082
1158
|
const { path, anchored } = args;
|
|
1083
1159
|
if (!path) throw new Error('path required for action=anchor');
|
|
1084
1160
|
if (typeof anchored !== 'boolean') throw new Error('anchored (boolean) required for action=anchor');
|
|
1161
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1162
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1085
1163
|
return ok(await api('POST', '/api/fs/anchor', { path, anchored }));
|
|
1086
1164
|
}
|
|
1087
1165
|
case 'search': {
|
|
@@ -1174,6 +1252,8 @@ tool('rm', 'Delete a frame or lane from the ACTIVE PROJECT. Response includes "p
|
|
|
1174
1252
|
path: z.string().describe('Path to delete: /{layer}/{lane}/{filename} or /{layer}/{lane} (deletes entire lane).'),
|
|
1175
1253
|
}, async ({ path }) => {
|
|
1176
1254
|
try {
|
|
1255
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1256
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1177
1257
|
const anchorErr = await checkAnchors(parseLayer(path));
|
|
1178
1258
|
if (anchorErr) return err(new Error(anchorErr));
|
|
1179
1259
|
const clean = path.replace(/^\/+|\/+$/g, '');
|
|
@@ -1303,6 +1383,8 @@ tool('asset', 'Manage supporting files (CSS, JS, images, fonts) in the ACTIVE PR
|
|
|
1303
1383
|
const projectId = getState().projectId;
|
|
1304
1384
|
if (!projectId) throw new Error('No active project. Call project(action="open") first.');
|
|
1305
1385
|
if (action === 'upload') {
|
|
1386
|
+
const skillErr = await checkProjectSkills(projectId);
|
|
1387
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1306
1388
|
const { asset_path, file_path, content, base64: rawBase64, content_type, frame_id } = args;
|
|
1307
1389
|
if (!asset_path) throw new Error('asset_path is required for action=upload');
|
|
1308
1390
|
if (asset_path.includes('..')) throw new Error('asset_path must not contain ".."');
|
|
@@ -1351,6 +1433,8 @@ tool('shape', 'Create a flowchart shape on the surface. Shapes are lightweight t
|
|
|
1351
1433
|
layoutIgnore: z.boolean().optional().describe('Exclude this shape from auto-layout. Use for annotations, labels, or manually positioned elements.'),
|
|
1352
1434
|
}, async ({ text, shape, layer, lane, color, fill, textColor, group, width, height, layoutIgnore }) => {
|
|
1353
1435
|
try {
|
|
1436
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1437
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1354
1438
|
const body = { text, shape };
|
|
1355
1439
|
if (layer) body.layer = layer;
|
|
1356
1440
|
if (lane) body.lane = lane;
|
|
@@ -1375,6 +1459,8 @@ tool('group', 'Create a group (swim lane / region) on the surface. Groups are la
|
|
|
1375
1459
|
order: z.number().optional().describe('Sort order for group positioning (lower = first). Default: 0'),
|
|
1376
1460
|
}, async ({ label, color, fill, layer, lane, order }) => {
|
|
1377
1461
|
try {
|
|
1462
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1463
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1378
1464
|
const body = { label };
|
|
1379
1465
|
if (color) body.color = color;
|
|
1380
1466
|
if (fill) body.fill = fill;
|
|
@@ -1397,6 +1483,8 @@ tool('connector', 'Create or remove connectors (arrows) between frames on the su
|
|
|
1397
1483
|
connectorId: z.string().optional().describe('[disconnect] connector ID to delete directly'),
|
|
1398
1484
|
}, async (args) => {
|
|
1399
1485
|
try {
|
|
1486
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1487
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1400
1488
|
const { action } = args;
|
|
1401
1489
|
if (action === 'connect') {
|
|
1402
1490
|
const { source, target, label, type = 'arrow-forward', color } = args;
|
|
@@ -1438,6 +1526,8 @@ tool('layout', 'Auto-arrange frames using graph layout algorithm. Positions conn
|
|
|
1438
1526
|
groups: z.boolean().optional().default(false).describe('Enable group clustering. When true, shapes assigned to a group (via the group param) are clustered together, and group frames are resized to enclose their members.'),
|
|
1439
1527
|
}, async ({ direction, groups }) => {
|
|
1440
1528
|
try {
|
|
1529
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1530
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1441
1531
|
const body = { direction };
|
|
1442
1532
|
if (groups) body.groups = true;
|
|
1443
1533
|
const result = await api('POST', '/api/layout', body);
|
|
@@ -1492,7 +1582,9 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
1492
1582
|
if (!skill) throw new Error('skill (ID or slug) is required for action=load');
|
|
1493
1583
|
const isUuid = /^[a-f0-9-]{36}$/.test(skill);
|
|
1494
1584
|
const endpoint = isUuid ? `/api/skills/${skill}` : `/api/skills/slug/${skill}`;
|
|
1495
|
-
|
|
1585
|
+
const result = await api('GET', endpoint);
|
|
1586
|
+
if (result?.id) loadedSkillIds.add(result.id);
|
|
1587
|
+
return ok(result);
|
|
1496
1588
|
}
|
|
1497
1589
|
case 'list': {
|
|
1498
1590
|
if (!getState().projectId) throw new Error('No active project. Call project(action="open") first.');
|
|
@@ -1527,13 +1619,17 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
1527
1619
|
const { skillId } = args;
|
|
1528
1620
|
if (!skillId) throw new Error('skillId required for action=attach');
|
|
1529
1621
|
if (!getState().projectId) throw new Error('No active project. Call project(action="open") first.');
|
|
1530
|
-
|
|
1622
|
+
const result = await api('POST', `/api/projects/${getState().projectId}/skills`, { skillId });
|
|
1623
|
+
invalidateProjectSkillsCache(getState().projectId);
|
|
1624
|
+
return ok(result);
|
|
1531
1625
|
}
|
|
1532
1626
|
case 'detach': {
|
|
1533
1627
|
const { skillId } = args;
|
|
1534
1628
|
if (!skillId) throw new Error('skillId required for action=detach');
|
|
1535
1629
|
if (!getState().projectId) throw new Error('No active project. Call project(action="open") first.');
|
|
1536
|
-
|
|
1630
|
+
const result = await api('DELETE', `/api/projects/${getState().projectId}/skills/${skillId}`);
|
|
1631
|
+
invalidateProjectSkillsCache(getState().projectId);
|
|
1632
|
+
return ok(result);
|
|
1537
1633
|
}
|
|
1538
1634
|
case 'favorite': {
|
|
1539
1635
|
const { skillId } = args;
|