drafted 1.24.2 → 1.24.3
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 +75 -0
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -485,6 +485,7 @@ const TOOL_ANNOTATIONS = {
|
|
|
485
485
|
|
|
486
486
|
// Canvas / view
|
|
487
487
|
mail: { title: 'Connected email threads', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Connect a Gmail thread to a project so a reply to it is noticed. Drafted never reads mail: the user\'s own computer checks connected threads with their read-only gws sign-in and tells Drafted only that a new message arrived. Dispatch by `action`: connect (project, gmailThreadId, title, onReply), list (project), set (id, onReply/title), handled (id: clear the New reply mark after you dealt with it), disconnect (id). onReply: notify = mark it and tell the user; work = also start the user\'s agent on it. Read the thread itself with gws, and send any reply with action(propose, type="send_email").' },
|
|
488
|
+
frame_db: { title: 'Frame documents', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Read and write an app frame\'s documents — the same store the frame\'s own JavaScript uses through `drafted.db`. Dispatch by `action`: list, get, set, merge, delete. Documents are addressed by (frame, collection, id); a write can carry `expectedUpdatedAt` so a stale write is refused instead of overwriting. Authorization is the frame\'s own: access to the frame, edit access for writes, and the `db` capability its author granted.' },
|
|
488
489
|
focus: { title: 'Focus on target', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Pan the canvas viewport for connected clients to a frame, lane, or layer.' },
|
|
489
490
|
tour: { title: 'Guided tours', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Play or stop an agent-authored guided tour (driver.js) on the project surface for all connected clients. Tours live on a frame\'s metadata.tour — author them with fs(write, metadata={tour: {title, steps}}). Dispatch by `action`: play (frameId) starts the walkthrough, stop (projectId) ends it. Use for presentations and collaborative walkthroughs.' },
|
|
490
491
|
screenshot: { title: 'Screenshot', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'Render a PNG via headless browser. `scope=frame` for a single frame, `scope=canvas` for a region of the project surface.' },
|
|
@@ -3083,6 +3084,80 @@ tool('mail', {
|
|
|
3083
3084
|
} catch (error) { return err(error); }
|
|
3084
3085
|
});
|
|
3085
3086
|
|
|
3087
|
+
tool('frame_db', `Read and write an app frame's documents — the same store the frame's own JavaScript uses through \`drafted.db\`.
|
|
3088
|
+
|
|
3089
|
+
An app frame has a document store now (collection → document, one document per row of whatever the app shows). This tool is the agent's side of it: seeding a list, reading what the team has recorded, correcting a row. Before it existed the store was reachable only from the frame's own code or a raw authenticated HTTP call.
|
|
3090
|
+
|
|
3091
|
+
Authorization is the frame's own: the caller must have access to the frame, writes need edit access, and the frame's author must have granted the \`db\` capability. A read-only share can READ (the data is what they were invited to look at) and is refused on writes.
|
|
3092
|
+
|
|
3093
|
+
Actions:
|
|
3094
|
+
list — documents in one collection (limit/offset, ordered by updatedAt or id)
|
|
3095
|
+
get — one document
|
|
3096
|
+
set — replace one document's data (creates it)
|
|
3097
|
+
merge — shallow-merge fields into one document
|
|
3098
|
+
delete — remove one document
|
|
3099
|
+
|
|
3100
|
+
\`expectedUpdatedAt\` is optimistic concurrency: pass the document's updatedAt as you last read it and a stale write is refused with code \`conflict\` plus the current document, instead of silently overwriting somebody's change. Omit it for a blind write.`, {
|
|
3101
|
+
action: z.enum(['list', 'get', 'set', 'merge', 'delete']).describe('the operation'),
|
|
3102
|
+
target: z.string().describe('the frame: its URL (…/o/<org>/projects/<project>/<layer>/<lane>/<file>), a /projects path, or a frame UUID'),
|
|
3103
|
+
collection: z.string().describe('the collection name, e.g. "tracking" — letters, digits and _ . : - only'),
|
|
3104
|
+
id: z.string().optional().describe('document id, required for get/set/merge/delete'),
|
|
3105
|
+
data: z.any().optional().describe('[set/merge] the document body: plain JSON, at most 64KB. merge is a shallow merge, so nested objects are replaced, not merged'),
|
|
3106
|
+
expectedUpdatedAt: z.string().optional().describe("the document updatedAt as you last read it; a stale write is refused with code \"conflict\" rather than overwriting"),
|
|
3107
|
+
limit: z.number().int().min(1).max(500).optional().describe('[list] page size (default 100, max 500)'),
|
|
3108
|
+
offset: z.number().int().min(0).optional().describe('[list] page offset — the cursor for walking a large collection'),
|
|
3109
|
+
orderBy: z.enum(['updatedAt', 'id']).optional().describe('[list] ordering key (default updatedAt)'),
|
|
3110
|
+
dir: z.enum(['asc', 'desc']).optional().describe('[list] direction (default desc)'),
|
|
3111
|
+
}, async (args) => {
|
|
3112
|
+
const { action, target, collection, id, data, expectedUpdatedAt, limit, offset, orderBy, dir } = args;
|
|
3113
|
+
if (!target) return err(new Error('frame_db needs a target: the frame URL, /projects path, or frame id'));
|
|
3114
|
+
if (!collection) return err(new Error('frame_db needs a collection'));
|
|
3115
|
+
|
|
3116
|
+
// The org is in the target when the target is a path; a bare UUID has none and
|
|
3117
|
+
// resolves in the session's org. Same rule as fs: the address carries the scope.
|
|
3118
|
+
const orgInTarget = (String(target).match(/\/o\/([^/]+)\//) || [])[1] || null;
|
|
3119
|
+
const orgHeader = orgInTarget ? { 'X-Drafted-Org': decodeURIComponent(orgInTarget) } : {};
|
|
3120
|
+
|
|
3121
|
+
let frameId;
|
|
3122
|
+
try {
|
|
3123
|
+
({ frameId } = await resolveFsFramePath(target));
|
|
3124
|
+
} catch (e) {
|
|
3125
|
+
return err(new Error(`${e.message}${orgInTarget ? '' : ' — include the /o/<org>/… URL when the frame is not in the workspace you are working in'}`));
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
const base = `/api/frame-runtime/${frameId}/db/${encodeURIComponent(collection)}`;
|
|
3129
|
+
const q = [];
|
|
3130
|
+
if (limit != null) q.push('limit=' + limit);
|
|
3131
|
+
if (offset != null) q.push('offset=' + offset);
|
|
3132
|
+
if (orderBy) q.push('orderBy=' + encodeURIComponent(orderBy));
|
|
3133
|
+
if (dir) q.push('dir=' + encodeURIComponent(dir));
|
|
3134
|
+
const qs = q.length ? '?' + q.join('&') : '';
|
|
3135
|
+
|
|
3136
|
+
try {
|
|
3137
|
+
if (action === 'list') {
|
|
3138
|
+
const res = await api('GET', base + qs, undefined, orgHeader);
|
|
3139
|
+
return ok(res);
|
|
3140
|
+
}
|
|
3141
|
+
if (!id) return err(new Error(`frame_db ${action} needs a document id`));
|
|
3142
|
+
const docPath = `${base}/${encodeURIComponent(id)}`;
|
|
3143
|
+
if (action === 'get') return ok(await api('GET', docPath, undefined, orgHeader));
|
|
3144
|
+
if (action === 'delete') return ok(await api('DELETE', docPath, undefined, orgHeader));
|
|
3145
|
+
if (data === undefined) return err(new Error(`frame_db ${action} needs data`));
|
|
3146
|
+
const res = await api(action === 'merge' ? 'PATCH' : 'PUT', docPath, { data, expectedUpdatedAt }, orgHeader);
|
|
3147
|
+
return ok(res);
|
|
3148
|
+
} catch (e) {
|
|
3149
|
+
// A conflict carries the current document; a locked frame names the holder.
|
|
3150
|
+
// Flattening either into "HTTP 409" would hide the thing the caller acts on.
|
|
3151
|
+
if (e && (e.code === 'conflict' || e.code === 'locked' || e.code === 'quota_exceeded' || e.code === 'not_granted')) {
|
|
3152
|
+
// api() attaches the parsed body: the conflict carries the CURRENT document
|
|
3153
|
+
// and the lock carries its holder. Both are what the caller acts on.
|
|
3154
|
+
const detail = e.body ? JSON.stringify(e.body).slice(0, 800) : '';
|
|
3155
|
+
return err(new Error(`${e.code}: ${e.message}${detail ? ' — ' + detail : ''}`));
|
|
3156
|
+
}
|
|
3157
|
+
throw e;
|
|
3158
|
+
}
|
|
3159
|
+
});
|
|
3160
|
+
|
|
3086
3161
|
tool('focus', {
|
|
3087
3162
|
target: z.string().describe('What to pan the canvas to: a frame path (/projects/<project>/<layer>/<lane>/<file>), a frame URL (any URL containing /f/{uuid} or /o/<org>/projects/...), or a frame ID (UUID). When a user shares a Drafted link, pass it directly here.'),
|
|
3088
3163
|
}, async ({ target }) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.3",
|
|
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
|
"bin": {
|