drafted 1.1.4 → 1.2.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 +86 -5
- 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;
|
|
@@ -1026,6 +1081,8 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1026
1081
|
if (!path) throw new Error('path required for action=write');
|
|
1027
1082
|
if (content != null && file_path) throw new Error('Provide content OR file_path, not both');
|
|
1028
1083
|
if (content == null && !file_path) throw new Error('Provide content or file_path');
|
|
1084
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1085
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1029
1086
|
const anchorErr = await checkAnchors(parseLayer(path));
|
|
1030
1087
|
if (anchorErr) return err(new Error(anchorErr));
|
|
1031
1088
|
const parts = path.replace(/^\/+/, '').split('/');
|
|
@@ -1061,6 +1118,8 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1061
1118
|
const { path, operations } = args;
|
|
1062
1119
|
if (!path) throw new Error('path required for action=edit');
|
|
1063
1120
|
if (!Array.isArray(operations) || operations.length === 0) throw new Error('operations (array) required for action=edit');
|
|
1121
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1122
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1064
1123
|
const anchorErr = await checkAnchors(parseLayer(path));
|
|
1065
1124
|
if (anchorErr) return err(new Error(anchorErr));
|
|
1066
1125
|
const result = await api('POST', '/api/fs/edit', { path, operations });
|
|
@@ -1072,6 +1131,8 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1072
1131
|
case 'mv': {
|
|
1073
1132
|
const { from, to } = args;
|
|
1074
1133
|
if (!from || !to) throw new Error('from and to required for action=mv');
|
|
1134
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1135
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1075
1136
|
const fromErr = await checkAnchors(parseLayer(from));
|
|
1076
1137
|
if (fromErr) return err(new Error(fromErr));
|
|
1077
1138
|
const toErr = await checkAnchors(parseLayer(to));
|
|
@@ -1082,6 +1143,8 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1082
1143
|
const { path, anchored } = args;
|
|
1083
1144
|
if (!path) throw new Error('path required for action=anchor');
|
|
1084
1145
|
if (typeof anchored !== 'boolean') throw new Error('anchored (boolean) required for action=anchor');
|
|
1146
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1147
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1085
1148
|
return ok(await api('POST', '/api/fs/anchor', { path, anchored }));
|
|
1086
1149
|
}
|
|
1087
1150
|
case 'search': {
|
|
@@ -1174,6 +1237,8 @@ tool('rm', 'Delete a frame or lane from the ACTIVE PROJECT. Response includes "p
|
|
|
1174
1237
|
path: z.string().describe('Path to delete: /{layer}/{lane}/{filename} or /{layer}/{lane} (deletes entire lane).'),
|
|
1175
1238
|
}, async ({ path }) => {
|
|
1176
1239
|
try {
|
|
1240
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1241
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1177
1242
|
const anchorErr = await checkAnchors(parseLayer(path));
|
|
1178
1243
|
if (anchorErr) return err(new Error(anchorErr));
|
|
1179
1244
|
const clean = path.replace(/^\/+|\/+$/g, '');
|
|
@@ -1303,6 +1368,8 @@ tool('asset', 'Manage supporting files (CSS, JS, images, fonts) in the ACTIVE PR
|
|
|
1303
1368
|
const projectId = getState().projectId;
|
|
1304
1369
|
if (!projectId) throw new Error('No active project. Call project(action="open") first.');
|
|
1305
1370
|
if (action === 'upload') {
|
|
1371
|
+
const skillErr = await checkProjectSkills(projectId);
|
|
1372
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1306
1373
|
const { asset_path, file_path, content, base64: rawBase64, content_type, frame_id } = args;
|
|
1307
1374
|
if (!asset_path) throw new Error('asset_path is required for action=upload');
|
|
1308
1375
|
if (asset_path.includes('..')) throw new Error('asset_path must not contain ".."');
|
|
@@ -1351,6 +1418,8 @@ tool('shape', 'Create a flowchart shape on the surface. Shapes are lightweight t
|
|
|
1351
1418
|
layoutIgnore: z.boolean().optional().describe('Exclude this shape from auto-layout. Use for annotations, labels, or manually positioned elements.'),
|
|
1352
1419
|
}, async ({ text, shape, layer, lane, color, fill, textColor, group, width, height, layoutIgnore }) => {
|
|
1353
1420
|
try {
|
|
1421
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1422
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1354
1423
|
const body = { text, shape };
|
|
1355
1424
|
if (layer) body.layer = layer;
|
|
1356
1425
|
if (lane) body.lane = lane;
|
|
@@ -1375,6 +1444,8 @@ tool('group', 'Create a group (swim lane / region) on the surface. Groups are la
|
|
|
1375
1444
|
order: z.number().optional().describe('Sort order for group positioning (lower = first). Default: 0'),
|
|
1376
1445
|
}, async ({ label, color, fill, layer, lane, order }) => {
|
|
1377
1446
|
try {
|
|
1447
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1448
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1378
1449
|
const body = { label };
|
|
1379
1450
|
if (color) body.color = color;
|
|
1380
1451
|
if (fill) body.fill = fill;
|
|
@@ -1397,6 +1468,8 @@ tool('connector', 'Create or remove connectors (arrows) between frames on the su
|
|
|
1397
1468
|
connectorId: z.string().optional().describe('[disconnect] connector ID to delete directly'),
|
|
1398
1469
|
}, async (args) => {
|
|
1399
1470
|
try {
|
|
1471
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1472
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1400
1473
|
const { action } = args;
|
|
1401
1474
|
if (action === 'connect') {
|
|
1402
1475
|
const { source, target, label, type = 'arrow-forward', color } = args;
|
|
@@ -1438,6 +1511,8 @@ tool('layout', 'Auto-arrange frames using graph layout algorithm. Positions conn
|
|
|
1438
1511
|
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
1512
|
}, async ({ direction, groups }) => {
|
|
1440
1513
|
try {
|
|
1514
|
+
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1515
|
+
if (skillErr) return err(new Error(skillErr));
|
|
1441
1516
|
const body = { direction };
|
|
1442
1517
|
if (groups) body.groups = true;
|
|
1443
1518
|
const result = await api('POST', '/api/layout', body);
|
|
@@ -1492,7 +1567,9 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
1492
1567
|
if (!skill) throw new Error('skill (ID or slug) is required for action=load');
|
|
1493
1568
|
const isUuid = /^[a-f0-9-]{36}$/.test(skill);
|
|
1494
1569
|
const endpoint = isUuid ? `/api/skills/${skill}` : `/api/skills/slug/${skill}`;
|
|
1495
|
-
|
|
1570
|
+
const result = await api('GET', endpoint);
|
|
1571
|
+
if (result?.id) loadedSkillIds.add(result.id);
|
|
1572
|
+
return ok(result);
|
|
1496
1573
|
}
|
|
1497
1574
|
case 'list': {
|
|
1498
1575
|
if (!getState().projectId) throw new Error('No active project. Call project(action="open") first.');
|
|
@@ -1527,13 +1604,17 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
1527
1604
|
const { skillId } = args;
|
|
1528
1605
|
if (!skillId) throw new Error('skillId required for action=attach');
|
|
1529
1606
|
if (!getState().projectId) throw new Error('No active project. Call project(action="open") first.');
|
|
1530
|
-
|
|
1607
|
+
const result = await api('POST', `/api/projects/${getState().projectId}/skills`, { skillId });
|
|
1608
|
+
invalidateProjectSkillsCache(getState().projectId);
|
|
1609
|
+
return ok(result);
|
|
1531
1610
|
}
|
|
1532
1611
|
case 'detach': {
|
|
1533
1612
|
const { skillId } = args;
|
|
1534
1613
|
if (!skillId) throw new Error('skillId required for action=detach');
|
|
1535
1614
|
if (!getState().projectId) throw new Error('No active project. Call project(action="open") first.');
|
|
1536
|
-
|
|
1615
|
+
const result = await api('DELETE', `/api/projects/${getState().projectId}/skills/${skillId}`);
|
|
1616
|
+
invalidateProjectSkillsCache(getState().projectId);
|
|
1617
|
+
return ok(result);
|
|
1537
1618
|
}
|
|
1538
1619
|
case 'favorite': {
|
|
1539
1620
|
const { skillId } = args;
|