drafted 1.11.38 → 1.12.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 +95 -13
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -197,6 +197,10 @@ CONTEXT RULES (follow these before every action):
|
|
|
197
197
|
- WIKI CHECK: Before acting on any request, search the org wiki for relevant conventions, existing designs, and prior decisions. Use wiki(action="search") with relevant keywords.
|
|
198
198
|
- LAYER CONTEXT: Before reading or mutating a frame, read all anchored frames in the same layer. Anchored frames are per-layer required reading (style guides, design systems, conventions). The server enforces this mechanically for writes/edits/deletes/moves — but proactively reading anchored frames before any frame operation prevents wasted work.
|
|
199
199
|
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.`,
|
|
200
|
+
}, {
|
|
201
|
+
// Initialize instructions: the agent-identity contract, so an agent learns its own
|
|
202
|
+
// tab name + the right way to read it WITHOUT having to "think to" call a tool.
|
|
203
|
+
instructions: `SESSION IDENTITY — read this first: you run as a NAMED session tab visible to the user on their Drafted surface. Your session has a human-readable name + emoji (e.g. "Sly Owl") — that name is how the user matches YOUR window to the tab they see, so identify yourself by it when it matters which agent you are. Read it from get_org (response field "session.name") or from the whoami tool; call whoami to refresh after a reconnect.`,
|
|
200
204
|
});
|
|
201
205
|
|
|
202
206
|
const layerKeys = Object.keys(LAYERS);
|
|
@@ -215,6 +219,9 @@ const TOOL_ANNOTATIONS = {
|
|
|
215
219
|
// Auth — initiates external browser / email flows
|
|
216
220
|
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
221
|
|
|
222
|
+
// Identity — read-only introspection of THIS agent's session
|
|
223
|
+
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.' },
|
|
224
|
+
|
|
218
225
|
// Projects
|
|
219
226
|
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
227
|
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.' },
|
|
@@ -1064,6 +1071,10 @@ function withProjectBreadcrumb(result) {
|
|
|
1064
1071
|
|
|
1065
1072
|
let agentWs = null;
|
|
1066
1073
|
let agentWsReconnectTimer = null;
|
|
1074
|
+
// Cached agent-hello-ack: this agent's own surface identity (name/emoji/alive) as
|
|
1075
|
+
// assigned by the server. The playful name is the correlation key between an agent
|
|
1076
|
+
// window and its web-app session tab; whoami reads it from here.
|
|
1077
|
+
let agentSurface = null;
|
|
1067
1078
|
|
|
1068
1079
|
function setMcpActiveProject(projectId, meta = null) {
|
|
1069
1080
|
const s = getState();
|
|
@@ -1115,6 +1126,21 @@ async function connectAgentWs() {
|
|
|
1115
1126
|
}
|
|
1116
1127
|
});
|
|
1117
1128
|
|
|
1129
|
+
agentWs.on('message', (raw) => {
|
|
1130
|
+
// agent-hello-ack carries this agent's server-assigned surface identity back on the
|
|
1131
|
+
// owning connection. Cache it so whoami can report the name without a prod WS probe.
|
|
1132
|
+
try {
|
|
1133
|
+
const m = JSON.parse(raw.toString());
|
|
1134
|
+
if (m.type === 'agent-hello-ack') {
|
|
1135
|
+
agentSurface = {
|
|
1136
|
+
sessionId: m.sessionId, userId: m.userId, orgId: m.orgId, projectId: m.projectId,
|
|
1137
|
+
name: m.name, emoji: m.emoji, alive: m.alive, surfaced: true,
|
|
1138
|
+
capturedAt: Date.now(),
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
} catch { /* ignore non-JSON / unexpected */ }
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1118
1144
|
agentWs.on('close', () => {
|
|
1119
1145
|
console.error('[MCP-WS] Disconnected, reconnecting in 5s...');
|
|
1120
1146
|
agentWs = null;
|
|
@@ -1191,9 +1217,11 @@ async function getCurrentOrgId() {
|
|
|
1191
1217
|
return ctx?.id || null;
|
|
1192
1218
|
}
|
|
1193
1219
|
|
|
1194
|
-
// Returns { id, name } for the org
|
|
1195
|
-
//
|
|
1196
|
-
//
|
|
1220
|
+
// Returns { id, name } for the org this MCP process is currently scoped to (the
|
|
1221
|
+
// session row's org_id, which the server resolves per-request via X-Drafted-Org).
|
|
1222
|
+
// Each MCP process is independent — parallel agents can run scoped to different
|
|
1223
|
+
// orgs. Cache is per-session-bucket, not module-global, so concurrent OAuth
|
|
1224
|
+
// users can't collide.
|
|
1197
1225
|
async function getCurrentOrgContext() {
|
|
1198
1226
|
const sess = getSessionState();
|
|
1199
1227
|
if (sess.cachedOrgId && Date.now() - sess.cachedOrgIdTime < 30000) return sess.cachedOrgId;
|
|
@@ -1489,6 +1517,58 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a ver
|
|
|
1489
1517
|
} catch (error) { return err(error); }
|
|
1490
1518
|
});
|
|
1491
1519
|
|
|
1520
|
+
// THIS-session surface identity: the playful name + emoji (correlation key between an
|
|
1521
|
+
// agent window and its web-app session tab), ids, and surfaced/alive state. Built from
|
|
1522
|
+
// the cached agent-hello-ack; falls back to /auth/me for userId/org before the first ack.
|
|
1523
|
+
// Shared by `whoami` AND surfaced in `get_org`'s response so an agent learns its name
|
|
1524
|
+
// from the tool it already calls — it must not have to "think to" ask.
|
|
1525
|
+
async function sessionSurfaceBlock() {
|
|
1526
|
+
const sessionId = agentSurface?.sessionId || getState().sessionId || null;
|
|
1527
|
+
if (agentSurface) {
|
|
1528
|
+
return {
|
|
1529
|
+
sessionId,
|
|
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 cookieSid = sessionId || getBootstrapSessionId();
|
|
1541
|
+
let me = null;
|
|
1542
|
+
if (cookieSid) {
|
|
1543
|
+
try {
|
|
1544
|
+
const res = await fetch(`${getServerUrl()}/auth/me`, { headers: { Cookie: `gc_session=${cookieSid}` } });
|
|
1545
|
+
if (res.ok) me = await res.json();
|
|
1546
|
+
} catch { /* not yet authenticated */ }
|
|
1547
|
+
}
|
|
1548
|
+
return {
|
|
1549
|
+
sessionId: cookieSid,
|
|
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
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// Identity: report THIS agent session's own surface identity. Read-only — no state changed.
|
|
1561
|
+
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 () => {
|
|
1562
|
+
try {
|
|
1563
|
+
return ok({
|
|
1564
|
+
server: getServerUrl(),
|
|
1565
|
+
editor: (process.env.DRAFTED_AGENT_NAME || '').trim() || null,
|
|
1566
|
+
agentLabel: getAgentLabel(),
|
|
1567
|
+
...(await sessionSurfaceBlock()),
|
|
1568
|
+
});
|
|
1569
|
+
} catch (error) { return err(error); }
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1492
1572
|
// ── Project management tools (direct HTTP) ────────────────────────
|
|
1493
1573
|
|
|
1494
1574
|
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.', {
|
|
@@ -1497,7 +1577,7 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
|
|
|
1497
1577
|
name: z.string().optional().describe('[create|update] project name'),
|
|
1498
1578
|
description: z.string().nullable().optional().describe('[create|update] project description'),
|
|
1499
1579
|
templateSlug: z.string().optional().describe('[create] template slug (e.g. "web-design", "mobile-app", "landing-page")'),
|
|
1500
|
-
org: z.string().optional().describe('[create] org slug or id to create the project in, without switching the
|
|
1580
|
+
org: z.string().optional().describe('[create] org slug or id to create the project in, without switching the active org. Defaults to the active org.'),
|
|
1501
1581
|
folder: z.string().nullable().optional().describe('[update] folder name (null to remove from folder)'),
|
|
1502
1582
|
layers: z.array(z.object({}).passthrough()).optional().describe('[update] full layers array replacement. Use ls / to read current layers first.'),
|
|
1503
1583
|
targetOrgId: z.string().optional().describe('[move] destination organization ID. Get org IDs from action=list (each project has an orgId field) or get_org. Both source and target org must include the current user.'),
|
|
@@ -1626,7 +1706,7 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
|
|
|
1626
1706
|
const body = { name };
|
|
1627
1707
|
if (description) body.description = description;
|
|
1628
1708
|
if (templateSlug) body.templateSlug = templateSlug;
|
|
1629
|
-
// `org` targets a specific org without switching the
|
|
1709
|
+
// `org` targets a specific org without switching the active org.
|
|
1630
1710
|
const createExtra = org ? { 'X-Drafted-Org': org } : {};
|
|
1631
1711
|
return ok(withProjectBreadcrumb(await api('POST', '/api/projects', body, createExtra)));
|
|
1632
1712
|
}
|
|
@@ -2065,12 +2145,13 @@ tool('get_org', {
|
|
|
2065
2145
|
const activeOrg = (orgs || []).map(o => ({ id: o.orgId || o.id, name: o.orgName || o.name })).find(o => o.id === me?.orgId) || null;
|
|
2066
2146
|
const googleDrive = await getGoogleDriveAvailability();
|
|
2067
2147
|
const mcpUpdate = await getMcpUpdateMetadata();
|
|
2068
|
-
|
|
2148
|
+
const session = await sessionSurfaceBlock();
|
|
2149
|
+
return ok({ switched: true, activeOrg, googleDrive, mcpVersion: PACKAGE_VERSION, mcpUpdate, session, note: 'Active org switched. Wiki and skill calls now target this org. Active project cleared — open a project (or stay org-scoped for wiki/skill). If googleDrive.connected is true, prefer Google Workspace frames for docs, sheets, and slides.' });
|
|
2069
2150
|
}
|
|
2070
2151
|
|
|
2071
|
-
// Source of truth =
|
|
2072
|
-
// hit). Each
|
|
2073
|
-
// parallel
|
|
2152
|
+
// Source of truth = the org this MCP process scopes requests to (what mutations
|
|
2153
|
+
// will actually hit). Each MCP process is independent — multiple agents can run
|
|
2154
|
+
// in parallel scoped to different orgs. /auth/me reads sessions.org_id directly.
|
|
2074
2155
|
const me = await api('GET', '/auth/me');
|
|
2075
2156
|
const sessionOrgId = me?.orgId || null;
|
|
2076
2157
|
|
|
@@ -2095,7 +2176,8 @@ tool('get_org', {
|
|
|
2095
2176
|
googleDrive,
|
|
2096
2177
|
mcpVersion: PACKAGE_VERSION,
|
|
2097
2178
|
mcpUpdate,
|
|
2098
|
-
|
|
2179
|
+
session: await sessionSurfaceBlock(),
|
|
2180
|
+
note: "activeOrg is the org this MCP process scopes its requests to (carried per-request as X-Drafted-Org); mutations target it. `session` is THIS agent's own surface identity — `session.name` is the human-readable tab name the user sees (use it to identify which agent you are); refresh it via whoami. To scope to a different org without opening a project, call get_org(action=\"switch\", orgId=\"...\"). Concurrent MCP sessions and browser tabs for the same user can be scoped to different orgs. If googleDrive.connected is true, strongly prefer Google Workspace frames for docs, sheets, and slides.",
|
|
2099
2181
|
});
|
|
2100
2182
|
} catch (error) { return err(error); }
|
|
2101
2183
|
});
|
|
@@ -3029,7 +3111,7 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
3029
3111
|
path: z.string().optional().describe('[read_file|update_file] relative path inside skill directory (e.g. "examples/react.md")'),
|
|
3030
3112
|
offset: z.number().optional().describe('[search|list] skip N results for pagination; [read_file] start reading at this byte offset (default 0) — for large files (e.g. a >90KB app-frame bundle) read in chunks using the returned nextOffset until truncated=false'),
|
|
3031
3113
|
maxBytes: z.number().optional().describe('[read_file] return at most this many bytes from offset (default: whole remaining file). Response reports totalSize/offset/truncated/nextOffset.'),
|
|
3032
|
-
org: z.string().optional().describe('[fork|push|update] resolve/fork into this Drafted org (id or name); scopes the request without switching the
|
|
3114
|
+
org: z.string().optional().describe('[fork|push|update] resolve/fork into this Drafted org (id or name); scopes the request without switching the active org'),
|
|
3033
3115
|
setup: z.array(z.string()).optional().describe('[add|update] setup command(s) (in order) run on materialize to build a source-only skill, e.g. ["npm ci","npm run build"]'),
|
|
3034
3116
|
files: z.array(z.object({ path: z.string(), content: z.string() })).optional().describe('[push] source files to push (path + UTF-8 content); server strips artifacts + enforces caps'),
|
|
3035
3117
|
dir: z.string().optional().describe('[push] local directory to push instead of files[]; walked locally (heavy dirs, .skillinstall/, and .skillignore pre-filtered), server re-enforces. On push the dir\'s .gitignore is auto-updated to exclude .skillinstall/ (the rebuildable bundle).'),
|
|
@@ -3251,7 +3333,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3251
3333
|
action: z.enum(['ls', 'recent', 'read', 'search', 'links', 'log', 'health', 'write', 'edit', 'mv', 'rm', 'source-register', 'source-list', 'source-get', 'bulk-write']).describe('Operation to perform.'),
|
|
3252
3334
|
path: z.string().optional().describe('[ls|read|links] wiki path. For ls: default / (root). For read: required. For links: required.'),
|
|
3253
3335
|
pageId: z.string().optional().describe('[read|edit|mv|rm|links] page UUID (from read/search). UUID-first: addresses the page directly, org auto-derives — no org needed and no path lookup. Preferred over path for an existing page.'),
|
|
3254
|
-
org: z.string().optional().describe('[write] org slug or id to create/target the page in, without switching the
|
|
3336
|
+
org: z.string().optional().describe('[write] org slug or id to create/target the page in, without switching the active org. [search] scope the search to this org (default: search ALL your orgs). Required to create a page when you belong to more than one org and none is active.'),
|
|
3255
3337
|
recursive: z.boolean().optional().describe('[ls] list recursively with depth indicators'),
|
|
3256
3338
|
limit: z.number().optional().describe('[recent|search] max results (recent default 10, search default 25)'),
|
|
3257
3339
|
query: z.string().optional().describe('[search] term to search in title, path, and content'),
|
|
@@ -3507,7 +3589,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3507
3589
|
if (frontmatter !== undefined) body.frontmatter = frontmatter;
|
|
3508
3590
|
|
|
3509
3591
|
// Check if page exists — if so, update; otherwise create. `orgHeader`
|
|
3510
|
-
// (the `org` arg) targets a specific org without switching the
|
|
3592
|
+
// (the `org` arg) targets a specific org without switching the active org.
|
|
3511
3593
|
try {
|
|
3512
3594
|
const existing = await api('GET', `/api/wiki/page?path=${encodeURIComponent(normalized)}`, undefined, orgHeader);
|
|
3513
3595
|
const result = await api('PATCH', `/api/wiki/pages/${existing.id}`, body, orgHeader);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.1",
|
|
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": [
|