drafted 1.1.1 → 1.1.2
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 +216 -14
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -27,6 +27,8 @@ An org contains projects. Each project has a zoomable canvas with frames (HTML f
|
|
|
27
27
|
|
|
28
28
|
WORKFLOW: list_projects → open_project → ls / → read/write/edit. Projects span all orgs -- open_project auto-switches org context. Every response includes a "project" field showing which project you're operating on -- always verify it matches your intent before writing.
|
|
29
29
|
|
|
30
|
+
SKILLS: Drafted has a skill library -- reusable agent instructions stored as SKILL.md files. When a user says "use the X skill", use search_skills to find it, then load_skill to get its instructions. Skills can cover anything: UX guidelines, copywriting rules, brand voice, coding standards, review checklists, etc.
|
|
31
|
+
|
|
30
32
|
IMPORTANT: Any URL containing /f/{uuid} is a Drafted frame link — ALWAYS use read(path=URL) to get frame content, focus(target=URL) to pan the canvas to it. Never curl or WebFetch Drafted URLs.`,
|
|
31
33
|
});
|
|
32
34
|
|
|
@@ -190,7 +192,11 @@ async function connectAgentWs() {
|
|
|
190
192
|
});
|
|
191
193
|
}
|
|
192
194
|
|
|
193
|
-
function joinAgentWsRoom(projectId) {
|
|
195
|
+
async function joinAgentWsRoom(projectId) {
|
|
196
|
+
// Ensure WS is connected (may not be after restart/session re-clone)
|
|
197
|
+
if (!agentWs || agentWs.readyState > WebSocket.OPEN) {
|
|
198
|
+
await connectAgentWs();
|
|
199
|
+
}
|
|
194
200
|
if (!agentWs) return;
|
|
195
201
|
const msg = JSON.stringify({ type: 'join', projectId, agent: true });
|
|
196
202
|
if (agentWs.readyState === WebSocket.OPEN) {
|
|
@@ -384,6 +390,20 @@ server.tool('list_projects', 'START HERE. Lists all projects across all orgs. Us
|
|
|
384
390
|
try {
|
|
385
391
|
const data = await api('GET', '/api/projects');
|
|
386
392
|
data.agentProject = agentActiveProjectId || null;
|
|
393
|
+
// Inject favorited skills so the agent knows about them from the first call
|
|
394
|
+
try {
|
|
395
|
+
const favData = await api('GET', '/api/skills/favorites');
|
|
396
|
+
const favs = favData.skills || [];
|
|
397
|
+
if (favs.length > 0) {
|
|
398
|
+
data.favoritedSkills = favs.map(s => ({
|
|
399
|
+
id: s.id,
|
|
400
|
+
name: s.name,
|
|
401
|
+
slug: s.slug,
|
|
402
|
+
description: s.description,
|
|
403
|
+
tags: s.tags,
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
} catch { /* skills not available */ }
|
|
387
407
|
return ok(data);
|
|
388
408
|
} catch (error) { return err(error); }
|
|
389
409
|
});
|
|
@@ -456,7 +476,8 @@ server.tool('fork_template', {
|
|
|
456
476
|
|
|
457
477
|
server.tool('open_project', 'Switch active project. REQUIRED before reading or writing — all fs tools (ls, read, write, edit, rm, mv, batch) operate on the active project. Get project IDs from list_projects.', {
|
|
458
478
|
projectId: z.string().describe('Project ID to switch to and open in browser'),
|
|
459
|
-
|
|
479
|
+
skipBrowser: z.boolean().optional().describe('Skip opening/navigating a browser tab (use when the user already has the project open, e.g. from an invite snippet)'),
|
|
480
|
+
}, async ({ projectId, skipBrowser }) => {
|
|
460
481
|
try {
|
|
461
482
|
const result = await api('POST', '/api/project/switch', { projectId });
|
|
462
483
|
setMcpActiveProject(projectId);
|
|
@@ -470,19 +491,39 @@ server.tool('open_project', 'Switch active project. REQUIRED before reading or w
|
|
|
470
491
|
if (proj?.slug) projectSlug = proj.slug;
|
|
471
492
|
} catch { /* fall back to projectId */ }
|
|
472
493
|
const url = `${base}/project/${projectSlug}`;
|
|
473
|
-
// Navigate existing browser tabs instead of opening new ones
|
|
474
494
|
let navigated = 0;
|
|
495
|
+
if (!skipBrowser) {
|
|
496
|
+
// Navigate existing browser tabs instead of opening new ones
|
|
497
|
+
try {
|
|
498
|
+
const nav = await api('POST', '/api/project/navigate', { projectId });
|
|
499
|
+
navigated = nav.navigated || 0;
|
|
500
|
+
} catch { /* server may not support navigate yet */ }
|
|
501
|
+
// Only open a new tab if no browser tabs were reached
|
|
502
|
+
if (navigated === 0) {
|
|
503
|
+
const { exec } = await import('child_process');
|
|
504
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
505
|
+
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Fetch attached skills (metadata only)
|
|
509
|
+
let projectSkillsList = [];
|
|
475
510
|
try {
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
} catch { /*
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
|
|
511
|
+
const skillData = await api('GET', `/api/projects/${projectId}/skills`);
|
|
512
|
+
projectSkillsList = skillData.skills || [];
|
|
513
|
+
} catch { /* skills not available yet */ }
|
|
514
|
+
|
|
515
|
+
// Auto-load content for projects with <= 3 attached skills
|
|
516
|
+
if (projectSkillsList.length > 0 && projectSkillsList.length <= 3) {
|
|
517
|
+
for (const s of projectSkillsList) {
|
|
518
|
+
try {
|
|
519
|
+
const full = await api('GET', `/api/skills/${s.id}`);
|
|
520
|
+
s.content = full.content;
|
|
521
|
+
s.files = full.files || [];
|
|
522
|
+
} catch { /* skip */ }
|
|
523
|
+
}
|
|
484
524
|
}
|
|
485
|
-
|
|
525
|
+
|
|
526
|
+
return ok({ ...result, url, opened: true, navigated, skills: projectSkillsList });
|
|
486
527
|
} catch (error) { return err(error); }
|
|
487
528
|
});
|
|
488
529
|
|
|
@@ -604,8 +645,8 @@ server.tool('remove_layer', {
|
|
|
604
645
|
const listing = await api('GET', `/api/fs?path=/${key}&projectId=${projectId}&recursive=true`);
|
|
605
646
|
const entries = listing.entries || [];
|
|
606
647
|
const frames = entries.filter(e => e.type === 'frame');
|
|
607
|
-
// Don't count the auto-generated
|
|
608
|
-
const realFrames = frames.filter(f => !f.path.endsWith('/_meta/_context.md'));
|
|
648
|
+
// Don't count the auto-generated context frame as real content
|
|
649
|
+
const realFrames = frames.filter(f => !f.path.endsWith('/_meta/_context.md') && !f.path.endsWith('/instructions/context.md'));
|
|
609
650
|
const frameCount = realFrames.length;
|
|
610
651
|
if (frameCount > 0) {
|
|
611
652
|
throw new Error(`Layer "${key}" contains ${frameCount} frame(s). Use force: true to confirm deletion.`);
|
|
@@ -1029,6 +1070,24 @@ server.tool('list_assets', 'List all assets in the ACTIVE PROJECT, optionally fi
|
|
|
1029
1070
|
} catch (error) { return err(error); }
|
|
1030
1071
|
});
|
|
1031
1072
|
|
|
1073
|
+
// ── Shape tool ───────────────────────────────────────────────────────
|
|
1074
|
+
|
|
1075
|
+
server.tool('shape', 'Create a flowchart shape on the surface. Shapes are lightweight text nodes — use them for flowcharts, process diagrams, and decision trees. Connect shapes with the connect tool, then call layout to auto-arrange.', {
|
|
1076
|
+
text: z.string().describe('Text to display inside the shape'),
|
|
1077
|
+
shape: z.enum(['rectangle', 'diamond', 'oval', 'pill']).optional().default('rectangle').describe('Shape type: rectangle (process/action), diamond (decision), oval (start/end), pill (rounded step)'),
|
|
1078
|
+
layer: z.string().optional().describe('Layer to place the shape in (default: plans)'),
|
|
1079
|
+
lane: z.string().optional().describe('Lane within the layer (default: default)'),
|
|
1080
|
+
color: z.string().optional().describe('Border color (CSS color string)'),
|
|
1081
|
+
}, async ({ text, shape, layer, lane, color }) => {
|
|
1082
|
+
try {
|
|
1083
|
+
const body = { text, shape };
|
|
1084
|
+
if (layer) body.layer = layer;
|
|
1085
|
+
if (lane) body.lane = lane;
|
|
1086
|
+
if (color) body.color = color;
|
|
1087
|
+
const result = await api('POST', '/api/fs/shape', body);
|
|
1088
|
+
return ok(result);
|
|
1089
|
+
} catch (error) { return err(error); }
|
|
1090
|
+
});
|
|
1032
1091
|
// ── Connector tools ───────────────────────────────────────────────
|
|
1033
1092
|
|
|
1034
1093
|
server.tool('connect', 'Create a connector (arrow) between two frames on the surface.', {
|
|
@@ -1089,6 +1148,149 @@ server.tool('layout', 'Auto-arrange frames using graph layout algorithm. Positio
|
|
|
1089
1148
|
} catch (error) { return err(error); }
|
|
1090
1149
|
});
|
|
1091
1150
|
|
|
1151
|
+
// ── Skill library tools ──────────────────────────────────────────
|
|
1152
|
+
|
|
1153
|
+
server.tool('search_skills', 'Search the Drafted skill library for reusable agent skills (e.g. "UX", "copywriting", "brand"). Skills are stored prompts, guidelines, and instructions that agents can load and follow. When a user says "use the X skill", search here first.', {
|
|
1154
|
+
query: z.string().optional().describe('Search term (matches name, description, content)'),
|
|
1155
|
+
tags: z.array(z.string()).optional().describe('Filter by tags'),
|
|
1156
|
+
scope: z.enum(['all', 'org', 'global']).optional().default('all').describe('Scope: all (default), org, or global'),
|
|
1157
|
+
}, async ({ query, tags, scope }) => {
|
|
1158
|
+
try {
|
|
1159
|
+
const params = new URLSearchParams();
|
|
1160
|
+
if (query) params.set('q', query);
|
|
1161
|
+
if (tags?.length) params.set('tags', tags.join(','));
|
|
1162
|
+
if (scope) params.set('scope', scope);
|
|
1163
|
+
const qs = params.toString();
|
|
1164
|
+
const endpoint = query ? '/api/skills/search' : '/api/skills';
|
|
1165
|
+
const result = await api('GET', `${endpoint}${qs ? '?' + qs : ''}`);
|
|
1166
|
+
return ok(result);
|
|
1167
|
+
} catch (error) { return err(error); }
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
server.tool('load_skill', 'Load a skill to get its full instructions. Returns the SKILL.md content plus any supporting files. Use search_skills to find skills by keyword first.', {
|
|
1171
|
+
skill: z.string().describe('Skill ID (UUID) or slug'),
|
|
1172
|
+
}, async ({ skill }) => {
|
|
1173
|
+
try {
|
|
1174
|
+
const isUuid = /^[a-f0-9-]{36}$/.test(skill);
|
|
1175
|
+
const endpoint = isUuid ? `/api/skills/${skill}` : `/api/skills/slug/${skill}`;
|
|
1176
|
+
const result = await api('GET', endpoint);
|
|
1177
|
+
return ok(result);
|
|
1178
|
+
} catch (error) { return err(error); }
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
server.tool('read_skill_file', 'Read a supporting file from a skill directory (e.g. an example, template, or config). Use load_skill first to see available files.', {
|
|
1182
|
+
skillId: z.string().describe('Skill ID'),
|
|
1183
|
+
path: z.string().describe('Relative file path within the skill (e.g. "examples/react.md")'),
|
|
1184
|
+
}, async ({ skillId, path }) => {
|
|
1185
|
+
try {
|
|
1186
|
+
const result = await api('GET', `/api/skills/${skillId}/files/${path}`);
|
|
1187
|
+
return ok(result);
|
|
1188
|
+
} catch (error) { return err(error); }
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
server.tool('add_skill', 'Create a new skill in the org skill library. Creates a skill directory with a root SKILL.md. Use update_skill_file to add supporting files afterward.', {
|
|
1192
|
+
name: z.string().describe('Skill name'),
|
|
1193
|
+
description: z.string().describe('One-line description of what the skill does'),
|
|
1194
|
+
content: z.string().describe('Root SKILL.md content (markdown with instructions/prompts)'),
|
|
1195
|
+
tags: z.array(z.string()).optional().describe('Tags for discovery'),
|
|
1196
|
+
triggerPatterns: z.array(z.string()).optional().describe('Patterns that suggest this skill (e.g. "designing a landing page", "writing tests")'),
|
|
1197
|
+
}, async ({ name, description, content, tags, triggerPatterns }) => {
|
|
1198
|
+
try {
|
|
1199
|
+
const body = { name, description, content };
|
|
1200
|
+
if (tags) body.tags = tags;
|
|
1201
|
+
if (triggerPatterns) body.triggerPatterns = triggerPatterns;
|
|
1202
|
+
const result = await api('POST', '/api/skills', body);
|
|
1203
|
+
return ok(result);
|
|
1204
|
+
} catch (error) { return err(error); }
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
server.tool('update_skill', 'Update an existing org skill. Can change name, description, content (SKILL.md), tags, or trigger patterns. Cannot edit global skills.', {
|
|
1208
|
+
skillId: z.string().describe('Skill ID to update'),
|
|
1209
|
+
name: z.string().optional().describe('New skill name'),
|
|
1210
|
+
description: z.string().optional().describe('New description'),
|
|
1211
|
+
content: z.string().optional().describe('New root SKILL.md content'),
|
|
1212
|
+
tags: z.array(z.string()).optional().describe('Replace tags'),
|
|
1213
|
+
triggerPatterns: z.array(z.string()).optional().describe('Replace trigger patterns'),
|
|
1214
|
+
}, async ({ skillId, name, description, content, tags, triggerPatterns }) => {
|
|
1215
|
+
try {
|
|
1216
|
+
const body = {};
|
|
1217
|
+
if (name !== undefined) body.name = name;
|
|
1218
|
+
if (description !== undefined) body.description = description;
|
|
1219
|
+
if (content !== undefined) body.content = content;
|
|
1220
|
+
if (tags !== undefined) body.tags = tags;
|
|
1221
|
+
if (triggerPatterns !== undefined) body.triggerPatterns = triggerPatterns;
|
|
1222
|
+
if (Object.keys(body).length === 0) throw new Error('At least one field is required');
|
|
1223
|
+
const result = await api('PUT', `/api/skills/${skillId}`, body);
|
|
1224
|
+
return ok(result);
|
|
1225
|
+
} catch (error) { return err(error); }
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
server.tool('update_skill_file', 'Add or update a supporting file in a skill directory. Use for examples, templates, configs -- anything beyond the root SKILL.md.', {
|
|
1229
|
+
skillId: z.string().describe('Skill ID'),
|
|
1230
|
+
path: z.string().describe('Relative file path (e.g. "examples/react.md", "templates/component.html")'),
|
|
1231
|
+
content: z.string().describe('File content'),
|
|
1232
|
+
}, async ({ skillId, path, content }) => {
|
|
1233
|
+
try {
|
|
1234
|
+
const result = await api('PUT', `/api/skills/${skillId}/files/${path}`, { content });
|
|
1235
|
+
return ok(result);
|
|
1236
|
+
} catch (error) { return err(error); }
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
server.tool('remove_skill', 'Delete a skill from the org library. Also detaches it from all projects. Cannot delete global skills.', {
|
|
1240
|
+
skillId: z.string().describe('Skill ID to delete'),
|
|
1241
|
+
}, async ({ skillId }) => {
|
|
1242
|
+
try {
|
|
1243
|
+
const result = await api('DELETE', `/api/skills/${skillId}`);
|
|
1244
|
+
return ok(result);
|
|
1245
|
+
} catch (error) { return err(error); }
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
server.tool('list_project_skills', 'List skills attached to the active project. These are the skills recommended for this project context.', {}, async () => {
|
|
1249
|
+
try {
|
|
1250
|
+
if (!agentActiveProjectId) throw new Error('No active project. Call open_project first.');
|
|
1251
|
+
const result = await api('GET', `/api/projects/${agentActiveProjectId}/skills`);
|
|
1252
|
+
return ok(result);
|
|
1253
|
+
} catch (error) { return err(error); }
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
server.tool('attach_skill', 'Attach a skill to the active project. Attached skills are auto-loaded when agents open this project.', {
|
|
1257
|
+
skillId: z.string().describe('Skill ID to attach'),
|
|
1258
|
+
}, async ({ skillId }) => {
|
|
1259
|
+
try {
|
|
1260
|
+
if (!agentActiveProjectId) throw new Error('No active project. Call open_project first.');
|
|
1261
|
+
const result = await api('POST', `/api/projects/${agentActiveProjectId}/skills`, { skillId });
|
|
1262
|
+
return ok(result);
|
|
1263
|
+
} catch (error) { return err(error); }
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
server.tool('detach_skill', 'Remove a skill from the active project.', {
|
|
1267
|
+
skillId: z.string().describe('Skill ID to detach'),
|
|
1268
|
+
}, async ({ skillId }) => {
|
|
1269
|
+
try {
|
|
1270
|
+
if (!agentActiveProjectId) throw new Error('No active project. Call open_project first.');
|
|
1271
|
+
const result = await api('DELETE', `/api/projects/${agentActiveProjectId}/skills/${skillId}`);
|
|
1272
|
+
return ok(result);
|
|
1273
|
+
} catch (error) { return err(error); }
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
server.tool('favorite_skill', 'Add a skill to your favorites (max 12). Favorited skills appear in list_projects so agents always know about them.', {
|
|
1277
|
+
skillId: z.string().describe('Skill ID to favorite'),
|
|
1278
|
+
}, async ({ skillId }) => {
|
|
1279
|
+
try {
|
|
1280
|
+
const result = await api('POST', `/api/skills/favorites/${skillId}`);
|
|
1281
|
+
return ok(result);
|
|
1282
|
+
} catch (error) { return err(error); }
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
server.tool('unfavorite_skill', 'Remove a skill from your favorites.', {
|
|
1286
|
+
skillId: z.string().describe('Skill ID to unfavorite'),
|
|
1287
|
+
}, async ({ skillId }) => {
|
|
1288
|
+
try {
|
|
1289
|
+
const result = await api('DELETE', `/api/skills/favorites/${skillId}`);
|
|
1290
|
+
return ok(result);
|
|
1291
|
+
} catch (error) { return err(error); }
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1092
1294
|
// ── Resource: canvas info ─────────────────────────────────────────
|
|
1093
1295
|
|
|
1094
1296
|
server.resource('info', 'drafted://info', {
|